G.O.S.S.I.P 阅读推荐 2026-10-29 AES-256 已翻车?
原创 G.O.S.S.I.P 2026-10-29 20:34 上海

“AES-256已翻车”权威解读
最近国外的AI核爆风气也传到了国内,今天小红书上也发了一条“AES翻车”的“重磅消息”,估计线下的用户已经分成了99.999%幸灾乐祸等着AES被破解的吃瓜群众和0.001%随时感觉天要塌下来了的密码学家。那么我们今天就要来解读一下,到底这个“AES-256 惊天翻车”是怎么翻的(大概率不是刹车踩断了)。

通过追查来源,我们发现,这个“伪”核爆新闻的来源指向了10月27日(嗯,很多人现在都认为,人间只一日,AI已千年,所以应该大概是2000多年前的旧闻了)的一篇博客文章:

在该博客文章中,作者揭示了多个和PDF文件加密密钥生成相关的严重安全问题,影响了MuPDF(1.28.5版本之前)、Foxit PDF Reader(2026.1.3.36551)、OpenPDF 3.0.5 和 iText 9.7.1 这些著名的PDF处理软件和代码库。要想了解关于这些安全问题的根源,我们先要了解一下PDF加密过程中使用的几个关键要素——文件加密密钥(后文都简称“密钥”)、用户口令和所有者密码以及它们之间的关系。
这里我们让GPT画了一张图,不一定非常精确,但是主要的意思应该是表达清楚了:实际上PDF作为一个比我们的很多读者年纪都要大的文件格式,里面存在多种不同的标准,加密方案也有好几套,目前用的比较多的实际上是下图中所谓的“旧方案”(R小于等于4),而今天我们要讨论的存在问题的则是所谓的“现代方案”(R等于6)。
在旧方案中,大家只需要记住“PDF的密钥是由用户口令和所有者密钥再结合一些元数据信息派生出来的”这个关键信息就好了;而不知道为什么,到了现代方案里面,PDF的密钥突然就和用户口令没有什么直接的关系了。

从上图里面我们可以看到,使用现代方案加密的PDF,虽然使用了更强的算法(AES-256),但是它的密钥是直接从一个随机数源里面取了256-bit的随机数生成的,而并不会和用户的口令以及所有者密钥有什么直接的依赖关系,用户的口令只是用来保护这个256-bit的随机数(也就是PDF的密钥):用户输入的口令,实际作用是通过特定的密钥派生过程(比如使用一些key derivation function)转成一个对称密钥,然后把刚刚的256-bit AES 密钥加密之后保存在PDF中。当别人需要打开一个加密的PDF文件(使用了现代方案加密)时,首先需要输入口令,只有口令正确了才能解密恢复那个256-bit AES 密钥来解密PDF的内容,否则就无法正确读取里面的信息。
好了,看到这里,你大概明白了问题可能出在哪里。对,就是臭名昭著的不安全密钥生成。在MuPDF(1.28.5版本之前)的代码中,不知道为什么用了一个可能是大学一年级学生都写不出来的非常烂的密钥生成过程,在它的代码中的static void fz_init_random_context(fz_context *ctx)这个函数中,使用了如下的密钥材料初始化过程:

而且这个fz_srand48函数非常奇葩,只使用了9个bit的时间戳的信息(具体的情况大家可以用AI去分析下代码),也就是说整个这个初始化过程最多只能输出2的9次方(也就是512)种可能的PDF密钥(尽管它既是AES-256密钥同时看起来也不是全0或者全1那么傻)。

聪明的你看到这里,应该也已经意识到问题的严重性了吧?或者你已经让AI帮你写好了“爆破”代码(这里说爆破是不是有点侮辱了CPU的运算能力)。那我们继续往下看,另一个软件Foxit PDF Reader(2026.1.3.36551)在生成PDF密钥的时候(当然也是现代方案)稍微好了那么一点点,达到了大一新生的水平:
key = SHA-256(uint32_le(time(NULL)) || <32 uninitialized bytes> || "there")
注意到这里的密钥的熵除了来自于时间戳,还有一个32字节的buffer,但是这个地方的代码忘记给它初始化了,然后分析表明这里大概率是全0填充的,所以这类PDF密钥的爆破嘛,不用我们说,聪明的你(的AI)已经会写代码了对不对?(不会也没关系:https://github.com/v12-security/pocs/tree/main/pdf_decryptors 自取)
最后是OpenPDF 3.0.5 和 iText 9.7.1这两个用Java开发的PDF处理引擎(iText是知名的pdftk工具的底层引擎)使用的PDF密钥生成算法(如下图),也是笑死人,拿时间戳来做种子也就算了,还把当前内存的剩余值也放在随机来源里面,这是当众处刑那些买不起大容量内存的人吗(尽管如此这个爆破依旧是快如闪电对不对?)

讲到这里,我们不禁要问,写个稍微安全一点的随机数获取代码真的有那么难吗???就算没有AI,这也是分分钟就能写一个不那么愚蠢的实现版本吧??不过呢,我们也顺手调研了一下国内的PDF加密的情况,好像大部分的软件(比如大家最常用的就是word另存为PDF然后顺便加个密)还都是在使用旧方案,也就是根据用户口令来派生PDF密钥,这样就可以无视所谓的“AES-256已翻车”的结论了。
于是这里我们就更想要回顾一下12年前的一场CTF比赛(嗯,下图其实只是想要炫耀下“我们拿到了全球参赛队伍中第二名的好成绩”这一点)里面的一道名为Cryptochief的逆向分析题目(其实是一道密码分析的题目,而且通过思考这道题目,间接催生了我们后来的CCS 2018论文 K-Hunt: Pinpointing Insecure Cryptographic Keys from Execution Traces):

在目前全网唯一能访问的关于这道题目的writeup中(另一篇法国人写的writeup被删了,只能通过time machine去访问了),我们写到:
我们发现程序的keyA生成主要在函数00404FA6处,但是函数本身相当复杂(虽然有ida hexrays但仍然表示逆向能力和耐心都弱得不行),于是大胆地决定把该函数当成黑盒过程(记为keyScheduler)来使用。回到开始的init_key()和gen_keyStream()函数上,观察到init_key()在读取keyfile后,会使用keyScheduler()生成8个字节,然后这8个字节传递给gen_keyStream(),作为初始值去从头开始产生新的密钥流!!!记得我们前面提到过,keyScheduler()函数里面使用了srand配合输入的字节流来作为初始化,并且输出也为8字节,而这时候因为init_key()不管读入什么样的keyfile,提供给gen_keyStream()的输入字节总是固定的8字节!!!因此,我们再一次将输入空间缩小到了2个字节(输入的头、尾字节,长度固定为8)!!!
当时还以为这个题目里面的错误只是因为它来自于一位德国民科(最初是Bruce Schneier在他的博客https://www.schneier.com/blog/archives/2006/06/the_doghouse_kr.html 上吐槽的),十分好笑,没想到12年过去了,世界再次验证了那句话:人类从历史中学到的唯一教训,就是人类无法从历史中学到任何教训(也许AI也适用)。
本文转载自微信公众号「安全研究GoSSIP」,查看原文。版权归原作者所有,如有侵权请联系删除。