更好的拉链炸弹

本文介绍了如何创建一个非递归 zip炸弹 ,该炸弹通过在zip容器内重叠文件来提供高度压缩。 “非递归”表示它不依赖于解压缩程序来解压缩zip存档中附加的文件:只有一轮。 输出大小从输入开始呈平方增加,在zip格式中压缩比达到2800万(10 MB→281 TB)以上。 使用64位扩展可以进一步扩展。 该设计仅使用最常用的DEFLATE压缩算法,并且与大多数zip解析器兼容。


源代码:
  git clone https://www.bamsoftware.com/git/zipbomb.git 
zipbomb-20190702.zip

数据和插图来源:
  git克隆https://www.bamsoftware.com/git/zipbomb-paper.git 

非递归递归的
档案大小未压缩尺寸比率未压缩尺寸比率
奎因·考克斯4404401.0
奎因·埃林森28,809425691.5
42.zip42,374 *55843213.24507981343343026161060亿
这种技术42,37454613076201290005461307620129000
这种技术98935252813954562449342800万2813954562449342800万
这种技术(Zip64)4587695245079814277067064599800万45079814277067064599800万

* 42.zip有两个版本: 旧的 42 374字节和较新的 42 428字节。 区别在于,新密码在打开包装之前需要输入密码。 我们仅与旧版本进行比较。 如果需要,这里是文件的副本: 42.zip

**我想知道并指出42.zip的作者,但找不到它-如果您有任何信息,请告诉我。

拉链炸弹必须克服以下事实:解析器最常支持的DEFLATE压缩算法不能超过 1032:1 压缩率。因此,拉链炸弹通常依靠递归解压缩,方法是将zip文件插入zip文件中以获得额外的比率。每层1032。 但是,该技巧仅适用于以递归方式解压缩的实现,而大多数情况下无效。 如果将所有六个层递归解压缩,则最著名的42.zip炸弹可扩展到强大的4.5 PB,但在顶层则只有0.6 MB的小空间。 像CoxEllingsen的那些一样,Zip quines会发行自己的副本,因此在递归解压缩时会无限期地扩展。 但是,一旦打开包装,它们也是完全安全的。

本文介绍了如何创建一个非递归zip炸弹,该炸弹的压缩率超过DEFLATE限制1032。它通过在zip容器内重叠文件以引用多个文件中的高度压缩数据的“核心”而不进行多次复制而起作用。 拉链炸弹的输出大小从输入大小开始呈平方增长; 即,压缩比随着炸弹尺寸的增加而提高。 设计取决于zip和DEFLATE的功能:无法将其直接传输为其他文件格式或压缩算法。 该炸弹与大多数zip解析器兼容,除了“流”解析器外,该解析器可以一遍分析文件而无需检查zip文件的中央目录。 我们试图平衡两个相互矛盾的目标:

  • 增加压缩率。 我们将压缩率定义为存档中所有文件大小的总和除以zip文件本身的大小。 它不考虑文件名或其他文件系统元数据,而仅考虑内容。
  • 保持兼容性。 Zip是一种复杂的格式,解析器也有所不同,尤其是在边界情况下,以及附加功能。 不要使用仅适用于某些解析器的技术。 我们将注意到在不增加兼容性的情况下提高拉链炸弹效率的一些方法。

压缩文件结构


压缩文件由文件链接的中央目录组成。



中央目录位于zip文件的末尾。 这是中央目录头的列表。 每个中央目录头均包含单个文件的元数据,例如文件名和CRC-32校验和,以及指向本地文件头的指针。 中央目录头的长度为46个字节,再加上文件名的长度。

该文件由本地文件的标头和紧随其后的压缩文件数据组成。 本地文件头的长度为30个字节加上文件名的长度。 它包含来自中央目录头的元数据的冗余副本,以及其后压缩和未压缩数据文件的大小。 Zip是容器格式,而不是压缩算法。 使用元数据中指定的算法(通常是DEFLATE )压缩每个文件的数据。

zip格式的此描述省略了许多了解zip炸弹所不需要的细节。 有关完整信息,请参见4.3 APPNOTE.TXT或Florian Buchholz 撰写PKZip文件结构 ,或参见源代码

大量的冗余和zip格式中的许多歧义为各种恶作剧提供了机会。 拉链炸弹只是冰山一角。 链接以供进一步阅读:


首次发现:文件重叠


通过压缩重复字节的长字符串,我们可以创建高度压缩数据的核心 。 内核本身的压缩率不能超过1032的DEFLATE限制,因此我们需要一种在许多文件中重用内核的方法,而不必在每个文件中创建单独的副本。 我们可以通过重叠文件来做到这一点:make使得中央目录的许多标题指向以数据为核心的单个文件。



考虑这个设计如何影响压缩比的示例。 假设在1 MB中解压缩了1000字节的核心。 然后,输出的第一个兆字节“消耗”了1078个字节的输入数据:

  • 本地文件头的31个字节(包括1个字节的文件名)
  • 中央目录头的47个字节(包括1个字节的文件名)
  • 每个内核1000个字节

但是第一个之后的每1 MB输出仅占用47个字节-我们不需要本地文件的另一个头或内核的另一个副本,而仅需要中央目录的另一个头。 因此,虽然来自核心的第一个链接具有1,000,000 / 1,078≈928的压缩率,但每个附加链接将系数移近1,000,000 / 47≈21,277,并且较大的核心会提高上限。

这个想法的问题是缺乏兼容性。 由于中央目录的许多标头指向本地文件的一个标头,因此每个文件的元数据(尤其是文件名)不能相同。 一些解析器对此发誓 。 例如, Info-ZIP UnZip (Unix上的标准unzip程序)检索文件,但带有警告:

  $解压缩overlay.zip
  充气:A
 B:与“本地”文件名(A)不匹配,
         继续“中央”文件名版本
  充气:B
 ... 

Python zipfile抛出异常

  $ python3 -m zipfile -e overlay.zip。
追溯(最近一次通话):
 ...
 __main __。BadZipFile:目录'B'中的文件名与标头b'A'的不同。 

接下来,我们将研究如何重新设计文件名的一致性,同时保留重叠文件的大部分好处。

第二次发现:引用本地文件的标头


我们需要在重用一个核心的同时,将每个文件的本地文件头分开。 简单地组合所有标头是行不通的,因为zip解析器将在本地文件的标头中找到希望DEFLATE流开始的位置。 但是,只要稍作更改,该想法便会奏效。 我们将使用未压缩块的DEFLATE函数来“引用”本地文件的头,以使它们看起来像是在内核中结束的同一DEFLATE流的一部分。 本地文件的每个标头(第一个文件头除外)将以两种方式解释:作为代码(zip文件结构的一部分)和作为数据(文件内容的一部分)。



DEFLATE流是一系列 ,每个块都可以压缩或不压缩。 我们通常只考虑压缩块,例如,内核是一个很大的压缩块。 但是,也有一些未压缩的文件5字节的标头开头,该标头带有一个length字段,这仅表示:“逐字打印接下来的n个字节”。 解压缩未压缩的块意味着仅删除5字节的标头。 压缩和未压缩的块可以在DEFLATE流中自由混合。 输出是按顺序解包所有块的结果的串联。 “未压缩”的概念仅在DEFLATE级别上起作用。 不管使用哪个块,仍在zip级别将文件数据视为“压缩”。

想象这种设计的最简单方法是从最后一个文件到第一个文件的内部重叠。 我们首先插入一个内核,该内核将构成每个文件的数据文件的结尾。 添加本地LFH N文件的标题和指向该文件的中央CDH N目录的标题。 将LFH N和CDH N中的“压缩大小”元数据字段设置为压缩核心大小。 现在,添加未压缩块的5字节标头(图中绿色),其长度字段等于LFHN 添加本地LFH文件N -1的第二个标题和指向该目录的中央目录CDH N -1的标题。 将“压缩大小”元数据字段设置为压缩内核大小加上未压缩块头大小(5个字节) 加上 LFH N大小的新头

目前,该zip文件包含两个名称分别为Y和Z的文件。让我们看一下解析器在解析时将看到的内容。 假设压缩的内核大小为1000个字节,而LFH N大小为31个字节。 我们从CDH N -1开始,并遵循LFH N -1的符号。 第一个文件的名称为Y,其数据文件的压缩大小为1036字节。 将接下来的103​​6个字节解释为DEFLATE流,我们首先遇到一个未压缩块的5个字节的标头,该标题表示要复制接下来的31个字节。 我们记下接下来的31个字节,即LFH N ,我们将其解压缩并添加到Y文件中。在DEFLATE流中进一步移动,我们找到要解压缩到Y文件中的压缩块(内核)。现在我们到达了压缩数据的末尾并完成了Y文件。

移至下一个文件,我们从CDH N指向LFH N指针,并找到一个名为Z的文件,其压缩大小为1000字节。 将这1000个字节解释为DEFLATE流,我们立即遇到一个压缩块(再次是核心)并将其解压缩为Z文件,现在我们到达了最终文件的末尾并完成了操作。 输出文件Z包含解压缩的内核; 输出文件Y相同,但可选地,前缀为31个字节LFHN

我们通过重复引用过程来完成构建,直到zip归档文件包含所需数量的文件为止。 每个新文件都会添加一个中央目录头,一个本地文件头和一个未压缩的块,以直接引用下一个本地文件头。 压缩文件数据通常是一串未压缩的DEFLATE块(引用的本地文件头),后跟压缩核心。 内核中的每个字节为输出大小贡献约1032 N ,因为每个字节是所有N个文件的一部分。 输出文件的大小也不同:较早的文件比后的文件大,因为它们对本地文件的标头的引用更多。 输出文件的内容没有多大意义,但是没有人说这应该有道理。

这种重叠引用设计比上一节中的完全重叠设计具有更好的兼容性,但是兼容性是通过压缩实现的。 在那里,每个添加的文件仅花费中央目录的标题,在这里它花费中央目录的标题,本地文件的标题以及引用标题的另外5个字节。

最佳化


收到拉链式炸弹的基本设计后,我们将尝试使其尽可能高效。 我们要回答两个问题:

  • 给定zip文件大小的最大压缩率是多少?
  • 给定zip格式的限制,最大压缩率是多少?

内核压缩


对我们来说,尽可能地压缩内核是有益的,因为每个未压缩的字节都乘以N。 为此,我们使用了一个称为bulk_deflate的自定义DEFLATE压缩器,专门用于压缩重复字节的字符串。

在循环不断的重复字节流中,所有不错的DEFLATE存档程序都接近1032的压缩率,但我们担心其具体大小。 在我们的存档大小中,bulk_deflate比通用存档器保存的数据更多:比zlib和Info-ZIP多约26 KB,比Zopfli多约15 KB,这出于压缩质量的考虑而牺牲了速度。



高压缩比bulk_deflate的价格-缺乏多功能性。 它只能压缩重复字节的行,并且只能压缩一定的长度,即517 + 258 k (整数k≥0)。除了良好的压缩效果外,bulk_deflate还可以快速工作,几乎在同一时间执行工作,而不管输入数据的大小如何,计算实际编写压缩字符串的工作。

档案名称


就我们的目的而言,文件名几乎是无用的。 尽管它们通过成为本地文件的带引号的标头来增加输出大小,但文件名中的字节所占的份额却比内核中的字节要少得多。 我们希望文件名尽可能短,但要有所不同,而又不要忘记兼容性。

花在文件名上的每个字节意味着不花在内核上的两个字节(两个,因为每个文件名出现两次:在中央目录的头和本地文件的头)。 文件名字节平均仅输出( N +1)/ 4字节输出,而内核中的一个字节计为1032N 示例: 1,2,3

兼容性的首要考虑因素是编码。 zip格式规范指出,如果设置了特定的标志位( APPNOTE.TXT,附录D ),则文件名应解释为CP 437UTF-8 。 这是zip解析器之间不兼容的要点,后者可以使用某些固定或特定于区域设置的编码来解释文件名。 因此,出于兼容性考虑,最好将自己限制为在CP 437和UTF-8中使用相同编码的字符。 即,这些是95个可打印的US-ASCII字符。

我们还受到文件系统命名限制的约束。 某些文件系统不区分大小写,因此'a'和'A'不被认为是不同的名称。 普通文件系统(例如FAT32) 禁止使用某些字符 ,例如“ *”和“?”。

作为安全但不一定最佳的折衷方案,我们的zip炸弹将使用36个字符的字母的文件名,其中不包括特殊字符和不同大小写的字符:

  0 1 2 3 4 5 6 7 8 9 ABCDEFGHIJKLMNOPQRSTU VWXYZ 

文件名以明显的方式生成,所有位置依次生成,并在循环末尾添加一个位置:

  “ 0”,“ 1”,“ 2”,...,“ Z”,
 “ 00”,“ 01”,“ 02”,...,“ 0Z”,
 ...
 “ Z0”,“ Z1”,“ Z2”,...,“ ZZ”,
 “ 000”,“ 001”,“ 002”,... 

一共有36个1个字符的文件名,362个2个字符的名,依此类推。 四个字节足以容纳1,727,604个不同的文件名。

鉴于档案中的文件名通常具有不同的长度,我该如何更好地对它们进行排序:从最短到最长,反之亦然? 如果您稍作考虑,最好将最长的名字放在最后。 与从最长到最短的排序相比,此排序向zblg.zip添加了900 MB以上的输出。 但是,这是次要的优化,因为900 MB仅占问题总大小的0,0003%。

核心尺寸


重叠的报价设计使您可以放置​​压缩的数据核,然后廉价地多次复制它。 对于zip文件X的特定大小,分配存储内核的最佳空间是多少,创建副本的空间是多少?

为了找到最佳平衡,您只需要优化一个变量N ,即zip归档文件中的文件数。 每个N值都需要一定数量的开销,用于中央目录的标题,本地文件的标题,引用块的标题和文件名。 其余空间将被核心占用。 由于N必须是整数,并且您只能在内核大小降至零之前放入一定数量的文件,因此检查N的每个可能值并选择输出最大的值就足够了。

将优化过程应用于42.zip的X = 42,374,可以找到N = 250的最大值。这250个文件需要21,195字节的开销,剩下21,179字节用于内核。 如此大小的内核被解压缩为21,841,249字节(1031.3与1的比率)。 250个解压缩内核的副本以及一些引用的本地文件头给出的总解压缩输出为5,461,307,620字节,压缩比为129,000。

  zipbomb --mode = quoted_overlap --num-files = 250 --compressed-size = 21179> zbsm.zip 

优化导致内核和文件头之间的空间几乎均匀分布。 这不是巧合。 考虑具有重叠的引文结构的简化模型。 在简化模型中,我们忽略文件名,并且由于引用本地文件的标头而导致输出文件的大小略有增加。 对简化模型的分析将显示内核与文件头之间的最佳分布是近似均匀的,具有最佳分布的输出大小将根据输入的大小成平方增长。

一些常量和变量的定义:

Xzip文件大小(已考虑固定)
ñzip存档中的文件数(为优化而可变)
Cdh= 46中央目录标头大小(无文件名)
h= 30本地文件头大小(无文件名)
= 5DEFLATE块未压缩的大小
ç≈1032内核压缩率

H(N)N个文件的标头的开销量。 请参阅图表以了解公式的本质。

HN=NCDH+LFH+N1Q


对于内核,保留X-H(N)个位置。 解压缩后的总大小S X (N)等于以比率C解压缩后的N个内核副本的大小(在此简化模型中,我们忽略了所提到的本地文件头的稍微扩展)。

$$显示$$ S_X(N)=(X-H(N))CN \\ =(X-(N⋅(CDH + LFH)+(N-1)⋅Q))CN \\ =-(CDH + LFH + Q)CN ^ 2 +(X + Q)CN $$显示$$


S X (N)是N部分的多项式,因此,最大值应为导数S'X(N)等于零的地方。 取导数并找到零会给我们N OPT ,即最佳文件数。

$$ display $$ S'X(N_ {OPT})= −2(CDH + LFH + Q)C N_ {OPT} +(X + Q)C \\ 0 = −2(CDH + LFH + Q)C N_ {OPT} +(X + Q)C \\ N_ {OPT} =(X + Q)/(CDH + LFH + Q)/ 2 $$显示$$


H(N OPT提供用于放置文件头的最佳空间。 它独立于CDH,LFH和C,并且接近X / 2

$$显示$$ H(N_ {OPT})= N_ {OPT}⋅(CDH + LFH)+(N_ {OPT}-1)⋅Q \\ =(X-Q)/ 2 $$显示$$


S X (N OPT )-最佳分布下的总未包装尺寸。 由此可见,输出的大小随输入数据的增加而平方增加。

SXNOPT=X+Q2C/CDH+LFH+Q/4


增加zip文件的大小,最后我们将遇到zip格式的限制:归档文件最多只能包含2 16 -1个文件,每个文件的大小不超过2 32 -1个字节。 更糟糕的是, 某些实现将最大值作为存在64位扩展的指标,因此我们的限制实际上是2 16 -2和2 32 -2。 碰巧,我们第一次遇到未压缩文件大小的限制。 zip文件大小为8 319 377字节时,天真优化将为我们提供文件数量47 837,最大文件数量为2 32 +311字节。

(实际上,一切都有些复杂,因为确切的限制取决于实现。Pythonzipfile 忽略文件数量,Go中的archive / zip 允许增加文件的数量,直到它们匹配较低的16位。但是为了通用兼容性,我们必须遵守确定的限制)。

如果我们不能无限增加N或内核大小,我们想在zip格式中找到最大压缩率。 您需要使用最大数量的文件使内核尽可能大。 尽管事实上我们不再能够在内核和文件头之间保持近似均匀的分隔,但是每个添加的文件仍会提高压缩率-就像不能继续增加内核一样快。 实际上,在添加文件时,我们将需要减小内核大小以释放最大文件大小的空间,该最大大小随添加的每个文件而略有增加。

该计划将导致一个包含2 16 −2文件和一个内核的zip归档文件,该文件解压缩为2 32 −2 178 825字节。 文件将在zip存档的开始处变得更大-第一个也是最大的文件将以2 32 −56字节解压缩。 这与我们可以使用bulk_deflate的粗略输出参数非常接近-编码最后的54个字节将比其大小花费更多(整个zip文件的压缩比为2800万,最后的54个字节将获得最多54⋅10 32⋅ 2 16-2 )≈36?5百万个字节,因此这仅在54个字节可以编码为一个字节时有用-我不能用少于2个编码,因此,如果不能将54个字节编码为1个字节,只会降低压缩率)。 该拉链炸弹的输出大小为281,395,456,244,934字节,是理论最大值(2 32-1)×(2 16-1)的99.97%。 只能通过减小输入信号的大小而不是通过增加输出来实现压缩率的任何显着提高。

  zipbomb --mode = quoted_overlap --num-files = 65534 --max-uncompressed-size = 4292788525> zblg.zip 

高效的CRC-32计算


中央目录的标头和本地文件的标头中的元数据中,包含未压缩文件数据CRC-32的校验和 。 这带来了一个问题,因为每个文件的CRC-32计算量与文件大小成正比,默认情况下该文件的大小非常大(毕竟,这是个拉链炸弹)。 我们希望所做的工作至少与档案的大小成正比。 有两个因素对我们有利:所有文件都有一个公共核心,而未压缩的内核是一串重复的字节。 想象一下CRC-32作为矩阵产品-这将使我们不仅可以快速计算内核的校验和,而且可以重用文件之间的计算。 本节描述的方法是zlib中crc32_combine函数的一个小扩展,Mark Adler 在此进行了解释。

可以将CRC-32建模为状态机,为每个输入位更新一个32位状态寄存器。 位0和位1的基本更新操作是:

 uint32 crc32_update_0(uint32 state) { // Shift out the least significant bit. bit b = state & 1; state = state >> 1; // If the shifted-out bit was 1, XOR with the CRC-32 constant. if (b == 1) state = state ^ 0xedb88320; return state; } uint32 crc32_update_1(uint32 state) { // Do as for a 0 bit, then XOR with the CRC-32 constant. return crc32_update_0(state) ^ 0xedb88320; } 

如果将状态寄存器表示为32个元素的二进制向量,并使用XOR进行加法和乘法,那么crc32_update_0线性映射 ; 即可以将其表示为32 * 32二进制转换矩阵的乘法。 要理解为什么,请注意,将矩阵乘以向量只是在将每一列乘以向量的对应元素之后,简单地将矩阵的列相加。 移位操作state >> 1只是简单地获取状态向量的每个比特i ,然后将其乘以一个除比特i -1以外的其他任何地方都为零的向量(从右到左对比特进行编号)。 相对而言,仅当位b等于1时, state ^ 0xedb88320发生最终的XOR state ^ 0xedb88320 。 这可以表示为b与0xedb88320的第一个乘法,然后与该状态进行XOR运算。

此外, crc32_update_1只是一个crc32_update_0加(XOR)常量。进行crc32_update_1 仿射变换:矩阵相乘,然后进行映射(即矢量加法)。如果将变换矩阵的大小增加到33×33,并向状态向量添加一个始终为1的元素(此表示称为齐次坐标,我们可以一步一步想象矩阵乘法和映射


转换矩阵为33×33 M 0和M 1,它们分别计算由位0和1引起的CRC-32状态改变。列向量的最高有效位存储在下面:从下至上读取第一列,您会看到多项式常数CRC-32 edb8832016 = 111 0 11 0 110 111 000 1 00000 11 00 1 00000 2。这两个矩阵仅在最后一列中有所不同,该列以同构坐标表示转换向量。在M 0中,平移为零,在M 1中为edb88320 16,多项式常数为CRC-32。单元立即如上操作的对角状态state >> 1

这两种操作crc32_update_0crc32_update_1可以通过33×33转移矩阵来表示。显示了矩阵M 0和M 1矩阵表示的优点是矩阵可以相乘。假设我们希望看到通过处理二进制表示为01100001 2的ASCII字符'a'引起的状态更改我们可以想象一个转换矩阵中这8位的CRC-32状态的累积变化:

M a = M 0 M 1 M 1 M 0 M 0 M 0 M 0 M 1


我们可以想象通过乘以M a的许多副本来重复“ a”的行状态的变化-将矩阵提升为幂。我们可以做到这一点很快使用快速指数算法,它可以计算将M ñ只要登录2个 ñ步骤。例如,这是一个用于更改9个字符'a'的字符串状态的矩阵:

M a 9 = M a M a M a M a M a M a M a M a M a M a M a M a=(MaMaMaMa)2Ma=((MaMa)2)2Ma=(((Ma)2)2)2Ma


矩阵快速乘法算法可用于计算M 内核(未压缩内核的矩阵),因为该内核是重复字节的字符串。要从矩阵中获取CRC-32校验和,请将矩阵乘以零向量(零向量在统一坐标中,即32个零,然后是单位;这里我们省略了校验和的预处理和后处理以检查一致性的轻微复杂性)。为了计算每个文件的校验和,我们的工作方向相反。我们从初始化M:= M kernel开始。内核校验和也是最终文件N的校验和,因此我们乘以M零矢量和存储在CDH接收的校验Ñ和LFH Ñ数据文件N-1中的相同文件中的数据文件Ñ,但具有添加前缀LFH Ñ因此,我们计算M L F H N,LFHN的状态变化矩阵和更新M = M M L F H N现在,M表示从处理原子核后面的LFH N起的累积状态变化我们计算文件N -1的校验和,再次将M乘以零向量。我们通过在M中累积状态更改矩阵来继续该过程,直到处理所有文件为止。

扩展名:Zip64


早些时候,由于zip格式的局限性,我们遇到了扩展问题-无论zip文件的打包方式如何,都无法发行超过281 TB的文件。您可以使用Zip64超越这些限制,是一种zip格式扩展名,可将某些标头字段的大小增加到64位。对Zip64的支持绝不是通用的,但它是最常用的扩展之一。至于压缩率,Zip64的作用是将中央目录的标头的大小从46个字节增加到58个字节,而本地目录的标头的大小从30个字节增加到50个字节。查看简化模型中的最佳膨胀系数公式,我们可以看到Zip64格式的拉链炸弹仍会二次增长,但由于分母较大而速度较慢-这可以在下图中看到。由于失去兼容性和增长延迟,我们几乎消除了文件大小的所有限制。

假设我们需要一个压缩炸弹,可以扩展到4.5 PB,例如42.zip。档案应该多大?使用二进制搜索,我们发现此类文件的最小大小为46 MB。

  • zbxl.zip 46 MB→4.5 PB(Zip64,不太兼容)
 zipbomb --mode = quoted_overlap --num-files = 190023 --compressed-size = 22982788 --zip64> zbxl.zip 

4.5 PB-事件地平线望远镜记录的黑洞第一张图像的数据量大致相同:机架和数据中心中带有硬盘的机架。

使用Zip64,考虑最大压缩率几乎没有意思,因为我们可以简单地继续增加zip文件的大小和压缩率,直到压缩的zip文件变得令人望而却步为止。但是,一个有趣的阈值是2 64个字节(18 EB或16 EiB)-因此,大多数文件系统上无法容纳太多数据。二进制搜索找到了最小的拉链炸弹,该炸弹可产生至少相同的输出:它包含1200万个文件和1.5 GB的压缩核心。 zip文件总大小为2.9 GB,并在2 64中解压缩+11 727 895 877字节,压缩比超过62亿:1。我没有上载此文件以供下载,但是您可以使用源代码自己生成它他的文件大小如此之大,甚至在Info-ZIP UnZip 6.0中也发现一个错误

 zipbomb --mode = quoted_overlap --num-files = 12056313 --compressed-size = 1482284040 --zip64> zbxxl.zip 

扩展名:bzip2


DEFLATE是zip格式中最常见的压缩算法,但这只是许多选项之一。第二种最常见的算法可能是bzip2。尽管不如DEFLATE兼容。从理论上讲,在bzip2中,最大压缩率约为140万比1,这可以使内核更密集地堆积。

bzip2首先对“游程长度编码”进行编码,从而将重复字节的字符串长度减少了51倍。然后,将数据划分为900 KB的块,并分别压缩每个块。从理论上讲,一个块最多可以压缩32个字节。 900 000×51/32 = 1 434375。

忽略兼容性的损失,bzip2是否能制造出更有效的炸弹?

是的-但仅适用于小文件。问题在于,在bzip2中,没有什么像我们用来引用本地文件头的未压缩的DEFLATE块一样。因此,不可能重叠文件并重用内核-对于每个文件,您都需要编写自己的副本,因此总体压缩率不会比任何单个文件的压缩率都要好。在下图中,我们看到不重叠,bzip2仅对大小约为兆字节的文件优于DEFLATE。

仅有希望在bzip2中引用标头的替代方法,这将在下一节中讨论。此外,如果您知道特定的zip解析器支持bzip2 如果允许使用不匹配的文件名,则可以使用完整的重叠结构,该结构不需要加引号。


比较不同拉链炸弹的压缩率。注意对数轴。显示的每个设计都有和没有Zip64。从轴的恒定比率可以看出,没有重叠的结构具有线性增长率。 bzip2图的垂直偏移意味着bzip2的压缩率大约是DEFLATE的压缩率的一千倍。所引用的DEFLATE构建体具有二次方增长率,这是通过倾斜2:1轴来证明的。 Zip64变体的效果稍差,但可容纳超过281 TB。当达到最大文件大小时,带有附加字段引用的bzip2图形从二次变为线性(2 32 −2字节),或允许的最大文件数

扩展:引用其他字段


到目前为止,我们已经使用DEFLATE函数来引用本地文件的标头,并且我们只看到此技巧在bzip2中不起作用。但是,还有一种替代性的引用方法,在某种程度上受到更多限制,该方法仅使用zip格式函数并且与压缩算法无关。

在本地文件的标头结构的末尾,有一个可变长度的附加字段,用于存储不适合通常的标头字段的信息(APPNOTE.TXT,第4.3.7节)其他信息可能包括,例如,Unix上的时间戳或uid / gid。Zip64信息也存储在其他字段中。另一个字段表示为长度值结构;如果您增加长度而未添加值,则附加字段将包括zip文件中其后面的内容,即本地文件的下一个标头。使用此方法,本地文件的每个标头都可以“引用”以下标头,并将它们包含在其自己的附加字段中。与DEFLATE相比,有三个优点:

  1. 引用一个额外的字段仅需要4个字节,而不是5个字节,从而为内核留出更多空间。
  2. 考虑到zip格式的限制,它不会增加文件大小,这意味着内核更大。
  3. 它在bzip2中提供了报价。

尽管有这些优点,但通过其他字段进行引用的灵活性较差。这不是一个链,就像在DEFLATE中一样:本地文件的每个标题不仅应包含紧随其后的标题,而且还应包含所有后续标题。当您接近zip文件的开头时,其他字段会增加。由于最大字段长度为2 16 -1个字节,因此假设名称按预期分配,最多只能引用1808个本地文件头(或Zip64中为1170)。(在DEFLATE的情况下,您可以使用其他字段来引用本地文件的第一个(最短)标头,然后将其切换为用DEFLATE引用)。另一个问题:为了与附加字段的内部数据结构相对应,有必要为引文数据之前的类型(APPNOTE.TXT,第4.5.2节选择一个16位标记。我们想要选择一个类型标记,它将导致解析器忽略引号中的数据,而不是尝试将它们解释为有意义的元数据。 Zip解析器应忽略未知类型的标签,因此我们可以随机选择标签,但存在将来某些标签会违反设计兼容性的风险。

上图说明了在bzip2,c中使用其他字段且不使用Zip64的可能性。在两个图表上都有一个转折点,当增长从二次增长变为线性增长时。如果没有Zip64,则会在未压缩文件达到最大大小的情况下发生这种情况(2 32−2个字节); 那么您只能增加文件数量,而不能增加文件大小。当文件数达到1809时,图形完全结束,然后我们用完了额外字段中的空间来引用其他标头。使用Zip64,1171个文件出现断裂,之后只能增加文件大小,而不能增加文件数量。在DEFLATE的情况下,附加字段会有所帮助,但是差异是如此之小,以至于在视觉上无法察觉。它将zbsm.zip的压缩率提高了1.2%;zblg.zip降低了0.019%;和zbxl.zip缩小0.0025%。

讨论区


在有关此主题的工作中,Pletz和同事使用文件重叠来创建几乎可以自我复制的zip存档。Ginvael Coldwind 早些时候建议文件重叠本身(幻灯片47)。

考虑到兼容性,我们开发了一种带引号重叠的zip炸弹的设计-实现方式方面存在许多差异,其中一些差异显示在下表中。最终的设计与以通常方式工作的zip解析器兼容,即首先检查中央目录并将其用作文件索引。其中,来自Nail的独特zip解析器它是根据形式语法自动生成的。但是,该设计与“流”解析器不兼容,后者无需首先读取中央目录就可以一开始分析zip文件,从头到尾进行分析。从本质上讲,流解析器不允许文件以任何方式重叠。他们很可能只会提取第一个文件。此外,它们甚至可能会引发错误,如sunzip那样,后者会解析末尾的中央目录,并检查与其已经看到的本地文件头的一致性。

如果希望提取的文件以与本地文件的标头字节不同的特定前缀开头,则可以在以下标头引用的未压缩块之前插入DEFLATE块。并非zip归档文件中的每个文件都应参与炸弹的创建:如有必要,您可以在归档文件中包含普通文件,以便与某些特殊格式相对应(--template此用例的源代码中有一个参数)。许多格式都使用zip作为容器,例如Java JAR文档,Android APK和LibreOffice。

聚甲醛在许多方面类似于zip。它在文件末尾具有一个指向以前对象的交叉引用表,并且它支持通过FlateDecode过滤器压缩对象。我没有尝试过,但是您可以使用重叠引用的想法制作PDF炸弹。也许您甚至不需要在这里努力工作:binaryhax0r 在博客文章写道,您只需在一个对象上指定多个FlateDecode层,然后创建PDF炸弹就变得微不足道了。

定义本文描述的一类特殊的拉链炸弹很容易:只需找到重叠的文件即可。马克·阿德勒(Mark Adler)撰写了补丁for Unzip Info-ZIP,就是这样做的。但是,通常,阻止重叠文件并不能防止所有类型的zip炸弹。如果您不了解将用于分析文件的解析器内部组件的确切知识,则很难预先预测文件是否为zip bomb。查看标头并对所有文件的“未压缩大小”字段求和是不起作用的,因为标头中的值可能与实际的未压缩大小不匹配(在兼容性表中,请参见“允许文件太小”行)。对zip炸弹的可靠保护包括在zip解析器运行期间限制时间,内存和磁盘空间。仔细解析zip文件,就像处理任何不受信任数据的复杂操作一样。


zip-, , zip-. DEFLATE Zip64, , CRC 32- 16- .

致谢


感谢Mark AdlerRuss CoxBrandon EnrightMarek MaykovskyJosh WolfeUSENIX WOOT 2019的审稿人对本文草案的评论。考兰·麦克纳马拉(Kaolan McNamara)评估了简易炸弹对LibreOffice安全的影响。

本文的版本已为USENIX WOOT 2019研讨会准备源代码可用。在研讨会上演示的工件zipbomb-woot19.zip文件中

您是否找到了投掷其中一颗炸弹的系统?炸弹是否可以帮助您发现错误或在错误搜索程序中赚钱?让我知道,我会在这里尝试提及。

LibreOffice 6.1.5.2


将zblg.zip重命名为zblg.odt或zblg.docx之后,LibreOffice会创建并删除一系列大约4 GB的临时文件,以尝试确定文件格式。最终,他完成了此操作,并在到达临时文件时将其删除,因此zip炸弹只会导致临时DoS而不会填满磁盘。Kaolan McNamara回答了我的错误消息。

Mozilla附加组件服务器2019.06.06


我尝试对本地addons-server安装使用zip炸弹,这是addons.mozilla.org软件的一部分。该系统可正常处理炸弹,提取文件的时间限制为110秒。压缩炸弹会迅速扩展,直到磁盘允许达到此时间限制为止,但是此过程将被终止,最终解压缩的文件将自动清除。

解压缩6.0


马克·阿德勒(Mark Adler)为UnZip编写了一个补丁,以检测此类拉链炸弹。

2019年7月5日:我注意到CVE-2019-13232已分配给UnZip。我个人认为,UnZip(或任何zip解析器)处理这种zip炸弹的能力/无能必然是一个漏洞,甚至是一个bug。这是不违反规范的自然实现,我可以说什么。本文中的炸弹类型只是一种类型,还有许多其他方法可以使zip解析器困惑。如上所述,如果要保护自己免受资源耗尽攻击的侵害,则不应尝试列出,检测和阻止每一个已知的攻击。相反,有必要建立对时间和其他资源的外部限制,以使解析器无论遇到什么类型的攻击都不会陷入这种情况。尝试检测和拒绝某些设计作为第一遍的优化没有错,但你不能停在那里。除非最终以不受信任的数据隔离和限制操作,否则您的系统可能仍然容易受到攻击。考虑与HTML中的跨站点脚本的类比:正确的保护不是要尝试过滤掉可以解释为代码的特定字节,而是要正确地避免一切。

防毒引擎


Twitter用户@TVqQAAMAAAAEAAA 报告:“我的测试机上的McAfee防病毒软件刚刚爆炸。” 我自己尚未对其进行测试,并且没有诸如版本号之类的详细信息。

塔维斯奥曼迪表示VirusTotal为zblg.zip有许多超时(从2019年6月6日的屏幕截图):安博士-V3,ClamAV的, DrWeb,残局,F-安全,的GData,K7AntiVirus,K7GW,MaxSecure,迈克菲,迈克菲-GW -版本,熊猫,奇虎360,Sophos ML,VBA32。zbsm.zip的结果2019年6月6日的屏幕截图)相似,但具有不同的超时引擎集:Baido,Bkav,ClamAV,CMC,DrWeb,Endgame,ESET-NOD32,F-Secure,GData,Kingsoft,McAfee-GW-Edition,NANO-Antivirus和Acronis。有趣的是,zbxl.zip结果中没有超时(2019年6月6日的屏幕截图)。也许某些防病毒软件不支持Zip64?几个引擎将文件检测为一种“压缩炸弹”。有趣的是,他们是否会继续进行较小的更改,例如更改文件名或向每个文件添加ASCII前缀。

最终声明


现在是时候结束Facebook了。这对您来说不是一项中立的工作:每天上班时,您在做错事。如果您拥有Facebook帐户,请将其删除。如果您在Facebook上工作,请下岗。

而且不要忘记必须销毁国家安全局。

Source: https://habr.com/ru/post/zh-CN459254/


All Articles