四年参赛老兵报道了。。
题目描述
(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 程序,字符串大法好

直接就能看到一个驱动通信的设备:
同时还看到两个全局事件:
Global\MazeMoveOK
Global\MazeMoveWall
信息获取完,直接看驱动程序,顺着 DriverEntry 找到关键逻辑,由于 sub_1400018A0 被 vm 了,所以它被识别为了 no return,取消掉就可以看清楚所有逻辑了。

具体通信设备就是 \\.\ShadowGate,开始分配了一个 0x1D8 大小的内存,指针保存在全局变量 P 中,tag 为 Maze。
往下翻可以发现驱动是通过 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 |
对应以下结构体:
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,假设输入结构为:
struct input{
int x;
int y;
int z;
}
那么要求 x ^ y ^ 0xDEAD1337 == z。
sub_140002161 的函数是被 V 了的,不好分析,但是它把迷宫结构体和 x 加密得到的 v22 传进了函数中,那么可以断定,输入基本上就只看前四个字节。
起应用层的调试器验证一下,断 DeviceIoControl,寻找第三个参数(输入buffer)的地址。

输入 w 得到
x = 0x52
y = 0
z = 0xDEAD1365
x ^ y ^ 0xDEAD1337 == z
那么这里就有一个不吃操作的打法,上下左右都按一遍就能得到操作的 buffer,直接用。
| 方向 |
按键 |
编码后的 x |
| UP |
W/I |
0x52 |
| DOWN |
S/K |
0xD3 |
| LEFT |
A/J |
0x53 |
| RIGHT |
D/L |
0xD0 |
拿到之后,它都提示 13*13 的棋盘了,果断猜迷宫就在 0 偏移上。

有点像,拿到直接解析成迷宫的格式。
.......
.....
.
.
.
.
.
.
.
...
.....
起点 (0,0),终点 (12,12),显然有一条最短路的,数据化一下,然后写 BFS 脚本:
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]
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\MazeMoveOK 和 Global\MazeMoveWall。逆向 EXE sub_140001340:
hEventOK = CreateEventW(NULL, TRUE, FALSE, L"Global\\MazeMoveOK");
hEventWall = CreateEventW(NULL, TRUE, FALSE, L"Global\\MazeMoveWall");
驱动端实现(sub_1400022B0): 使用 ZwOpenEvent + ZwSetEvent 在内核态信号化事件:
if (!result || result == 2)
name = L"\\BaseNamedObjects\\MazeMoveOK";
else
name = L"\\BaseNamedObjects\\MazeMoveWall";
ZwOpenEvent(&hEvent, EVENT_MODIFY_STATE, &objAttrs);
ZwSetEvent(hEvent, NULL);
ZwClose(hEvent);
检测代码:
ResetEvent(hEvtOK);
ResetEvent(hEvtWall);
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~0x140003C30 和 0x140003B80~0x140003BD0。
解码结果:
OK信号量: Global\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}
Wall信号量: Global\{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}
驱动端确认(sub_140319A37): 驱动同样存储 XOR 0x4B 编码的名称(0x140004160 和 0x1400041E0),通过 ObReferenceObjectByName 获取信号量内核对象后调用 KeReleaseSemaphore。
检测代码:
hSemOK = CreateSemaphoreW(NULL, 0, 16,
L"Global\\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}");
while (WaitForSingleObject(hSemOK, 0) == WAIT_OBJECT_0);
if (WaitForSingleObject(hSemOK, 100) == WAIT_OBJECT_0)
TEB 字段/LastError
驱动端实现(sub_140316ADF): 完整的跨进程 TEB 写入:

这里有两种检测方法,首先是 _TEB + 0x68 保存的值是 LastError
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 进行通信检测,当然,直接用户层读取该字段也可以,两个检测方法本质是一样的。
GetLastError() == (int)0xC0DE0001;
BYTE* teb = (BYTE*)__readgsqword(0x30);
val = *(int*)(teb + 0x68);
val == (int)0xC0DE0001;
TEB+句柄通信
注意到驱动中的函数

观察后续汇编可发现,应用层可以向 TEB+0x1748 传入一个 HANDLE,然后供内核调用
ZwSetInformationObject(
h,
ObjectHandleFlagInformation,
&info,
sizeof(info)
);

因此可以把 HANDLE 放到 TEB+0x1748 中,观察是否被设置 HANDLE_FLAG_PROTECT_FROM_CLOSE
时间
讲道理感觉最后一种有可能是和这个相关,但是多次测试没有发现特别明显的规律。
总结
下面是我总共的代码,可以跑出 flag,并且使用四种方式成功检测到移动成功的信息。
#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;
Sleep(150);
if (!found && hEvtOK && WaitForSingleObject(hEvtOK, 100) == WAIT_OBJECT_0)
found = 1;
if (!found && hSemOK && WaitForSingleObject(hSemOK, 100) == WAIT_OBJECT_0)
found = 2;
if (!found && GetLastError() == (int)0xC0DE0001)
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();
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);
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();
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"); }
}
printf("\n[*] === Maze Map (0=passable, 1=wall) ===\n\n");
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 的顺序,后续的通信判定似乎是随机,具体没有研究出来。
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!
最后于 5小时前
被xi@0ji233编辑
,原因: