HackThisSite - Steganography Mission 14

Challenge

题目提供 stego14.tar.gz,提示 Get the image here。需要先看清 tar 内部结构,再从图片里挖出隐藏的密钥与密文,最后用古典密码还原口令。 A tar.gz is provided; the answer is recovered by unpacking it, carving a RAR archive out of the JPG, and decrypting a classical cipher with a key that is itself hidden inside the archive.

Solution

1
2
3
$ tar tzf stego14.tar.gz
6578747261637400.jpg
$ tar xzf stego14.tar.gz

成员名 6578747261637400 是一串 ASCII 十六进制,解码后是 extract\0。这个名字本身就在提示:真正的载荷不在图片像素里,而在图片之后附加的数据里,需要 extract(提取)出来。

图片本体是一张 150x20 的基线 JPEG:

1
2
$ file 6578747261637400.jpg
JPEG image data, JFIF standard 1.02, baseline, precision 8, 150x20, components 3

JPEG 以 EOI(FF D9)结束,但文件真实长度超过了 EOI 的位置,后面还跟了一段数据。扫描标记来定位:

1
2
3
4
5
6
data = open("6578747261637400.jpg", "rb").read()
eoi = data.rfind(b"\xff\xd9")
print("file len :", len(data))
print("EOI at :", eoi)
print("trailing :", len(data) - eoi - 2, "bytes")
print("signature:", data[eoi + 2:eoi + 9])

输出显示 EOI 之后还有 5617 字节,且以 Rar! 开头,是一段 RAR v4 归档。把它切出来解开:

1
2
3
4
5
6
7
8
$ python3 -c "d=open('6578747261637400.jpg','rb').read(); i=d.rfind(b'\xff\xd9'); open('appended.rar','wb').write(d[i+2:])"
$ file appended.rar
appended.rar: RAR archive data, v4, os: Win32
$ 7z l appended.rar
Date Time Attr Size Compressed Name
------------------- ----- ------------ ------------ ------------------------
2009-01-23 01:02:14 ....A 12758 5546 key.jpg
$ 7z x appended.rar

归档里只有一个文件 key.jpg(48x13)。

key.jpg 是纯黑白位图,像素值只有黑(0)与白(255)两色,放大后是一行小字。直接按 ASCII 打印像素即可读:

1
2
3
4
5
6
from PIL import Image

im = Image.open("key.jpg").convert("L")
w, h = im.size
for y in range(h):
print("".join("#" if im.getpixel((x, y)) < 128 else "." for x in range(w)))

打印出来的是 *5+10。这正是仿射密码(Affine cipher)的密钥,含义为 E(x) = (5 * x + 10) mod 26,也就是 a = 5, b = 10

6578747261637400.jpg 的像素对比度极低,灰度只落在 197–255 之间,肉眼看上去几乎全白。把灰度拉伸到完整范围后,里面藏着一行大写密文 PGNNZCFYXD

1
2
3
4
5
6
from PIL import Image
import numpy as np

a = np.asarray(Image.open("6578747261637400.jpg").convert("L")).astype(int)
b = np.clip((255 - a) * 255.0 / (a.max() - a.min()), 0, 255).astype("uint8")
Image.fromarray(b).resize((a.shape[1] * 8, a.shape[0] * 8)).save("cipher_zoom.png")

a = 5 与 26 互质,模逆元为 215 * 21 = 105 ≡ 1 (mod 26))。解密公式是 D(y) = 21 * (y - 10) mod 26

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
def affine_decrypt(cipher, a=5, b=10, alphabet="ABCDEFGHIJKLMNOPQRSTUVWXYZ"):
a_inv = pow(a, -1, len(alphabet))
out = []
for ch in cipher:
if ch.upper() in alphabet:
x = alphabet.index(ch.upper())
out.append(alphabet[(a_inv * (x - b)) % len(alphabet)])
else:
out.append(ch)
return "".join(out)

def affine_encrypt(plain, a=5, b=10, alphabet="ABCDEFGHIJKLMNOPQRSTUVWXYZ"):
out = []
for ch in plain:
if ch.upper() in alphabet:
x = alphabet.index(ch.upper())
out.append(alphabet[(a * x + b) % len(alphabet)])
else:
out.append(ch)
return "".join(out)

ct = "PGNNZCFYXD"
pt = affine_decrypt(ct)
print(pt) # BULLDOZINJ
print(affine_encrypt(pt) == ct) # True:往返一致

解出的明文 BULLDOZINJ 即口令(大小写敏感,站点惯用全小写)。提交后,个人资料页的 Stego: 一行出现 (14),关卡页也显示 You have already done this mission.,确认通关。

Key points

  • 遇到 .tar.gztar tzf 看成员名:6578747261637400 是十六进制编码的 extract\0,直接点破了手法。
  • JPEG 文件可以合法地在 EOI 之后追加任意数据:rfind(b"\xff\xd9") 定位真正的图像结尾,尾部紧跟 Rar! 就是一段藏在图片后面的压缩包。
  • 低对比度像素是常见隐写载体:把灰度拉伸(197–255 → 0–255)就能让几乎全白的图浮出密文。
  • 密钥图片是纯黑白位图时,直接打印像素字符画比逐像素放大更省事。
  • 仿射密码的密钥形如 *a+b(这里是 *5+10);解密先求 a 关于 26 的模逆元,再套 D(y) = a_inv * (y - b) mod 26
bulldozinj