关掉 ACE 预启动,随便签一个泄露签名,即可加载驱动,稳定性极高,就蓝屏了两次。
每次 move 前先调用 SetHandleInformation(h, HANDLE_FLAG_PROTECT_FROM_CLOSE, 0)进行清零,在move 后调用GetHandleInformation(h, &flags)
函数会通过 KUSER_SHARED_DATA 访问 TickCountLowDeprecated ,并且在异或后被编码进入返回缓冲,可以在用户层恢复被驱动使用的这个 Tick 值
然后计算
就是理论上这个线路被延时的毫秒数,然后我们可以计算一个 IO 请求的耗时
如果 predicted_ms 和 time_ms 很接近,就能说明本次触发到了该泄漏点,但是无法判断具体的结果。
在二进制文件中放了大量的小混淆,导致不管是正向分析还是逆向分析,都不舒服,比如发现了某一个函数,发现 xref 不可用,因为上游的调用点有花指令,混淆等,导致了 IDA 不能正确分析,在驱动分析中尤其明显,但是由于驱动导入表暴露了大量的信息,所以都可以通过导入表入手,不完整的分析全流程,也能得到相关结论,如果赛题在导入函数上做手脚,比如动态解析,难度会更大。
很好本来都要提交了,感觉这个寻路脚本咋会这么慢,时间还不稳定,又重新用IDA看了一眼驱动,发现有随机的 Sleep 感觉大概率是没找齐全,然后就找到了之前没找到的两个泄漏点(但是他们没有影响我得到flag....)
从上述文本能够判断是迷宫题目,但是应该不是简单的迷宫,应该无法通过传统的迷宫分析方法分析,需要完整的逆向,至少要找到文档中提到的:
并未发现有保护壳,直接运行得到
可以有一个基本的了解 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 次成功移动会依次触发五种不同的泄露机制,顺序是固定的。(这里其实还是有一点疑惑的,最终只找到了三类泄露方式,但也可以算作五个泄漏点)
字符串没有做混淆,在字符串表暴露了很多信息。结合字符串的交叉引用,有如下分析:
虽然还没有分析驱动,但是应用创建了两个全局命名事件,名称强烈暗示它们分别对应移动成功和撞墙,即失败。
通信方面使用了最基本的 IO 通讯,分析 DeviceIoControl 的交叉引用可以得到所有的 IO 码
显然 0x8001200C 用于查询迷宫信息,接下来就能发现 main 函数有大量的花指令了,因为正常的 R 等逻辑的处理代码都不存在
只需要把
改为
伪代码就重建好了,然后依旧是出题人的小礼物啊
这里会有垂落,处理的是大小写 R 的情况
显然 0x80012008 用于重新开始,那剩下的操作码肯定是操作迷宫了的
紧接着还要详细分析,相关的按键被映射为了十六进制整数,如下
TryMove 对于路径的打包如下
其中,具体的加密逻辑是这样
足够我们重建结构体了,但是伪代码有一个问题,驱动如何校验 v17 的正确性呢?切换到汇编,根据 DeviceIoControl 的参数可以知道
然后看汇编层面是如何写入 [r11-30h] 的
统一成 buf 的偏移就是
也就能重建结构体为
接下来是输入 T 的问题
命令 T的作用并不是显示当前位置,而是输出应用层维护的成功移动日志。主循环中,每当一次移动被认为成功时,程序都会将对应方向字符 L R U D 追加到缓冲区 v22 中,而 T/t分支则负责打印当前操作计数及该字符串,也就是这里只能打印成功的路径。
先玩一下,也就是可以借助前五个正确路径找到五个泄漏点,因为提到了前五次成功顺次泄露,会在后边起关键作用
也是见到没有壳子轻松达到 4 MB 的驱动文件了,混淆或者花指令估计是跑不了了,先迎接出题人的小礼物,重建 sub_140003208
patch 为
伪代码重建成功
接下来寻5个泄漏点,刚才在应用层找到的事件,先看字符串定位过去,找到第一个泄漏点
该函数根据传入状态参数,选择 **\BaseNamedObjects\MazeMoveOK **或 \BaseNamedObjects\MazeMoveWall
随后通过 ZwOpenEvent 打开对应命名事件,并调用 ZwSetEvent 将其置位,最后关闭句柄。
由于应用层事先创建了同名全局事件 Global\MazeMoveOK 与 Global\MazeMoveWall
因此用户态可在每次发送移动请求后轮询这两个事件,从而判断本次移动是成功推进还是撞墙失败。
然后在导入表发现了函数 KeReleaseSemaphore 和 ObReferenceObjectByName 跟到,sub_140319A37 ,有明显的选择和解密流程
就要看看 unk_1400041E0 和 unk_140004160 对应的到底是什么字符串了
得到输出,也就找到了第二个泄漏点,
但是暂时不能确定哪个是撞墙,哪个是成功,要具体测试一下
测试 DDDDD 得到
那么路径正确时候会触发 Global\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}
还是要看导入表
首先想到的可能是调试状态,或者 LastError ,对于 LastError 的话,尝试找找有没有类似的写入逻辑
显然可以看到
那么 LastError 应该是下一个泄漏点了
刚才我们提到 qword_140005080 是 PsGetThreadTeb ,相关的交叉引用还有一个函数
通过汇编可以分析出,
这还说啥了,如果不动态调试的话估计是没戏了,然后问 GPT 能不能猜测一下这是哪个函数?
查阅相关资料
然后需要确定 ZwSetInformationObject 提供的什么信号墙,什么信号是通路?可以先创建一个事件句柄 slot4_probe ,把这个句柄值写到当前线程 TEB + 0x1748 ,每次 move 前先调用 SetHandleInformation(h, HANDLE_FLAG_PROTECT_FROM_CLOSE, 0)进行清零,在move 后调用GetHandleInformation(h, &flags),因为题目提到,reset 后前五次成功步按顺序泄露
并且稳定观测到(我们测试到了前几步都是D,并且可以通过回环 DAD 走法来增加正确步骤的次数)
这个的发现要归功于提交之前的几次测试,我感觉这个寻路没有道理这么慢,内核很可能有随机的,甚至是故意的延时,发现导入了函数 KeDelayExecutionThread
函数会通过 KUSER_SHARED_DATA 访问 TickCountLowDeprecated ,并且在异或后被编码进入返回缓冲,可以在用户层恢复被驱动使用的这个 Tick 值
然后计算
就是理论上这个线路被延时的毫秒数,然后我们可以计算一个 IO 请求的耗时
如果 predicted_ms 和 time_ms 很接近,就能说明本次触发到了该泄漏点,但是更进一步,好像没有合适的办法确定到底是撞墙了还是成功了(从现有代码逻辑看是这样,向上的交叉引用又追不过去),其实也好办,我们能够观测到是不是触发了这个泄漏点,当观测到的时候,直接重试就好了,依赖前四个泄漏点做路线判断。
算法如下
按协议构造 MOVE_REQ
encoded_dir = ror8(move_code ^ 0x5A, 5)
checksum = encoded_dir ^ op_count ^ 0xDEAD1337
判断从当前格子朝某方向走一步,这条边是通还是墙?
做法是:
每次图有新边之后,用 shortest_path() 在当前已恢复图上跑 BFS:
通过如下命令执行算法(需要加载驱动)
得到输出
得到了最短路径和 flag ,具体执行时间有差异,在我的设备上应该在 70s 左右
在前者的基础上设计建图脚本
核心数据结构:
流程如下:
完整建图跑完后,脚本会得到完整的 ASCII 迷宫地图,open_edges,随后脚本再用 shortest_path() 在完整恢复的图上跑 BFS,得到起点到终点的最短路,并replay 一次,从返回缓冲中提取 credential。
通过如下命令执行算法(需要加载驱动)
得到输出
得到了地图、最短路径和 flag ,具体执行时间有差异,在我的设备上应该在 600s 左右(上个厕所回来就跑出来了)
IOCTL
作用
0x80012004
移动/核心交互
0x80012008
重置到起点
0x8001200C
查询迷宫信息
Code
含义
0xC0DE0001
ok
0xC0DE0002
到达终点?
0xC0DE0000
wall
0xDEADDEAD
干扰值
flag{SHAD0WNT_HYPERVMX}
DDDDDDSSDDDDWWDDSSSSSSSSAASSSSDD
RRRRRRDDRRRRUURRDDDDDDDDLLDDDDRR
if ( flags & HANDLE_FLAG_PROTECT_FROM_CLOSE != 0 )
handle_ok = true
else
handle_ok = False
tick_xor = struct.unpack_from("<I" , raw, 0 )[0 ]
tick = tick_xor ^ 0xBAADF00D
predicted_ms = (tick % 50 ) + 10
t0 = time.perf_counter()
DeviceIoControl(...)
time_ms = (time.perf_counter() - t0) * 1000.0
C:\Users\Euarno\Desktop\2026 游戏安全技术竞赛-PC客户端安全-初赛>ShadowGateApp.exe
==============================================
Shadow Palace Gate - ACCESS DENIED
==============================================
ACE has intercepted Shadow's palace gate
system. A kernel driver hides an encrypted
maze inside. Navigate through it to extract
the credential for Shadow' s internal network.
The palace gives NO feedback on whether
your moves succeed or hit a wall.
Or does it? The system is not as silent
as it seems. Five hidden flaws betray
the result of every move 鈥?but each move
exposes only one of them.
Hint: after each reset, the first five
successful moves reveal each flaw exactly
once, in a fixed order.
[*] Connecting to Shadow gate driver...
[+] Gate module online.
[*] Maze grid: 13x13, Entry=(0 ,0 ), Exit=(12 ,12 )
Commands:
W/A/S/D - Navigate Up/Left/Down/Right
I/J/K/L - Navigate Up/Left/Down/Right (alt)
R - Reset to entry point
T - Show operation log (position hidden)
H - Show this help
Q / ESC - Abort mission
__int64 OpenShadowGateDevice ()
{
__int64 result;
DWORD LastError;
result = (__int64)CreateFileW(L"\\\\.\\ShadowGate" , 0xC0000000 , 0 , 0 , 3u , 0x80u , 0 );
if ( result == -1 )
{
LastError = GetLastError();
sub_140001010("[ERROR] Failed to open device '%ws'\n" , L"\\\\.\\ShadowGate" );
sub_140001010("[ERROR] Error code: %lu (0x%08lX)\n" , LastError, LastError);
if ( LastError == 2 )
{
sub_140001010("[HINT] Driver not loaded. Use: sc create ShadowGate type=kernel binPath=<path>\\ShadowGateSys.sys\n" );
sub_140001010("[HINT] Then: sc start ShadowGate\n" );
return -1 ;
}
else
{
if ( LastError == 5 )
sub_140001010("[HINT] Run as Administrator.\n" );
return -1 ;
}
}
return result;
}
HANDLE CreateLeakEvents ()
{
HANDLE result;
hObject = CreateEventW(0 , 1 , 0 , L"Global\\MazeMoveOK" );
result = CreateEventW(0 , 1 , 0 , L"Global\\MazeMoveWall" );
qword_140005688 = result;
return result;
}
if ( QueryMaza(v6, &v9) )
printf (
"[*] Maze grid: %ux%u, Entry=(%u,%u), Exit=(%u,%u)\n" ,
(_DWORD)v9,
DWORD1(v9),
DWORD2(v9),
HIDWORD(v9),
v10,
HIDWORD(v10));
BOOL __fastcall QueryMaza (__int64 a1, void *lpOutBuffer)
{
DWORD BytesReturned;
BytesReturned = 0 ;
return DeviceIoControl(hDevice, 0x8001200C , 0 , 0 , lpOutBuffer, 0x18u , &BytesReturned, 0 );
}
push rcx
ret
jmp rcx
v8 -= 27 ;
switch ( v8 )
case '7' :
case 'W' :
ResetMaze(_RCX);
v6 = 0 ;
dword_140005668 = 0 ;
v22[0 ] = 0 ;
v7 = 0 ;
printf ("[*] Reset to entry point.\n" );
continue ;
BOOL ResetMaze ()
{
DWORD BytesReturned;
BytesReturned = 0 ;
return DeviceIoControl(hDevice, 0x80012008 , 0 , 0 , 0 , 0 , &BytesReturned, 0 );
}
case '&' :
case '/' :
case 'F' :
case 'O' :
LOBYTE(direct) = 0x30 ;
v12 = 76 ;
goto LABEL_11;
case ')' :
case '1' :
case 'I' :
case 'Q' :
LOBYTE(direct) = 0x40 ;
__asm { rcl al, cl }
v12 = 82 ;
goto LABEL_11;
case '.' :
case '<' :
case 'N' :
case '\\' :
LOBYTE(direct) = 0x10 ;
v12 = 85 ;
goto LABEL_11;
case '0' :
case '8' :
case 'P' :
case 'X' :
LOBYTE(direct) = 0x20 ;
v12 = 68 ;
LABEL_11:
if ( v6 < 255 )
{
v22[v7] = v12;
++v6;
++v7;
if ( (unsigned int )v6 >= 0x100 )
sub_140001E88(_RCX, direct);
v22[v7] = 0 ;
}
v14 = (unsigned int )dword_140005668;
memset (v21, 0 , sizeof (v21));
v18 = 0 ;
++dword_140005668;
v15 = TryMove(hDevice, direct, v14, v21, &v18);
__int64 v16;
unsigned int v17;
v16 = (unsigned __int8)v9;
v17 = a3 ^ v9 ^ 0xDEAD1337 ;
DeviceIoControl(a1, 0x80012004 , &v16, 0xCu , v14, 0x84u , &v15, 0 )
__int64 __fastcall TryMove (void *a1, __int64 _RDX, int op_count, _BYTE *a4, _DWORD *a5)
{
v6 = _RDX ^ 0xFA ;
v9 = (unsigned __int8)(_RDX | (8 * v6));
v16 = (unsigned __int8)v9;
v17 = op_count ^ v9 ^ 0xDEAD1337
if ( DeviceIoControl(a1, 0x80012004 , &v16, 0xCu , v14, 0x84u , &v15, 0 ) )
...
}
lea r8, [r11-30h] ; lpInBuffer
mov [r11-30h], al
mov [r11-2Ch], r8d
mov [r11-28h], eax
[buf+0 ] 写入 1 字节
[buf+4 ] 写入 4 字节
[buf+8 ] 写入 4 字节
struct {
uint8_t field0;
uint8_t pad[3 ];
uint32_t field4;
uint32_t field8;
};
typedef struct _MOVE_REQ {
uint8_t encoded_dir;
uint8_t pad[3 ];
uint32_t op_count;
uint32_t checksum;
} MOVE_REQ;
case '9' :
case 'Y' :
sub_140001010(asc_1400038E0, (unsigned int )v6);
v17 = v22;
if ( v6 <= 0 )
v17 = "(none)" ;
sub_140001010(asc_140003908, v17);
continue ;
- D D D D D T
程序输出:
- Sequence : RRRRR
0x140003244 call sub_1400018A0
mov eax, 0
__int64 __fastcall sub_140003208 (struct _DRIVER_OBJECT *a1)
{
NTSTATUS v3;
NTSTATUS v4;
struct _UNICODE_STRING DestinationString ;
P = (PVOID)ExAllocatePool2(64 , 472 , 1702519117 );
if ( !P )
return 3221225626LL ;
KeInitializeSpinLock(&SpinLock);
KeInitializeSpinLock(&qword_1400050D0);
((void (*)(void ))loc_140001E60)();
DestinationString = 0 ;
RtlInitUnicodeString(&DestinationString, L"\\Device\\ShadowGate" );
v3 = IoCreateDevice(a1, 0 , &DestinationString, 0x22u , 0x100u , 0 , &DeviceObject);
v4 = v3;
if ( v3 < 0 )
{
_mm_lfence();
ExFreePoolWithTag(P, 0x657A614Du );
LABEL_5:
P = 0 ;
return (unsigned int )v4;
}
RtlInitUnicodeString(&SymbolicLinkName, L"\\??\\ShadowGate" );
v4 = IoCreateSymbolicLink(&SymbolicLinkName, &DestinationString);
if ( v4 < 0 )
{
_mm_lfence();
IoDeleteDevice(DeviceObject);
ExFreePoolWithTag(P, 0x657A614Du );
DeviceObject = 0 ;
goto LABEL_5;
}
a1->DriverUnload = (PDRIVER_UNLOAD)sub_140001840;
a1->MajorFunction[0 ] = (PDRIVER_DISPATCH)sub_1400014B0;
a1->MajorFunction[2 ] = (PDRIVER_DISPATCH)sub_140001410;
a1->MajorFunction[14 ] = (PDRIVER_DISPATCH)sub_140001540;
DeviceObject->Flags |= 4u ;
DeviceObject->Flags &= ~0x80u ;
return 0 ;
}
int __fastcall sub_1400022B0 (__int64 a1, int a2)
{
int result;
const WCHAR *v4;
struct _UNICODE_STRING DestinationString ;
struct _OBJECT_ATTRIBUTES ObjectAttributes ;
void *EventHandle;
result = (unsigned __int8)dword_140005000;
if ( (((_BYTE)dword_140005000 * ((_BYTE)dword_140005000 - 1 )) & 1 ) == 0 )
{
if ( !a2 || (v4 = L"\\BaseNamedObjects\\MazeMoveWall" , a2 == 2 ) )
v4 = L"\\BaseNamedObjects\\MazeMoveOK" ;
RtlInitUnicodeString(&DestinationString, v4);
ObjectAttributes.Length = 48 ;
ObjectAttributes.ObjectName = &DestinationString;
ObjectAttributes.RootDirectory = 0 ;
ObjectAttributes.Attributes = 576 ;
EventHandle = 0 ;
*(_OWORD *)&ObjectAttributes.SecurityDescriptor = 0 ;
result = ZwOpenEvent(&EventHandle, 2u , &ObjectAttributes);
if ( result >= 0 )
{
ZwSetEvent(EventHandle, 0 );
return ZwClose(EventHandle);
}
}
return result;
}
if ( !v5 || (v9 = &unk_1400041E0, v5 == 2 ) )
v9 = &unk_140004160;
v10 = 57 ;
v11 = v9 - (_BYTE *)v18;
v12 = v18;
do
{
*v12 = *(WCHAR *)((char *)v12 + v11) ^ 0x4B ;
++v12;
--v10;
}
while ( v10 );
}
RtlInitUnicodeString((PUNICODE_STRING)v17, v18);
ObReferenceObjectByName(v17, 64 , 0 );
def decode_guid_name (words ):
return '' .join(chr (w ^ 0x4B ) for w in words)
unk_140004160 = [0x0017 , 0x0009 , 0x002A , 0x0038 , 0x002E , 0x0005 , 0x002A , 0x0026 , 0x002E , 0x002F , 0x0004 , 0x0029 , 0x0021 , 0x002E , 0x0028 , 0x003F , 0x0038 , 0x0017 , 0x0030 , 0x000A , 0x007C , 0x000D , 0x0078 , 0x0009 , 0x0079 , 0x0008 , 0x007A , 0x0066 , 0x0072 , 0x000E , 0x007F , 0x000F , 0x0066 , 0x007F , 0x0008 , 0x0073 , 0x000A , 0x0066 , 0x0009 , 0x007E , 0x000F , 0x007D , 0x0066 , 0x007A , 0x000D , 0x0079 , 0x000E , 0x0078 , 0x000A , 0x007F , 0x0009 , 0x007E , 0x0008 , 0x007D , 0x000F , 0x0036 , 0x004B , 0x0000 , 0x0000 , 0x0000 , 0x0000 , 0x0000 , 0x0000 , 0x0000 ]
unk_1400041E0 = [0x0017 , 0x0009 , 0x002A , 0x0038 , 0x002E , 0x0005 , 0x002A , 0x0026 , 0x002E , 0x002F , 0x0004 , 0x0029 , 0x0021 , 0x002E , 0x0028 , 0x003F , 0x0038 , 0x0017 , 0x0030 , 0x0009 , 0x0073 , 0x000E , 0x0079 , 0x0008 , 0x0078 , 0x000F , 0x007B , 0x0066 , 0x007B , 0x000D , 0x007E , 0x000A , 0x0066 , 0x007E , 0x000F , 0x0072 , 0x0009 , 0x0066 , 0x0008 , 0x007D , 0x000E , 0x007C , 0x0066 , 0x0079 , 0x000A , 0x0078 , 0x000D , 0x007F , 0x0009 , 0x007E , 0x0008 , 0x007D , 0x000F , 0x007C , 0x000E , 0x0036 , 0x004B , 0x0000 , 0x0000 , 0x0000 , 0x000E , 0x0033 , 0x0018 , 0x002E ]
print (decode_guid_name(unk_140004160))
print (decode_guid_name(unk_1400041E0))
\BaseNamedObjects\{A7F3B2C1-9E4 D-4 C8A-B5D6-1F 2E3A4B5C6D}KKKKKKK
\BaseNamedObjects\{B8E2C3D0-0F 5A-5 D9B-C6E7-2 A3F4B5C6D7E}KKKExSe
\BaseNamedObjects\{A7F3B2C1-9E4 D-4 C8A-B5D6-1F 2E3A4B5C6D}
\BaseNamedObjects\{B8E2C3D0-0F 5A-5 D9B-C6E7-2 A3F4B5C6D7E}
static const wchar_t* DEVICE_NAME = L"\\\\.\\ShadowGate" ;
static const wchar_t* GUID1_NAME = L"Global\\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}" ;
static const wchar_t* GUID2_NAME = L"Global\\{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}" ;
static const DWORD IOCTL_MOVE = 0x80012004 ;
static const DWORD IOCTL_RESET = 0x80012008 ;
static const DWORD IOCTL_QUERY = 0x8001200C ;
struct MOVE_REQ {
uint8_t encoded_dir;
uint8_t pad[3 ];
uint32_t op_count;
uint32_t checksum;
};
static uint8_t ror8(uint8_t x, int n) {
return static_cast<uint8_t>((x >> n) | (x << (8 - n)));
}
static uint8_t encode_dir(uint8_t move_code) {
return ror8(static_cast<uint8_t>(move_code ^ 0x5A ), 5 );
}
static uint8_t move_code_from_char(char ch) {
switch (ch) {
case 'W' : case 'w' : return 0x10 ;
case 'S' : case 's' : return 0x20 ;
case 'A' : case 'a' : return 0x30 ;
case 'D' : case 'd' : return 0x40 ;
default: return 0 ;
}
}
static void drain_semaphore(HANDLE hSem) {
while (WaitForSingleObject(hSem, 0 ) == WAIT_OBJECT_0) {
}
}
static bool poll_semaphore(HANDLE hSem) {
DWORD rc = WaitForSingleObject(hSem, 0 );
return rc == WAIT_OBJECT_0;
}
static bool reset_maze(HANDLE hDev) {
DWORD bytesReturned = 0 ;
return DeviceIoControl(
hDev,
IOCTL_RESET,
nullptr, 0 ,
nullptr, 0 ,
&bytesReturned,
nullptr
) != FALSE;
}
static bool move_once(HANDLE hDev, char ch, uint32_t opCount) {
uint8_t mc = move_code_from_char(ch);
if (!mc) {
std::cerr << "invalid move char: " << ch << "\n" ;
return false;
}
MOVE_REQ req{};
req.encoded_dir = encode_dir(mc);
req.op_count = opCount;
req.checksum = req.encoded_dir ^ opCount ^ 0xDEAD1337u;
uint8_t outbuf[0x84 ] = {};
DWORD bytesReturned = 0 ;
return DeviceIoControl(
hDev,
IOCTL_MOVE,
&req,
sizeof(req),
outbuf,
sizeof(outbuf),
&bytesReturned,
nullptr
) != FALSE;
}
int wmain(int argc, wchar_t* argv[]) {
std::string path = "DDDDD" ;
if (argc >= 2 ) {
std::wstring ws = argv[1 ];
path.assign(ws.begin(), ws.end());
}
HANDLE hDev = CreateFileW(
DEVICE_NAME,
GENERIC_READ | GENERIC_WRITE,
0 ,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr
);
if (hDev == INVALID_HANDLE_VALUE) {
std::cerr << "CreateFileW failed, gle=0x" << std::hex << GetLastError() << "\n" ;
return 1 ;
}
HANDLE hGuid1 = CreateSemaphoreW(nullptr, 0 , 0x7fffffff , GUID1_NAME);
HANDLE hGuid2 = CreateSemaphoreW(nullptr, 0 , 0x7fffffff , GUID2_NAME);
if (!hGuid1 || !hGuid2) {
std::cerr << "CreateSemaphoreW failed, gle=0x" << std::hex << GetLastError() <<
"\n" ;
CloseHandle(hDev);
return 1 ;
}
if (!reset_maze(hDev)) {
std::cerr << "reset failed, gle=0x" << std::hex << GetLastError() << "\n" ;
CloseHandle(hGuid1);
CloseHandle(hGuid2);
CloseHandle(hDev);
return 1 ;
}
uint32_t opCount = 0 ;
std::cout << "Testing path: " << path << "\n" ;
for (size_t i = 0 ; i < path.size(); ++i) {
drain_semaphore(hGuid1);
drain_semaphore(hGuid2);
char ch = path[i];
if (!move_once(hDev, ch, opCount)) {
std::cerr << "move " << ch << " failed at step " << i
<< ", gle=0x" << std::hex << GetLastError() << "\n" ;
break ;
}
bool g1 = poll_semaphore(hGuid1);
bool g2 = poll_semaphore(hGuid2);
std::cout << "step " << (i + 1 )
<< " move=" << ch
<< " opCount=" << std::dec << opCount
<< " guid1=" << (g1 ? "triggered" : "no" )
<< " guid2=" << (g2 ? "triggered" : "no" )
<< "\n" ;
++opCount;
}
CloseHandle(hGuid1);
CloseHandle(hGuid2);
CloseHandle(hDev);
return 0 ;
}
Testing path: DDDDD
step 1 move=D opCount=0 guid1=no guid2=no
step 2 move=D opCount=1 guid1=triggered guid2=no
step 3 move=D opCount=2 guid1=no guid2=no
step 4 move=D opCount=3 guid1=no guid2=no
step 5 move=D opCount=4 guid1=no guid2=no
- PsGetCurrentProcessId
- PsGetCurrentThreadId
- PsLookupProcessByProcessId
- PsLookupThreadByThreadId
- KeStackAttachProcess
- KeUnstackDetachProcess
- PsGetProcessPeb
- ZwQueryVirtualMemory
int __fastcall sub_140316ADF (__int64 _RCX, int a2)
{
__int16 _AX;
unsigned __int64 v4;
__int64 v6;
void *v7;
__int64 v8;
int *v9;
int v10;
__int64 v12;
PEPROCESS Process;
PVOID Object;
struct _KAPC_STATE ApcState ;
_AX = 0 ;
__asm { rcl ax, cl }
v4 = (unsigned __int64)&v12 ^ _security_cookie;
v6 = _RCX;
v7 = *(void **)(_RCX + 464 );
if ( v7 )
{
if ( *(_QWORD *)(v6 + 456 ) )
{
if ( qword_140005080 )
{
Object = 0 ;
LODWORD(v4) = PsLookupThreadByThreadId(v7, (PETHREAD *)&Object);
if ( (v4 & 0x80000000 ) == 0LL )
{
Process = 0 ;
if ( PsLookupProcessByProcessId(*(HANDLE *)(v6 + 456 ), &Process) >= 0 )
{
KeStackAttachProcess(Process, &ApcState);
v8 = qword_140005080(Object);
if ( v8 )
{
v9 = (int *)(v8 + 0x68 );
if ( (((_BYTE)dword_140005000 * ((_BYTE)dword_140005000 - 1 )) & 1 ) != 0 )
{
v10 = 0xDEADDEAD ;
}
else if ( a2 )
{
v10 = 0xC0DE0002 ;
if ( a2 != 2 )
v10 = 0xC0DE0000 ;
}
else
{
v10 = 0xC0DE0001 ;
}
ProbeForWrite(v9, 4u , 4u );
*v9 = v10;
}
KeUnstackDetachProcess(&ApcState);
ObfDereferenceObject(Process);
}
LODWORD(v4) = ObfDereferenceObject(Object);
}
}
}
}
return v4;
}
v9 = (int *)(v8 + 0x68 );
*v9 = v10;
void sub_14031857E ()
{
__int64 v0;
_QWORD *v1;
if ( PsGetThreadTeb )
{
if ( qword_140005090 )
{
v0 = _guard_dispatch_icall_fptr();
if ( v0 )
{
v1 = (_QWORD *)(v0 + 0x1748 );
ProbeForRead((volatile void *)(v0 + 0x1748 ), 8u , 8u );
if ( *v1 )
_guard_dispatch_icall_fptr();
}
}
}
}
1403185EF lea rbx, [rax+1748h] //rbx = TEB + 0x1748
14031861E mov rcx, rbx //ProbeForRead(TEB+0x1748, 8, 8)
140318621 call cs:ProbeForRead
140318627 mov r10, [rbx] //读出这个位置的 8 字节值 r10 = *(QWORD*)(TEB+0x1748)
140318630 test r10, r10 //如果这个值是 0
140318638 jz loc_140318758
1403186F6 mov [rsp+38h+arg_10], 0
140318701 mov [rsp+38h+arg_11], r8b
14031871D lea r8, [rsp+38h+arg_10] //r8 = &2_byte_buffer
140318710 mov r9d, 2 //r9d = 2
14031872B mov rcx, r10 // rcx = *(QWORD*)(TEB+0x1748)
qword_140005090(
*(QWORD*)(TEB + 0x1748 ),
4 ,
&two_byte_buf,
2
);
为什么我把它解释成 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 (
_In_ HANDLE Handle,
_In_ OBJECT_INFORMATION_CLASS ObjectInformationClass,
_In_reads_bytes_(ObjectInformationLength) PVOID ObjectInformation,
_In_ ULONG ObjectInformationLength
) ;
#endif
#endif
typedef struct _OBJECT_HANDLE_FLAG_INFORMATION {
BOOLEAN Inherit;
BOOLEAN ProtectFromClose;
} OBJECT_HANDLE_FLAG_INFORMATION;
if ( flags & HANDLE_FLAG_PROTECT_FROM_CLOSE != 0 )
handle_ok = true
else
handle_ok = False
- DAD + A
- handle_ok = True
- DAD + W
- handle_ok = False
void __fastcall sub_1400026B4 (unsigned int *a1)
{
sub_140002038(a1);
sub_140002388();
JUMPOUT(0x140001FF0L L);
}
void __fastcall sub_140002038 (unsigned int *a1)
{
_BYTE *v1;
__int64 v2;
unsigned int v3;
if ( a1 )
{
v1 = a1 + 1 ;
v2 = 56 ;
v3 = TickCountLowDeprecated ^ 0xBAADF00D ;
*a1 = TickCountLowDeprecated ^ 0xBAADF00D ;
do
{
v3 = 1103515245 * v3 + 12345 ;
*v1++ = BYTE2(v3);
--v2;
}
while ( v2 );
}
}
NTSTATUS sub_140002388 ()
{
union _LARGE_INTEGER Interval ;
Interval.QuadPart = -10000LL * (TickCountLowDeprecated % 50u + 10 );
return KeDelayExecutionThread(0 , 0 , &Interval);
}
tick_xor = struct.unpack_from("<I" , raw, 0 )[0 ]
tick = tick_xor ^ 0xBAADF00D
predicted_ms = (tick % 50 ) + 10
t0 = time.perf_counter()
DeviceIoControl(...)
time_ms = (time.perf_counter() - t0) * 1000.0
import argparse
import collections
import ctypes
from ctypes import wintypes
import struct
import sys
import time
kernel32 = ctypes.WinDLL("kernel32" , use_last_error=True )
ntdll = ctypes.WinDLL("ntdll" )
GENERIC_READ = 0x80000000
GENERIC_WRITE = 0x40000000
OPEN_EXISTING = 3
FILE_ATTRIBUTE_NORMAL = 0x80
INVALID_HANDLE_VALUE = wintypes.HANDLE(-1 ).value
WAIT_OBJECT_0 = 0x00000000
WAIT_TIMEOUT = 0x00000102
HANDLE_FLAG_PROTECT_FROM_CLOSE = 0x00000002
IOCTL_MOVE = 0x80012004
IOCTL_RESET = 0x80012008
IOCTL_QUERY = 0x8001200C
WIN_MAGIC = 0x57494E21
LEAK_WALL = 0xC0DE0000
LEAK_OK = 0xC0DE0001
LEAK_EXIT = 0xC0DE0002
LEAK_POISON = 0xDEADDEAD
DEVICE_NAME = r"\\.\ShadowGate"
EVENT_OK_NAME = r"Global\MazeMoveOK"
EVENT_WALL_NAME = r"Global\MazeMoveWall"
SEMAPHORE_OK_GUID = "{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}"
SEMAPHORE_WALL_GUID = "{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}"
DIRS = {
"W" : (0 , -1 ),
"A" : (-1 , 0 ),
"S" : (0 , 1 ),
"D" : (1 , 0 ),
}
INVERSE = {"W" : "S" , "S" : "W" , "A" : "D" , "D" : "A" }
MOVE_CODE = {"W" : 0x10 , "A" : 0x30 , "S" : 0x20 , "D" : 0x40 }
kernel32.CreateFileW.argtypes = [
wintypes.LPCWSTR,
wintypes.DWORD,
wintypes.DWORD,
wintypes.LPVOID,
wintypes.DWORD,
wintypes.DWORD,
wintypes.HANDLE,
]
kernel32.CreateFileW.restype = wintypes.HANDLE
kernel32.DeviceIoControl.argtypes = [
wintypes.HANDLE,
wintypes.DWORD,
wintypes.LPVOID,
wintypes.DWORD,
wintypes.LPVOID,
wintypes.DWORD,
ctypes.POINTER(wintypes.DWORD),
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 = [
wintypes.LPVOID,
wintypes.LONG,
wintypes.LONG,
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 = None
class CLIENT_ID (ctypes.Structure):
_fields_ = [("UniqueProcess" , wintypes.HANDLE), ("UniqueThread" , wintypes.HANDLE)]
class THREAD_BASIC_INFORMATION (ctypes.Structure):
_fields_ = [
("ExitStatus" , wintypes.LONG),
("TebBaseAddress" , wintypes.LPVOID),
("ClientId" , CLIENT_ID),
("AffinityMask" , ctypes.c_size_t),
("Priority" , wintypes.LONG),
("BasePriority" , wintypes.LONG),
]
ntdll.NtQueryInformationThread.argtypes = [
wintypes.HANDLE,
wintypes.ULONG,
wintypes.LPVOID,
wintypes.ULONG,
ctypes.POINTER(wintypes.ULONG),
]
ntdll.NtQueryInformationThread.restype = wintypes.LONG
class WinError (RuntimeError ):
pass
def check_handle (handle, what ):
if handle in (None , 0 , INVALID_HANDLE_VALUE):
raise WinError(f"{what} failed, last_error=0x{ctypes.get_last_error():08X} " )
return handle
def ror8 (value, shift ):
value &= 0xFF
return ((value >> shift) | (value << (8 - shift))) & 0xFF
def encode_move_code (move_code ):
return ror8(move_code ^ 0x5A , 5 )
def current_teb_base ():
tbi = THREAD_BASIC_INFORMATION()
returned = wintypes.ULONG()
status = ntdll.NtQueryInformationThread(
kernel32.GetCurrentThread(),
0 ,
ctypes.byref(tbi),
ctypes.sizeof(tbi),
ctypes.byref(returned),
)
if status != 0 :
raise WinError(f"NtQueryInformationThread failed, status=0x{status & 0xFFFFFFFF :08X} " )
return int (ctypes.cast(tbi.TebBaseAddress, ctypes.c_void_p).value)
def slot5_predicted_delay_ms (raw ):
if len (raw) < 4 :
return None
tick_xor = struct.unpack_from("<I" , raw, 0 )[0 ]
tick = tick_xor ^ 0xBAADF00D
return float ((tick % 0x32 ) + 10 )
def slot5_timing_hit (result, tolerance_ms=12.0 ):
sig = result["signals" ]
if sig["event_ok" ] or sig["event_wall" ] or sig["sem_ok" ] or sig["sem_wall" ] or sig["handle_ok" ]:
return False
if result["last_error" ] in (LEAK_OK, LEAK_WALL, LEAK_EXIT, LEAK_POISON):
return False
predicted = result["slot5_predicted_ms" ]
if predicted is None :
return False
return abs (result["time_ms" ] - predicted) <= tolerance_ms
class ShadowGate :
def __init__ (self ):
self .handle = None
self .event_ok = None
self .event_wall = None
self .sem_ok = None
self .sem_wall = None
self .slot4_probe = None
self ._teb_1748_slot = None
self ._teb_1748_old = None
self .op_count = 0
def open (self ):
self .handle = check_handle(
kernel32.CreateFileW(
DEVICE_NAME,
GENERIC_READ | GENERIC_WRITE,
0 ,
None ,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
None ,
),
f"CreateFileW({DEVICE_NAME} )" ,
)
self .event_ok = self ._ensure_event(EVENT_OK_NAME)
self .event_wall = self ._ensure_event(EVENT_WALL_NAME)
self .sem_ok = self ._ensure_semaphore(SEMAPHORE_OK_GUID)
self .sem_wall = self ._ensure_semaphore(SEMAPHORE_WALL_GUID)
self ._install_slot4_probe()
return self
def close (self ):
if self ._teb_1748_slot is not None and self ._teb_1748_old is not None :
self ._teb_1748_slot.value = self ._teb_1748_old
self ._teb_1748_slot = None
self ._teb_1748_old = None
for attr in ("slot4_probe" , "sem_wall" , "sem_ok" , "event_wall" , "event_ok" , "handle" ):
handle = getattr (self , attr)
if handle:
kernel32.CloseHandle(handle)
setattr (self , attr, None )
def _ensure_event (self, name ):
candidates = [name]
if name.startswith("Global\\" ):
candidates.append(name.split("\\" , 1 )[1 ])
last_exc = None
for candidate in candidates:
try :
return check_handle(kernel32.CreateEventW(None , True , False , candidate), f"CreateEventW({candidate} )" )
except WinError as exc:
last_exc = exc
raise last_exc or WinError(f"CreateEventW({name} ) failed" )
def _ensure_semaphore (self, guid ):
candidates = (rf"Global\{guid}" , guid)
last_exc = None
for candidate in candidates:
try :
return check_handle(
kernel32.CreateSemaphoreW(None , 0 , 0x7FFFFFFF , candidate),
f"CreateSemaphoreW({candidate} )" ,
)
except WinError as exc:
last_exc = exc
raise last_exc or WinError(f"CreateSemaphoreW({guid} ) failed" )
def _install_slot4_probe (self ):
teb = current_teb_base()
self .slot4_probe = check_handle(kernel32.CreateEventW(None , True , False , None ), "CreateEventW(slot4_probe)" )
self ._teb_1748_slot = ctypes.c_uint64.from_address(teb + 0x1748 )
self ._teb_1748_old = self ._teb_1748_slot.value
self ._teb_1748_slot.value = int (self .slot4_probe)
def _ioctl (self, code, in_bytes=b"" , out_size=0 ):
in_buf = ctypes.create_string_buffer(in_bytes, len (in_bytes)) if in_bytes else None
out_buf = ctypes.create_string_buffer(out_size) if out_size else None
returned = wintypes.DWORD()
kernel32.SetLastError(0 )
ok = kernel32.DeviceIoControl(
self .handle,
code,
in_buf,
len (in_bytes),
out_buf,
out_size,
ctypes.byref(returned),
None ,
)
last_error = ctypes.get_last_error() & 0xFFFFFFFF
if not ok:
raise WinError(f"DeviceIoControl(0x{code:08X} ) failed, last_error=0x{last_error:08X} " )
data = out_buf.raw[: returned.value] if out_buf else b""
return data, last_error
def query_maze (self ):
data, _ = self ._ioctl(IOCTL_QUERY, b"" , 0x18 )
return struct.unpack("<6I" , data[:0x18 ])
def reset (self ):
self ._clear_sync_objects()
self ._ioctl(IOCTL_RESET)
self .op_count = 0
def move (self, ch ):
self ._clear_sync_objects()
if self .slot4_probe:
kernel32.SetHandleInformation(self .slot4_probe, HANDLE_FLAG_PROTECT_FROM_CLOSE, 0 )
move_code = MOVE_CODE[ch]
encoded = encode_move_code(move_code)
packet = struct.pack("<B3xII" , encoded, self .op_count, encoded ^ self .op_count ^ 0xDEAD1337 )
t0 = time.perf_counter()
data, last_error = self ._ioctl(IOCTL_MOVE, packet, 0x84 )
time_ms = (time.perf_counter() - t0) * 1000.0
signals = self ._collect_signals()
outcome = self ._classify_outcome(last_error, signals)
predicted = slot5_predicted_delay_ms(data)
self .op_count += 1
credential = None
if len (data) >= 0x84 and struct.unpack_from("<I" , data, 0x3C )[0 ] == WIN_MAGIC:
length = struct.unpack_from("<I" , data, 0x80 )[0 ]
credential = data[0x40 : 0x40 + min (length, 0x3F )].split(b"\x00" , 1 )[0 ].decode("ascii" , errors="replace" )
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" : False ,
"raw" : data,
"credential" : credential,
}
result["slot5_hit" ] = slot5_timing_hit(result)
return result
def _clear_sync_objects (self ):
if self .event_ok:
kernel32.ResetEvent(self .event_ok)
if self .event_wall:
kernel32.ResetEvent(self .event_wall)
for sem in (self .sem_ok, self .sem_wall):
if sem:
while kernel32.WaitForSingleObject(sem, 0 ) == WAIT_OBJECT_0:
pass
def _collect_signals (self ):
handle_ok = False
if self .slot4_probe:
flags = wintypes.DWORD()
if kernel32.GetHandleInformation(self .slot4_probe, ctypes.byref(flags)):
handle_ok = bool (flags.value & HANDLE_FLAG_PROTECT_FROM_CLOSE)
return {
"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,
}
def _classify_outcome (self, last_error, signals ):
if last_error == LEAK_EXIT:
return "exit"
if last_error == LEAK_OK or signals["event_ok" ] or signals["sem_ok" ] or signals["handle_ok" ]:
return "ok"
if last_error == LEAK_WALL or signals["event_wall" ] or signals["sem_wall" ]:
return "wall"
if last_error == LEAK_POISON:
return "poison"
return "unknown"
@staticmethod
def _poll_event (handle ):
if not handle:
return False
return kernel32.WaitForSingleObject(handle, 0 ) == WAIT_OBJECT_0
@staticmethod
def _poll_semaphore (handle ):
if not handle:
return False
rc = kernel32.WaitForSingleObject(handle, 0 )
return rc == WAIT_OBJECT_0
def shortest_path (open_edges, start, goal ):
queue = collections.deque([start])
prev = {start: (None , None )}
while queue:
cur = queue.popleft()
if cur == goal:
break
for move, nxt in open_edges.get(cur, {}).items():
if nxt not in prev:
prev[nxt] = (cur, move)
queue.append(nxt)
if goal not in prev:
return None
path = []
cur = goal
while prev[cur][0 ] is not None :
cur, move = prev[cur]
path.append(move)
path.reverse()
return "" .join(path)
def is_explicit_success (result ):
return result["outcome" ] in ("ok" , "exit" )
def is_explicit_wall (result ):
return result["outcome" ] in ("wall" , "poison" )
def replay_path (gate, path ):
for move in path:
result = gate.move(move)
if result["outcome" ] in ("wall" , "poison" ):
raise WinError(f"replay failed at move {move} : {result} " )
def probe_move (gate, route, move, cycle, attempts=8 ):
result = None
for shift in range (attempts):
gate.reset()
try :
replay_path(gate, route)
except WinError:
continue
if cycle:
out_move, back_move = cycle
blocked = False
for _ in range (shift):
out_res = gate.move(out_move)
if is_explicit_wall(out_res):
blocked = True
break
back_res = gate.move(back_move)
if is_explicit_wall(back_res):
blocked = True
break
if blocked:
continue
result = gate.move(move)
if result["slot5_hit" ]:
continue
if is_explicit_success(result):
return True , result
if is_explicit_wall(result):
return False , result
return None , result
def solve_via_iterative_exploration (gate, width, height, start, goal, max_rounds=180 ):
discovered = {start}
open_edges = collections.defaultdict(dict )
walls = set ()
pending = collections.deque([start])
credential = None
rounds = 0
while rounds < max_rounds and pending:
rounds += 1
progress = False
queue = collections.deque(list (dict .fromkeys(pending)))
pending.clear()
while queue:
cell = queue.popleft()
route = shortest_path(open_edges, start, cell) or ""
cycle = None
if route:
cycle = (INVERSE[route[-1 ]], route[-1 ])
elif "D" in open_edges[start]:
cycle = ("D" , "A" )
for move, delta in DIRS.items():
nx = cell[0 ] + delta[0 ]
ny = cell[1 ] + delta[1 ]
if not (0 <= nx < width and 0 <= ny < height):
continue
nxt = (nx, ny)
if move in open_edges[cell] or (cell, nxt) in walls:
continue
decision, result = probe_move(gate, route, move, cycle, attempts=8 )
if result and result["credential" ]:
credential = result["credential" ]
if decision is True :
progress = True
open_edges[cell][move] = nxt
open_edges[nxt][INVERSE[move]] = cell
if nxt not in discovered:
discovered.add(nxt)
pending.append(nxt)
pending.append(cell)
elif decision is False :
progress = True
walls.add((cell, nxt))
walls.add((nxt, cell))
else :
pending.append(cell)
path = shortest_path(open_edges, start, goal)
if path:
if credential is None :
gate.reset()
replay_result = None
for move in path:
replay_result = gate.move(move)
if replay_result and replay_result["credential" ]:
credential = replay_result["credential" ]
return path, credential, open_edges
if not progress and not pending:
break
raise WinError("solve did not converge" )
def render_ascii (width, height, open_edges ):
rows = [["#" ] * (width * 2 + 1 ) for _ in range (height * 2 + 1 )]
for y in range (height):
for x in range (width):
rows[y * 2 + 1 ][x * 2 + 1 ] = "."
for (x, y), edges in open_edges.items():
for move, _ in edges.items():
if move == "D" :
rows[y * 2 + 1 ][x * 2 + 2 ] = " "
elif move == "S" :
rows[y * 2 + 2 ][x * 2 + 1 ] = " "
rows[1 ][1 ] = "S"
rows[height * 2 - 1 ][width * 2 - 1 ] = "E"
return "\n" .join("" .join(r) for r in rows)
def main ():
parser = argparse.ArgumentParser()
parser.add_argument("--probe" , metavar="PATH" )
parser.add_argument("--solve" , action="store_true" )
parser.add_argument("--show-map" , action="store_true" )
args = parser.parse_args()
gate = ShadowGate().open ()
try :
width, height, sx, sy, ex, ey = gate.query_maze()
print (f"maze: {width} x{height} start=({sx} ,{sy} ) exit=({ex} ,{ey} )" )
print (f"events: ok={bool (gate.event_ok)} wall={bool (gate.event_wall)} " )
print (f"semaphores: ok={bool (gate.sem_ok)} wall={bool (gate.sem_wall)} " )
if args.probe:
gate.reset()
for ch in args.probe.upper():
result = gate.move(ch)
print (
f"{ch} : outcome={result['outcome' ]} "
f"last_error=0x{result['last_error' ]:08X} "
f"time_ms={result['time_ms' ]:.2 f} "
f"slot5_predicted_ms={result['slot5_predicted_ms' ]} "
f"slot5_hit={result['slot5_hit' ]} "
f"signals={result['signals' ]} "
)
return 0
if args.solve:
path, credential, open_edges = solve_via_iterative_exploration(
gate, width, height, (sx, sy), (ex, ey)
)
print (f"path={path} " )
if credential:
print (f"credential={credential} " )
if args.show_map:
print (render_ascii(width, height, open_edges))
return 0
print ("use --solve or --probe" )
return 0
finally :
gate.close()
if __name__ == "__main__" :
sys.exit(main())
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 3天前
被江树编辑
,原因: 补充地图
上传的附件: