这篇文章是我对 ShadowGate 题目的完整复盘整理版。
和我之前的阶段性记录不同,这一版的目标不是只写“我已经做到哪”,而是尽可能把整道题的分析链路整理完整,形成一篇可以单独阅读的文章。
不过我想把边界说清楚:
· 文中有一部分内容,是我自己本地逆向、写工具、验证协议后得到的结论;
· 还有一部分,是我当时没有完全做出来,后面参考了吾爱破解上一篇公开 writeup 才补齐的内容;
· 所以这篇文章不是“纯原创完整通关”,而是一篇以我的分析为主、并结合公开 writeup 补全后续步骤的复盘文。
这样写的好处是比较诚实,也更接近真实的做题过程。
题目样本主要包括三部分:
· 驱动:ShadowGateSys.sys
· IDA 数据库:ShadowGateSys.sys.i64
· 用户态程序:ShadowGateApp.exe
从题目行为来看,它表面上是一个“迷宫 + 驱动 + 最短路”的组合题,但真正核心并不是迷宫本身,而是:
· 用户态如何与驱动通信;
· 驱动如何把“移动结果”反馈回用户态;
· 这些反馈中,哪些是正常返回,哪些是隐藏泄露通道。
如果把题目拆开,其实就是:
1. 加载驱动并正确通信;
2. 找出所有隐匿通信方式;
3. 还原迷宫地图;
4. 求最短路;
5. 到达终点并恢复最终 Flag。
这题最先让我锁定方向的,不是驱动,而是用户态控制台程序。
从 ShadowGateApp.exe 的字符串和控制逻辑里,可以直接得到:
· 设备路径:\\.\ShadowGate
· 两个全局事件:
o Global\MazeMoveOK
o Global\MazeMoveWall
对应截图如下:


这一步非常关键,因为它说明:
· 这题并不是完全黑盒;
· 用户态和驱动之间存在一个清晰可复现的通信入口;
· 同时,“移动结果”很可能还通过命名事件额外传回用户态。
回到 ShadowGateSys.sys 之后,我本地确认到:
· DriverEntry 在 0x140008000
· 很快会跳转到实际初始化例程
初始化阶段至少做了这些事情:
1. 分配一块大小为 0x1D8 的内存;
2. 创建设备对象 \Device\ShadowGate;
3. 创建符号链接 \??\ShadowGate;
4. 注册分发函数。
驱动侧对应设备路径与用户态完全对上:
· 驱动:\Device\ShadowGate
· 符号链接:\??\ShadowGate
· 用户态:\\.\ShadowGate
这意味着我后续完全可以围绕 CreateFileW + DeviceIoControl 来做实验。
我本地确认到的主要分发表如下:
· IRP_MJ_CREATE -> 0x1400014B0
· IRP_MJ_CLOSE -> 0x140001410
· IRP_MJ_DEVICE_CONTROL -> 0x140001540
· DriverUnload -> 0x140001840
所以这题真正要啃的核心函数,其实就是:
· IRP_MJ_DEVICE_CONTROL
也就是说,整道题的协议层本质上就是若干个 IOCTL。
我当时本地最先恢复出来的三个控制码是:
IOCTL
功能
输入
输出
0x8001200C
查询迷宫几何信息
无
24 字节
0x80012008
重置迷宫状态
无
无
0x80012004
执行移动
12 字节
0x84 字节
其中 0x8001200C 返回六个 DWORD:
· width
· height
· entry_x
· entry_y
· exit_x
· exit_y
公开 writeup 里对这三个 IOCTL 的识别图如下:

这一部分我本地和公开 writeup 是能互相印证的。
从 ShadowGateApp.exe 的命令跳转表中,我本地能恢复出一套应用层方向语义:
· UP = 0x10
· DOWN = 0x20
· LEFT = 0x30
· RIGHT = 0x40
接受的按键分别是:
· W / I
· S / K
· A / J
· D / L
程序还支持:
· R:重置
· T:查看日志
· H:帮助
· Q / ESC:退出
到这里为止,我已经能比较稳定地还原“控制台程序怎么看待方向”的这一层。
这是我当时最重视的一部分,因为我觉得这题的本质不在迷宫,而在“反馈机制”。
已经明确存在:
· Global\MazeMoveOK
· Global\MazeMoveWall
这组名字几乎已经把用途写脸上了:
· 成功前进 -> MazeMoveOK
· 撞墙失败 -> MazeMoveWall
我本地还恢复出了两组混淆后的名字:
· Global\{B8E2C3D0
· Global\{A7F3B2C1
结合驱动中出现的:
· ObReferenceObjectByName
· KeReleaseSemaphore
· ObfDereferenceObject
我能比较有把握地说:
· 除了事件以外,驱动还会通过命名信号量额外传递状态。
为了不让分析停留在“静态猜测”,我本地还写了一个工具:
· tools/shadowgate_tool.py
它现在已经能做:
· 打开 \\.\ShadowGate
· 调 query / reset / move
· 监控命名事件
· 监控命名信号量
· 统计耗时
· 读取返回缓冲区关键字段
· 可选监控用户态内存变化
也就是说,我当时虽然还没完整把题做完,但已经把后续实验框架搭起来了。
这一节的内容,严格来说不属于“我当时已经独立做出来的结果”,而是我后面参考了吾爱破解那篇文章之后,重新整理并补进来的。
我会尽量用自己的表达去写,但要明确说明来源。
参考文章:
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 8小时前
被kanxue编辑
,原因: