四年参赛老兵报道了。。

题目描述

(1) 成功加载驱动并与之正确通信,理解题目基本机制,识别并排除干扰信息。需在 writeup 中说明分析过程。(满分1.5分)

(2) 发现「宫殿」系统的隐匿通信手段并编写可工作的检测工具,发现的手段种类越多、利用越完整,得分越高,需提交相关源码并在 writeup 中说明分析过程(满分5.0分)

(3) 探索出「宫殿」系统完整的迷宫墙壁布局,需提交迷宫地图和自动化探索脚本。(满分1.5分)

(4) 在还原的迷宫地图上求解起点到终点的最短路径。需提交求解算法和路径结果。(满分0.5分)

(5) 提交正确的 Flag 得 1.5 分;到达终点但未能正确解密得 0.5 分。(满分1.5分)

本篇分析按照 1->3->4->5->2 的顺序分析。

加载驱动并通信

首先分析 R3 程序,字符串大法好

直接就能看到一个驱动通信的设备:

  • \\.\ShadowGate

同时还看到两个全局事件:

  • Global\MazeMoveOK
  • Global\MazeMoveWall

信息获取完,直接看驱动程序,顺着 DriverEntry 找到关键逻辑,由于 sub_1400018A0vm 了,所以它被识别为了 no return,取消掉就可以看清楚所有逻辑了。

具体通信设备就是 \\.\ShadowGate,开始分配了一个 0x1D8 大小的内存,指针保存在全局变量 P 中,tagMaze

往下翻可以发现驱动是通过 IRP 通信的

具体就是 R3 程序通过 DeviceIoControl 与驱动通信。

探索地图&求解最短路

查看 DeviceControl 函数,有三个控制码

经过逆向分析,猜测出 P 的结构。

0x00~0xA8 169B 13×13 迷宫网格(0=通行,1=墙壁)
偏移 大小 说明
0xAC~0xB3 8B 当前坐标 (x, y)
0xB4~0xBB 8B 其他状态
0xBC~0xBF 4B 总移动计数(每次 checksum 通过的 IOCTL 递增)
0xC0 1B 状态标志
0x1C0 8B SpinLock
0x1C8~0x1CF 8B 调用进程 PID
0x1D0~0x1D7 8B 调用线程 TID

对应以下结构体:

1
2
3
4
5
6
7
8
9
10
11
12
struct Maze
{
UCHAR maze[172];
struct Pos Position;
UCHAR OtherState[8];
UINT32 MoveCount;
UCHAR StatusFlag;
UCHAR Reserved1[255];
UINT64 SpinLock;
UINT64 ProcessId;
UINT64 ThreadId;
};

恢复结构体之后,分析得到三个控制码的功能:

IOCTL Code 功能 输入 输出
0x8001200C 获取迷宫信息 24字节:width/height/entry_x/entry_y/exit_x/exit_y(均DWORD)
0x80012008 重置到入口
0x80012004 移动操作 12字节 132字节

它默认识别的有一点问题,稍微改名改类型之后主要分析移动操作。

显然输入的时候还有一个 checksum,假设输入结构为:

1
2
3
4
5
struct input{
int x;
int y;
int z;
}

那么要求 x ^ y ^ 0xDEAD1337 == z

sub_140002161 的函数是被 V 了的,不好分析,但是它把迷宫结构体和 x 加密得到的 v22 传进了函数中,那么可以断定,输入基本上就只看前四个字节。

起应用层的调试器验证一下,断 DeviceIoControl,寻找第三个参数(输入buffer)的地址。

输入 w 得到

1
2
3
4
5
x = 0x52
y = 0
z = 0xDEAD1365
x ^ y ^ 0xDEAD1337 == z
# True

那么这里就有一个不吃操作的打法,上下左右都按一遍就能得到操作的 buffer,直接用。

方向 按键 编码后的 x
UP W/I 0x52
DOWN S/K 0xD3
LEFT A/J 0x53
RIGHT D/L 0xD0

拿到之后,它都提示 13*13 的棋盘了,果断猜迷宫就在 0 偏移上。

有点像,拿到直接解析成迷宫的格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
.......#.....
######.###.#.
.....#.....#.
.###.#######.
.#.........#.
.#.#.#####.#.
.#.#.#...#.#.
.#.###.#.###.
.#.....#.#...
.#######.#.##
...#...#.#.#.
##.#.#.#.#.#.
.....#.#.....

起点 (0,0),终点 (12,12),显然有一条最短路的,数据化一下,然后写 BFS 脚本:

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
from collections import deque

maze = [
[0,0,0,0,0,0,0,1,0,0,0,0,0],
[1,1,1,1,1,1,0,1,1,1,0,1,0],
[0,0,0,0,0,1,0,0,0,0,0,1,0],
[0,1,1,1,0,1,1,1,1,1,1,1,0],
[0,1,0,0,0,0,0,0,0,0,0,1,0],
[0,1,0,1,0,1,1,1,1,1,0,1,0],
[0,1,0,1,0,1,0,0,0,1,0,1,0],
[0,1,0,1,1,1,0,1,0,1,1,1,0],
[0,1,0,0,0,0,0,1,0,1,0,0,0],
[0,1,1,1,1,1,1,1,0,1,0,1,1],
[0,0,0,1,0,0,0,1,0,1,0,1,0],
[1,1,0,1,0,1,0,1,0,1,0,1,0],
[0,0,0,0,0,1,0,1,0,0,0,0,0],
]

dx = [0, 0, -1, 1] # UP, DOWN, LEFT, RIGHT
dy = [-1, 1, 0, 0]

q = deque([((0,0), [])])
visited = {(0,0)}
while q:
(cx,cy), path = q.popleft()
if (cx,cy) == (12,12):
print("Path:", "".join("WSAD"[d] for d in path))
break
for d in range(4):
nx, ny = cx+dx[d], cy+dy[d]
if 0<=nx<13 and 0<=ny<13 and (nx,ny) not in visited and maze[ny][nx]==0:
visited.add((nx,ny))
q.append(((nx,ny), path+[d]))

得到路径:DDDDDDSSDDDDWWDDSSSSSSSSAASSSSDD

依次输入,可以获得 flag

所以拿到 flag:flag{SHAD0WNT_HYPERVMX}

系统隐蔽的通信手段

隐蔽的通信手段一共有五种,前两种在字符串那边就盯真了。

  • 命名事件对象
  • 命名信号量
  • TEB隐藏字段/LastError
  • PEB字段+句柄保护标志
  • IOCTL输出缓冲区/时间

命名事件对象

发现过程: EXE 字符串表直接暴露 Global\MazeMoveOKGlobal\MazeMoveWall。逆向 EXE sub_140001340

1
2
hEventOK   = CreateEventW(NULL, TRUE, FALSE, L"Global\\MazeMoveOK");
hEventWall = CreateEventW(NULL, TRUE, FALSE, L"Global\\MazeMoveWall");

驱动端实现(sub_1400022B0): 使用 ZwOpenEvent + ZwSetEvent 在内核态信号化事件:

1
2
3
4
5
6
7
if (!result || result == 2)  // 0=成功 -> OK事件, 2=撞墙 -> Wall事件
name = L"\\BaseNamedObjects\\MazeMoveOK";
else
name = L"\\BaseNamedObjects\\MazeMoveWall";
ZwOpenEvent(&hEvent, EVENT_MODIFY_STATE, &objAttrs);
ZwSetEvent(hEvent, NULL);
ZwClose(hEvent);

检测代码:

1
2
3
4
5
ResetEvent(hEvtOK);
ResetEvent(hEvtWall);
// ... 发送移动 IOCTL ...
if (WaitForSingleObject(hEvtOK, 100) == WAIT_OBJECT_0) // 移动成功
if (WaitForSingleObject(hEvtWall, 100) == WAIT_OBJECT_0) // 撞墙

命名信号量

发现过程: EXE sub_14021B91F 中通过 SSE 指令(_mm_xor_ps)批量 XOR 解码字符串后调用 CreateSemaphoreW。密钥为 0x004B(wide char),数据存储在 EXE 0x140003BE0~0x140003C300x140003B80~0x140003BD0

解码结果:

1
2
OK信号量:   Global\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}
Wall信号量: Global\{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}

驱动端确认(sub_140319A37): 驱动同样存储 XOR 0x4B 编码的名称(0x1400041600x1400041E0),通过 ObReferenceObjectByName 获取信号量内核对象后调用 KeReleaseSemaphore

检测代码:

1
2
3
4
5
6
hSemOK = CreateSemaphoreW(NULL, 0, 16,
L"Global\\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}");
// 移动前排空
while (WaitForSingleObject(hSemOK, 0) == WAIT_OBJECT_0);
// ... 发送移动 IOCTL ...
if (WaitForSingleObject(hSemOK, 100) == WAIT_OBJECT_0) // 移动成功

TEB 字段/LastError

驱动端实现(sub_140316ADF): 完整的跨进程 TEB 写入:

这里有两种检测方法,首先是 _TEB + 0x68 保存的值是 LastError

1
2
3
4
5
6
7
8
9
10
11
kd> dt _TEB
nt!_TEB
+0x000 NtTib : _NT_TIB
+0x038 EnvironmentPointer : Ptr64 Void
+0x040 ClientId : _CLIENT_ID
+0x050 ActiveRpcHandle : Ptr64 Void
+0x058 ThreadLocalStoragePointer : Ptr64 Void
+0x060 ProcessEnvironmentBlock : Ptr64 _PEB
+0x068 LastErrorValue : Uint4B
+0x06c CountOfOwnedCriticalSections : Uint4B
+0x070 CsrClientThread : Ptr64 Void

因此可以在用户层调用 GetLastError 进行通信检测,当然,直接用户层读取该字段也可以,两个检测方法本质是一样的。

1
2
3
4
5
GetLastError() == (int)0xC0DE0001; // 方法1

BYTE* teb = (BYTE*)__readgsqword(0x30);
val = *(int*)(teb + 0x68);
val == (int)0xC0DE0001; // 方法2

TEB+句柄通信

注意到驱动中的函数

观察后续汇编可发现,应用层可以向 TEB+0x1748 传入一个 HANDLE,然后供内核调用

1
2
3
4
5
6
ZwSetInformationObject(
h,
ObjectHandleFlagInformation, // 4
&info,
sizeof(info) // 2
);

因此可以把 HANDLE 放到 TEB+0x1748 中,观察是否被设置 HANDLE_FLAG_PROTECT_FROM_CLOSE

时间

讲道理感觉最后一种有可能是和这个相关,但是多次测试没有发现特别明显的规律。

总结

下面是我总共的代码,可以跑出 flag,并且使用四种方式成功检测到移动成功的信息。

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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
#include <windows.h>
#include <stdio.h>
#include <string.h>

#define IOCTL_INFO 0x8001200Cu
#define IOCTL_RESET 0x80012008u
#define IOCTL_MOVE 0x80012004u
#define WIN_MAGIC 0x57494E21u

static const BYTE ENC_UP = 0x52, ENC_DOWN = 0xD3, ENC_LEFT = 0x53, ENC_RIGHT = 0xD0;

static HANDLE hDev;
static HANDLE hEvtOK, hEvtWall, hSemOK, hSemWall;

static int dev_open(void) {
hDev = CreateFileW(L"\\\\.\\ShadowGate",
GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hDev == INVALID_HANDLE_VALUE) {
printf("[!] open fail %lu\n", GetLastError());
return 0;
}
printf("[+] Device opened\n");
return 1;
}

static void dev_reset(void) {
DWORD br = 0;
DeviceIoControl(hDev, IOCTL_RESET, NULL, 0, NULL, 0, &br, NULL);
}

static double last_move_ms = 0.0;

static int dev_move(BYTE enc, BYTE out[132], DWORD* pbr) {
BYTE in_buf[12];
memset(in_buf, 0, 12);
in_buf[0] = enc;
DWORD ck = (DWORD)(enc ^ 0xDEAD1337u);
memcpy(in_buf + 8, &ck, 4);
memset(out, 0, 132);
*pbr = 0;
LARGE_INTEGER t0, t1, freq;
QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&t0);
int ret = DeviceIoControl(hDev, IOCTL_MOVE, in_buf, 12, out, 132, pbr, NULL);
QueryPerformanceCounter(&t1);
last_move_ms = (double)(t1.QuadPart - t0.QuadPart) * 1000.0 / freq.QuadPart;
printf(" [dev_move] Enc=0x%02X, Time=%.2fms, BytesReturned=%lu\n", enc, last_move_ms, *pbr);
return ret;
}

static void ch_init(void) {
hEvtOK = CreateEventW(NULL, TRUE, FALSE, L"Global\\MazeMoveOK");
hEvtWall = CreateEventW(NULL, TRUE, FALSE, L"Global\\MazeMoveWall");
hSemOK = CreateSemaphoreW(NULL, 0, 16, L"Global\\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}");
hSemWall = CreateSemaphoreW(NULL, 0, 16, L"Global\\{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}");
}

static int read_teb_field(void) {
int val = 0;
__try {
BYTE* teb = (BYTE*)__readgsqword(0x30);
val = *(int*)(teb + 0x68);
}
__except (1) { val = 0; }
return val;
}

static void clear_teb_field(void) {
__try {
BYTE* teb = (BYTE*)__readgsqword(0x30);
*(int*)(teb + 0x68) = 0;
}
__except (1) {}
}

static void set_teb_handle() {
BYTE* teb = (BYTE*)__readgsqword(0x30);
*(HANDLE *)(teb + 0x1748) = hDev;
}

static int check_ok(void) {
int found = 0;

/* Wait for driver to finish processing (random delay up to 60ms) */
Sleep(150);

/* Channel 1: Named Event - non-blocking check */
if (!found && hEvtOK && WaitForSingleObject(hEvtOK, 100) == WAIT_OBJECT_0)
found = 1;

/* Channel 2: Named Semaphore - non-blocking check */
if (!found && hSemOK && WaitForSingleObject(hSemOK, 100) == WAIT_OBJECT_0)
found = 2;

/* Channel 3: TEB hidden field (TEB+0x68) */
if (!found && GetLastError() == (int)0xC0DE0001) // 或者使用 read_teb_field() 判断,等效
found = 3;

DWORD handle_flags = 0;
GetHandleInformation(hDev, &handle_flags);
if (HANDLE_FLAG_PROTECT_FROM_CLOSE == (handle_flags & HANDLE_FLAG_PROTECT_FROM_CLOSE)) {
found = 4;
}

return found;
}

static void ch_pre(void) {
if (hEvtOK) ResetEvent(hEvtOK);
if (hEvtWall) ResetEvent(hEvtWall);
if (hSemOK) while (WaitForSingleObject(hSemOK, 0) == WAIT_OBJECT_0);
if (hSemWall) while (WaitForSingleObject(hSemWall, 0) == WAIT_OBJECT_0);
SetHandleInformation(hDev, HANDLE_FLAG_PROTECT_FROM_CLOSE, 0);
clear_teb_field();
}

int main(void) {
printf("============================================================\n");
printf(" ShadowGate Final Solver\n");
printf("============================================================\n\n");

if (!dev_open()) return 1;
ch_init();
set_teb_handle();
/* Known shortest path: DDDDDDSSDDDDWWDDSSSSSSSSAASSSSDD (32 steps) */
/* D=RIGHT(0xD0), S=DOWN(0xD3), W=UP(0x52), A=LEFT(0x53) */
const char* path_str = "DDDDDDSSDDDDWWDDSSSSSSSSAASSSSDD";
int path_len = (int)strlen(path_str);

BYTE path_enc[64];
for (int i = 0; i < path_len; i++) {
switch (path_str[i]) {
case 'W': path_enc[i] = ENC_UP; break;
case 'S': path_enc[i] = ENC_DOWN; break;
case 'A': path_enc[i] = ENC_LEFT; break;
case 'D': path_enc[i] = ENC_RIGHT; break;
}
}

printf("[+] Maze: 13x13, Entry=(0,0), Exit=(12,12)\n");
printf("[+] Shortest path (%d steps): %s\n\n", path_len, path_str);

/* Reset and walk the path */
dev_reset();
printf("[*] Walking path...\n");

char flag[256] = { 0 };
int got_flag = 0;

for (int i = 0; i < path_len; i++) {
ch_pre();
BYTE out[132]; DWORD br = 0;
dev_move(path_enc[i], out, &br);

int ok = check_ok();

/* Check WIN magic */
DWORD magic = 0;
if (br >= 0x84) memcpy(&magic, out + 0x3C, 4);
if (magic == WIN_MAGIC) {
DWORD cl = 0;
memcpy(&cl, out + 0x80, 4);
if (cl > 0 && cl <= 63) {
memcpy(flag, out + 0x40, cl);
flag[cl] = 0;
got_flag = 1;
}
}

printf(" Step %2d: %c (enc=0x%02X) -> %s%d%s\n",
i, path_str[i], path_enc[i],
ok ? "OK" : "??",
ok,
(magic == WIN_MAGIC) ? " *** WIN! ***" : "");

if (got_flag) break;
}

if (got_flag) {
printf("\n============================================================\n");
printf(" FLAG: %s\n", flag);
printf("============================================================\n");
FILE* f = fopen("flag.txt", "w");
if (f) { fprintf(f, "%s\n", flag); fclose(f); printf("[+] Saved flag.txt\n"); }
}

/* Print maze map */
printf("\n[*] === Maze Map (0=passable, 1=wall) ===\n\n");
/* Hardcoded from memory dump */
static const BYTE maze[169] = {
0,0,0,0,0,0,0,1,0,0,0,0,0,
1,1,1,1,1,1,0,1,1,1,0,1,0,
0,0,0,0,0,1,0,0,0,0,0,1,0,
0,1,1,1,0,1,1,1,1,1,1,1,0,
0,1,0,0,0,0,0,0,0,0,0,1,0,
0,1,0,1,0,1,1,1,1,1,0,1,0,
0,1,0,1,0,1,0,0,0,1,0,1,0,
0,1,0,1,1,1,0,1,0,1,1,1,0,
0,1,0,0,0,0,0,1,0,1,0,0,0,
0,1,1,1,1,1,1,1,0,1,0,1,1,
0,0,0,1,0,0,0,1,0,1,0,1,0,
1,1,0,1,0,1,0,1,0,1,0,1,0,
0,0,0,0,0,1,0,1,0,0,0,0,0
};
for (int y = 0; y < 13; y++) {
printf(" ");
for (int x = 0; x < 13; x++) {
if (x == 0 && y == 0) printf("S");
else if (x == 12 && y == 12) printf("E");
else printf("%c", maze[y * 13 + x] ? '#' : ' ');
}
printf("\n");
}

CloseHandle(hEvtOK); CloseHandle(hEvtWall);
if (hSemOK) CloseHandle(hSemOK);
if (hSemWall) CloseHandle(hSemWall);
CloseHandle(hDev);
printf("\n[*] Done.\n");
return 0;
}

通信机制总结

题目通过 \\.\ShadowGate 设备,使用 DeviceIoControl 进行通信。

IOCTL Code 功能 输入 输出
0x8001200C 获取迷宫信息 24字节:width/height/entry_x/entry_y/exit_x/exit_y(均DWORD)
0x80012008 重置到入口
0x80012004 移动操作 12字节 132字节

题目使用五种隐蔽的方式告诉应用层本次移动是撞墙还是可通行,驱动内部由全局变量 dword_140005004 来决定本次使用什么信道通信。最开始的时候一定是 1 2 3 4 5 的顺序,后续的通信判定似乎是随机,具体没有研究出来。