伪造JPEG
背景
我一直在讨论Spigot,这是一个小型Web应用程序,它使用马尔可夫链动态生成虚假的网页层次结构,为激进的网络爬虫提供无意义的内容。Spigot已经运行了几个月,每天提供超过一百万页的服务。我并没有密切关注它的活动,但偶尔会查看日志以了解哪些爬虫在访问它。
遗憾的是,两个最活跃的爬虫极力隐藏其身份,生成随机且不太可能的浏览器签名(例如,在Windows 98上运行Firefox 134.0版本,64位),并从随机地址访问。这很可能是通过僵尸网络完成的——非法滥用数千人的设备。唉。
识别新爬虫
几周前,我注意到了一个名为“ImageSiftBot”的新活跃爬虫。尽管Spigot的输出不包含图像,但ImageSiftBot每小时以数千次请求疯狂访问,拼命寻找图像来摄取。我对它无果的搜索感到同情,并开始思考如何满足它。
挑战与解决方案
Spigot的主要目标是低CPU消耗。动态生成图像在CPU负载方面并非易事。如果要创建爬虫相信的像素数据,几乎必须提供压缩数据,而动态压缩是CPU密集型的。这对Spigot不利,且生成一次性垃圾完全是浪费。
我想到:压缩往往会增加比特流的熵。如果文件内容看起来不随机,则可压缩,而最优压缩的数据几乎与随机数据无法区分。JPEG压缩得很好,因此JPEG中的压缩数据看起来是随机的,对吧?
如果有一个JPEG文件模板,包含“结构化”部分(大小、颜色深度等信息)和标记指示高度压缩数据的位置,我可以通过用随机数据填充“压缩”区域来构造类似JPEG的东西。这是一个非常低CPU的操作。接收方会看到类似JPEG的内容,并将随机数据视为可解压缩的内容。
JPEG结构分析
我研究了一下JPEG文件的结构,发现它们可能很复杂,但这并不重要。JPEG文件由块组成。每个块有一个标记和长度(有时隐式为零,有时仅通过读取块内容并寻找下一个标记来确定)。因此,解析JPEG相对简单。我有大量JPEG文件。所以:如果我扫描一批现有文件,丢弃“注释”块,仅记录“像素数据”块的长度并保留其余部分,结果会多大?
目前,我的网站上有514个JPEG,总计约150MB。如果扫描所有文件,仅保留“结构化”块并记录“像素”块长度,生成的数据集不到500KB——微不足道。这给了我514个各种大小、颜色深度等的现实模板。
生成JPEG可以简化为:
|
|
实现与优化
我编写了一些测试代码,发现并不那么简单。真实的像素数据并不完全随机——它是霍夫曼编码的,有一定的结构。如果用纯随机数据填充像素块,解码器会注意到数据不正确的地方。我相信比我更有头脑/时间/意愿的人能够解析模板中的其他块,以确定可以在像素块中插入的确切霍夫曼代码,而无需实际进行压缩。
但我在此止步了。因为……我尝试的每个JPEG查看器都接受我的垃圾数据并显示图像。尽管解码器注意到问题,它仍然发出像素数据。这可能足以给网络爬虫带来不便。我打赌它们大多数不关心错误,只要不导致图像损坏。即使它们关心错误,也必须抓取数据并尝试解码才能知道它是坏的。这会增加它们的成本,这对我来说很好。
本文顶部的图像是一个示例,由代码动态生成。尽管是有缺陷的JPEG,您的浏览器可能会显示它。
性能评估
回到效率:我能多快生成这些垃圾图像?如前所述,我使用基于网站图像的模板。我通常为Web优化图像,导致JPEG具有各种大小,但大多 around 1280x960像素和200-300KB。快速测试显示,使用此方法(在Python中),我的Web服务器每秒可以生成约900张这样的图像。这大约是190MB/秒,远快于我的Web服务器与互联网的连接。很好!
集成与效果
我将上述内容集成到Spigot中,现在约60%的Spigot生成页面将包含垃圾JPEG。与Spigot一样,我为图像的随机数生成器提供源自URL的值。因此,尽管上述图像是动态生成的,但如果您重新加载,将获得相同的图像。
ImageSiftBot对此非常满意,今天抓取了约15,000张垃圾图像。我预计随着它找到更多链接,其速率将在未来几天内增加。Meta的bot、AmazonBot和GPTBot也变得兴奋!
代码发布与改进
我需要整理执行此操作的Python类,但将在适当时候发布它。代码不到100行(但可能需要更多注释!)。
[2025-03-26] 现已在GitHub上发布
[2025-03-28] 经过对霍夫曼代码的深入思考,我添加了一个针对生成像素数据的位掩码。将每个生成的字节与0x6D进行“与”操作,确保比特流中不会出现三个或更多连续1的字符串。这大大降低了生成具有无效霍夫曼代码的JPEG的概率(从>90%降至<4%),而无需更多CPU。重点是以最低成本生成垃圾,并为滥用网络爬虫带来最高成本。检查JPEG如何使用霍夫曼代码后,生成完全有效的霍夫曼流并不特别困难,但会消耗更多CPU,收益甚微。