我认为这是一个非常有趣的问题和问题,所以我自己尝试实施攻击,但针对的是“真”或“假”以外的东西。
为了简单起见,我使用了一个静态页面作为文件,并在最后将内容注入其中。目标机密如下所示:
<input type="hidden" name="secret" value="7253b8f45f322b411ce39b12c6e9a84c" />
我以 MSDN 页面的源代码为例进行构建。我使用的完整目标页面源可以在这里找到;秘密在第 33 行。这里的一个重要因素是页面内容包含许多其他十六进制字符串,这模仿了可能更困难的攻击场景。
为了解决这个问题,我使用了一种相当幼稚的方法:
- 追加
<input type="hidden" name="secret" value="
到页面的末尾,然后追加我们之前的猜测(如果有的话),然后依次追加0000
,ffff
并在压缩时寻找最小的结果。
- 对于产生最小结果的 4 个字符的十六进制字符串,将前三个字符添加到我们的猜测中。删除最后一个字符很重要,因为最后一个字符存在相当多的不确定性,如果它确实弄错了,它往往只会为所有剩余的猜测重复生成最后一个字符(例如
7251
,第一个字符会1111
在所有后续猜测中生成)。
- 循环第 1 步和第 2 步,直到我们有 <4 个字符要猜测。遍历剩余的可能组合,在每种情况下添加
" />
到附加的字符串以完成标记。
我用 C# 编写了一个完整的并行实现,你可以在这里找到。
以下是 GZip 的结果:
................................
Guessed f45a
Secret: 7253b8f45
................................
Guessed f322
Secret: 7253b8f45f32
................................
Guessed 2b41
Secret: 7253b8f45f322b4
................................
Guessed 11ca
Secret: 7253b8f45f322b411c
................................
Guessed e39a
Secret: 7253b8f45f322b411ce39
................................
Guessed b12a
Secret: 7253b8f45f322b411ce39b12
................................
Guessed c6e9
Secret: 7253b8f45f322b411ce39b12c6e
................................
Guessed 9a84
Secret: 7253b8f45f322b411ce39b12c6e9a8
Reached last block.
Guessed 4c
Secret: 7253b8f45f322b411ce39b12c6e9a84c
Done!
这只需要几分钟的时间来运行,它正确地发现了这个秘密。
现在让我们再次尝试使用 Brotli,使用“最快”的压缩模式,使用相同的攻击配置:
................................
Guessed 7253
Secret: 725
................................
Guessed 3b77
Secret: 7253b7
................................
Guessed 4aa7
Secret: 7253b74aa
................................
Guessed 47aa
Secret: 7253b74aa47a
................................
Guessed 0aaa
Secret: 7253b74aa47a0aa
................................
Guessed 0aaa
Secret: 7253b74aa47a0aa0aa
................................
Guessed 3a0a
Secret: 7253b74aa47a0aa0aa3a0
................................
Guessed 0aaa
Secret: 7253b74aa47a0aa0aa3a00aa
................................
Guessed 4aaa
Secret: 7253b74aa47a0aa0aa3a00aa4aa
................................
Guessed 2aaa
Secret: 7253b74aa47a0aa0aa3a00aa4aa2aa
Reached last block.
Guessed 7a
Secret: 7253b74aa47a0aa0aa3a00aa4aa2aa7a
Done!
不太好。第二个猜测出错了,这会抛出整个算法。所以也许我们需要更加谨慎,每个块只接受 2/4 个字符而不是 3/4 个字符。
................................
Guessed 411c
Secret: 7253b8f45f322b41
................................
Guessed 1c77
Secret: 7253b8f45f322b411c
................................
Guessed e377
Secret: 7253b8f45f322b411ce3
................................
Guessed 9b12
Secret: 7253b8f45f322b411ce39b
................................
Guessed 1277
Secret: 7253b8f45f322b411ce39b12
................................
Guessed c677
Secret: 7253b8f45f322b411ce39b12c6
................................
Guessed e977
Secret: 7253b8f45f322b411ce39b12c6e9
................................
Guessed a84c
Secret: 7253b8f45f322b411ce39b12c6e9a8
Reached last block.
Guessed 4c
Secret: 7253b8f45f322b411ce39b12c6e9a84c
Done!
在那里我们可以看到它再次起作用了!所以事实证明,以这种方式攻击 Brotli 并非不可能。
解释为什么你的攻击失败的关键部分是预定义的字典:
与大多数通用压缩算法不同,Brotli 除了动态填充(“滑动窗口”)字典外,还使用预定义的 120 KB 字典。预定义字典包含超过 13000 个常用词、短语和其他子字符串,这些子字符串源自大量文本和 HTML 文档
(来自Brotli - 维基百科)
那么“真”和“假”出现在这本词典里吗?是的!我在这里找到了字典的文本副本,您可以分别在第 6829 行和第 1204 行找到真假。
我的攻击有效而您的攻击无效的原因是我正在寻找字典中没有的东西。对于像“true”或“false”这样的单个标记,使用字典索引比尝试使用距离编码将字符与流中的另一个实例折叠在一起要有效得多。
但是,为什么我使用 Brotli 执行成功的压缩预言攻击比使用 GZip 更难呢?字典中似乎没有很多十六进制字符串。我相信这仅仅是因为 Brotli 压缩系统比 GZip 更复杂。GZip 使用 LZ77 和 Huffman 编码的组合,它非常注重重复消除,这对于压缩预言攻击非常有效。另一方面,Brotli 有更多的技巧,导致在重复不是很长的前几个块中使用其他压缩机制而不是重复消除的可能性更高。改进 Brotli 攻击的一种方法是在第一个块中暴力破解一组更长的值(例如 6 个甚至 8 个字符)以尝试引发重复消除。