Challenge
Steganography
The only hint you get is
The LaughingMan. 唯一提示是The LaughingMan。题目提供一张 BMP 图片,密码藏在文件结构中。资源地址是
https://www.hackthissite.org/missions/stego/lvl/13.bmp。
Solution
Analysis
先确认 BMP 的声明信息和实际文件长度:
$ file 13.bmp13.bmp: PC bitmap, Windows 3.x format, 138 x 34 x 24, image size 14144, cbSize 14198, bits offset 54再解析 BMP 文件头。24bpp、宽度 138 的行长度按 4 字节对齐后为 416 字节:
file length = 21002bfSize = 14198pixel offset = 54width × height = 138 × 34bits per pixel = 24row bytes = 416pixel bytes = 416 × 34 = 14144expected end = 54 + 14144 = 14198文件头声明的合法结束位置是 14198,但实际文件有 21002 字节;多出的 6804 字节并不是简单地集中在文件尾部,而是混入了文件流。
对整个文件搜索题目中反复出现的 ASCII 文本:
from pathlib import Path
raw = Path("13.bmp").read_bytes()junk = b"I thought what I'd do was, I'd pretend I was one of those deaf-mutes"
positions = []start = 0while True: pos = raw.find(junk, start) if pos < 0: break positions.append(pos) start = pos + 1
print("file length:", len(raw))print("junk length:", len(junk))print("junk occurrences:", len(positions))print("junk bytes:", len(junk) * len(positions))file length: 21002junk length: 69junk occurrences: 56junk bytes: 3864这里的字符串中,I'd 和 pretend 之间有两个空格;少一个空格就无法匹配实际数据。除了这 56 段文本,垃圾块后面还跟着一批 0x00 字节:
extra bytes = 21002 - 14198 = 6804repeated strings = 56 × 69 = 3864zero padding = 6804 - 3864 = 2940因此只删除 ASCII 文本还不够,残留的零填充仍会让像素行错位。清理逻辑是:遇到完整的垃圾字符串时跳过它,然后继续跳过紧随其后的 0x00 字节;其他字节按原顺序保留。
Step 1: 重建合法 BMP
#!/usr/bin/env python3import structfrom pathlib import Path
source = Path("13.bmp").read_bytes()junk = b"I thought what I'd do was, I'd pretend I was one of those deaf-mutes"
clean = bytearray()pos = 0junk_count = 0zero_count = 0
while pos < len(source): if source.startswith(junk, pos): junk_count += 1 pos += len(junk) while pos < len(source) and source[pos] == 0: zero_count += 1 pos += 1 continue15 collapsed lines
clean.append(source[pos]) pos += 1
clean = bytes(clean)Path("13_clean.bmp").write_bytes(clean)
_, declared_size, _, _, pixel_offset = struct.unpack_from( "<2sIHHI", clean, 0)_, width, height, _, bpp, _, image_size = struct.unpack_from( "<IiiHHII", clean, 14)row_bytes = ((width * (bpp // 8) + 3) // 4) * 4expected_size = pixel_offset + row_bytes * abs(height)
print("junk blocks:", junk_count)print("zero padding:", zero_count)print("clean length:", len(clean))print("declared size:", declared_size)print("expected size:", expected_size)print("image size:", image_size)assert len(clean) == declared_size == expected_sizejunk blocks: 56zero padding: 2940clean length: 14198declared size: 14198expected size: 14198image size: 14144清理后的文件重新满足 BMP 头部的全部长度约束。直接打开或放大 13_clean.bmp,即可看到隐藏文本:
$ convert 13_clean.bmp -resize 400% 13_clean.png$ file 13_clean.bmp13_clean.bmp: PC bitmap, Windows 3.x format, 138 x 34 x 24, image size 14144, cbSize 14198, bits offset 54acf42hvx10