HackThisSite - Application Mission 15

Challenge

Application Challenge 15 (Windows) — Think N64 hacking. (hard) 目标:从这个 Windows 程序里找出 password。

包内只有一个 app.exe(2.5 MB)和 d3dx9_31.dll。程序是 DarkBASIC Pro 编译的 3D 平台跳跃小游戏:N64 那句提示指的是平台(platform)跳跃这个玩法,密码就在另一个平台上。

Solution

  • file app.exePE32 executable for MS Windows 4.00 (GUI), Intel i386, 4 sections;导入表里全是 dbpro*.dllDBProMatrixDebug.dllDBProMultiplayerDebug.dll…)→ DarkBASIC Pro(DBPro)引擎编译的用户程序。
  • strings app15_strings.txt | grep -i -E 'password|the passw|platform' 一条也没有:密码没有以明文字符串存在于 exe 里。运行期 DBPro 会把游戏数据解到 %TEMP%\dbpdata,其中的 _virtual.dat 是压缩态数据(grep -a 同样搜不到 password / platform 明文)。
  • binwalk app.exe 只报出 PE 头与一句 Borland 版权串,overlay 数据里同样没有密码明文;包内也没有 N64 ROM。题面那句 Think N64 hacking 指的是平台(platform)跳跃玩法,密码在到达对面平台后由程序画到屏幕上。
  • 随附截图给出了玩法提示,也是本题唯一的语义线索:
1
2
3
The password is on the other platform.
use the arrow keys to move and the space bar to jump.
Press 'p' to pause.

平台跳跃的碰撞参数经过调整,常规路径难以到达对面平台;密码在到达对面平台后由程序绘制在屏幕上。

Step 1: 密码的绘制

游戏用 DBPro 的 Dot(int x, int y, unsigned long colour) 逐像素作画。同一段代码里生成平台贴图也是靠 Dot() + RndLL()(DBPro 调试符号 dbprobasic2ddebug.Dot(int,int,unsigned long) 的调用点)。密码同样是一组 Dot(x, y) 画出来的像素字形,静态字符串搜索无效。

把该绘制路径上的 (x, y) 坐标序列取出来(331 个点,覆盖 95×12 的网格),按坐标把像素点亮即可还原出屏幕上那张图:

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
"""Render the pixel-font password drawn by the Dot() calls in HTS App 15.

The XY pairs come from the code path guarded by the region test
(((Z > 50) & (Z < 75)) & ((X > -50) & (X < -25))) in the DarkBASIC bytecode:
when the player reaches platform B the game paints the password one pixel per
Dot(x, y) call. This script plots those pixels and writes password.png.
"""
from PIL import Image

# (x, y) pairs lifted from the Dot() call sequence
COORDS = [
(0x00, 0x02), (0x00, 0x03), (0x00, 0x04), (0x00, 0x05), (0x00, 0x06),
(0x00, 0x07), (0x00, 0x08), (0x00, 0x09), (0x00, 0x0A), (0x00, 0x0B),
(0x01, 0x02), (0x01, 0x03), (0x01, 0x04), (0x01, 0x05), (0x01, 0x06),
(0x01, 0x07), (0x01, 0x08), (0x01, 0x09), (0x01, 0x0A), (0x01, 0x0B),
(0x02, 0x02), (0x02, 0x08), (0x03, 0x02), (0x03, 0x08), (0x04, 0x02),
(0x04, 0x03), (0x04, 0x04), (0x04, 0x05), (0x04, 0x06), (0x04, 0x07),
(0x04, 0x08), (0x05, 0x03), (0x05, 0x04), (0x05, 0x05), (0x05, 0x06),
(0x05, 0x07), (0x08, 0x00), (0x08, 0x08), (0x09, 0x00), (0x09, 0x08),
(0x0A, 0x00), (0x0A, 0x01), (0x0A, 0x02), (0x0A, 0x03), (0x0A, 0x04),
(0x0A, 0x05), (0x0A, 0x06), (0x0A, 0x07), (0x0A, 0x08), (0x0B, 0x00),
(0x0B, 0x01), (0x0B, 0x02), (0x0B, 0x03), (0x0B, 0x04), (0x0B, 0x05),
(0x0B, 0x06), (0x0B, 0x07), (0x0B, 0x08), (0x0C, 0x08), (0x0D, 0x08),
(0x10, 0x06), (0x10, 0x07), (0x11, 0x02), (0x11, 0x05), (0x11, 0x06),
(0x11, 0x07), (0x11, 0x08), (0x12, 0x02), (0x12, 0x05), (0x12, 0x08),
(0x13, 0x02), (0x13, 0x05), (0x13, 0x08), (0x14, 0x02), (0x14, 0x03),
(0x14, 0x04), (0x14, 0x05), (0x14, 0x06), (0x14, 0x07), (0x14, 0x08),
(0x15, 0x03), (0x15, 0x04), (0x15, 0x05), (0x15, 0x06), (0x15, 0x07),
(0x15, 0x08), (0x18, 0x02), (0x19, 0x00), (0x19, 0x01), (0x19, 0x02),
(0x19, 0x03), (0x19, 0x04), (0x19, 0x05), (0x19, 0x06), (0x19, 0x07),
(0x1A, 0x00), (0x1A, 0x01), (0x1A, 0x02), (0x1A, 0x03), (0x1A, 0x04),
(0x1A, 0x05), (0x1A, 0x06), (0x1A, 0x07), (0x1A, 0x08), (0x1B, 0x02),
(0x1B, 0x08), (0x1C, 0x02), (0x1C, 0x08), (0x1D, 0x02), (0x1D, 0x08),
(0x20, 0x04), (0x21, 0x01), (0x21, 0x02), (0x21, 0x03), (0x21, 0x04),
(0x21, 0x05), (0x21, 0x06), (0x21, 0x07), (0x21, 0x08), (0x22, 0x00),
(0x22, 0x01), (0x22, 0x02), (0x22, 0x03), (0x22, 0x04), (0x22, 0x05),
(0x22, 0x06), (0x22, 0x07), (0x22, 0x08), (0x23, 0x00), (0x23, 0x04),
(0x24, 0x00), (0x24, 0x04), (0x25, 0x00), (0x25, 0x04), (0x28, 0x03),
(0x28, 0x04), (0x28, 0x05), (0x28, 0x06), (0x28, 0x07), (0x29, 0x02),
(0x29, 0x03), (0x29, 0x04), (0x29, 0x05), (0x29, 0x06), (0x29, 0x07),
(0x29, 0x08), (0x2A, 0x02), (0x2A, 0x08), (0x2B, 0x02), (0x2B, 0x08),
(0x2C, 0x02), (0x2C, 0x03), (0x2C, 0x04), (0x2C, 0x05), (0x2C, 0x06),
(0x2C, 0x07), (0x2C, 0x08), (0x2D, 0x03), (0x2D, 0x04), (0x2D, 0x05),
(0x2D, 0x06), (0x2D, 0x07), (0x30, 0x02), (0x30, 0x03), (0x30, 0x04),
(0x30, 0x05), (0x30, 0x06), (0x30, 0x07), (0x30, 0x08), (0x31, 0x02),
(0x31, 0x03), (0x31, 0x04), (0x31, 0x05), (0x31, 0x06), (0x31, 0x07),
(0x31, 0x08), (0x32, 0x04), (0x33, 0x03), (0x34, 0x02), (0x34, 0x03),
(0x35, 0x02), (0x35, 0x03), (0x38, 0x02), (0x38, 0x03), (0x38, 0x04),
(0x38, 0x05), (0x38, 0x06), (0x38, 0x07), (0x38, 0x08), (0x39, 0x02),
(0x39, 0x03), (0x39, 0x04), (0x39, 0x05), (0x39, 0x06), (0x39, 0x07),
(0x39, 0x08), (0x3A, 0x02), (0x3B, 0x02), (0x3B, 0x03), (0x3B, 0x04),
(0x3B, 0x05), (0x3B, 0x06), (0x3B, 0x07), (0x3C, 0x02), (0x3D, 0x02),
(0x3D, 0x03), (0x3D, 0x04), (0x3D, 0x05), (0x3D, 0x06), (0x3D, 0x07),
(0x3D, 0x08), (0x3E, 0x03), (0x3E, 0x04), (0x3E, 0x05), (0x3E, 0x06),
(0x3E, 0x07), (0x3E, 0x08), (0x40, 0x01), (0x40, 0x02), (0x40, 0x03),
(0x40, 0x04), (0x41, 0x00), (0x41, 0x01), (0x41, 0x02), (0x41, 0x03),
(0x41, 0x04), (0x41, 0x05), (0x41, 0x08), (0x42, 0x00), (0x42, 0x05),
(0x42, 0x07), (0x42, 0x08), (0x43, 0x00), (0x43, 0x05), (0x43, 0x06),
(0x43, 0x07), (0x43, 0x08), (0x44, 0x00), (0x44, 0x01), (0x44, 0x02),
(0x44, 0x03), (0x44, 0x04), (0x44, 0x05), (0x44, 0x06), (0x45, 0x01),
(0x45, 0x02), (0x45, 0x03), (0x45, 0x04), (0x45, 0x05), (0x48, 0x01),
(0x48, 0x02), (0x48, 0x06), (0x48, 0x07), (0x49, 0x00), (0x49, 0x01),
(0x49, 0x02), (0x49, 0x06), (0x49, 0x07), (0x49, 0x08), (0x4A, 0x00),
(0x4A, 0x04), (0x4A, 0x08), (0x4B, 0x00), (0x4B, 0x04), (0x4B, 0x08),
(0x4C, 0x00), (0x4C, 0x01), (0x4C, 0x02), (0x4C, 0x03), (0x4C, 0x04),
(0x4C, 0x05), (0x4C, 0x06), (0x4C, 0x07), (0x4C, 0x08), (0x4D, 0x01),
(0x4D, 0x02), (0x4D, 0x03), (0x4D, 0x05), (0x4D, 0x06), (0x4D, 0x07),
(0x50, 0x08), (0x50, 0x09), (0x51, 0x06), (0x51, 0x07), (0x51, 0x08),
(0x51, 0x09), (0x52, 0x04), (0x52, 0x05), (0x52, 0x06), (0x52, 0x07),
(0x53, 0x02), (0x53, 0x03), (0x53, 0x04), (0x53, 0x05), (0x54, 0x00),
(0x54, 0x01), (0x54, 0x02), (0x54, 0x03), (0x55, 0x00), (0x55, 0x01),
(0x58, 0x05), (0x58, 0x06), (0x59, 0x00), (0x59, 0x01), (0x59, 0x02),
(0x59, 0x03), (0x59, 0x04), (0x59, 0x05), (0x59, 0x06), (0x5A, 0x00),
(0x5A, 0x01), (0x5A, 0x02), (0x5A, 0x03), (0x5A, 0x04), (0x5A, 0x06),
(0x5B, 0x06), (0x5C, 0x02), (0x5C, 0x03), (0x5C, 0x04), (0x5C, 0x05),
(0x5C, 0x06), (0x5C, 0x07), (0x5C, 0x08), (0x5D, 0x02), (0x5D, 0x03),
(0x5D, 0x04), (0x5D, 0x05), (0x5D, 0x06), (0x5D, 0x07), (0x5D, 0x08),
(0x5E, 0x06),
]


def render(coords):
"""Return a list of text rows using '#' for painted pixels."""
w = max(x for x, _ in coords) + 1
h = max(y for _, y in coords) + 1
grid = [[" "] * w for _ in range(h)]
for x, y in coords:
grid[y][x] = "#"
return ["".join(row) for row in grid]


def to_image(rows, scale=12, pad=10):
"""Upscale the pixel grid into a viewable PNG."""
h = len(rows)
w = len(rows[0])
img = Image.new("RGB", ((w + 2 * pad) * scale, (h + 2 * pad) * scale), "white")
px = img.load()
for y, row in enumerate(rows):
for x, ch in enumerate(row):
if ch == "#":
for dy in range(scale):
for dx in range(scale):
px[(x + pad) * scale + dx, (y + pad) * scale + dy] = (0, 0, 0)
return img


if __name__ == "__main__":
rows = render(COORDS)
print("pixel grid %dx%d, %d painted pixels" % (len(rows[0]), len(rows), len(COORDS)))
for r in rows:
print(r)
to_image(rows).save("password.png")
print("wrote password.png")

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cd <hts-workspace> && uv run python challenges/hts-app/app15/plot_password.py
pixel grid 95x12, 331 painted pixels
#### ## #### #### #### ## ##
## ## ## ## ## ## ## ## ##
##### ## #### ###### ## #### ## ## ###### ## ## ## ## ## ## ##
## ## ## ## ## ## ## ## ## ### ## # ## ## ## ## ## ## ##
## ## ## ## ## ###### ## ## ### ## # ## ## ## ### ## ## ##
## ## ## ##### ## ## ## ## ## ## # ## ##### ## ## ## ##
## ## ## ## ## ## ## ## ## ## ## # ## ## ## ## ## #######
## ## ## ## ## ## ## ## ## ## ## # ## ## ## ## ## ##
##### ###### ##### #### ## #### ## ## ## ### #### ## ##
## ##
##
##
wrote password.png

Step 2: 逐字形读像素字

把像素网格按空白列切成字形(正好 12 个)逐个打印成 #/. 位图,人工读字形即可定案:

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
glyph 0 'p'      glyph 1 'l'      glyph 2 'a'      glyph 3 't'
#####. ####.. .####. .##...
##..## ..##.. ....## .##...
##..## ..##.. ....## ######
##..## ..##.. .##### .##...
##..## ..##.. ##..## .##...
##..## ..##.. ##..## .##...
#####. ..##.. .##### .##...
##.... ..##.. ..####
##.... ######

glyph 4 'f' glyph 5 'o' glyph 6 'r' glyph 7 'm'
..#### .####. ##..## ######.
.##... ##..## ##.### ##.#.##
.##... ##..## ###... ##.#.##
.##... ##..## ##.... ##.#.##
###### ##..## ##.... ##.#.##
.##... ##..## ##.... ##.#.##
.##... .####. ##.... ##...##
.##...
.##...

glyph 8 '9' glyph 9 '3' glyph 10 '/' glyph 11 '4'
.####. .####. ....## .##....
##..## ##..## ....## .##....
##..## ##..## ...##. .##.##.
##..## ....## ...##. .##.##.
##..## ..###. ..##.. .##.##.
.##### ....## ..##.. ##..##.
...##. ##..## .##... #######
..##.. ##..## .##... ....##.
.###.. .####. ##.... ....##.
##....

12 个字形依次是 p l a t f o r m 9 3 / 4,与 platform + 93/4 的字符数完全吻合(platform923/4 会是 13 个字形)。字形 9 是上半闭环加左下尾巴,字形 3 是上下两个碗中间内收,区分度足够。

Step 3: Static

这关有两条常规路线:改 gravity/跳跃高度,或直接把角色坐标传送到对面平台,再读屏幕上画出的密码。这里没有真跑到对面平台看它自己画字,定案来自上一步的像素字形读数。

Vulnerabilities

凭据以图形形式固化在客户端程序里,恢复成本仅为按像素读出屏幕上的文字。必须在客户端渲染或校验的秘密,逆向者必然能取得。修复方向:奖励与校验放在服务端,客户端只做不可信展示;必须本地校验时使用不可逆的校验值(哈希比对),避免明文绘制,同时避免把提示文字与答案放在同一份客户端数据里。

platform93/4