首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
CTF对抗
发新帖
1
2
[原创]2026腾讯游戏安全PC初赛Writeup
发表于: 2026-4-13 11:14
2406
[原创]2026腾讯游戏安全PC初赛Writeup
xi@0ji233
16
2026-4-13 11:14
2406
四年参赛老兵报道了。。 <!--more--> ## 题目描述 >(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_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 | 对应以下结构体: ```C 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,假设输入结构为: ```C 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 # True ``` 那么这里就有一个不吃操作的打法,上下左右都按一遍就能得到操作的 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 脚本: ```python 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\MazeMoveOK` 和 `Global\MazeMoveWall`。逆向 EXE `sub_140001340`: ```c hEventOK = CreateEventW(NULL, TRUE, FALSE, L"Global\\MazeMoveOK"); hEventWall = CreateEventW(NULL, TRUE, FALSE, L"Global\\MazeMoveWall"); ``` **驱动端实现(`sub_1400022B0`):** 使用 `ZwOpenEvent` + `ZwSetEvent` 在内核态信号化事件: ```c 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); ``` **检测代码:** ```c 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~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`。 **检测代码:** ```c 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` ``` 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` 进行通信检测,当然,直接用户层读取该字段也可以,两个检测方法本质是一样的。 ```C++ GetLastError() == (int)0xC0DE0001; // 方法1 BYTE* teb = (BYTE*)__readgsqword(0x30); val = *(int*)(teb + 0x68); val == (int)0xC0DE0001; // 方法2 ``` ### TEB+句柄通信 注意到驱动中的函数  观察后续汇编可发现,应用层可以向 TEB+0x1748 传入一个 HANDLE,然后供内核调用 ```C ZwSetInformationObject( h, ObjectHandleFlagInformation, // 4 &info, sizeof(info) // 2 ); ```  因此可以把 HANDLE 放到 TEB+0x1748 中,观察是否被设置 `HANDLE_FLAG_PROTECT_FROM_CLOSE` ### 时间 讲道理感觉最后一种有可能是和这个相关,但是多次测试没有发现特别明显的规律。 ### 总结 下面是我总共的代码,可以跑出 flag,并且使用四种方式成功检测到移动成功的信息。 ```C++ #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` 的顺序,后续的通信判定似乎是随机,具体没有研究出来。
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
最后于
2026-4-13 11:19 被xi@0ji233编辑 ,原因:
#Reverse
收藏
・
1
点赞
・
2
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
wx_晨梦
这个讨论对我很有帮助,谢谢!
2026-7-16 08:40
Saileaxh
+3
谢谢你的细致分析,受益匪浅!
2026-4-13 11:19
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
2
)
XCicada
雪 币:
3085
活跃值:
(2680)
能力值:
( LV6,RANK:80 )
在线值:
发帖
3
回帖
323
粉丝
101
关注
私信
XCicada
2
2
楼
牛逼
2026-4-13 11:20
0
Saileaxh
雪 币:
3110
活跃值:
(2919)
能力值:
( LV6,RANK:80 )
在线值:
发帖
18
回帖
49
粉丝
108
关注
私信
Saileaxh
1
3
楼
还能这么搞
2026-4-13 11:21
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
xi@0ji233
16
32
发帖
53
回帖
833
RANK
关注
私信
他的文章
[原创][原创]2026腾讯游戏安全PC决赛Writeup
12372
[原创]2026腾讯游戏安全PC初赛Writeup
2406
[原创]腾讯游戏安全竞赛2025复赛题解(加强版)
6863
[原创]强网杯2025决赛复现
14337
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部