首页
社区
课程
招聘
[原创]2026腾讯游戏安全技术竞赛 初赛 直读内存拿明文地图
发表于: 6天前 1858

[原创]2026腾讯游戏安全技术竞赛 初赛 直读内存拿明文地图

6天前
1858

       前言:决赛打完再回顾提前开好的香槟,觉得自己很懂虚拟化,结果被ACE团队爆杀,我还是孤陋寡闻了,判断出HYPER-V被劫持,但是不知道怎么题目是怎么实现的,我内核驱动遍历页表都读不出HYPER-V的内存(EFI的hv不会向Windows映射这段内存),想不通怎么从GUEST读到HOST内存(后面想明白应该在虚拟机里做这个题,可以直接秒杀),DDMA都没看出来,只做了一半,还是得好好沉淀好好学习。
               这个初赛WrtiteUP是提交上去的版本稍微润色,第一次参加CTF,思路可能有不足,题解仅代表我自己的思路。

0.
题目总体分析与解题思路

打开压缩包一看,上来就是内核驱动。一个 4M  VM 混淆的 sys,旁边站着一个体积还没它大的控制台程序。R3 发指令、驱动做读写。
本题核心考点:检测 IOCTL  (我觉得对于写外挂,IOCTL通信是很原始的做法,真实反作弊会在IOCTL相关函数注册回调)


程序跑起来之后,老老实实告诉了我一切:

                                                                             

0-1: ShadowGateApp 运行界面 —— 题目把规则说得明明白白

关键信息提取:
· 驱动里藏了个 13×13 的加密迷阵,入口 (0,0),出口 (12,12)
· "The palace gives NO feedback" —— 表面上不给任何移动结果反馈
· "Five hidden flaws betray the result of every move" —— 但存在 5 个隐藏的信息泄露缺陷
· 每次 RESET 后,前 5 步成功移动依次暴露一种缺陷
· 终极目标:找到最短路径,走到终点提取内网凭证(Flag

题目的核心设计:驱动核心逻辑被 VMP保护,选手只能看到接口和调用规则。在"黑盒"条件下,通过分析 5 条侧信道来摸清迷宫布局,最终提取 Flag

但文件到了选手的电脑上,自然是各显神通。除了题目设计的纯侧信道路线外,直读内核内存(甚至直接 kdmapper 加载一个读写驱动都行)、DMA(遍历 PTE 页表读物理内存)、池标签搜索……都可以直接拿到迷宫真值。

路线 A — 纯传统逆向 + 侧信道探测:
  IDA 逆向 IOCTL 协议导入表发现 5 种侧信道编写检测脚本 → EXIT_MARKER 验证法精确探测 → DFS探索地图+最短路径 Flag

路线 B — 自研 Hypervisor 牛刀杀鸡直接读内存:
  Hypervisor → 读驱动 g_MazeContext 内核内存(实际上这一步既读了完整的地图,也读了最短路线)把读出来的编码转换成地图(0=通道,1=墙)输出完整 13×13 精确地图

准确地说它的名字叫 HyperRay。稍后有一个AIMCP调用它的示例,简要介绍它的架构:
               


    整套工具解耦设计,模块化开发。驱动通过
WSK 网络栈通信(以原生支持双机调试)GUI 通过 DLL 后端抽象层连接。MCP 层让 AI 可以直接调用 VT 调试器的全部能力 , 包括本次比赛中用到的内核内存读取。     
                     这个调试器过几天会开源,请关注后续看雪发帖

拿到驱动文件 ShadowGateSys.sys,甚至都没有签名,只好掏出我压箱底的过期EV,给他签上加载了。  (常规操作应该是测试签+测试模式)


sc create + sc start 一步到位

sc create ShadowGate type=kernel binPath=C:\ShadowGateSys.sys

sc start ShadowGate

IDA 打开 EXE,先看它怎么连驱动的。 main 附近很快找到 CreateFileW 调用:

                              

1-2: IDA EXE CreateFileW —— 打开 \\.\ShadowGate

代码里连提示都帮你写好了:驱动没加载就告诉你用 sc create,权限不够就告诉你 Run as Administrator。出题人很贴心。

IDA 打开 sys,直奔 DriverEntry。位于 INIT 段,没被 VMP 保护——VMP不保护 INIT 段,因为系统初始化完成后这段内存会被释放
       

1-3: DriverEntry —— IoCreateDevice + IoCreateSymbolicLink

DriverEntry 做了几件事:
1. ExAllocatePool2 分配迷宫上下文(后面详说)
2. 调用 MazeInit() 初始化迷宫(在 VMProtect 段内,看不到细节)
3. IoCreateDevice 创建 \Device\ShadowGate
4. IoCreateSymbolicLink 创建符号链接 \??\ShadowGate
5. 注册 IRP handlerCREATE / CLOSE / DEVICE_CONTROL

对于 VMP保护的驱动,大部分时候可以从导入表入手IAT 必须保留明文函数指针,VMP 拿它没办法。翻一遍导入表,几乎所有侧信道的函数签名都暴露了:

                                                                     

1-4: 导入表全貌 —— ntoskrnl 导入函数列表
                              

导入表一读完,心里就有数了:5 个侧信道对应 5 组可疑的 API 调用。接下来只需要对每个 API 做交叉引用(XREF),找到调用点,看看具体怎么用的。

这个调用值得单独拿出来说。导入表的 ExAllocatePool2 XREF 指向 DriverEntry


                                                        
                                     图
1-5: ExAllocatePool2(64, 472, 0x657A614D)

参数拆解:
· 64 = POOL_FLAG_NON_PAGED
· 472 = 0x1D8 字节 = 迷宫上下文结构体大小
· 1702519117 = 0x657A614D = ASCII 'Maze'(池标签)

13×13 = 169 字节迷宫 + 3 字节对齐 = 172 字节在上下文头部,剩余 300 字节存坐标、状态、计数器等。
返回值存入 .data 段全局变量,偏移 0x50B8 —— 这就是 VT 路线的定位点。

IrpDeviceControl 入口虽然跳到 VMProtect thunk,但 switch-case 框架还是看得到三个码:
        

1-6: IOCTL 分发 —— 三码 switch-case

                                                                                                     

MOVE 输入 12 字节:QWORD(编码后方向)+ DWORD(校验值)。编码算法从 EXE 里逆出来:

encode(raw) = ((raw ^ 0x40) >> 5) | (8 * (raw ^ 0xFA))

tamper = LOBYTE(encoded) ^ 0xDEAD1337

1-7: MOVE handler 伪代码 —— 方向编码 + 校验

          

1-8: 0xDEAD1337 防篡改校验逻辑

MOVE 132 字节输出几乎全是 LCG 伪随机垃圾(种子 = KeSystemTime ^ 0xBAADF00D,乘子 1103515245)。纯干扰,完全无意义。

唯一例外:到达终点时,offset 60 = EXIT_MARKER (1464421921)offset 64 开始存 Flag 明文,offset 128 = Flag 长度。这是整个输出缓冲区里唯一有价值的数据 —— 也是后面精确探测法的基石。


 

导入表已经把 5 个侧信道的线索摆在桌面上了。接下来的工作:对每个可疑 API XREF,定位调用点,逆向具体逻辑,编写 R3 检测脚本。

线索:ZwOpenEvent + ZwSetEvent
推断:驱动根据移动结果打开并触发不同的命名事件。

XREF 定位到一个 .text 段小函数 SignalMoveResultRVA 0x22B0213 字节)。没被 VMProtect 保护,伪代码一目了然:

            

2-1: SignalMoveResult —— 命名事件侧信道

// 核心逻辑

if (result == 0 || result == 2)   // 成功

    eventName = L"\\BaseNamedObjects\\MazeMoveOK";

else                               // 撞墙

    eventName = L"\\BaseNamedObjects\\MazeMoveWall";

ZwOpenEvent(&hEvent, EVENT_MODIFY_STATE, &oa);

ZwSetEvent(hEvent, NULL);

ZwClose(hEvent);

检测方式:R3 预先 CreateEventW 创建同名事件,MOVE WaitForSingleObject(50ms) 检测哪个被触发。最稳定的侧信道,约 40-50% 步骤会触发。

线索:KeReleaseSemaphore + ObReferenceObjectByName
推断:通过命名信号量传递信号。问题是名字是什么?

XREF 定位到一个 VMProtect 段内的函数,里面有 XOR 0x4B 解密 GUID 字符串的逻辑:

                                                            

2-2: 信号量 GUID 解密 —— XOR 0x4B 解密 + ObReferenceObjectByName

// 解密后的两个信号量

Global\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}  → 成功

Global\{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}  → 撞墙

调用链:RtlInitUnicodeString → ObReferenceObjectByName → KeReleaseSemaphore

                                                                     

2-3: KeReleaseSemaphore 调用

这里有一个踩坑:信号量有最大计数限制。探索脚本若不及时消费,KeReleaseSemaphore 返回 STATUS_SEMAPHORE_LIMIT_EXCEEDED → 蓝屏。解决方法:每次 MOVE 后立刻 drain 掉信号量。

线索:KeDelayExecutionThread
推断:成功移动时加延迟,撞墙时不加。R3 测时间就行。

                                                                           

2-4: KeDelayExecutionThread —— 延时参数

延时计算:Interval = -10000 * (SharedUserData!SystemTime % 50 + 10),即 10-60ms 随机延迟。
检测:IOCTL 前后取 QPC> 8ms 判成功,< 8ms 判撞墙。
缺点:受系统负载影响大,误判率高。适合当兜底。(需要强调时序检测并不十分可靠,不仅仅是针对这个题目,其实决赛检测hyperV的劫持,也不一定可靠,而且十分依赖启发式的方法)

线索:PsLookup + KeStackAttachProcess + ProbeForWrite 三件套
推断:驱动跨进程附加到调用者地址空间,在 TEB/PEB 写入结果码。

                                                                                                                            

2-5: TEB/PEB 修改全链路 —— PsLookup → KeStackAttach → ProbeForWrite

完整调用链:
1. PsLookupThreadByThreadId → 拿调用者线程对象
2. PsLookupProcessByProcessId → 拿调用者进程对象
3. KeStackAttachProcess → 附加到调用者进程
4. ProbeForWrite → 安全写入结果码
5. KeUnstackDetachProcess → 分离

结果码:PEB+0x68 = 0xC0DE0001(成功)/ 0xC0DE0002(撞墙)

既然有五个侧信道,说明无论哪一个单一侧信道都不是 100% 可靠的。所以我选择多通道优先级链式投票:

优先级:

  event_ok   → 成功     (最高)

  event_wall → 撞墙

  sem_ok     → 成功

  sem_wall   → 撞墙

  peb_code   → 成功/撞墙

  timing>8ms → 成功     (兜底)

  otherwise  → 撞墙     (默认)

 

有了五个侧信道检测工具,接下来的问题是:怎么系统性地把整张 13×13 迷宫摸清楚?

答案是 DFS(深度优先搜索)+ 原地回溯。核心发现:撞墙不改变玩家位置。这意味着不需要 RESET+replay 的传统 BFS 方式,而是可以直接在迷宫中行走与回溯——每次尝试一个方向,用五通道投票判断成功还是撞墙。如果成功,前进到新格子继续探索;如果撞墙,位置不变,试下一个方向。所有方向都试完了就走反方向回溯到上一个格子。

DFS 的天然优势:不需要 RESET,回溯只需走反方向。每条通道只需 2 IOCTL(前进+回退),每堵墙只需 1 次。全 13×13 迷宫约 500 IOCTL30 秒跑完。

探索完成后,在 DFS 建立的邻接表上跑 BFS 求解最短路径。当 BFS 找到从 (0,0) (12,12) 的路径时,即为最短路径。RESET 后沿该路径走到终点,从输出缓冲区提取 Flag

RRRRRRDDRRRRUURRDDDDDDDDLLDDDDRR32 步)
     得到 flag{SHAD0WNT_HY PERVMX}  
                                                                                             

先从 IDA 确定读哪里:


[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

最后于 6天前 被XieCZ1337编辑 ,原因: 调整排版
收藏
免费 1
支持
分享
最新回复 (4)
雪    币: 216
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
你好,初赛的题可以给一份吗
6天前
0
雪    币: 220
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
mb_soqpzdaf 你好,初赛的题可以给一份吗
官网可以下载
5天前
0
雪    币: 216
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
XieCZ1337 官网可以下载
现在不让下载了
5天前
0
雪    币: 7990
活跃值: (7017)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
asd
5
期待后续
16小时前
0
游客
登录 | 注册 方可回帖
返回