Challenge
Thank you t_n83
Solution
Analysis
curl -sL -b "$HTS_COOKIE" -o stego8.bmp \ 'https://www.hackthissite.org/missions/stego/lvl/stego8.bmp'file stego8.bmpstego8.bmp: PC bitmap, Windows 3.x format, 157 x 97 x 24, image size 45784, cbSize 45838, bits offset 54一张 157×97 的 24 位 BMP,体积很小。直接打开只看到一幅白底图上印着一行字和一条下划线。
BMP 的隐写常见路径只有几条:尾部附加数据、头部声明尺寸和真实长度不符、调色板里的垃圾项。 先排除这几条常见路径:
import struct
d = open("stego8.bmp", "rb").read()bf_type, bf_size, _, _, off = struct.unpack_from("<2sIHHI", d, 0)hsz, w, h, _, bpp, comp, img_size = struct.unpack_from("<IiiHHII", d, 14)row = ((w * 3 + 3) // 4) * 4 # 24bpp 每行按 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("trailing", len(d) - off - row * h)bfType b'BM' bfSize 45838 dataOffset 54w 157 h 97 bpp 24 biSizeImage 45784row_bytes 472 pixel_bytes 45784trailing 0像素区正好 字节,文件在像素之后没有任何多余数据(trailing=0);
24 bpp 的 BMP 没有调色板,palette 这条线也不用查。信息只能在像素值里。
分别统计 R、G、B 三个通道的取值分布:
import numpy as npfrom PIL import Image
a = np.array(Image.open("stego8.bmp").convert("RGB"))for i, name in enumerate("RGB"): vals, counts = np.unique(a[:, :, i], return_counts=True) top = sorted(zip(counts.tolist(), vals.tolist()), reverse=True)[:4] print(name, "uniq =", len(vals), "top =", top)R uniq = 26 top = [(14849, 255), (293, 0), (20, 240), (15, 124)]G uniq = 14 top = [(14849, 255), (308, 0), (20, 240), (15, 124)]B uniq = 14 top = [(14849, 255), (308, 0), (20, 240), (15, 124)]图像呈灰度外观,但通道统计显示:R 通道比 G、B 多出一批取值。进一步比对三个通道:
import numpy as npfrom PIL import Image
a = np.array(Image.open("stego8.bmp").convert("RGB")).astype(int)r, g, b = a[:, :, 0], a[:, :, 1], a[:, :, 2]print("R!=G:", int((r != g).sum()), "G!=B:", int((g != b).sum()))ys, xs = np.where(r != g)print("bbox x=[%d..%d] y=[%d..%d]" % (xs.min(), xs.max(), ys.min(), ys.max()))R!=G: 15 G!=B: 0bbox x=[68..82] y=[50..50]绝大多数像素满足 R == G == B(真正的灰度),只有 15 个像素例外,而且全部
集中在同一行 y=50、x 从 68 到 82 的一段连续像素上。这 15 个点就是全部秘密。
这 15 个像素的形态是 (R, 0, 0):绿、蓝被清零,红通道独自保留一个值。
把这一行原样打印出来:
import numpy as npfrom PIL import Image
a = np.array(Image.open("stego8.bmp").convert("RGB")).astype(int)for x in range(66, 85): print(x, tuple(a[50, x].tolist()))66 (255, 255, 255)67 (255, 255, 255)68 (112, 0, 0)69 (97, 0, 0)70 (115, 0, 0)71 (115, 0, 0)72 (119, 0, 0)73 (111, 0, 0)74 (114, 0, 0)75 (100, 0, 0)76 (61, 0, 0)77 (89, 0, 0)78 (114, 0, 0)79 (82, 0, 0)80 (111, 0, 0)81 (116, 0, 0)82 (55, 0, 0)83 (255, 255, 255)84 (255, 255, 255)这些值不构成图案,它们本身就是字节。
一串代码把上面几步串起来即可,提取结果:
import numpy as npfrom PIL import Image
a = np.array(Image.open("stego8.bmp").convert("RGB")).astype(int)r, g, b = a[:, :, 0], a[:, :, 1], a[:, :, 2]ys, xs = np.where((r != g) & (g == b)) # 红独有像素 = 载体order = np.argsort(xs)xs, ys = xs[order], ys[order]text = "".join(chr(int(a[y, x, 0])) for x, y in zip(xs.tolist(), ys.tolist()))print(text)print(text.split("=", 1)[1]) # 取等号后的答案YrRot7