HackThisSite - Programming Mission 5
Challenge
Level 5 — Fix a corrupted file
Get this file HERE, reconstruct it and send the password as answer.
有人用 Windows 命令行 ftp 客户端下载了一个 bz2 压缩的 PNG,PNG 里有一个重要密码;他漏考虑了一件事,文件因此损坏。下载
corrupted.png.bz2,把它修好并提交里面的密码。限时 600 秒。
Solution
- 实例页
https://www.hackthissite.org/missions/prog/5/,文件链接是/missions/prog/5/corrupted.png.bz2/。 - 文件每个实例都不一样(两次抓到的分别是 13075 / 13961 / 15232 字节,内容与密码都不同),所以必须取文件 → 修复 → 读密码 → 提交在一次 600 秒窗口内完成。
file报bzip2 compressed data,但bzip2 -t失败 → 这就是题面说的损坏。
1 | $ curl -sL -b '<mission-cookie>' 'https://www.hackthissite.org/missions/prog/5/corrupted.png.bz2/' -o corrupted.png.bz2 |
题面提示用 Windows ftp 客户端下载、漏考虑的一件事,是没有切换到
binary 模式。FTP 的 ASCII type
面向文本传输,会在本地字符表示与标准 NVT-ASCII
表示之间转换,并按平台约定处理行尾;Windows 文档将它与按 one-byte units
传输的 binary type 区分。对本题的跨 Unix/Windows
路径,原始压缩流中作为 bare LF 的 0x0A
被当作行尾并扩展为 CRLF,也就是插入
0x0D。压缩流没有行结构,0x0A
只是普通数据字节,插入一个字节会改变后续 bzip2
bitstream,进而使校验与解压失效。这里的 0x0A → 0x0D 0x0A
是本题具体传输路径的结果,实际转换还取决于发送端、接收端和平台的行尾处理规则;本题的
CR/LF 统计与后续 PNG CRC 对照支持这一判断。
证据就在字节统计里:
1 | $ python3 -c "d=open('corrupted.png.bz2','rb').read(); print(len(d), d.count(b'\r'), d.count(b'\n'), d.count(b'\r\n'))" |
\r比\n多了 58 个 → 就是被插入的 CR;- 文件里
\r\r\n出现 0 次 → 说明原本就是\r\n的位置没有被二次膨胀,插入只发生在原来的0x0A前; - 另外还有 2 处
\n\r(例如... 0d 0a 0d ...),说明原始位流里确实有少量天然的\r\n相邻(转换后转换结果呈现为 CRLF,但其中的 CR 是原始数据)。
所以修复不是简单 dos2unix:要决定哪些 CRLF 里的
CR
是插入的、哪些是原始数据。只删掉一部分、猜错一个位置,解出来的即为无意义的垃圾数据。
观测到的 CRLF 对很少(50-60 个),而天然 CRLF 通常只有 0-2 个。于是直接枚举保留子集,用解压结果是否以 PNG 魔数开头来判定:
1 | import bz2 |
真实的一次运行:
1 | [1] downloaded 15232 bytes |
k=0(全删)和 k=1 都不行,k=2
在枚举到第 1398 个组合时命中(即这次的两个 CR 属于原始数据)。恢复出的
PNG 还能过 chunk CRC 校验,说明重建完全正确。
PNG 是 1750×115 的横条,密码用粗体无衬线字体画在浅色背景上。
注意:密码是每个实例一次生成的(同一账号换一次页面就是另一串)。
- 不能用
replace(b'\r\n', b'\n'):相邻 CRLF 会互相影响,必须逐对扫描决定保留哪一个 CR。
Vulnerabilities
这道题的损坏是经典的真实世界传输事故:文本模式的 FTP
会把二进制文件当文本处理,静默插入
0x0D。防御上:二进制传输永远走 binary
模式(ftp 里 bin,或直接
scp/curl),下载后校验哈希/魔数;对压缩包这类强结构数据,还应当用内容特征(魔数、CRC)而不是命令退出码来判断完整性。本例中解压器在数据已损坏时仍会输出字节流,只看退出码会误判。另外,密码以明文绘制在图片里再压缩交付,本身就是把答案交给客户端的做法。