HackThisSite - Steganography Mission 4

Challenge

A GIF image is provided. The only hint is I am being hexed!. 题目提供了一张 GIF 图片,唯一的提示是 I am being hexed!(我被十六进制了)。

Solution

file 只按魔数认出这是 GIF89a,175x36,看不出别的:

1
2
3
4
$ file stego4.gif
stego4.gif: GIF image data, version 89a, 175 x 36
$ wc -c stego4.gif
4703 stego4.gif

提示指向十六进制,于是直接 xxd 看尾部。GIF 的 trailer 是单字节 0x3b;),标准文件到它就结束了:

1
2
3
4
5
00001210: aeda 6c01 0df0 4ef2 2002 b713 0400 3b30  ..l...N. .....;0
00001220: 3131 3130 3030 3030 3031 3130 3131 3030 1110000001101100
00001230: 3031 3131 3030 3030 3131 3030 3031 3130 0111000011000110
00001240: 3131 3130 3030 3130 3031 3130 3030 3130 1110001001100010
00001250: 3131 3031 3030 3030 3131 3030 3031 30 110100001100010

trailer 后面跟着一串 ASCII 字符 '0''1'(字节值 0x30/0x31)。0x3b 直接收尾的话,文件在 0x121e 就该结束了,实际还有 64 字节。

光看尾部不够,先确认整张图没有别的附载(注释扩展、应用扩展、调色板里塞数据)。按 GIF 规范从 header 往后解析每个块:

1
2
3
4
5
6
header: GIF89a
LSD: 175x36 packed=0xd5 gct=True gct_entries=64 bg=0 aspect=0
@0x00d5 EXT GraphicControl (0xf9) subblocks=1 len=4 preview=b'\x00\x00\x00\x00'
@0x00df IMAGE_DESC at (0,0) 175x36 packed=0x00 lct=False lct_entries=0 interlace=False
LZW_min=6 data_len=4395
@0x121e TRAILER 0x3B; remaining bytes after = 64

结构干净:一个 GCT(64 项)、一个 Graphic Control Extension、一个 Image Descriptor 加 LZW 数据,之后就是 0x3B trailer。没有 Comment 或 Application Extension,附载只有 trailer 之后那 64 字节。

要注意 0x3B 这个字节值在 LZW 图像数据里也会自然出现,本文件里一共出现 23 次,0x121e 是最后一次。所以定位 trailer 不能取第一个 0x3B,要么按块走到 trailer,要么取最靠后的那一个。

trailer 之后的 64 个 '0'/'1' 字符每 8 位一组拼成字节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/usr/bin/env python3
"""HackThisSite Steganography 4 ("I am being hexed!") — extract the appended payload.

The GIF is a single-frame GIF89a image. After the normal image data it carries a
standard GIF trailer byte 0x3B, and then 64 extra bytes that are not part of the
image stream: the ASCII characters '0' and '1'. Grouped into bytes they spell an
8-character lowercase password.
"""
import sys

def extract(path):
data = open(path, "rb").read()
trailer = data.rfind(b"\x3b") # last GIF trailer byte (0x3B = ';')
trailing = data[trailer + 1:] # everything after the trailer
if not set(trailing) <= set(b"01"):
raise SystemExit("trailing data is not a '0'/'1' bit string: %r" % trailing[:32])
bits = trailing.decode("ascii")
if len(bits) % 8:
raise SystemExit("bit string length %d is not a multiple of 8" % len(bits))
out = bytes(int(bits[i:i + 8], 2) for i in range(0, len(bits), 8))
return trailer, bits, out

def main():
path = sys.argv[1] if len(sys.argv) > 1 else "stego4.gif"
trailer, bits, out = extract(path)
print("trailer 0x3B at offset:", hex(trailer))
print("trailing bit string (%d bits):" % len(bits), bits)
print("decoded bytes (hex):", out.hex())
print("decoded ASCII :", out.decode("ascii"))

if __name__ == "__main__":
main()
p68cq1hb