测试密码学
去年冬天,测试我在一个安全审计项目里碰到这么个事:某物联网设备厂商的密码固件升级签名机制,用的测试是RSA-1024。他们工程师拍胸脯说“绝对安全,密码银行都在用”。测试我当场用一台笔记本跑了不到两小时,密码就把私钥分解出来了。测试那哥们脸都绿了——他以为密码学是密码道魔法墙,其实不过是测试一扇没锁好的门。
测试密码学跟开发密码学是密码两码事。开发的测试人想的是“怎么把数据锁起来”,测试的密码人想的是“锁芯里有没有弹簧没装”。你拿到一个加密模块,测试第一件事不是密码看它用了AES-256还是ChaCha20,而是测试翻它的随机数生成器。我见过太多例子,算法本身没毛病,但IV(初始化向量)硬编码成全零,或者nonce重复使用。这就像你家防盗门是钛合金的,结果钥匙就插在锁孔里没拔。
有个经典案例:某支付App的会话令牌生成逻辑,用的是System.currentTimeMillis()加一个固定盐值做SHA-256。看起来挺像回事。我写了个脚本,把时间窗口前后各扩5秒,跑了一万次哈希,直接撞出了有效令牌。他们测试团队之前只测了“令牌长度够不够”“有没有过期时间”,压根没碰熵源质量。密码学测试的核心就一句话:别信任何“看起来随机”的东西,去量它的熵。
再说个反直觉的。很多人觉得加密越强越好,测试时专挑密钥长度。但实际攻击往往走侧信道。我参与过一次智能卡破解,算法是3DES,密钥管理严丝合缝。结果呢?我们拿示波器抓功耗曲线,用差分功耗分析(DPA),两千条曲线就把密钥字节一个个抠出来了。那卡片的加密逻辑在数学上无懈可击,在物理世界上千疮百孔。所以测试密码学得有两副眼镜:一副看数学证明,一副看实现细节。
填充 oracle 攻击也是个老演员了。CBC模式下的PKCS#7填充,如果服务端返回的报错信息区分了“填充错误”和“MAC错误”,攻击者就能像剥洋葱一样逐字节解密。我去年测一个API,它返回的JSON里error_code字段居然区分了这两种情况。我写了段Python,用requests库发了大概三百万次请求,把一段加密的用户地址解出来了。厂商后来修了,但修的方式是把错误信息统一成“解密失败”——治标不治本,时序侧信道还在。正确的做法是用恒定时间比较,并且对填充和MAC做统一处理。
测试工具方面,别只盯着Burp Suite的插件。hashcat和john是暴力破解的,但密码学测试更多是逻辑漏洞。openssl命令行能帮你快速验证填充行为,pycryptodome写PoC比Java快十倍。我习惯用sageMath做格攻击相关的计算,比如针对RSA的Coppersmith攻击。有次一个CTF题目,e=3,明文很短,我直接用gmpy2.iroot开三次方就拿到了flag。现实里也有这种,某固件升级包签名验证居然没做填充检查,直接对签名做e次方就还原出了哈希值。
说到底,测试密码学不是去证明算法错了,而是去证明实现者偷懒了。密钥轮换周期是不是形同虚设?加密后的数据有没有做完整性校验?随机数是不是从/dev/urandom读的,还是自己写了个线性同余生成器?我见过最离谱的,一个区块链钱包的助记词生成用了Math.random()。那玩意在V8引擎里就是个xorshift128+,状态空间小得可怜。测试的时候,我抓了连续生成的1000个助记词,用z3求解器反推出了内部状态,然后预测了下一个助记词。厂商花了三个月重写随机数模块。
这行干久了,你会对“安全”这个词产生生理性怀疑。任何密码学实现,只要没经过公开的、对抗性的测试,默认它就是纸糊的。别信文档,别信认证,自己动手去撞、去猜、去侧信道。测试密码学的乐趣就在这——你不是在找bug,你是在拆穿谎言。
(责任编辑:热点)
