腾讯游戏安全大赛2026初赛题解
四年参赛老兵报道了。。
题目描述
(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\MazeMoveOKGlobal\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 |
对应以下结构体:
1 | struct Maze |
恢复结构体之后,分析得到三个控制码的功能:
| IOCTL Code | 功能 | 输入 | 输出 |
|---|---|---|---|
0x8001200C |
获取迷宫信息 | 无 | 24字节:width/height/entry_x/entry_y/exit_x/exit_y(均DWORD) |
0x80012008 |
重置到入口 | 无 | 无 |
0x80012004 |
移动操作 | 12字节 | 132字节 |
它默认识别的有一点问题,稍微改名改类型之后主要分析移动操作。
显然输入的时候还有一个 checksum,假设输入结构为:
1 | struct input{ |
那么要求 x ^ y ^ 0xDEAD1337 == z。
sub_140002161 的函数是被 V 了的,不好分析,但是它把迷宫结构体和 x 加密得到的 v22 传进了函数中,那么可以断定,输入基本上就只看前四个字节。
起应用层的调试器验证一下,断 DeviceIoControl,寻找第三个参数(输入buffer)的地址。
输入 w 得到
1 | x = 0x52 |
那么这里就有一个不吃操作的打法,上下左右都按一遍就能得到操作的 buffer,直接用。
| 方向 | 按键 | 编码后的 x |
|---|---|---|
| UP | W/I | 0x52 |
| DOWN | S/K | 0xD3 |
| LEFT | A/J | 0x53 |
| RIGHT | D/L | 0xD0 |
拿到之后,它都提示 13*13 的棋盘了,果断猜迷宫就在 0 偏移上。
有点像,拿到直接解析成迷宫的格式。
1 | .......#..... |
起点 (0,0),终点 (12,12),显然有一条最短路的,数据化一下,然后写 BFS 脚本:
1 | from collections import deque |
得到路径:DDDDDDSSDDDDWWDDSSSSSSSSAASSSSDD。
依次输入,可以获得 flag。
所以拿到 flag:flag{SHAD0WNT_HYPERVMX}
系统隐蔽的通信手段
隐蔽的通信手段一共有五种,前两种在字符串那边就盯真了。
- 命名事件对象
- 命名信号量
- TEB隐藏字段/LastError
- PEB字段+句柄保护标志
- IOCTL输出缓冲区/时间
命名事件对象
发现过程: EXE 字符串表直接暴露 Global\MazeMoveOK 和 Global\MazeMoveWall。逆向 EXE sub_140001340:
1 | hEventOK = CreateEventW(NULL, TRUE, FALSE, L"Global\\MazeMoveOK"); |
驱动端实现(sub_1400022B0): 使用 ZwOpenEvent + ZwSetEvent 在内核态信号化事件:
1 | if (!result || result == 2) // 0=成功 -> OK事件, 2=撞墙 -> Wall事件 |
检测代码:
1 | ResetEvent(hEvtOK); |
命名信号量
发现过程: EXE sub_14021B91F 中通过 SSE 指令(_mm_xor_ps)批量 XOR 解码字符串后调用 CreateSemaphoreW。密钥为 0x004B(wide char),数据存储在 EXE 0x140003BE0~0x140003C30 和 0x140003B80~0x140003BD0。
解码结果:
1 | OK信号量: Global\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D} |
驱动端确认(sub_140319A37): 驱动同样存储 XOR 0x4B 编码的名称(0x140004160 和 0x1400041E0),通过 ObReferenceObjectByName 获取信号量内核对象后调用 KeReleaseSemaphore。
检测代码:
1 | hSemOK = CreateSemaphoreW(NULL, 0, 16, |
TEB 字段/LastError
驱动端实现(sub_140316ADF): 完整的跨进程 TEB 写入:
这里有两种检测方法,首先是 _TEB + 0x68 保存的值是 LastError
1 | kd> dt _TEB |
因此可以在用户层调用 GetLastError 进行通信检测,当然,直接用户层读取该字段也可以,两个检测方法本质是一样的。
1 | GetLastError() == (int)0xC0DE0001; // 方法1 |
TEB+句柄通信
注意到驱动中的函数
观察后续汇编可发现,应用层可以向 TEB+0x1748 传入一个 HANDLE,然后供内核调用
1 | ZwSetInformationObject( |
因此可以把 HANDLE 放到 TEB+0x1748 中,观察是否被设置 HANDLE_FLAG_PROTECT_FROM_CLOSE
时间
讲道理感觉最后一种有可能是和这个相关,但是多次测试没有发现特别明显的规律。
总结
下面是我总共的代码,可以跑出 flag,并且使用四种方式成功检测到移动成功的信息。
1 |
|
通信机制总结
题目通过 \\.\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 的顺序,后续的通信判定似乎是随机,具体没有研究出来。