2026年腾讯游戏安全初赛-PC方向

admin 2026-05-07 05:17:53 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文档记录了2026年腾讯游戏安全竞赛PC方向初赛题解,重点分析了迷宫驱动题的五个信息泄露点(全局事件、GUID信号量、LastError回写、句柄标志检测和延时分析),提供了驱动加载方法、迷宫地图和最短路径。作者通过逆向分析发现驱动导入表暴露关键信息,但存在花指令混淆增加分析难度,最终成功提取flag{SHAD0WNT_HYPERVMX}。 综合评分: 85 文章分类: CTF,逆向分析,漏洞分析,WEB安全,实战经验


cover_image

2026年腾讯游戏安全初赛-PC方向

江树 江树

看雪学苑

2026年5月5日 18:22 上海

在小说阅读器读本章

去阅读

仅为本人题解,并非参考答案不能保证正确,请参照官方公布题解。

〇、得分点

如何加载驱动

关掉ACE预启动,随便签一个泄露签名,即可加载驱动,稳定性极高,就蓝屏了两次。

Flag

flag{SHAD0WNT_HYPERVMX}

最短路径

DDDDDDSSDDDDWWDDSSSSSSSSAASSSSDD
//格式化为
RRRRRRDDRRRRUURRDDDDDDDDLLDDDDRR

地图

###########################
#S . . . . . .#.#. . . . .#
############# ####### ### #
#.#.#.#.#.#.#.#.#.#.#.#.#.#
############# ####### ### #
#. . . . .#.#. . . . .#.#.#
# ####### ############### #
#.#.#.#.#.#.#.#.#.#.#.#.#.#
# ####### ############### #
#.#.#. . . . . . . . .#.#.#
# ### ### ########### ### #
#.#.#.#.#.#.#.#.#.#.#.#.#.#
# ### ### ########### ### #
#.#.#.#.#.#.#. . .#.#.#.#.#
# ### ####### ### ####### #
#.#.#.#.#.#.#.#.#.#.#.#.#.#
# ### ####### ### ####### #
#.#.#. . . . .#.#.#.#. . .#
# ############### ### #####
#.#.#.#.#.#.#.#.#.#.#.#.#.#
# ############### ### #####
#. . .#.#. . .#.#.#.#.#.#.#
##### ### ### ### ### ### #
#.#.#.#.#.#.#.#.#.#.#.#.#.#
##### ### ### ### ### ### #
#. . . . .#.#.#.#. . . . E#
###########################

五个泄漏点(最后一个可能有误)

1. 两个对象名

①MazeMoveOK 事件

  • 类型:命名事件
  • 对象名:Global\MazeMoveOK
  • 驱动侧实现:ZwOpenEvent + ZwSetEvent
  • 含义:成功相关反馈槽位

②MazeMoveWall 事件

  • 类型:命名事件
  • 对象名:Global\MazeMoveWall
  • 驱动侧实现:ZwOpenEvent + ZwSetEvent
  • 含义:失败/撞墙相关反馈槽位

2. 两个 GUID

①GUID1 命名信号量

  • 类型:命名信号量

  • 对象名:

  • Global{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}

  • 驱动侧实现:

  • 解码对象名

  • ObReferenceObjectByName

  • KeReleaseSemaphore

  • 含义:成功相关反馈槽位

②GUID2 命名信号量

  • 类型:命名信号量

  • 对象名:

  • Global{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}

  • 驱动侧实现同上

  • 含义:失败/撞墙相关反馈槽位

3. LastError

  • 类型:线程用户态上下文回写

  • 驱动侧关键函数:sub_140316ADF

  • 关键行为:

  • +0x68 (TEB -> LastErrorValue)

  • 写入值包括:

  • 0xC0DE0001 -> ok

  • 0xC0DE0000 -> wall

4. ZwSetInformationObject

每次move前先调用SetHandleInformation(h, HANDLE_FLAG_PROTECT_FROM_CLOSE, 0)进行清零,在move后调用GetHandleInformation(h, &flags)

if( flags & HANDLE_FLAG_PROTECT_FROM_CLOSE !=0)
   handle_ok =true
else
   handle_ok =False
  • handle_ok = True

  • 表示:成功

  • handle_ok = False

  • 表示:失败/撞墙

5. KeDelayExecutionThread

函数会通过KUSER_SHARED_DATA访问TickCountLowDeprecated,并且在异或后被编码进入返回缓冲,可以在用户层恢复被驱动使用的这个Tick值。

tick_xor&nbsp;= struct.unpack_from("<I", raw,&nbsp;0)[0]
tick&nbsp;= tick_xor ^&nbsp;0xBAADF00D

然后计算

predicted_ms&nbsp;= (tick %&nbsp;50) +&nbsp;10

就是理论上这个线路被延时的毫秒数,然后我们可以计算一个IO请求的耗时

&nbsp;t0 =&nbsp;time.perf_counter()
&nbsp;DeviceIoControl(...)
&nbsp;time_ms = (time.perf_counter() - t0) *&nbsp;1000.0

如果predicted_mstime_ms很接近,就能说明本次触发到了该泄漏点,但是无法判断具体的结果。

  • 很接近

  • 表示:命中该泄漏点,应重试,直到命中前几个可明确结果泄漏点

  • 不接近

  • 表示:应观测其他泄漏点获取本次move的结果

总结

在二进制文件中放了大量的小混淆,导致不管是正向分析还是逆向分析,都不舒服,比如发现了某一个函数,发现xref不可用,因为上游的调用点有花指令,混淆等,导致了IDA不能正确分析,在驱动分析中尤其明显,但是由于驱动导入表暴露了大量的信息,所以都可以通过导入表入手,不完整的分析全流程,也能得到相关结论,如果赛题在导入函数上做手脚,比如动态解析,难度会更大。

很好本来都要提交了,感觉这个寻路脚本咋会这么慢,时间还不稳定,又重新用IDA看了一眼驱动,发现有随机的Sleep感觉大概率是没找齐全,然后就找到了之前没找到的两个泄漏点(但是他们没有影响我得到flag….)

一、赛题文件

文档:2026游戏安全技术竞赛-PC客户端安全-初赛.docx

#「宫殿」的验证机制并不寻常:系统由用户态控制台和内核驱动组成,驱动内部隐藏着一个加密迷阵,所有操作指令必须由控制台通过驱动接口下
# 发。表面上,系统不会对你的任何操作给出反馈——你无法直接判断每一步操作是成功推进,还是被防线拦截。

从上述文本能够判断是迷宫题目,但是应该不是简单的迷宫,应该无法通过传统的迷宫分析方法分析,需要完整的逆向,至少要找到文档中提到的:

# 某些异常现象暗示着隐藏的信息泄露。找到这些线索,你就能感知每一步的结果;发现得越多,破解效率越高。

二、应用层:ShadowGateApp.exe

并未发现有保护壳,直接运行得到:

C:\Users\Euarno\Desktop\2026游戏安全技术竞赛-PC客户端安全-初赛>ShadowGateApp.exe

==============================================
&nbsp; &nbsp; Shadow Palace Gate &nbsp;-&nbsp; ACCESS DENIED
==============================================

&nbsp; ACE has intercepted Shadow's palace gate
&nbsp; system. A kernel driver hides an encrypted
&nbsp; maze inside. Navigate through it to extract
&nbsp; the credential for Shadow's internal network.

&nbsp; The palace gives&nbsp;NO&nbsp;feedback&nbsp;on&nbsp;whether
&nbsp; your moves succeed&nbsp;or&nbsp;hit a wall.

Or&nbsp;does it? The&nbsp;system&nbsp;is&nbsp;not&nbsp;as&nbsp;silent
as&nbsp;it seems. Five hidden flaws betray
&nbsp; the&nbsp;result&nbsp;of&nbsp;every&nbsp;move 鈥?but&nbsp;each&nbsp;move
&nbsp; exposes&nbsp;only&nbsp;one&nbsp;of&nbsp;them.

&nbsp; Hint: after&nbsp;each&nbsp;reset, the&nbsp;first&nbsp;five
&nbsp; successful moves reveal&nbsp;each&nbsp;flaw exactly
&nbsp; once,&nbsp;in&nbsp;a fixed order.

[*] Connecting&nbsp;to&nbsp;Shadow gate driver...
[+] Gate&nbsp;module&nbsp;online.

[*] Maze grid:&nbsp;13x13, Entry=(0,0), Exit=(12,12)
Commands:
&nbsp; W/A/S/D &nbsp; &nbsp;-&nbsp;Navigate Up/Left/Down/Right
&nbsp; I/J/K/L &nbsp; &nbsp;-&nbsp;Navigate Up/Left/Down/Right&nbsp;(alt)
&nbsp; R &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;-&nbsp;Reset&nbsp;to&nbsp;entry point
&nbsp; T &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;-Show&nbsp;operation&nbsp;log&nbsp;(position hidden)
&nbsp; H &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;-Show&nbsp;this help
&nbsp; Q&nbsp;/&nbsp;ESC &nbsp; &nbsp;-&nbsp;Abort mission

可以有一个基本的了解13*13的迷宫,“The palace gives NO feedback on whether your moves succeed or hit a wall.”表面不会有反馈,“Five hidden flaws betray the result of every move ”要通过五个侧信道得到迷宫的反馈,**“after each reset, the first five successful moves reveal each flaw exactly once, in a fixed order. ”**每次你按R重置后,前5次成功移动会依次触发五种不同的泄露机制,顺序是固定的。(这里其实还是有一点疑惑的,最终只找到了三类泄露方式,但也可以算作五个泄漏点)

字符串没有做混淆,在字符串表暴露了很多信息。结合字符串的交叉引用,有如下分析:

__int64&nbsp;OpenShadowGateDevice()
{
&nbsp; __int64 result;&nbsp;// rax
DWORDLastError;&nbsp;// edi

//设备路径
&nbsp; result&nbsp;=&nbsp;(__int64)CreateFileW(L"\\\\.\\ShadowGate",&nbsp;0xC0000000,&nbsp;0,&nbsp;0, 3u, 0x80u,&nbsp;0);
if&nbsp;( result&nbsp;==-1&nbsp;)
&nbsp; {
LastError=GetLastError();
&nbsp; &nbsp; sub_140001010("[ERROR] Failed to open device '%ws'\n",&nbsp;L"\\\\.\\ShadowGate");
&nbsp; &nbsp; sub_140001010("[ERROR] Error code: %lu (0x%08lX)\n",&nbsp;LastError,&nbsp;LastError);
if&nbsp;(&nbsp;LastError==2&nbsp;)
&nbsp; &nbsp; {
//要求我们应该提前加载好驱动 驱动没有签名 我的解决方案是找一个泄露签名 退出ACE预启动 即可加载
&nbsp; &nbsp; &nbsp; sub_140001010("[HINT] Driver not loaded. Use: sc create ShadowGate type=kernel binPath=<path>\\ShadowGateSys.sys\n");
&nbsp; &nbsp; &nbsp; sub_140001010("[HINT] Then: sc start ShadowGate\n");
return&nbsp;-1;
&nbsp; &nbsp; }
else
&nbsp; &nbsp; {
if&nbsp;(&nbsp;LastError==5&nbsp;)
&nbsp; &nbsp; &nbsp; &nbsp; sub_140001010("[HINT] Run as Administrator.\n");
return&nbsp;-1;
&nbsp; &nbsp; }
&nbsp; }
return&nbsp;result;
}

虽然还没有分析驱动,但是应用创建了两个全局命名事件,名称强烈暗示它们分别对应移动成功和撞墙,即失败。

HANDLE CreateLeakEvents()
{
&nbsp; HANDLE&nbsp;result; // rax

&nbsp; hObject = CreateEventW(0,&nbsp;1,&nbsp;0, L"Global\\MazeMoveOK");
result&nbsp;= CreateEventW(0,&nbsp;1,&nbsp;0, L"Global\\MazeMoveWall");
&nbsp; qword_140005688 =&nbsp;result;
return&nbsp;result;
}

通信方面使用了最基本的IO通讯,分析DeviceIoControl的交叉引用可以得到所有的IO码。

if&nbsp;( QueryMaza(v6, &v9) )
printf(
"[*] Maze grid: %ux%u, Entry=(%u,%u), Exit=(%u,%u)\n",
&nbsp; &nbsp; &nbsp; &nbsp; (_DWORD)v9,
&nbsp; &nbsp; &nbsp; &nbsp; DWORD1(v9),
&nbsp; &nbsp; &nbsp; &nbsp; DWORD2(v9),
&nbsp; &nbsp; &nbsp; &nbsp; HIDWORD(v9),
&nbsp; &nbsp; &nbsp; &nbsp; v10,
&nbsp; &nbsp; &nbsp; &nbsp; HIDWORD(v10));

BOOL __fastcall QueryMaza(__int64 a1, void *lpOutBuffer)
{
&nbsp; DWORD BytesReturned;&nbsp;//&nbsp;[rsp+40h] [rbp-18h] BYREF

&nbsp; BytesReturned =&nbsp;0;
return&nbsp;DeviceIoControl(hDevice,&nbsp;0x8001200C,&nbsp;0,&nbsp;0, lpOutBuffer,&nbsp;0x18u, &BytesReturned,&nbsp;0);
}

显然0x8001200C用于查询迷宫信息,接下来就能发现main函数有大量的花指令了,因为正常的R等逻辑的处理代码都不存在。

只需要把:

push&nbsp;rcx
ret

改为:

jmp rcx

伪代码就重建好了,然后依旧是出题人的小礼物啊:

v8 -=&nbsp;27;
switch&nbsp;( v8 )

这里会有垂落,处理的是大小写R的情况:

case&nbsp;'7':
case&nbsp;'W':
ResetMaze(_RCX);
&nbsp; &nbsp; &nbsp;v6 =&nbsp;0;
&nbsp; &nbsp; &nbsp;dword_140005668 =&nbsp;0;
&nbsp; &nbsp; &nbsp;v22[0] =&nbsp;0;
&nbsp; &nbsp; &nbsp;v7 =&nbsp;0;
printf("[*] Reset to entry point.\n");
continue;

BOOL&nbsp;ResetMaze(){
&nbsp; DWORD BytesReturned;&nbsp;// [rsp+40h] [rbp-18h] BYREF

&nbsp; BytesReturned =&nbsp;0;
return&nbsp;DeviceIoControl(hDevice,&nbsp;0x80012008,&nbsp;0,&nbsp;0,&nbsp;0,&nbsp;0, &BytesReturned,&nbsp;0);
}

显然0x80012008用于重新开始,那剩下的操作码肯定是操作迷宫了的。

| IOCTL | 作用 | | — | — | | 0x80012004 | 移动/核心交互 | | 0x80012008 | 重置到起点 | | 0x8001200C | 查询迷宫信息 |

紧接着还要详细分析,相关的按键被映射为了十六进制整数,如下:

case&nbsp;'&':
case&nbsp;'/':
case&nbsp;'F':
case&nbsp;'O':
&nbsp; &nbsp; &nbsp; &nbsp; LOBYTE(direct) =&nbsp;0x30;&nbsp;//A &nbsp;J
&nbsp; &nbsp; &nbsp; &nbsp; v12 =&nbsp;76;
goto&nbsp;LABEL_11;
case&nbsp;')':
case&nbsp;'1':
case&nbsp;'I':
case&nbsp;'Q':
&nbsp; &nbsp; &nbsp; &nbsp; LOBYTE(direct) =&nbsp;0x40;// D &nbsp;L
&nbsp; &nbsp; &nbsp; &nbsp; __asm&nbsp;{ rcl &nbsp; &nbsp; al, cl }
&nbsp; &nbsp; &nbsp; &nbsp; v12 =&nbsp;82;
goto&nbsp;LABEL_11;
case&nbsp;'.':
case&nbsp;'<':
case&nbsp;'N':
case&nbsp;'\\':
&nbsp; &nbsp; &nbsp; &nbsp; LOBYTE(direct) =&nbsp;0x10;&nbsp;//W &nbsp;I
&nbsp; &nbsp; &nbsp; &nbsp; v12 =&nbsp;85;
goto&nbsp;LABEL_11;
case&nbsp;'0':
case&nbsp;'8':
case&nbsp;'P':
case&nbsp;'X':
&nbsp; &nbsp; &nbsp; &nbsp; LOBYTE(direct) =&nbsp;0x20;&nbsp;// S &nbsp; K
&nbsp; &nbsp; &nbsp; &nbsp; v12 =&nbsp;68;
LABEL_11:
if&nbsp;( v6 <&nbsp;255&nbsp;)
&nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; v22[v7] = v12;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ++v6;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ++v7;
if&nbsp;( (unsignedint)v6 >=&nbsp;0x100&nbsp;)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sub_140001E88(_RCX, direct);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; v22[v7] =&nbsp;0;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; v14 = (unsignedint)dword_140005668;
&nbsp; &nbsp; &nbsp; &nbsp; memset(v21,&nbsp;0,&nbsp;sizeof(v21));
&nbsp; &nbsp; &nbsp; &nbsp; v18 =&nbsp;0;
&nbsp; &nbsp; &nbsp; &nbsp; ++dword_140005668;
&nbsp; &nbsp; &nbsp; &nbsp; v15 = TryMove(hDevice, direct, v14, v21, &v18);

TryMove对于路径的打包如下:

__int64 v16;
unsignedint&nbsp;v17;

v16 = (unsigned&nbsp;__int8)v9;&nbsp;//只用了第一字节?
v17 = a3 ^ v9 ^&nbsp;0xDEAD1337;

DeviceIoControl(a1,&nbsp;0x80012004, &v16,&nbsp;0xCu, v14,&nbsp;0x84u, &v15,&nbsp;0)&nbsp;//12字节大小数据包 从v16开始写入

其中,具体的加密逻辑是这样:

__int64 __fastcall&nbsp;TryMove(void&nbsp;*a1, __int64 _RDX,&nbsp;int&nbsp;op_count, _BYTE *a4, _DWORD *a5){
//rdx是代表方向的十六进制整数 op_count是一个计数器,代表次数
&nbsp; &nbsp; v6 = _RDX ^&nbsp;0xFA;
&nbsp; &nbsp; v9 = (unsigned&nbsp;__int8)(_RDX | (8&nbsp;* v6));
&nbsp; &nbsp; v16 = (unsigned&nbsp;__int8)v9;&nbsp;//结构体开始
&nbsp; &nbsp; v17 = op_count ^ v9 ^&nbsp;0xDEAD1337
if&nbsp;(&nbsp;DeviceIoControl(a1,&nbsp;0x80012004, &v16,&nbsp;0xCu, v14,&nbsp;0x84u, &v15,&nbsp;0) )
&nbsp; &nbsp; &nbsp; &nbsp; ...
}

足够我们重建结构体了,但是伪代码有一个问题,驱动如何校验v17的正确性呢?切换到汇编,根据DeviceIoControl的参数可以知道。

lea &nbsp; &nbsp; r8,&nbsp;[r11-30h]&nbsp; &nbsp;; lpInBuffer

然后看汇编层面是如何写入[r11-30h]的:

mov &nbsp; &nbsp;&nbsp;[r11-30h], al
mov &nbsp; &nbsp;&nbsp;[r11-2Ch], r8d
mov &nbsp; &nbsp;&nbsp;[r11-28h], eax

统一成buf的偏移就是:

[buf+0] 写入&nbsp;1&nbsp;字节
[buf+4] 写入&nbsp;4&nbsp;字节
[buf+8] 写入&nbsp;4&nbsp;字节

struct&nbsp;{
uint8_t&nbsp; field0;
uint8_t&nbsp; pad[3];
uint32_t&nbsp;field4;
uint32_t&nbsp;field8;
};

也就能重建结构体为:

typedef&nbsp;struct&nbsp;_MOVE_REQ&nbsp;{
uint8_t&nbsp; encoded_dir;&nbsp;//映射为了十六进制整数
uint8_t&nbsp; pad[3]; &nbsp; &nbsp; &nbsp;//数据对齐
uint32_t&nbsp;op_count; &nbsp; &nbsp;//操作次数
uint32_t&nbsp;checksum; &nbsp; &nbsp;//校验点
&nbsp;} MOVE_REQ;&nbsp;//一共12字节

接下来是输入T的问题:

case&nbsp;'9':
case&nbsp;'Y':
sub_140001010(asc_1400038E0, (unsignedint)v6);
&nbsp; &nbsp; &nbsp; v17 = v22;
if&nbsp;( v6 <=&nbsp;0&nbsp;)
&nbsp; &nbsp; &nbsp; &nbsp; v17 =&nbsp;"(none)";
sub_140001010(asc_140003908, v17);
continue;

命令 T的作用并不是显示当前位置,而是输出应用层维护的成功移动日志。主循环中,每当一次移动被认为成功时,程序都会将对应方向字符L R U D追加到缓冲区 v22 中,而 T/t分支则负责打印当前操作计数及该字符串,也就是这里只能打印成功的路径。

先玩一下,也就是可以借助前五个正确路径找到五个泄漏点,因为提到了前五次成功顺次泄露,会在后边起关键作用。

- D D D D D T

程序输出:

- Sequence: RRRRR

三、驱动层:ShadowGateSys.sys

也是见到没有壳子轻松达到4 MB的驱动文件了,混淆或者花指令估计是跑不了了,先迎接出题人的小礼物,重建sub_140003208:

0x140003244&nbsp;&nbsp;call&nbsp;sub_1400018A0

patch为:

mov eax, 0

伪代码重建成功:

__int64 __fastcall&nbsp;sub_140003208(struct&nbsp;_DRIVER_OBJECT *a1){
&nbsp; NTSTATUS v3;&nbsp;// eax
&nbsp; NTSTATUS v4;&nbsp;// edi
struct&nbsp;_UNICODE_STRING&nbsp;DestinationString;&nbsp;// [rsp+40h] [rbp-18h] BYREF

&nbsp; P = (PVOID)ExAllocatePool2(64,&nbsp;472,&nbsp;1702519117);
if&nbsp;( !P )
return&nbsp;3221225626LL;
KeInitializeSpinLock(&SpinLock);
KeInitializeSpinLock(&qword_1400050D0);
&nbsp; ((void&nbsp;(*)(void))loc_140001E60)();
&nbsp; DestinationString =&nbsp;0;
RtlInitUnicodeString(&DestinationString,&nbsp;L"\\Device\\ShadowGate");
&nbsp; v3 =&nbsp;IoCreateDevice(a1,&nbsp;0, &DestinationString,&nbsp;0x22u,&nbsp;0x100u,&nbsp;0, &DeviceObject);
&nbsp; v4 = v3;
if&nbsp;( v3 <&nbsp;0&nbsp;)
&nbsp; {
&nbsp; &nbsp; _mm_lfence();
ExFreePoolWithTag(P,&nbsp;0x657A614Du);
LABEL_5:
&nbsp; &nbsp; P =&nbsp;0;
return&nbsp;(unsignedint)v4;
&nbsp; }
RtlInitUnicodeString(&SymbolicLinkName,&nbsp;L"\\??\\ShadowGate");
&nbsp; v4 =&nbsp;IoCreateSymbolicLink(&SymbolicLinkName, &DestinationString);
if&nbsp;( v4 <&nbsp;0&nbsp;)
&nbsp; {
&nbsp; &nbsp; _mm_lfence();
IoDeleteDevice(DeviceObject);
ExFreePoolWithTag(P,&nbsp;0x657A614Du);
&nbsp; &nbsp; DeviceObject =&nbsp;0;
goto&nbsp;LABEL_5;
&nbsp; }
&nbsp; a1->DriverUnload = (PDRIVER_UNLOAD)sub_140001840;
&nbsp; a1->MajorFunction[0] = (PDRIVER_DISPATCH)sub_1400014B0;
&nbsp; a1->MajorFunction[2] = (PDRIVER_DISPATCH)sub_140001410;
&nbsp; a1->MajorFunction[14] = (PDRIVER_DISPATCH)sub_140001540;
&nbsp; DeviceObject->Flags |=&nbsp;4u;
&nbsp; DeviceObject->Flags &= ~0x80u;
return&nbsp;0;
}

接下来寻5个泄漏点,刚才在应用层找到的事件,先看字符串定位过去,找到第一个泄漏点。

① 事件泄露

int&nbsp;__fastcall&nbsp;sub_1400022B0(__int64 a1,&nbsp;int&nbsp;a2){
int&nbsp;result;&nbsp;// eax
const&nbsp;WCHAR *v4;&nbsp;// rdx
struct&nbsp;_UNICODE_STRING&nbsp;DestinationString;&nbsp;// [rsp+20h] [rbp-40h] BYREF
struct&nbsp;_OBJECT_ATTRIBUTES&nbsp;ObjectAttributes;&nbsp;// [rsp+30h] [rbp-30h] BYREF
void&nbsp;*EventHandle;&nbsp;// [rsp+80h] [rbp+20h] BYREF

&nbsp; result = (unsigned&nbsp;__int8)dword_140005000;
if&nbsp;( (((_BYTE)dword_140005000 * ((_BYTE)dword_140005000 -&nbsp;1)) &&nbsp;1) ==&nbsp;0&nbsp;)
&nbsp; {
if&nbsp;( !a2 || (v4 =&nbsp;L"\\BaseNamedObjects\\MazeMoveWall", a2 ==&nbsp;2) )
&nbsp; &nbsp; &nbsp; v4 =&nbsp;L"\\BaseNamedObjects\\MazeMoveOK";
RtlInitUnicodeString(&DestinationString, v4);
&nbsp; &nbsp; ObjectAttributes.Length =&nbsp;48;
&nbsp; &nbsp; ObjectAttributes.ObjectName = &DestinationString;
&nbsp; &nbsp; ObjectAttributes.RootDirectory =&nbsp;0;
&nbsp; &nbsp; ObjectAttributes.Attributes =&nbsp;576;
&nbsp; &nbsp; EventHandle =&nbsp;0;
&nbsp; &nbsp; *(_OWORD *)&ObjectAttributes.SecurityDescriptor =&nbsp;0;
&nbsp; &nbsp; result =&nbsp;ZwOpenEvent(&EventHandle,&nbsp;2u, &ObjectAttributes);
if&nbsp;( result >=&nbsp;0&nbsp;)
&nbsp; &nbsp; {
ZwSetEvent(EventHandle,&nbsp;0);
return&nbsp;ZwClose(EventHandle);
&nbsp; &nbsp; }
&nbsp; }
return&nbsp;result;
}

该函数根据传入状态参数,选择  **\BaseNamedObjects\MazeMoveOK **或\BaseNamedObjects\MazeMoveWall

随后通过ZwOpenEvent打开对应命名事件,并调用ZwSetEvent将其置位,最后关闭句柄。

由于应用层事先创建了同名全局事件Global\MazeMoveOKGlobal\MazeMoveWall

因此用户态可在每次发送移动请求后轮询这两个事件,从而判断本次移动是成功推进还是撞墙失败。

② GUID泄露

然后在导入表发现了函数KeReleaseSemaphoreObReferenceObjectByName跟到,sub_140319A37,有明显的选择和解密流程。

if&nbsp;( !v5 || (v9 = &unk_1400041E0, v5 ==&nbsp;2) )
&nbsp; &nbsp; &nbsp; &nbsp; v9 = &unk_140004160;
&nbsp; &nbsp; &nbsp; v10 =&nbsp;57;
&nbsp; &nbsp; &nbsp; v11 = v9 - (_BYTE *)v18;
&nbsp; &nbsp; &nbsp; v12 = v18;
do
&nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; *v12 = *(WCHAR *)((char&nbsp;*)v12 + v11) ^&nbsp;0x4B;&nbsp;//解密
&nbsp; &nbsp; &nbsp; &nbsp; ++v12;
&nbsp; &nbsp; &nbsp; &nbsp; --v10;
&nbsp; &nbsp; &nbsp; }
while&nbsp;( v10 );
&nbsp; &nbsp; }
RtlInitUnicodeString((PUNICODE_STRING)v17, v18);&nbsp;//构建字符串
ObReferenceObjectByName(v17,&nbsp;64,&nbsp;0);

就要看看unk_1400041E0unk_140004160对应的到底是什么字符串了。

def&nbsp;decode_guid_name(words):
&nbsp; &nbsp; return&nbsp;''.join(chr(w ^&nbsp;0x4B) for w in words)

unk_140004160 = [0x0017,&nbsp;0x0009,&nbsp;0x002A,&nbsp;0x0038,&nbsp;0x002E,&nbsp;0x0005,&nbsp;0x002A,&nbsp;0x0026,&nbsp;0x002E,&nbsp;0x002F,&nbsp;0x0004,&nbsp;0x0029,&nbsp;0x0021,&nbsp;0x002E,&nbsp;0x0028,&nbsp;0x003F,&nbsp;0x0038,&nbsp;0x0017,&nbsp;0x0030,&nbsp;0x000A,&nbsp;0x007C,&nbsp;0x000D,&nbsp;0x0078,&nbsp;0x0009,&nbsp;0x0079,&nbsp;0x0008,&nbsp;0x007A,&nbsp;0x0066,&nbsp;0x0072,&nbsp;0x000E,&nbsp;0x007F,&nbsp;0x000F,&nbsp;0x0066,&nbsp;0x007F,&nbsp;0x0008,&nbsp;0x0073,&nbsp;0x000A,&nbsp;0x0066,&nbsp;0x0009,&nbsp;0x007E,&nbsp;0x000F,&nbsp;0x007D,&nbsp;0x0066,&nbsp;0x007A,&nbsp;0x000D,&nbsp;0x0079,&nbsp;0x000E,&nbsp;0x0078,&nbsp;0x000A,&nbsp;0x007F,&nbsp;0x0009,&nbsp;0x007E,&nbsp;0x0008,&nbsp;0x007D,&nbsp;0x000F,&nbsp;0x0036,&nbsp;0x004B,&nbsp;0x0000,&nbsp;0x0000,&nbsp;0x0000,&nbsp;0x0000,&nbsp;0x0000,&nbsp;0x0000,&nbsp;0x0000]

unk_1400041E0 = [0x0017,&nbsp;0x0009,&nbsp;0x002A,&nbsp;0x0038,&nbsp;0x002E,&nbsp;0x0005,&nbsp;0x002A,&nbsp;0x0026,&nbsp;0x002E,&nbsp;0x002F,&nbsp;0x0004,&nbsp;0x0029,&nbsp;0x0021,&nbsp;0x002E,&nbsp;0x0028,&nbsp;0x003F,&nbsp;0x0038,&nbsp;0x0017,&nbsp;0x0030,&nbsp;0x0009,&nbsp;0x0073,&nbsp;0x000E,&nbsp;0x0079,&nbsp;0x0008,&nbsp;0x0078,&nbsp;0x000F,&nbsp;0x007B,&nbsp;0x0066,&nbsp;0x007B,&nbsp;0x000D,&nbsp;0x007E,&nbsp;0x000A,&nbsp;0x0066,&nbsp;0x007E,&nbsp;0x000F,&nbsp;0x0072,&nbsp;0x0009,&nbsp;0x0066,&nbsp;0x0008,&nbsp;0x007D,&nbsp;0x000E,&nbsp;0x007C,&nbsp;0x0066,&nbsp;0x0079,&nbsp;0x000A,&nbsp;0x0078,&nbsp;0x000D,&nbsp;0x007F,&nbsp;0x0009,&nbsp;0x007E,&nbsp;0x0008,&nbsp;0x007D,&nbsp;0x000F,&nbsp;0x007C,&nbsp;0x000E,&nbsp;0x0036,&nbsp;0x004B,&nbsp;0x0000,&nbsp;0x0000,&nbsp;0x0000,&nbsp;0x000E,&nbsp;0x0033,&nbsp;0x0018,&nbsp;0x002E]

print(decode_guid_name(unk_140004160))
print(decode_guid_name(unk_1400041E0))

得到输出,也就找到了第二个泄漏点:

\BaseNamedObjects\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}KKKKKKK
\BaseNamedObjects\{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}KKKExSe
//手动去尾巴ing
\BaseNamedObjects\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}
\BaseNamedObjects\{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}

但是暂时不能确定哪个是撞墙,哪个是成功,要具体测试一下。

#include&nbsp;<windows.h>
#include&nbsp;<iostream>
#include&nbsp;<string>
#include&nbsp;<vector>
#include&nbsp;<cstdint>

staticconstwchar_t* DEVICE_NAME =&nbsp;L"\\\\.\\ShadowGate";
staticconstwchar_t* GUID1_NAME =&nbsp;L"Global\\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}";
staticconstwchar_t* GUID2_NAME =&nbsp;L"Global\\{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}";

staticconst&nbsp;DWORD IOCTL_MOVE =&nbsp;0x80012004;
staticconst&nbsp;DWORD IOCTL_RESET =&nbsp;0x80012008;
staticconst&nbsp;DWORD IOCTL_QUERY =&nbsp;0x8001200C;

#pragma&nbsp;pack(push, 1)
struct&nbsp;MOVE_REQ&nbsp;{
uint8_t&nbsp; encoded_dir;
uint8_t&nbsp; pad[3];
uint32_t&nbsp;op_count;
uint32_t&nbsp;checksum;
};
#pragma&nbsp;pack(pop)

staticuint8_tror8(uint8_t&nbsp;x,&nbsp;int&nbsp;n)&nbsp;{
return&nbsp;static_cast<uint8_t>((x >> n) | (x << (8&nbsp;- n)));
}

staticuint8_tencode_dir(uint8_t&nbsp;move_code)&nbsp;{
return&nbsp;ror8(static_cast<uint8_t>(move_code ^&nbsp;0x5A),&nbsp;5);
}

staticuint8_tmove_code_from_char(char&nbsp;ch)&nbsp;{
switch&nbsp;(ch) {
case&nbsp;'W':&nbsp;case&nbsp;'w':&nbsp;return&nbsp;0x10;
case&nbsp;'S':&nbsp;case&nbsp;'s':&nbsp;return&nbsp;0x20;
case&nbsp;'A':&nbsp;case&nbsp;'a':&nbsp;return&nbsp;0x30;
case&nbsp;'D':&nbsp;case&nbsp;'d':&nbsp;return&nbsp;0x40;
default:&nbsp;return&nbsp;0;
&nbsp; &nbsp; }
}

staticvoiddrain_semaphore(HANDLE hSem)&nbsp;{
while&nbsp;(WaitForSingleObject(hSem,&nbsp;0) == WAIT_OBJECT_0) {
&nbsp; &nbsp; }
}

staticboolpoll_semaphore(HANDLE hSem)&nbsp;{
&nbsp; &nbsp; DWORD rc =&nbsp;WaitForSingleObject(hSem,&nbsp;0);
return&nbsp;rc == WAIT_OBJECT_0;
}

staticboolreset_maze(HANDLE hDev)&nbsp;{
&nbsp; &nbsp; DWORD bytesReturned =&nbsp;0;
return&nbsp;DeviceIoControl(
&nbsp; &nbsp; &nbsp; &nbsp; hDev,
&nbsp; &nbsp; &nbsp; &nbsp; IOCTL_RESET,
nullptr,&nbsp;0,
nullptr,&nbsp;0,
&nbsp; &nbsp; &nbsp; &nbsp; &bytesReturned,
nullptr
&nbsp; &nbsp; ) != FALSE;
}

staticboolmove_once(HANDLE hDev,&nbsp;char&nbsp;ch,&nbsp;uint32_t&nbsp;opCount)&nbsp;{
uint8_t&nbsp;mc =&nbsp;move_code_from_char(ch);
if&nbsp;(!mc) {
&nbsp; &nbsp; &nbsp; &nbsp; std::cerr <<&nbsp;"invalid move char: "&nbsp;<< ch <<&nbsp;"\n";
return&nbsp;false;
&nbsp; &nbsp; }

&nbsp; &nbsp; MOVE_REQ req{};
&nbsp; &nbsp; req.encoded_dir =&nbsp;encode_dir(mc);
&nbsp; &nbsp; req.op_count = opCount;
&nbsp; &nbsp; req.checksum = req.encoded_dir ^ opCount ^&nbsp;0xDEAD1337u;

uint8_t&nbsp;outbuf[0x84] = {};
&nbsp; &nbsp; DWORD bytesReturned =&nbsp;0;

return&nbsp;DeviceIoControl(
&nbsp; &nbsp; &nbsp; &nbsp; hDev,
&nbsp; &nbsp; &nbsp; &nbsp; IOCTL_MOVE,
&nbsp; &nbsp; &nbsp; &nbsp; &req,
sizeof(req),
&nbsp; &nbsp; &nbsp; &nbsp; outbuf,
sizeof(outbuf),
&nbsp; &nbsp; &nbsp; &nbsp; &bytesReturned,
nullptr
&nbsp; &nbsp; ) != FALSE;
}

intwmain(int&nbsp;argc,&nbsp;wchar_t* argv[])&nbsp;{
&nbsp; &nbsp; std::string path =&nbsp;"DDDDD";
if&nbsp;(argc >=&nbsp;2) {
&nbsp; &nbsp; &nbsp; &nbsp; std::wstring ws = argv[1];
&nbsp; &nbsp; &nbsp; &nbsp; path.assign(ws.begin(), ws.end());
&nbsp; &nbsp; }

&nbsp; &nbsp; HANDLE hDev =&nbsp;CreateFileW(
&nbsp; &nbsp; &nbsp; &nbsp; DEVICE_NAME,
&nbsp; &nbsp; &nbsp; &nbsp; GENERIC_READ | GENERIC_WRITE,
0,
nullptr,
&nbsp; &nbsp; &nbsp; &nbsp; OPEN_EXISTING,
&nbsp; &nbsp; &nbsp; &nbsp; FILE_ATTRIBUTE_NORMAL,
nullptr
&nbsp; &nbsp; );
if&nbsp;(hDev == INVALID_HANDLE_VALUE) {
&nbsp; &nbsp; &nbsp; &nbsp; std::cerr <<&nbsp;"CreateFileW failed, gle=0x"&nbsp;<< std::hex <<&nbsp;GetLastError() <<&nbsp;"\n";
return&nbsp;1;
&nbsp; &nbsp; }

&nbsp; &nbsp; HANDLE hGuid1 =&nbsp;CreateSemaphoreW(nullptr,&nbsp;0,&nbsp;0x7fffffff, GUID1_NAME);
&nbsp; &nbsp; HANDLE hGuid2 =&nbsp;CreateSemaphoreW(nullptr,&nbsp;0,&nbsp;0x7fffffff, GUID2_NAME);
if&nbsp;(!hGuid1 || !hGuid2) {
&nbsp; &nbsp; &nbsp; &nbsp; std::cerr <<&nbsp;"CreateSemaphoreW failed, gle=0x"&nbsp;<< std::hex <<&nbsp;GetLastError() <<
"\n";
CloseHandle(hDev);
return&nbsp;1;
&nbsp; &nbsp; }

if&nbsp;(!reset_maze(hDev)) {
&nbsp; &nbsp; &nbsp; &nbsp; std::cerr <<&nbsp;"reset failed, gle=0x"&nbsp;<< std::hex <<&nbsp;GetLastError() <<&nbsp;"\n";
CloseHandle(hGuid1);
CloseHandle(hGuid2);
CloseHandle(hDev);
return&nbsp;1;
&nbsp; &nbsp; }

uint32_t&nbsp;opCount =&nbsp;0;
&nbsp; &nbsp; std::cout <<&nbsp;"Testing path: "&nbsp;<< path <<&nbsp;"\n";

for&nbsp;(size_t&nbsp;i =&nbsp;0; i < path.size(); ++i) {
drain_semaphore(hGuid1);
drain_semaphore(hGuid2);

char&nbsp;ch = path[i];
if&nbsp;(!move_once(hDev, ch, opCount)) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; std::cerr <<&nbsp;"move "&nbsp;<< ch <<&nbsp;" failed at step "&nbsp;<< i
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <<&nbsp;", gle=0x"&nbsp;<< std::hex <<&nbsp;GetLastError() <<&nbsp;"\n";
break;
&nbsp; &nbsp; &nbsp; &nbsp; }

bool&nbsp;g1 =&nbsp;poll_semaphore(hGuid1);
bool&nbsp;g2 =&nbsp;poll_semaphore(hGuid2);

&nbsp; &nbsp; &nbsp; &nbsp; std::cout <<&nbsp;"step "&nbsp;<< (i +&nbsp;1)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <<&nbsp;" move="&nbsp;<< ch
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <<&nbsp;" opCount="&nbsp;<< std::dec << opCount
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <<&nbsp;" guid1="&nbsp;<< (g1 ?&nbsp;"triggered"&nbsp;:&nbsp;"no")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <<&nbsp;" guid2="&nbsp;<< (g2 ?&nbsp;"triggered"&nbsp;:&nbsp;"no")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <<&nbsp;"\n";

&nbsp; &nbsp; &nbsp; &nbsp; ++opCount;
&nbsp; &nbsp; }

CloseHandle(hGuid1);
CloseHandle(hGuid2);
CloseHandle(hDev);
return&nbsp;0;
}

测试DDDDD得到:

Testing path: DDDDD
step&nbsp;1&nbsp;move=D opCount=0&nbsp;guid1=no&nbsp;guid2=no
step&nbsp;2&nbsp;move=D opCount=1&nbsp;guid1=triggered guid2=no
step&nbsp;3&nbsp;move=D opCount=2&nbsp;guid1=no&nbsp;guid2=no
step&nbsp;4&nbsp;move=D opCount=3&nbsp;guid1=no&nbsp;guid2=no
step&nbsp;5&nbsp;move=D opCount=4&nbsp;guid1=no&nbsp;guid2=no

那么路径正确时候会触发Global{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}

#Global\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D} -> 成功信号量
#Global\{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E} -> 撞墙信号量

③ TEB LastError泄露

还是要看导入表:

&nbsp; -&nbsp;PsGetCurrentProcessId
&nbsp; -&nbsp;PsGetCurrentThreadId
&nbsp; -&nbsp;PsLookupProcessByProcessId
&nbsp; -&nbsp;PsLookupThreadByThreadId
&nbsp; -&nbsp;KeStackAttachProcess
&nbsp; -&nbsp;KeUnstackDetachProcess
&nbsp; -&nbsp;PsGetProcessPeb
&nbsp; -&nbsp;ZwQueryVirtualMemory

首先想到的可能是调试状态,或者LastError,对于LastError的话,尝试找找有没有类似的写入逻辑。

#结构:TEB -> LastErrorValue (DWORD)
#偏移:
#x86 (32 位):TEB + 0x34
#x64 (64 位):TEB + 0x68
int&nbsp;__fastcall&nbsp;sub_140316ADF(__int64 _RCX,&nbsp;int&nbsp;a2){
&nbsp; __int16 _AX;&nbsp;// ax
unsigned&nbsp;__int64 v4;&nbsp;// rax
&nbsp; __int64 v6;&nbsp;// rbx
void&nbsp;*v7;&nbsp;// rcx
&nbsp; __int64 v8;&nbsp;// rax
int&nbsp;*v9;&nbsp;// rsi
int&nbsp;v10;&nbsp;// ebx
&nbsp; __int64 v12;&nbsp;// [rsp-20h] [rbp-78h] BYREF
&nbsp; PEPROCESS Process;&nbsp;// [rsp+0h] [rbp-58h] BYREF
&nbsp; PVOID Object;&nbsp;// [rsp+8h] [rbp-50h] BYREF
struct&nbsp;_KAPC_STATE&nbsp;ApcState;&nbsp;// [rsp+10h] [rbp-48h] BYREF

&nbsp; _AX =&nbsp;0;
&nbsp; __asm { rcl &nbsp; &nbsp; ax, cl }
&nbsp; v4 = (unsigned&nbsp;__int64)&v12 ^ _security_cookie;
&nbsp; v6 = _RCX;
&nbsp; v7 = *(void&nbsp;**)(_RCX +&nbsp;464);
if&nbsp;( v7 )
&nbsp; {
if&nbsp;( *(_QWORD *)(v6 +&nbsp;456) )
&nbsp; &nbsp; {
if&nbsp;( qword_140005080 )
&nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; Object =&nbsp;0;
LODWORD(v4) =&nbsp;PsLookupThreadByThreadId(v7, (PETHREAD *)&Object);
if&nbsp;( (v4 &&nbsp;0x80000000) ==&nbsp;0LL&nbsp;)
&nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Process =&nbsp;0;
if&nbsp;(&nbsp;PsLookupProcessByProcessId(*(HANDLE *)(v6 +&nbsp;456), &Process) >=&nbsp;0&nbsp;)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
KeStackAttachProcess(Process, &ApcState);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; v8 =&nbsp;qword_140005080(Object);&nbsp;//qword_140005080 是 PsGetThreadTeb v8是Teb了
if&nbsp;( v8 )
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; v9 = (int&nbsp;*)(v8 +&nbsp;0x68);&nbsp;//这里取出LastError的地址
if&nbsp;( (((_BYTE)dword_140005000 * ((_BYTE)dword_140005000 -&nbsp;1)) &&nbsp;1) !=&nbsp;0&nbsp;)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; v10 =&nbsp;0xDEADDEAD; &nbsp;&nbsp;//1
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
else&nbsp;if&nbsp;( a2 )
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; v10 =&nbsp;0xC0DE0002; &nbsp;&nbsp;//2
if&nbsp;( a2 !=&nbsp;2&nbsp;)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; v10 =&nbsp;0xC0DE0000;&nbsp;//3
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
else
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; v10 =&nbsp;0xC0DE0001; &nbsp;&nbsp;//4
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
ProbeForWrite(v9,&nbsp;4u,&nbsp;4u);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; *v9 = v10;&nbsp;//这里对LaseError做了覆写
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
KeUnstackDetachProcess(&ApcState);
ObfDereferenceObject(Process);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
LODWORD(v4) =&nbsp;ObfDereferenceObject(Object);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp; }
return&nbsp;v4;
}

显然可以看到:

v9 = (int&nbsp;*)(v8 +&nbsp;0x68);&nbsp;//这里取出LastError的地址
*v9 = v10;&nbsp;//这里对LaseError做了覆写

那么LastError应该是下一个泄漏点了:

| Code | 含义 | | — | — | | 0xC0DE0001 | ok | | 0xC0DE0002 | 到达终点? | | 0xC0DE0000 | wall | | 0xDEADDEAD | 干扰值 |

④ ZwSetInformationObject

刚才我们提到qword_140005080PsGetThreadTeb,相关的交叉引用还有一个函数。

void&nbsp;sub_14031857E()
{
&nbsp; __int64 v0;&nbsp;// rax
&nbsp; _QWORD *v1;&nbsp;// rbx

if&nbsp;( PsGetThreadTeb )
&nbsp; {
if&nbsp;( qword_140005090 )
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; v0 = _guard_dispatch_icall_fptr();&nbsp;// Call PsGetThreadTeb
if&nbsp;( v0 )
&nbsp; &nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; &nbsp; v1 = (_QWORD *)(v0 +&nbsp;0x1748);&nbsp;//TEB + 0x1748
&nbsp; &nbsp; &nbsp; &nbsp; ProbeForRead((volatile&nbsp;void&nbsp;*)(v0 +&nbsp;0x1748),&nbsp;8u,&nbsp;8u);
if&nbsp;( *v1 )&nbsp;//如果TEB + 0x1748不为0
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; _guard_dispatch_icall_fptr();&nbsp;// Call qword_140005090
&nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp; }
}
1403185EFlearbx,&nbsp;[rax+1748h]//rbx = TEB + 0x1748

14031861Emovrcx,&nbsp;rbx//ProbeForRead(TEB+0x1748, 8, 8)
140318621&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;callcs:ProbeForRead

140318627&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;movr10,&nbsp;[rbx]//读出这个位置的 8 字节值 r10 = *(QWORD*)(TEB+0x1748)
140318630&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;testr10,&nbsp;r10//如果这个值是 0
140318638&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;jzloc_140318758

1403186F6mov[rsp+38h+arg_10],&nbsp;0
140318701&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;mov[rsp+38h+arg_11],&nbsp;r8b
14031871Dlear8,&nbsp;[rsp+38h+arg_10]//r8 &nbsp;= &2_byte_buffer
140318710&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;movr9d,&nbsp;2&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;//r9d = 2
14031872Bmovrcx,&nbsp;r10// rcx = *(QWORD*)(TEB+0x1748)

通过汇编可以分析出:

qword_140005090(
&nbsp; &nbsp; &nbsp; *(QWORD*)(TEB +&nbsp;0x1748),
4,
&nbsp; &nbsp; &nbsp; &two_byte_buf,
2
&nbsp; );

这还说啥了,如果不动态调试的话估计是没戏了,然后问 GPT 能不能猜测一下这是哪个函数?

为什么我把它解释成 ZwSetInformationObject
不是只凭感觉,而是因为参数形状正好匹配:
- HANDLE
- OBJECT_INFORMATION_CLASS = 4
- PVOID buffer
- ULONG length = 2

再结合动态实验:

- 把 TEB+0x1748 预置成事件句柄
- move 后这个句柄的 HANDLE_FLAG_PROTECT_FROM_CLOSE 会变化

所以这才进一步把它收敛成:

ZwSetInformationObject(handle, ObjectHandleFlagInformation, &info, 2)

查阅相关资料:

NTSYSCALLAPI
NTSTATUS
NTAPI
NtSetInformationObject(
&nbsp; &nbsp; _In_ HANDLE Handle,
&nbsp; &nbsp; _In_ OBJECT_INFORMATION_CLASS ObjectInformationClass,
&nbsp; &nbsp; _In_reads_bytes_(ObjectInformationLength) PVOID ObjectInformation,
&nbsp; &nbsp; _In_ ULONG ObjectInformationLength &nbsp;// 为2
&nbsp; &nbsp; );

#endif
#endif

typedef&nbsp;struct&nbsp;_OBJECT_HANDLE_FLAG_INFORMATION&nbsp;{
&nbsp; &nbsp; &nbsp; BOOLEAN Inherit;
&nbsp; &nbsp; &nbsp; BOOLEAN ProtectFromClose;
&nbsp; } OBJECT_HANDLE_FLAG_INFORMATION;

然后需要确定ZwSetInformationObject提供的什么信号墙,什么信号是通路?可以先创建一个事件句柄slot4_probe,把这个句柄值写到当前线程TEB + 0x1748,每次move前先调用SetHandleInformation(h, HANDLE_FLAG_PROTECT_FROM_CLOSE, 0)进行清零,在move后调用GetHandleInformation(h, &flags),因为题目提到,reset后前五次成功步按顺序泄露。

if( flags&nbsp;&&nbsp;HANDLE_FLAG_PROTECT_FROM_CLOSE&nbsp;!=0)
&nbsp; &nbsp;handle_ok&nbsp;=true
else
&nbsp; &nbsp;handle_ok&nbsp;=False

并且稳定观测到(我们测试到了前几步都是D,并且可以通过回环 DAD 走法来增加正确步骤的次数)。

- DAD + A
&nbsp; - handle_ok = True
- DAD + W
&nbsp; - handle_ok = False

⑤ KeDelayExecutionThread

这个的发现要归功于提交之前的几次测试,我感觉这个寻路没有道理这么慢,内核很可能有随机的,甚至是故意的延时,发现导入了函数KeDelayExecutionThread。

void&nbsp;__fastcall&nbsp;sub_1400026B4(unsignedint&nbsp;*a1){
sub_140002038(a1);
sub_140002388();
JUMPOUT(0x140001FF0LL);
}
void&nbsp;__fastcall&nbsp;sub_140002038(unsignedint&nbsp;*a1){
&nbsp; _BYTE *v1;&nbsp;// rdx
&nbsp; __int64 v2;&nbsp;// r8
unsignedint&nbsp;v3;&nbsp;// eax

if&nbsp;( a1 )
&nbsp; {
&nbsp; &nbsp; v1 = a1 +&nbsp;1;
&nbsp; &nbsp; v2 =&nbsp;56;
//MEMORY[0xFFFFF78000000320] == KUSER_SHARED_DATA + 0x320 == TickCountLowDeprecated
&nbsp; &nbsp; v3 = TickCountLowDeprecated ^&nbsp;0xBAADF00D;
&nbsp; &nbsp; *a1 = TickCountLowDeprecated ^&nbsp;0xBAADF00D;&nbsp;// *(DWORD*)a1 = TickCount ^ 0xBAADF00D;
// drive 用来算 delay 的那个 TickCount 同时也被编码写进了返回缓冲
do
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; v3 =&nbsp;1103515245&nbsp;* v3 +&nbsp;12345;
&nbsp; &nbsp; &nbsp; *v1++ =&nbsp;BYTE2(v3);
&nbsp; &nbsp; &nbsp; --v2;
&nbsp; &nbsp; }
while&nbsp;( v2 );
&nbsp; }
}
NTSTATUS&nbsp;sub_140002388(){
union&nbsp;_LARGE_INTEGER&nbsp;Interval;&nbsp;// [rsp+30h] [rbp+8h] BYREF

&nbsp; Interval.QuadPart =&nbsp;-10000LL&nbsp;* (TickCountLowDeprecated %&nbsp;50u&nbsp;+&nbsp;10);&nbsp;//转换到毫秒 10~59ms
return&nbsp;KeDelayExecutionThread(0,&nbsp;0, &Interval);
}

函数会通过KUSER_SHARED_DATA访问TickCountLowDeprecated,并且在异或后被编码进入返回缓冲,可以在用户层恢复被驱动使用的这个Tick值。

tick_xor&nbsp;= struct.unpack_from("<I", raw,&nbsp;0)[0]
tick&nbsp;= tick_xor ^&nbsp;0xBAADF00D

然后计算:

predicted_ms&nbsp;= (tick %&nbsp;50) +&nbsp;10

就是理论上这个线路被延时的毫秒数,然后我们可以计算一个IO请求的耗时。

&nbsp;t0 =&nbsp;time.perf_counter()
&nbsp;DeviceIoControl(...)
&nbsp;time_ms = (time.perf_counter() - t0) *&nbsp;1000.0

如果predicted_mstime_ms很接近,就能说明本次触发到了该泄漏点,但是更进一步,好像没有合适的办法确定到底是撞墙了还是成功了(从现有代码逻辑看是这样,向上的交叉引用又追不过去),其实也好办,我们能够观测到是不是触发了这个泄漏点,当观测到的时候,直接重试就好了,依赖前四个泄漏点做路线判断。

四、寻路算法并获取 Flag

算法如下:

import&nbsp;argparse
import&nbsp;collections
import&nbsp;ctypes
from&nbsp;ctypes&nbsp;import&nbsp;wintypes
import&nbsp;struct
import&nbsp;sys
import&nbsp;time

kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
ntdll = ctypes.WinDLL("ntdll")

GENERIC_READ =&nbsp;0x80000000
GENERIC_WRITE =&nbsp;0x40000000
OPEN_EXISTING =&nbsp;3
FILE_ATTRIBUTE_NORMAL =&nbsp;0x80
INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value

WAIT_OBJECT_0 =&nbsp;0x00000000
WAIT_TIMEOUT =&nbsp;0x00000102

HANDLE_FLAG_PROTECT_FROM_CLOSE =&nbsp;0x00000002

IOCTL_MOVE =&nbsp;0x80012004
IOCTL_RESET =&nbsp;0x80012008
IOCTL_QUERY =&nbsp;0x8001200C

WIN_MAGIC =&nbsp;0x57494E21
LEAK_WALL =&nbsp;0xC0DE0000
LEAK_OK =&nbsp;0xC0DE0001
LEAK_EXIT =&nbsp;0xC0DE0002
LEAK_POISON =&nbsp;0xDEADDEAD

DEVICE_NAME =&nbsp;r"\\.\ShadowGate"
EVENT_OK_NAME =&nbsp;r"Global\MazeMoveOK"
EVENT_WALL_NAME =&nbsp;r"Global\MazeMoveWall"
SEMAPHORE_OK_GUID =&nbsp;"{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}"
SEMAPHORE_WALL_GUID =&nbsp;"{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}"

DIRS = {
"W": (0, -1),
"A": (-1,&nbsp;0),
"S": (0,&nbsp;1),
"D": (1,&nbsp;0),
}
INVERSE = {"W":&nbsp;"S",&nbsp;"S":&nbsp;"W",&nbsp;"A":&nbsp;"D",&nbsp;"D":&nbsp;"A"}
MOVE_CODE = {"W":&nbsp;0x10,&nbsp;"A":&nbsp;0x30,&nbsp;"S":&nbsp;0x20,&nbsp;"D":&nbsp;0x40}

kernel32.CreateFileW.argtypes = [
&nbsp; &nbsp; wintypes.LPCWSTR,
&nbsp; &nbsp; wintypes.DWORD,
&nbsp; &nbsp; wintypes.DWORD,
&nbsp; &nbsp; wintypes.LPVOID,
&nbsp; &nbsp; wintypes.DWORD,
&nbsp; &nbsp; wintypes.DWORD,
&nbsp; &nbsp; wintypes.HANDLE,
]
kernel32.CreateFileW.restype = wintypes.HANDLE

kernel32.DeviceIoControl.argtypes = [
&nbsp; &nbsp; wintypes.HANDLE,
&nbsp; &nbsp; wintypes.DWORD,
&nbsp; &nbsp; wintypes.LPVOID,
&nbsp; &nbsp; wintypes.DWORD,
&nbsp; &nbsp; wintypes.LPVOID,
&nbsp; &nbsp; wintypes.DWORD,
&nbsp; &nbsp; ctypes.POINTER(wintypes.DWORD),
&nbsp; &nbsp; wintypes.LPVOID,
]
kernel32.DeviceIoControl.restype = wintypes.BOOL

kernel32.CreateEventW.argtypes = [wintypes.LPVOID, wintypes.BOOL, wintypes.BOOL, wintypes.LPCWSTR]
kernel32.CreateEventW.restype = wintypes.HANDLE

kernel32.CreateSemaphoreW.argtypes = [
&nbsp; &nbsp; wintypes.LPVOID,
&nbsp; &nbsp; wintypes.LONG,
&nbsp; &nbsp; wintypes.LONG,
&nbsp; &nbsp; wintypes.LPCWSTR,
]
kernel32.CreateSemaphoreW.restype = wintypes.HANDLE

kernel32.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD]
kernel32.WaitForSingleObject.restype = wintypes.DWORD

kernel32.ResetEvent.argtypes = [wintypes.HANDLE]
kernel32.ResetEvent.restype = wintypes.BOOL

kernel32.GetCurrentThread.argtypes = []
kernel32.GetCurrentThread.restype = wintypes.HANDLE

kernel32.GetHandleInformation.argtypes = [wintypes.HANDLE, ctypes.POINTER(wintypes.DWORD)]
kernel32.GetHandleInformation.restype = wintypes.BOOL

kernel32.SetHandleInformation.argtypes = [wintypes.HANDLE, wintypes.DWORD, wintypes.DWORD]
kernel32.SetHandleInformation.restype = wintypes.BOOL

kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
kernel32.CloseHandle.restype = wintypes.BOOL

kernel32.SetLastError.argtypes = [wintypes.DWORD]
kernel32.SetLastError.restype =&nbsp;None

class&nbsp;CLIENT_ID(ctypes.Structure):
&nbsp; &nbsp; _fields_ = [("UniqueProcess", wintypes.HANDLE), ("UniqueThread", wintypes.HANDLE)]

class&nbsp;THREAD_BASIC_INFORMATION(ctypes.Structure):
&nbsp; &nbsp; _fields_ = [
&nbsp; &nbsp; &nbsp; &nbsp; ("ExitStatus", wintypes.LONG),
&nbsp; &nbsp; &nbsp; &nbsp; ("TebBaseAddress", wintypes.LPVOID),
&nbsp; &nbsp; &nbsp; &nbsp; ("ClientId", CLIENT_ID),
&nbsp; &nbsp; &nbsp; &nbsp; ("AffinityMask", ctypes.c_size_t),
&nbsp; &nbsp; &nbsp; &nbsp; ("Priority", wintypes.LONG),
&nbsp; &nbsp; &nbsp; &nbsp; ("BasePriority", wintypes.LONG),
&nbsp; &nbsp; ]

ntdll.NtQueryInformationThread.argtypes = [
&nbsp; &nbsp; wintypes.HANDLE,
&nbsp; &nbsp; wintypes.ULONG,
&nbsp; &nbsp; wintypes.LPVOID,
&nbsp; &nbsp; wintypes.ULONG,
&nbsp; &nbsp; ctypes.POINTER(wintypes.ULONG),
]
ntdll.NtQueryInformationThread.restype = wintypes.LONG

class&nbsp;WinError(RuntimeError):
pass

def&nbsp;check_handle(handle, what):
if&nbsp;handle&nbsp;in&nbsp;(None,&nbsp;0, INVALID_HANDLE_VALUE):
raise&nbsp;WinError(f"{what}&nbsp;failed, last_error=0x{ctypes.get_last_error():08X}")
return&nbsp;handle

def&nbsp;ror8(value, shift):
&nbsp; &nbsp; value &=&nbsp;0xFF
return&nbsp;((value >> shift) | (value << (8&nbsp;- shift))) &&nbsp;0xFF

def&nbsp;encode_move_code(move_code):
return&nbsp;ror8(move_code ^&nbsp;0x5A,&nbsp;5)

def&nbsp;current_teb_base():
&nbsp; &nbsp; tbi = THREAD_BASIC_INFORMATION()
&nbsp; &nbsp; returned = wintypes.ULONG()
&nbsp; &nbsp; status = ntdll.NtQueryInformationThread(
&nbsp; &nbsp; &nbsp; &nbsp; kernel32.GetCurrentThread(),
0,
&nbsp; &nbsp; &nbsp; &nbsp; ctypes.byref(tbi),
&nbsp; &nbsp; &nbsp; &nbsp; ctypes.sizeof(tbi),
&nbsp; &nbsp; &nbsp; &nbsp; ctypes.byref(returned),
&nbsp; &nbsp; )
if&nbsp;status !=&nbsp;0:
raise&nbsp;WinError(f"NtQueryInformationThread failed, status=0x{status &&nbsp;0xFFFFFFFF:08X}")
return&nbsp;int(ctypes.cast(tbi.TebBaseAddress, ctypes.c_void_p).value)

def&nbsp;slot5_predicted_delay_ms(raw):
if&nbsp;len(raw) <&nbsp;4:
return&nbsp;None
&nbsp; &nbsp; tick_xor = struct.unpack_from("<I", raw,&nbsp;0)[0]
&nbsp; &nbsp; tick = tick_xor ^&nbsp;0xBAADF00D
return&nbsp;float((tick %&nbsp;0x32) +&nbsp;10)

def&nbsp;slot5_timing_hit(result, tolerance_ms=12.0):
&nbsp; &nbsp; sig = result["signals"]
if&nbsp;sig["event_ok"]&nbsp;or&nbsp;sig["event_wall"]&nbsp;or&nbsp;sig["sem_ok"]&nbsp;or&nbsp;sig["sem_wall"]&nbsp;or&nbsp;sig["handle_ok"]:
return&nbsp;False
if&nbsp;result["last_error"]&nbsp;in&nbsp;(LEAK_OK, LEAK_WALL, LEAK_EXIT, LEAK_POISON):
return&nbsp;False
&nbsp; &nbsp; predicted = result["slot5_predicted_ms"]
if&nbsp;predicted&nbsp;is&nbsp;None:
return&nbsp;False
return&nbsp;abs(result["time_ms"] - predicted) <= tolerance_ms

class&nbsp;ShadowGate:
def&nbsp;__init__(self):
&nbsp; &nbsp; &nbsp; &nbsp; self.handle =&nbsp;None
&nbsp; &nbsp; &nbsp; &nbsp; self.event_ok =&nbsp;None
&nbsp; &nbsp; &nbsp; &nbsp; self.event_wall =&nbsp;None
&nbsp; &nbsp; &nbsp; &nbsp; self.sem_ok =&nbsp;None
&nbsp; &nbsp; &nbsp; &nbsp; self.sem_wall =&nbsp;None
&nbsp; &nbsp; &nbsp; &nbsp; self.slot4_probe =&nbsp;None
&nbsp; &nbsp; &nbsp; &nbsp; self._teb_1748_slot =&nbsp;None
&nbsp; &nbsp; &nbsp; &nbsp; self._teb_1748_old =&nbsp;None
&nbsp; &nbsp; &nbsp; &nbsp; self.op_count =&nbsp;0

def&nbsp;open(self):
&nbsp; &nbsp; &nbsp; &nbsp; self.handle = check_handle(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; kernel32.CreateFileW(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; DEVICE_NAME,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; GENERIC_READ | GENERIC_WRITE,
0,
None,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; OPEN_EXISTING,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; FILE_ATTRIBUTE_NORMAL,
None,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ),
f"CreateFileW({DEVICE_NAME})",
&nbsp; &nbsp; &nbsp; &nbsp; )
&nbsp; &nbsp; &nbsp; &nbsp; self.event_ok = self._ensure_event(EVENT_OK_NAME)
&nbsp; &nbsp; &nbsp; &nbsp; self.event_wall = self._ensure_event(EVENT_WALL_NAME)
&nbsp; &nbsp; &nbsp; &nbsp; self.sem_ok = self._ensure_semaphore(SEMAPHORE_OK_GUID)
&nbsp; &nbsp; &nbsp; &nbsp; self.sem_wall = self._ensure_semaphore(SEMAPHORE_WALL_GUID)
&nbsp; &nbsp; &nbsp; &nbsp; self._install_slot4_probe()
return&nbsp;self

def&nbsp;close(self):
if&nbsp;self._teb_1748_slot&nbsp;is&nbsp;not&nbsp;None&nbsp;and&nbsp;self._teb_1748_old&nbsp;is&nbsp;not&nbsp;None:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self._teb_1748_slot.value = self._teb_1748_old
&nbsp; &nbsp; &nbsp; &nbsp; self._teb_1748_slot =&nbsp;None
&nbsp; &nbsp; &nbsp; &nbsp; self._teb_1748_old =&nbsp;None
for&nbsp;attr&nbsp;in&nbsp;("slot4_probe",&nbsp;"sem_wall",&nbsp;"sem_ok",&nbsp;"event_wall",&nbsp;"event_ok",&nbsp;"handle"):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; handle =&nbsp;getattr(self, attr)
if&nbsp;handle:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; kernel32.CloseHandle(handle)
setattr(self, attr,&nbsp;None)

def&nbsp;_ensure_event(self, name):
&nbsp; &nbsp; &nbsp; &nbsp; candidates = [name]
if&nbsp;name.startswith("Global\"):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; candidates.append(name.split("\",&nbsp;1)[1])
&nbsp; &nbsp; &nbsp; &nbsp; last_exc =&nbsp;None
for&nbsp;candidate&nbsp;in&nbsp;candidates:
try:
return&nbsp;check_handle(kernel32.CreateEventW(None,&nbsp;True,&nbsp;False, candidate),&nbsp;f"CreateEventW({candidate})")
except&nbsp;WinError&nbsp;as&nbsp;exc:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; last_exc = exc
raise&nbsp;last_exc&nbsp;or&nbsp;WinError(f"CreateEventW({name}) failed")

def&nbsp;_ensure_semaphore(self, guid):
&nbsp; &nbsp; &nbsp; &nbsp; candidates = (rf"Global\{guid}", guid)
&nbsp; &nbsp; &nbsp; &nbsp; last_exc =&nbsp;None
for&nbsp;candidate&nbsp;in&nbsp;candidates:
try:
return&nbsp;check_handle(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; kernel32.CreateSemaphoreW(None,&nbsp;0,&nbsp;0x7FFFFFFF, candidate),
f"CreateSemaphoreW({candidate})",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; )
except&nbsp;WinError&nbsp;as&nbsp;exc:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; last_exc = exc
raise&nbsp;last_exc&nbsp;or&nbsp;WinError(f"CreateSemaphoreW({guid}) failed")

def&nbsp;_install_slot4_probe(self):
&nbsp; &nbsp; &nbsp; &nbsp; teb = current_teb_base()
&nbsp; &nbsp; &nbsp; &nbsp; self.slot4_probe = check_handle(kernel32.CreateEventW(None,&nbsp;True,&nbsp;False,&nbsp;None),&nbsp;"CreateEventW(slot4_probe)")
&nbsp; &nbsp; &nbsp; &nbsp; self._teb_1748_slot = ctypes.c_uint64.from_address(teb +&nbsp;0x1748)
&nbsp; &nbsp; &nbsp; &nbsp; self._teb_1748_old = self._teb_1748_slot.value
&nbsp; &nbsp; &nbsp; &nbsp; self._teb_1748_slot.value =&nbsp;int(self.slot4_probe)

def&nbsp;_ioctl(self, code, in_bytes=b"", out_size=0):
&nbsp; &nbsp; &nbsp; &nbsp; in_buf = ctypes.create_string_buffer(in_bytes,&nbsp;len(in_bytes))&nbsp;if&nbsp;in_bytes&nbsp;else&nbsp;None
&nbsp; &nbsp; &nbsp; &nbsp; out_buf = ctypes.create_string_buffer(out_size)&nbsp;if&nbsp;out_size&nbsp;else&nbsp;None
&nbsp; &nbsp; &nbsp; &nbsp; returned = wintypes.DWORD()
&nbsp; &nbsp; &nbsp; &nbsp; kernel32.SetLastError(0)
&nbsp; &nbsp; &nbsp; &nbsp; ok = kernel32.DeviceIoControl(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.handle,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; code,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; in_buf,
len(in_bytes),
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; out_buf,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; out_size,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ctypes.byref(returned),
None,
&nbsp; &nbsp; &nbsp; &nbsp; )
&nbsp; &nbsp; &nbsp; &nbsp; last_error = ctypes.get_last_error() &&nbsp;0xFFFFFFFF
if&nbsp;not&nbsp;ok:
raise&nbsp;WinError(f"DeviceIoControl(0x{code:08X}) failed, last_error=0x{last_error:08X}")
&nbsp; &nbsp; &nbsp; &nbsp; data = out_buf.raw[: returned.value]&nbsp;if&nbsp;out_buf&nbsp;else&nbsp;b""
return&nbsp;data, last_error

def&nbsp;query_maze(self):
&nbsp; &nbsp; &nbsp; &nbsp; data, _ = self._ioctl(IOCTL_QUERY,&nbsp;b"",&nbsp;0x18)
return&nbsp;struct.unpack("<6I", data[:0x18])

def&nbsp;reset(self):
&nbsp; &nbsp; &nbsp; &nbsp; self._clear_sync_objects()
&nbsp; &nbsp; &nbsp; &nbsp; self._ioctl(IOCTL_RESET)
&nbsp; &nbsp; &nbsp; &nbsp; self.op_count =&nbsp;0

def&nbsp;move(self, ch):
&nbsp; &nbsp; &nbsp; &nbsp; self._clear_sync_objects()
if&nbsp;self.slot4_probe:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; kernel32.SetHandleInformation(self.slot4_probe, HANDLE_FLAG_PROTECT_FROM_CLOSE,&nbsp;0)
&nbsp; &nbsp; &nbsp; &nbsp; move_code = MOVE_CODE[ch]
&nbsp; &nbsp; &nbsp; &nbsp; encoded = encode_move_code(move_code)
&nbsp; &nbsp; &nbsp; &nbsp; packet = struct.pack("<B3xII", encoded, self.op_count, encoded ^ self.op_count ^&nbsp;0xDEAD1337)
&nbsp; &nbsp; &nbsp; &nbsp; t0 = time.perf_counter()
&nbsp; &nbsp; &nbsp; &nbsp; data, last_error = self._ioctl(IOCTL_MOVE, packet,&nbsp;0x84)
&nbsp; &nbsp; &nbsp; &nbsp; time_ms = (time.perf_counter() - t0) *&nbsp;1000.0
&nbsp; &nbsp; &nbsp; &nbsp; signals = self._collect_signals()
&nbsp; &nbsp; &nbsp; &nbsp; outcome = self._classify_outcome(last_error, signals)
&nbsp; &nbsp; &nbsp; &nbsp; predicted = slot5_predicted_delay_ms(data)
&nbsp; &nbsp; &nbsp; &nbsp; self.op_count +=&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp; credential =&nbsp;None
if&nbsp;len(data) >=&nbsp;0x84&nbsp;and&nbsp;struct.unpack_from("<I", data,&nbsp;0x3C)[0] == WIN_MAGIC:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; length = struct.unpack_from("<I", data,&nbsp;0x80)[0]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; credential = data[0x40&nbsp;:&nbsp;0x40&nbsp;+&nbsp;min(length,&nbsp;0x3F)].split(b"\x00",&nbsp;1)[0].decode("ascii", errors="replace")
&nbsp; &nbsp; &nbsp; &nbsp; result = {
"char": ch,
"move_code": move_code,
"encoded": encoded,
"outcome": outcome,
"last_error": last_error,
"signals": signals,
"time_ms": time_ms,
"slot5_predicted_ms": predicted,
"slot5_hit":&nbsp;False,
"raw": data,
"credential": credential,
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; result["slot5_hit"] = slot5_timing_hit(result)
return&nbsp;result

def&nbsp;_clear_sync_objects(self):
if&nbsp;self.event_ok:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; kernel32.ResetEvent(self.event_ok)
if&nbsp;self.event_wall:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; kernel32.ResetEvent(self.event_wall)
for&nbsp;sem&nbsp;in&nbsp;(self.sem_ok, self.sem_wall):
if&nbsp;sem:
while&nbsp;kernel32.WaitForSingleObject(sem,&nbsp;0) == WAIT_OBJECT_0:
pass

def&nbsp;_collect_signals(self):
&nbsp; &nbsp; &nbsp; &nbsp; handle_ok =&nbsp;False
if&nbsp;self.slot4_probe:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; flags = wintypes.DWORD()
if&nbsp;kernel32.GetHandleInformation(self.slot4_probe, ctypes.byref(flags)):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; handle_ok =&nbsp;bool(flags.value & HANDLE_FLAG_PROTECT_FROM_CLOSE)
return&nbsp;{
"event_ok": self._poll_event(self.event_ok),
"event_wall": self._poll_event(self.event_wall),
"sem_ok": self._poll_semaphore(self.sem_ok),
"sem_wall": self._poll_semaphore(self.sem_wall),
"handle_ok": handle_ok,
&nbsp; &nbsp; &nbsp; &nbsp; }

def&nbsp;_classify_outcome(self, last_error, signals):
if&nbsp;last_error == LEAK_EXIT:
return&nbsp;"exit"
if&nbsp;last_error == LEAK_OK&nbsp;or&nbsp;signals["event_ok"]&nbsp;or&nbsp;signals["sem_ok"]&nbsp;or&nbsp;signals["handle_ok"]:
return&nbsp;"ok"
if&nbsp;last_error == LEAK_WALL&nbsp;or&nbsp;signals["event_wall"]&nbsp;or&nbsp;signals["sem_wall"]:
return&nbsp;"wall"
if&nbsp;last_error == LEAK_POISON:
return&nbsp;"poison"
return&nbsp;"unknown"

&nbsp; &nbsp; @staticmethod
def&nbsp;_poll_event(handle):
if&nbsp;not&nbsp;handle:
return&nbsp;False
return&nbsp;kernel32.WaitForSingleObject(handle,&nbsp;0) == WAIT_OBJECT_0

&nbsp; &nbsp; @staticmethod
def&nbsp;_poll_semaphore(handle):
if&nbsp;not&nbsp;handle:
return&nbsp;False
&nbsp; &nbsp; &nbsp; &nbsp; rc = kernel32.WaitForSingleObject(handle,&nbsp;0)
return&nbsp;rc == WAIT_OBJECT_0

def&nbsp;shortest_path(open_edges, start, goal):
&nbsp; &nbsp; queue = collections.deque([start])
&nbsp; &nbsp; prev = {start: (None,&nbsp;None)}
while&nbsp;queue:
&nbsp; &nbsp; &nbsp; &nbsp; cur = queue.popleft()
if&nbsp;cur == goal:
break
for&nbsp;move, nxt&nbsp;in&nbsp;open_edges.get(cur, {}).items():
if&nbsp;nxt&nbsp;not&nbsp;in&nbsp;prev:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; prev[nxt] = (cur, move)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; queue.append(nxt)
if&nbsp;goal&nbsp;not&nbsp;in&nbsp;prev:
return&nbsp;None
&nbsp; &nbsp; path = []
&nbsp; &nbsp; cur = goal
while&nbsp;prev[cur][0]&nbsp;is&nbsp;not&nbsp;None:
&nbsp; &nbsp; &nbsp; &nbsp; cur, move = prev[cur]
&nbsp; &nbsp; &nbsp; &nbsp; path.append(move)
&nbsp; &nbsp; path.reverse()
return&nbsp;"".join(path)

def&nbsp;is_explicit_success(result):
return&nbsp;result["outcome"]&nbsp;in&nbsp;("ok",&nbsp;"exit")

def&nbsp;is_explicit_wall(result):
return&nbsp;result["outcome"]&nbsp;in&nbsp;("wall",&nbsp;"poison")

def&nbsp;replay_path(gate, path):
for&nbsp;move&nbsp;in&nbsp;path:
&nbsp; &nbsp; &nbsp; &nbsp; result = gate.move(move)
if&nbsp;result["outcome"]&nbsp;in&nbsp;("wall",&nbsp;"poison"):
raise&nbsp;WinError(f"replay failed at move&nbsp;{move}:&nbsp;{result}")

def&nbsp;probe_move(gate, route, move, cycle, attempts=8):
&nbsp; &nbsp; result =&nbsp;None
for&nbsp;shift&nbsp;in&nbsp;range(attempts):
&nbsp; &nbsp; &nbsp; &nbsp; gate.reset()
try:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; replay_path(gate, route)
except&nbsp;WinError:
continue
if&nbsp;cycle:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; out_move, back_move = cycle
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; blocked =&nbsp;False
for&nbsp;_&nbsp;in&nbsp;range(shift):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; out_res = gate.move(out_move)
if&nbsp;is_explicit_wall(out_res):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; blocked =&nbsp;True
break
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; back_res = gate.move(back_move)
if&nbsp;is_explicit_wall(back_res):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; blocked =&nbsp;True
break
if&nbsp;blocked:
continue
&nbsp; &nbsp; &nbsp; &nbsp; result = gate.move(move)
if&nbsp;result["slot5_hit"]:
continue
if&nbsp;is_explicit_success(result):
return&nbsp;True, result
if&nbsp;is_explicit_wall(result):
return&nbsp;False, result
return&nbsp;None, result

def&nbsp;solve_via_iterative_exploration(gate, width, height, start, goal, max_rounds=180):
&nbsp; &nbsp; discovered = {start}
&nbsp; &nbsp; open_edges = collections.defaultdict(dict)
&nbsp; &nbsp; walls =&nbsp;set()
&nbsp; &nbsp; pending = collections.deque([start])
&nbsp; &nbsp; credential =&nbsp;None

&nbsp; &nbsp; rounds =&nbsp;0
while&nbsp;rounds < max_rounds&nbsp;and&nbsp;pending:
&nbsp; &nbsp; &nbsp; &nbsp; rounds +=&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp; progress =&nbsp;False
&nbsp; &nbsp; &nbsp; &nbsp; queue = collections.deque(list(dict.fromkeys(pending)))
&nbsp; &nbsp; &nbsp; &nbsp; pending.clear()

while&nbsp;queue:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cell = queue.popleft()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; route = shortest_path(open_edges, start, cell)&nbsp;or&nbsp;""
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cycle =&nbsp;None
if&nbsp;route:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cycle = (INVERSE[route[-1]], route[-1])
elif&nbsp;"D"&nbsp;in&nbsp;open_edges[start]:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cycle = ("D",&nbsp;"A")

for&nbsp;move, delta&nbsp;in&nbsp;DIRS.items():
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; nx = cell[0] + delta[0]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ny = cell[1] + delta[1]
if&nbsp;not&nbsp;(0&nbsp;<= nx < width&nbsp;and&nbsp;0&nbsp;<= ny < height):
continue
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; nxt = (nx, ny)
if&nbsp;move&nbsp;in&nbsp;open_edges[cell]&nbsp;or&nbsp;(cell, nxt)&nbsp;in&nbsp;walls:
continue

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; decision, result = probe_move(gate, route, move, cycle, attempts=8)
if&nbsp;result&nbsp;and&nbsp;result["credential"]:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; credential = result["credential"]

if&nbsp;decision&nbsp;is&nbsp;True:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; progress =&nbsp;True
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; open_edges[cell][move] = nxt
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; open_edges[nxt][INVERSE[move]] = cell
if&nbsp;nxt&nbsp;not&nbsp;in&nbsp;discovered:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; discovered.add(nxt)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pending.append(nxt)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pending.append(cell)
elif&nbsp;decision&nbsp;is&nbsp;False:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; progress =&nbsp;True
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; walls.add((cell, nxt))
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; walls.add((nxt, cell))
else:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pending.append(cell)

&nbsp; &nbsp; &nbsp; &nbsp; path = shortest_path(open_edges, start, goal)
if&nbsp;path:
if&nbsp;credential&nbsp;is&nbsp;None:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; gate.reset()
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; replay_result =&nbsp;None
for&nbsp;move&nbsp;in&nbsp;path:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; replay_result = gate.move(move)
if&nbsp;replay_result&nbsp;and&nbsp;replay_result["credential"]:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; credential = replay_result["credential"]
return&nbsp;path, credential, open_edges

if&nbsp;not&nbsp;progress&nbsp;and&nbsp;not&nbsp;pending:
break

raise&nbsp;WinError("solve did not converge")

def&nbsp;render_ascii(width, height, open_edges):
&nbsp; &nbsp; rows = [["#"] * (width *&nbsp;2&nbsp;+&nbsp;1)&nbsp;for&nbsp;_&nbsp;in&nbsp;range(height *&nbsp;2&nbsp;+&nbsp;1)]
for&nbsp;y&nbsp;in&nbsp;range(height):
for&nbsp;x&nbsp;in&nbsp;range(width):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; rows[y *&nbsp;2&nbsp;+&nbsp;1][x *&nbsp;2&nbsp;+&nbsp;1] =&nbsp;"."
for&nbsp;(x, y), edges&nbsp;in&nbsp;open_edges.items():
for&nbsp;move, _&nbsp;in&nbsp;edges.items():
if&nbsp;move ==&nbsp;"D":
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; rows[y *&nbsp;2&nbsp;+&nbsp;1][x *&nbsp;2&nbsp;+&nbsp;2] =&nbsp;" "
elif&nbsp;move ==&nbsp;"S":
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; rows[y *&nbsp;2&nbsp;+&nbsp;2][x *&nbsp;2&nbsp;+&nbsp;1] =&nbsp;" "
&nbsp; &nbsp; rows[1][1] =&nbsp;"S"
&nbsp; &nbsp; rows[height *&nbsp;2&nbsp;-&nbsp;1][width *&nbsp;2&nbsp;-&nbsp;1] =&nbsp;"E"
return&nbsp;"\n".join("".join(r)&nbsp;for&nbsp;r&nbsp;in&nbsp;rows)

def&nbsp;main():
&nbsp; &nbsp; parser = argparse.ArgumentParser()
&nbsp; &nbsp; parser.add_argument("--probe", metavar="PATH")
&nbsp; &nbsp; parser.add_argument("--solve", action="store_true")
&nbsp; &nbsp; parser.add_argument("--show-map", action="store_true")
&nbsp; &nbsp; args = parser.parse_args()

&nbsp; &nbsp; gate = ShadowGate().open()
try:
&nbsp; &nbsp; &nbsp; &nbsp; width, height, sx, sy, ex, ey = gate.query_maze()
print(f"maze:&nbsp;{width}x{height}&nbsp;start=({sx},{sy}) exit=({ex},{ey})")
print(f"events: ok={bool(gate.event_ok)}&nbsp;wall={bool(gate.event_wall)}")
print(f"semaphores: ok={bool(gate.sem_ok)}&nbsp;wall={bool(gate.sem_wall)}")

if&nbsp;args.probe:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; gate.reset()
for&nbsp;ch&nbsp;in&nbsp;args.probe.upper():
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; result = gate.move(ch)
print(
f"{ch}: outcome={result['outcome']}&nbsp;"
f"last_error=0x{result['last_error']:08X}&nbsp;"
f"time_ms={result['time_ms']:.2f}&nbsp;"
f"slot5_predicted_ms={result['slot5_predicted_ms']}&nbsp;"
f"slot5_hit={result['slot5_hit']}&nbsp;"
f"signals={result['signals']}"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; )
return&nbsp;0

if&nbsp;args.solve:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; path, credential, open_edges = solve_via_iterative_exploration(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; gate, width, height, (sx, sy), (ex, ey)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; )
print(f"path={path}")
if&nbsp;credential:
print(f"credential={credential}")
if&nbsp;args.show_map:
print(render_ascii(width, height, open_edges))
return&nbsp;0

print("use --solve or --probe")
return&nbsp;0
finally:
&nbsp; &nbsp; &nbsp; &nbsp; gate.close()

if&nbsp;__name__ ==&nbsp;"__main__":
&nbsp; &nbsp; sys.exit(main())

① 单步观测 ShadowGate.move()

按协议构造MOVE_REQ

  • encoded_dir = ror8(move_code ^ 0x5A, 5)

  • checksum = encoded_dir ^ op_count ^ 0xDEAD1337

  • 事件:MazeMoveOK / MazeMoveWall

  • 信号量:A7F3… / B8E2…

  • LastError

  • TEB+0x1748 句柄标志

  • slot5 timing:raw[0:4] ^ 0xBAADF00D 反推出 predicted_ms

  • 调 DeviceIoControl(IOCTL_MOVE)

  • 同时采集 5 个槽位的观测:

② 单边判定 probe_move()

判断从当前格子朝某方向走一步,这条边是通还是墙?

做法是:

  • reset

  • replay 到当前待测格子

  • 用一个已知成功的小回环 out/back 调整成功步相位

  • 真正发一次待测 move

  • 如果命中前四个结果槽位:

  • 直接判 ok 或 wall

  • 如果命中第五槽位:

  • 只说明当前落在 phase5

  • 不做结果判定,继续下一次尝试

③ 图搜索 solve_via_iterative_exploration()

  • 从起点开始

  • 对每个已发现格子的四个方向做 probe_move()

  • decision == True:

  • 记成通路边,加入图

  • decision == False:

  • 记成墙

  • decision == None:

  • 暂时放回队列,后续再试

④ 最短路径

每次图有新边之后,用 shortest_path() 在当前已恢复图上跑 BFS:

  • 找到起点到终点的最短路
  • 一旦终点连通,再 replay 一次路径
  • 从返回缓冲取 credential

通过如下命令执行算法(需要加载驱动)

python shadowgate_solve.py--solve

得到输出

maze: 13x13 start=(0,0) exit=(12,12)
events: ok=True wall=True
semaphores: ok=True wall=True
path=DDDDDDSSDDDDWWDDSSSSSSSSAASSSSDD
credential=flag{SHAD0WNT_HYPERVMX}

得到了最短路径和flag,具体执行时间有差异,在我的设备上应该在70s左右

五、构建地图

在前者的基础上设计建图脚本

⑤ 完整建图 explore_full_map()

核心数据结构:

  • open_edges

  • 已确认通路边

  • walls

  • 已确认墙边

  • discovered

  • 已发现可达格子

  • tasks

  • 尚未确认的候选边 (cell, move)

  • attempts

  • 每条候选边已经回炉过多少次

流程如下:

  • 从起点开始,把起点四个方向加入任务队列

  • 每次从队列中取出一条候选边 (cell, move)

  • 用 shortest_path(open_edges, start, cell) 求从起点到该格子的当前已知路径

  • 调用 probe_move() 判断这条边

  • 若 decision == True

  • 将这条边加入 open_edges

  • 把新到达的格子加入 discovered

  • 再把这个新格子的候选边加入任务队列

  • 若 decision == False

  • 将该边加入 walls

  • 若 decision == None

  • 表示这次只命中 slot5 或暂时未定

  • 该边最多回炉有限次数,再次入队

⑥ 地图收敛与最短路径

完整建图跑完后,脚本会得到完整的 ASCII 迷宫地图,open_edges,随后脚本再用 shortest_path() 在完整恢复的图上跑 BFS,得到起点到终点的最短路,并replay 一次,从返回缓冲中提取 credential。

通过如下命令执行算法(需要加载驱动):

python shadowgate_map.py--map

得到输出:

maze: 13x13 start=(0,0) exit=(12,12)
events: ok=True wall=True
semaphores: ok=True wall=True
###########################
#S . . . . . .#.#. . . . .#
############# ####### ### #
#.#.#.#.#.#.#.#.#.#.#.#.#.#
############# ####### ### #
#. . . . .#.#. . . . .#.#.#
#&nbsp;####### ############### #
#.#.#.#.#.#.#.#.#.#.#.#.#.#
#&nbsp;####### ############### #
#.#.#. . . . . . . . .#.#.#
#&nbsp;### ### ########### ### #
#.#.#.#.#.#.#.#.#.#.#.#.#.#
#&nbsp;### ### ########### ### #
#.#.#.#.#.#.#. . .#.#.#.#.#
#&nbsp;### ####### ### ####### #
#.#.#.#.#.#.#.#.#.#.#.#.#.#
#&nbsp;### ####### ### ####### #
#.#.#. . . . .#.#.#.#. . .#
#&nbsp;############### ### #####
#.#.#.#.#.#.#.#.#.#.#.#.#.#
#&nbsp;############### ### #####
#. . .#.#. . .#.#.#.#.#.#.#
##### ### ### ### ### ### #
#.#.#.#.#.#.#.#.#.#.#.#.#.#
##### ### ### ### ### ### #
#. . . . .#.#.#.#. . . . E#
###########################
path=DDDDDDSSDDDDWWDDSSSSSSSSAASSSSDD
credential=flag{SHAD0WNT_HYPERVMX}
discovered=97
unresolved=0

得到了地图、最短路径和flag,具体执行时间有差异,在我的设备上应该在600s左右。

#

看雪ID:江树

https://bbs.kanxue.com/user-home-1001108.htm

*本文为看雪论坛精华文章,由 江树 原创,转载请注明来自看雪社区

往期推荐

安卓逆向基础知识之frida Hook

2025 强网杯和强网拟态部分题解

在逆向分析方面-unidbg真的适合 MCP 吗?

AI静态分析,内核模块隐藏 Frida 特征,绕过linker私有结构遍历崩溃链

某安全so库深度解析

球分享

球点赞

球在看

点击阅读原文查看更多


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:看雪学苑 江树 江树《2026年腾讯游戏安全初赛-PC方向》

评论:0   参与:  0