HackThisSite - Programming Mission 5
Challenge
Level 5 — Fix a corrupted file
有人用 Windows 命令行 ftp 客户端下载了一个 bz2 压缩的 PNG,PNG 里有一个重要密码;他漏考虑了一件事,文件因此损坏。下载
corrupted.png.bz2,把它修好并提交里面的密码。限时 600 秒。Get this file HERE, reconstruct it and send the password as answer.
状态:verified(服务端返回
Good Job, ***, You have successfully completed this mission)。
Solution
- 实例页
https://www.hackthissite.org/missions/prog/5/,文件链接是/missions/prog/5/corrupted.png.bz2。链接必须补尾斜杠(/corrupted.png.bz2/)才是真实资源,少一个斜杠会 301 到一个空 body(下载到 0 字节)。 - 文件每个实例都不一样(两次抓到的分别是 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
模式:ASCII 模式会把流里的每个 0x0A 换成
0x0D 0x0A。压缩数据是近乎随机的字节流,被这样插字节之后,整个
bz2 位流就错位了。
证据就在字节统计里:
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 的横条,密码用粗体无衬线字体画在浅色背景上。读取分两步:按列切分字形(同一行文字,字符之间有空列;这里不能把 1 像素的小缝隙合并,否则两个字符会粘成一个、10 个字符读成 9 个,后面全错位),再逐字形分类。
分类用参考字体渲染 + 高度类约束 + 位图 IoU,关键是把相对基线的高度归一化到 x-height 再比较(否则数字/升部字母与 x-height 字母完全比不上):
1 | def classify(glyph_bitmap, refs, top, bot, xh): |
低于基线、只有几个像素高的横条直接判成下划线(之前的尝试把它当成
m/w,因为字母表里没有 _)。
自动读出的结果另用字形位图人工复核一次:把每个字形打成
ASCII 点阵(# = 有墨),数字与字母的形状即可辨识:
1 | for i, (s, e) in enumerate(segments(mask), 1): |
一次真实的字形点阵(8):
1 | ..##.. |
十个字形依次读出
8 j q 9 s l 1 p 8 h(j、q、p
的降部都落在基线以下,与点阵里的长度一致),提交后服务端接受:
1 | [6] submitted '8jq9sl1p8h' -> accepted=True |
注意:密码是每个实例一次生成的(同一账号换一次页面就是另一串),所以 spoiler 里给的是本次被接受的实例值;重做时按上面流程现算现交即可。
- 不能用
replace(b'\r\n', b'\n'):相邻 CRLF 会互相影响,必须逐对扫描决定保留哪一个 CR。 - 修复判据要用内容而不是解压不报错:删错 CR 的版本依然能解出 15000-20000 字节数据(bz2 的 Huffman 解码会成功),只有 PNG 魔数能证明修对了。
Vulnerabilities
这道题的损坏是经典的真实世界传输事故:文本模式的 FTP
会把二进制文件当文本处理,静默插入
0x0D。防御上:二进制传输永远走 binary
模式(ftp 里 bin,或直接
scp/curl),下载后校验哈希/魔数;对压缩包这类强结构数据,还应当用内容特征(魔数、CRC)而不是命令退出码来判断完整性。本例中解压器在数据已损坏时仍会输出字节流,只看退出码会误判。另外,密码以明文绘制在图片里再压缩交付,本身就是把答案交给客户端的做法。