HackThisSite - Application Mission 11

Challenge

Application Challenge 11 (Windows) — Find the password. (medium) 目标:从这个 Windows GUI 程序里找出 password。

包内只有一个 app11win.exe(40 KB,PE32 GUI)。程序是个 Visual Basic 6 写的窗口,界面上有一个 Text1 输入框和一个标题为 Check 的按钮:典型的输入密码点确认形态。真正的东西不在代码里,而在程序随身带着的一张图片里:password 被画成文字印在内嵌的 JPEG 上

Solution

  • file app11win.exePE32 executable for MS Windows 4.00 (GUI), Intel i386, 3 sections;导入表只有 MSVBVM60.DLLVB6 编译的程序
  • strings 里能看到控件名和工程信息:Form1Command1CheckText1App Challengechallenge5Project1.text 段偏移 0x54BC 处有 VB 工程头魔数 VB5!。入口点把 &DAT_004054bc(就是这个工程头)交给 VB 运行时初始化,是标准的 VB6 启动流程。
  • 二进制里 搜不到任何明文答案Search / Destroy / Awnser / Answer / password 在 ASCII 和 UTF-16 两种编码下命中数都是 0。密码是一组像素。
  • binwalk app11win.exe 直接报出 JPEG:
1
2
3
4
5
$ binwalk app11win.exe
DECIMAL HEXADECIMAL DESCRIPTION
------------------------------------------------------------------------
0 0x0 Windows PE binary, machine type: Intel x86
4766 0x129E JPEG image, total size: 4536 bytes

Step 1: 提取内嵌图片

binwalk 只给出了一个 blob,但那实际上是两个叠加在一起的 JPEG。手工遍历一遍 JPEG marker 才能看清:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
--- walk from 0x129E ---
0x129E: FFD8
0x12A0: FFE0 len=16 JFIF
0x12B2: FFE1 len=1710 Exif <- 内含一张 160 字节的 Exif 缩略图
0x1962: FFED len=2920 Photoshop <- 2300 字节的 APP13,里面塞了一整张独立 JPEG
0x24CC: FFE1 len=4680 XMP <- APP1 XMP 元数据
0x3716: FFEE len=14 APP14 <- Adobe 标记
0x3726: FFDB len=132 DQT <- 量化表
0x37AC: FFC0 len=17 b'\x08\x00:\x00\xe9\x03...' <- SOF0: 高 0x3A=58, 宽 0xE9=233
0x3969: FFDA len=12 SOS
...scan data then EOI at 0x4B35

--- walk from 0x1ED6 ---
0x1ED6: FFD8
0x1ED8: FFE0 len=16 JFIF
0x1F8E: FFC0 len=17 b'\x08\x00 \x00\x80\x03...' <- SOF0: 高 0x20=32, 宽 0x80=128
0x20E8: FFDA len=12 SOS
...scan data then EOI at 0x2454

结论:

  • image A:从 0x129E0x4B35233×58,14489 字节。它不能按第一个 FF D9 截断:A 的 APP1 Exif 段里那张缩略图的 EOI(0x1960)会先命中,截出来的图打不开(identify 直接报错),脚本因此先定位 SOS 再找它后面第一个真正的 EOI。
  • image B:从 0x1ED60x2454128×32,1408 字节;它整张位于 A 的 APP13(Photoshop IRB)段内,本质是 A 的缩略图(把 A 缩到 128×32 与 B 逐像素比,平均绝对差只有 5.2/255,内容一致);它只有 1408 字节,像素量不足以读出可靠文本,读数只能以 image A 为依据。

完整脚本:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
"""Carve the two real JPEGs out of app11win.exe.

Marker walk shows:
* JPEG A at 0x129E: SOI, JFIF, APP1 Exif(1710, embeds a 160-byte thumbnail),
APP13 Photoshop(2920, embeds the whole of JPEG B), APP1 XMP, ... SOF0 233x58,
SOS, scan data, EOI at 0x4B35.
* JPEG B at 0x1ED6: complete 128x32 baseline JPEG living inside A's APP13.

The naive "first FF D9" carve truncates A at the Exif thumbnail's EOI, so we
locate SOS explicitly and take everything up to the first real EOI after it.
"""
import struct

data = open("app11win.exe", "rb").read()


def carve(soi):
"""Return (start, end) of a full JPEG beginning at `soi`."""
p = soi + 2
while p < len(data) - 1:
while data[p] != 0xFF:
p += 1
m = data[p + 1]
if m == 0xDA: # SOS -> skip header, then scan for EOI
ln = struct.unpack_from(">H", data, p + 2)[0]
p = p + 2 + ln
break
if m in (0xD8, 0xD9) or 0xD0 <= m <= 0xD7:
p += 2
continue
if m == 0xFF:
p += 1
continue
ln = struct.unpack_from(">H", data, p + 2)[0]
p = p + 2 + ln
# scan entropy-coded data for EOI (FF D9)
q = p
while q < len(data) - 1:
if data[q] == 0xFF and data[q + 1] == 0xD9:
return soi, q + 2
q += 1
return soi, len(data)


for name, off in (("image_a.jpg", 0x129E), ("image_b.jpg", 0x1ED6)):
s, e = carve(off)
blob = data[s:e]
open("extracted/" + name, "wb").write(blob)
print(f"{name}: 0x{s:X}..0x{e:X} size={len(blob)}")
1
2
3
4
5
6
7
$ cd <hts-workspace>/challenges/hts-app/app11 && uv run python carve_jpegs.py
image_a.jpg: 0x129E..0x4B37 size=14489
image_b.jpg: 0x1ED6..0x2456 size=1408

$ file extracted/image_a.jpg extracted/image_b.jpg
extracted/image_a.jpg: JPEG image data, JFIF ... baseline, precision 8, 233x58, components 3
extracted/image_b.jpg: JPEG image data, JFIF ... baseline, precision 8, 128x32, components 3

直接读,得到 The Awnser is: Search&Destroy