HackThisSite - Steganography Mission 12

Challenge

This is an encoded message, the only tip you get is 'I am the not of a file'.

Solution

Analysis

1
2
3
curl -sL -b "$HTS_COOKIE" -o 12.bmp \
'https://www.hackthissite.org/missions/stego/lvl/12.bmp'
file 12.bmp
1
2
12.bmp: PC bitmap, Windows 3.x format, 123 x 1 x 8, image size 126,
resolution 2834 x 2834 px/m, cbSize 1204, bits offset 1078

一张 123×1、只有 1 个像素高的 8 bpp(调色板) BMP,整个文件才 1204 字节。

先把 BMP 头字段算清楚:

1
2
3
4
5
6
7
8
9
10
11
import struct

d = open("12.bmp", "rb").read()
bf_type, bf_size, _, _, off = struct.unpack_from("<2sIHHI", d, 0)
hsz, w, h, planes, bpp, comp, img_size = struct.unpack_from("<IiiHHII", d, 14)
row = ((w * bpp + 31) // 32) * 4 # 8bpp 每行按 4 字节对齐
print("bfType", bf_type, "bfSize", bf_size, "dataOffset", off)
print("w", w, "h", h, "bpp", bpp, "biSizeImage", img_size)
print("row_bytes", row, "pixel_bytes", row * h)
print("palette_entries", (off - 14 - hsz) // 4)
print("trailing", len(d) - off - row * h)
1
2
3
4
5
bfType b'BM' bfSize 1204 dataOffset 1078
w 123 h 1 bpp 8 biSizeImage 126
row_bytes 124 pixel_bytes 124
palette_entries 256
trailing 2

dataOffset=1078 正好等于 14 + 40 + 1024, 即文件头 14 字节 + 信息头 40 字节 + 256 项调色板(每项 4 字节 = 1024)。 像素区是 124 × 1 = 124 字节(123 个像素 + 1 个对齐填充),像素之后只剩两个 0000 填充, 没有尾部附加文件。信息只能在像素值里。

确认这个调色板的内容:

1
2
3
4
5
6
7
import struct

d = open("12.bmp", "rb").read()
off = struct.unpack_from("<I", d, 10)[0]
pal = d[14 + 40:off]
gray = all(tuple(pal[i:i + 3]) == (i // 4, i // 4, i // 4) for i in range(0, len(pal), 4))
print("palette is grayscale identity:", gray)
1
palette is grayscale identity: True

调色板是恒等灰度斜坡:第 i 项就是 (i, i, i)。所以像素索引值等于它显示出来的灰度, 每字节一个像素,直接读原始字节即可。

题面 I am the not of a file 里的 not 就是按位取反:255 - x。 把 124 个像素字节逐个取反:

1
2
pixel bytes : af b4 fc fb eb ff ff ff ff ff 8b 7f 04 c7 a5 b5 ...
NOT (255-x): 50 4b 03 04 14 00 00 00 00 00 74 80 fb 38 5a 4a ...

取反后的头四个字节是 50 4b 03 04PK\x03\x04:一个 ZIP 归档的本地文件头。 原来这张图片的像素值,本身就是某个文件按位取反后的样子,正如提示所说: 它就是某个文件的 not

把取反结果当作 ZIP 打开:

1
2
3
4
5
6
7
8
9
10
11
12
13
import io
import struct
import zipfile

d = open("12.bmp", "rb").read()
off = struct.unpack_from("<I", d, 10)[0]
w, h, planes, bpp = struct.unpack_from("<iiHH", d, 18)
row = ((w * bpp + 31) // 32) * 4
px = d[off:off + row * h]
decoded = bytes(255 - b for b in px) # bitwise NOT
with zipfile.ZipFile(io.BytesIO(decoded)) as z:
print(z.namelist())
print(z.read("pass.txt").decode())
6ae4nt5TB