这比赛也太难了
1. 成功加载驱动并与之正确通信,理解题目基本机制,识别并排除干扰信息。需在 writeup 中说明分析过程 成功加载驱动并与之通信
关闭强制驱动签名后即可手动加载驱动并运行ring 3 应用与之通信交互, 将ring 3 应用改为用管理员打开也可让ring 3 应用自动加载驱动并与之通信
题目基本机制 ring 3 发送IOCTL 在ring 3 应用的14021AFC9处为主要循环, 可以看到运行时提示的各种信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 int __fastcall main_0 (int argc, const char **argv, const char **envp) { __int64 v3; __int64 v4; __int64 v6; unsigned int op; unsigned int jmp_code; maze_info mazeinfo; _BYTE p_ContextRecord[256 ]; __int64 v11; __int64 v12; banner(argc, argv, envp); printf ("[*] Connecting to Shadow gate driver...\n" ); hDevice = (HANDLE)load_driver(); if ( hDevice != (HANDLE)-1LL ) { v12 = v3; v11 = v4; printf ("[+] Gate module online.\n\n" ); init(); memset (&mazeinfo, 0 , sizeof (mazeinfo)); if ( read_maze_info(v6, &mazeinfo) ) printf ( "[*] Maze grid: %ux%u, Entry=(%u,%u), Exit=(%u,%u)\n" , mazeinfo.hight, mazeinfo.width, mazeinfo.entry_x, mazeinfo.entry_y, mazeinfo.exit_x, mazeinfo.exit_y); menu(); memset (p_ContextRecord, 0 , sizeof (p_ContextRecord)); while ( 1 ) { printf ("[op #%d] > " , 0 ); op = getch(); printf ("%c\n" , op); jmp_code = op - 0x1B ; if ( jmp_code <= 0x5C ) break ; printf ("[?] Unknown command. 'h' for help.\n" ); } __asm { retn } } printf ("[!] Cannot continue without driver.\n" ); printf ("[*] Press any key to exit...\n" ); getch(); return 1 ; }
其中mazeinfo为手动定义结构体:
1 2 3 4 5 6 7 8 9 struct maze_info { _DWORD hight; _DWORD width; _DWORD entry_x; _DWORD entry_y; _DWORD exit_x; _DWORD exit_y; };
1400012E0处通过DeviceIoControl与驱动层建立通信, 并且可以确定控制码0x8001200C对应的功能为让驱动返回迷宫信息:
1 2 3 4 5 6 7 BOOL __fastcall read_maze_info (__int64 a1, void *lpOutBuffer) { DWORD BytesReturned; BytesReturned = 0 ; return DeviceIoControl(hDevice, 0x8001200C , 0 , 0 , lpOutBuffer, 0x18 u, &BytesReturned, 0 ); }
交叉引用DeviceIoControl还可以找到另外两处调用分别在140001280和14021B64C中, 通过调试可以发现140001280中的对应的是重置迷宫状态, 14021B64C对应的是进行一步移动, 其中的一个关键信息是某一步特殊的移动会让驱动层返回的信息中包含一个4字节的数据'WIN!', 猜测是到达终点时的标志:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 __int64 __fastcall do_move_op (void *hDevice, __int64 _RDX, int step, _BYTE *a4, _DWORD *a5) { __int64 v5; char v6; unsigned int v7; int direction; unsigned int Size; __int64 Size_1; _OWORD OutBuffer[9 ]; DWORD lpBytesReturned; mov_info mov_infomation; __int64 v17; v17 = v5; v6 = _RDX ^ 0xFA ; v7 = 0 ; memset (OutBuffer, 0 , 0x84 ); LOBYTE(_RDX) = (unsigned __int8)(_RDX ^ 0x40 ) >> 5 ; direction = (unsigned __int8)(_RDX | (8 * v6)); __asm { rcr rdx, 8B h } *(_QWORD *)&mov_infomation.direction = (unsigned __int8)direction; lpBytesReturned = 0 ; mov_infomation.key = step ^ direction ^ 0xDEAD1337 ; if ( DeviceIoControl(hDevice, 0x80012004 , &mov_infomation, 0xC u, OutBuffer, 0x84 u, &lpBytesReturned, 0 ) ) { if ( HIDWORD(OutBuffer[3 ]) == 'WIN!' ) { v7 = 1 ; if ( a4 ) { if ( a5 ) { Size = OutBuffer[8 ]; *a5 = OutBuffer[8 ]; if ( Size - 1 > 0x3E ) { *a4 = 0 ; } else { Size_1 = Size; memcpy (a4, &OutBuffer[4 ], Size); a4[Size_1] = 0 ; } } } } } else { v7 = -1 ; } memset (OutBuffer, 0 , 0x84 u); return v7; }
这是 3 中控制指令中唯一一个需要传入数据给 ring 0 的, 一共是 3 个 DWORD 数据共0xC字节, 多次调试可以观察出这 3 个数据的含义分别为当前这一步的方向, 当前这一步是第几步, 一个校验值:
1 2 3 4 5 6 7 8 9 struct mov_info { _DWORD direction; _DWORD step; _DWORD key; };
至此可以分析出 3 个基本操作对应的控制码:
1 2 3 move : 0x80012004 reset : 0x80012008 mazeinfo: 0x8001200C
ring 0 对IOCTL分发处理 通过搜索控制码的立即数可以定位到 ring 0 的分发器:
分发器伪代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 __int64 __fastcall ioctl_dispatch (__int64 a1, IRP *a2, __int64 a3, char a4) { __int16 v4; __int16 v5; __int16 v6; struct _IO_STACK_LOCATION *CurrentStackLocation ; __int16 _DI; unsigned int v10; ULONG_PTR n132; DWORD ioctl_code; unsigned int in_len; unsigned int out_len; mov_info *in_buf; unsigned int v16; KIRQL NewIrql_1; KSPIN_LOCK *SpinLock; unsigned __int8 v19; KIRQL NewIrql; KSPIN_LOCK *P; __int64 v22; v6 = __ROR2__(v5, 1 ); LOBYTE(v6) = a4 - v6; CurrentStackLocation = a2->Tail.Overlay.CurrentStackLocation; _DI = v6 - v4; __asm { rcr di, 0 AFh } v10 = 0 ; n132 = 0 ; ioctl_code = CurrentStackLocation->Parameters.Read.ByteOffset.LowPart; in_len = CurrentStackLocation->Parameters.Create.Options; out_len = CurrentStackLocation->Parameters.Read.Length; in_buf = (mov_info *)a2->AssociatedIrp.MasterIrp; if ( ::P ) { switch ( ioctl_code ) { case 0x80012004 : if ( in_buf && in_len >= 0xC && out_len >= 0x84 ) { v19 = *(_QWORD *)&in_buf->direction; if ( in_buf->key == (v19 ^ HIDWORD(*(_QWORD *)&in_buf->direction) ^ 0xDEAD1337 ) ) { NewIrql = KeAcquireSpinLockRaiseToDpc((PKSPIN_LOCK)::P + 56 ); P = (KSPIN_LOCK *)::P; ++*((_DWORD *)::P + 47 ); KeReleaseSpinLock(P + 56 , NewIrql); LOBYTE(v22) = ((32 * v19) | (v19 >> 3 )) ^ 0x5A ; sub_140002161(::P, v22); } sub_1400038C0(in_buf, 0 , 0x84 u); sub_140002038(&in_buf->direction); n132 = 132 ; goto LABEL_18; } break ; case 0x80012008 : sub_140001A7C((KSPIN_LOCK *)::P); sub_14031A53E(); n132 = 0 ; goto LABEL_18; case 0x8001200C : if ( in_buf && out_len >= 0x18 ) { NewIrql_1 = KeAcquireSpinLockRaiseToDpc((PKSPIN_LOCK)::P + 56 ); in_buf[1 ].direction = 0 ; in_buf->direction = 13 ; *(_QWORD *)&in_buf->step = 13 ; SpinLock = (KSPIN_LOCK *)((char *)::P + 448 ); in_buf[1 ].step = 12 ; in_buf[1 ].key = 12 ; KeReleaseSpinLock(SpinLock, NewIrql_1); n132 = 24 ; goto LABEL_18; } break ; default : v10 = 0xC0000010 ; LABEL_18: v16 = v10; return sub_140001000(a2, v16, n132); } v10 = -1073741789 ; goto LABEL_18; } v16 = -1073741661 ; return sub_140001000(a2, v16, n132); }
重点关注推进进展要用到的处理移动指令的 handler, 它先校验了传入的数据包是否能通过校验值校验, 然后拿其中的关键信息, 移动的方向当作第 2 个参数调用了140002161, 进入函数分析发现关键逻辑被严重混淆无法静态分析, 同时IDA还会因为该函数结构分析错误导致伪代码丢失该函数之后的控制流, 暂时 patch 为nop指令即可恢复剩余控制流:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 .... case 0x80012004 :if ( in_buf && in_len >= 0xC && out_len >= 0x84 ){ v38 = *(_QWORD *)&in_buf->direction; if ( in_buf->key == ((unsigned __int8)v38 ^ HIDWORD(v38) ^ 0xDEAD1337 ) ) { NewIrql = KeAcquireSpinLockRaiseToDpc((PKSPIN_LOCK)::P + 56 ); P = (KSPIN_LOCK *)::P; ++*((_DWORD *)::P + 47 ); KeReleaseSpinLock(P + 56 , NewIrql); n2 = n2_1; sub_1400038C0(in_buf, 0 , 0x84 u); NewIrql_4 = KeAcquireSpinLockRaiseToDpc((PKSPIN_LOCK)::P + 56 ); P_1 = ::P; NewIrql_2 = NewIrql_4; CurrentThreadId = PsGetCurrentThreadId(); SpinLock_1 = (KSPIN_LOCK *)((char *)::P + 448 ); P_1[58 ] = CurrentThreadId; KeReleaseSpinLock(SpinLock_1, NewIrql_2); ((void (__fastcall *)(PVOID, _QWORD))loc_14040305A)(::P, n2); if ( n2 == 2 ) { SpinLock_2 = (KSPIN_LOCK *)((char *)::P + 448 ); in_buf[5 ].direction = 'WIN!' ; NewIrql_5 = KeAcquireSpinLockRaiseToDpc(SpinLock_2); _DI = _DI_1; __asm { rcl dil, 0B Eh } NewIrql_3 = NewIrql_5; LOBYTE(_R8D) = _DI; __asm { rcr r8d, 22 h } n255 = *((unsigned int *)::P + 45 ); if ( (unsigned int )n255 > 0xFF ) n255 = 255 ; sub_140003600(buf, (__m128 *)::P + 12 , (unsigned int )n255); _mm_lfence(); SpinLock_3 = (KSPIN_LOCK *)((char *)::P + 448 ); buf[0 ].m128_i8[n255] = 0 ; KeReleaseSpinLock(SpinLock_3, NewIrql_3); key = 0 ; sub_1403F7C6D(buf, (unsigned int )n255, &in_buf[5 ].step, &key); in_buf[10 ].key = key; memset (buf, 0 , sizeof (buf)); } sub_1400026B4(&in_buf->direction); } else { sub_1400038C0(in_buf, 0 , 0x84 u); sub_140002038(&in_buf->direction); } n132 = 132 ; goto LABEL_23; } ....
可以看到执行完140002161的函数之后就会判断一个结果并决定是否在回包中加入关键数据(in_buf[5].direction = 'WIN!';), 也就是说关键移动逻辑和地图状态变化完全在140002161中
2. 发现「宫殿」系统的隐匿通信手段并编写可工作的检测工具,发现的手段种类越多、利用越完整,得分越高,需提交相关源码并在 writeup 中说明分析过程 「宫殿」系统的隐匿通信手段 ring0 1400022B0 set_event 设置全局事件通过可疑字符串找到ring 3 程序的140001340处的函数创建了两个全局事件:
1 2 3 4 5 6 7 8 9 HANDLE sub_140001340 () { HANDLE result; hObject = CreateEventW(0 , 1 , 0 , L"Global\\MazeMoveOK" ); result = CreateEventW(0 , 1 , 0 , L"Global\\MazeMoveWall" ); hObject_0 = result; return result; }
通过相同的字符串可疑搜索到对应驱动层相关逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 int __fastcall set_event (__int64 a1, int n2) { const WCHAR *event_name; int result; struct _UNICODE_STRING DestinationString ; _OBJECT_ATTRIBUTES ObjectAttributes; void *EventHandle; if ( !n2 || (event_name = L"\\BaseNamedObjects\\MazeMoveWall" , n2 == 2 ) ) event_name = L"\\BaseNamedObjects\\MazeMoveOK" ; RtlInitUnicodeString(&DestinationString, event_name); 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; }
根据事件名可以猜测到这应该就是泄露出当前这一步是否撞墙的一条信道
ring0 140319A37 release_semaphore 释放信号量交叉引用ring 3 应用创建全局事件的函数后找到它接下来执行的一个函数14021B91F, 函数解密了两个字符串并使用CreateSemaphoreW创建了两个信号量, 通过调试可以获取到两个信号量分别是:{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}
{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}
在驱动层导入表中找到信号量相关的函数交叉引用找到驱动层设置信号量的函数140319A37
在ring 3 应用多次发送移动控制指令发现部分移动指令会让驱动层进入这两个信道泄露相关的函数, 同时可以观察到这两个函数通过进入函数时的第 2 个参数(rdx)决定了泄露信道时表达的二值信号, 并且都会对这个参数是否与2相等进行判断, 通过这个特征匹配所有:
cmp e?x, 2
cmp r?d, 2
的指令可以再找到以下几个泄露的信道
函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 void __fastcall setObjectHandleFlagInformation (__int64 a1, int n2, void *ObjectInformation, ULONG Length) { __int64 v5; void **v6; void *ObjectHandle; bool v8; char ObjectInformation_[2 ]; if ( PsGetThreadTeb ) { if ( qword_140005090 ) { LODWORD(v5) = idirect_call( KeGetCurrentThread(), (OBJECT_INFORMATION_CLASS)idirect_call, ObjectInformation, Length); if ( v5 ) { v6 = (void **)(v5 + 0x1748 ); ProbeForRead((volatile void *)(v5 + 0x1748 ), 8u , 8u ); ObjectHandle = *v6; if ( *v6 ) { v8 = 1 ; if ( n2 ) v8 = n2 == 2 ; ObjectInformation_[0 ] = 0 ; ObjectInformation_[1 ] = v8; idirect_call(ObjectHandle, ObjectHandleFlagInformation, ObjectInformation_, 2u ); } } } } }
使用间接调用隐藏调用的API, 可以调试得到第一个间接调用调用的是PsGetThreadTeb, 第二个是ZwSetInformationObject, 写入了两个字节的数据, 调试可以发现依旧是一个二值信号, 这两个字节只会是00 00或00 01, 依然由进入该函数的第 2 个参数决定
ring0 140316ADF set_lasterror 设置当前线程的LastError函数体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 int __fastcall SetLastError (__int64 _RCX, int n2) { __int16 _AX; unsigned __int64 v4; __int64 _RCX_1; void *ThreadId; OBJECT_INFORMATION_CLASS ObjectInformationClass; PVOID ObjectInformation; ULONG Length; TEB *TEB; unsigned int *Address; unsigned int walk_back; __int64 v15; PEPROCESS Process; PVOID Object; _KAPC_STATE ApcState; _AX = 0 ; __asm { rcl ax, cl } v4 = (unsigned __int64)&v15 ^ _security_cookie; _RCX_1 = _RCX; ThreadId = *(void **)(_RCX + 0x1D0 ); if ( ThreadId ) { if ( *(_QWORD *)(_RCX_1 + 0x1C8 ) ) { if ( PsGetThreadTeb ) { Object = 0 ; LODWORD(v4) = PsLookupThreadByThreadId(ThreadId, (PETHREAD *)&Object); if ( (v4 & 0x80000000 ) == 0LL ) { Process = 0 ; if ( PsLookupProcessByProcessId(*(HANDLE *)(_RCX_1 + 0x1C8 ), &Process) >= 0 ) { KeStackAttachProcess(Process, &ApcState); LODWORD(TEB) = idirect_call(Object, ObjectInformationClass, ObjectInformation, Length); if ( TEB ) { Address = &TEB->LastErrorValue; if ( n2 ) { walk_back = 0xC0DE0002 ; if ( n2 != 2 ) walk_back = 0xC0DE0000 ; } else { walk_back = 0xC0DE0001 ; } ProbeForWrite(Address, 4u , 4u ); *Address = walk_back; } KeUnstackDetachProcess(&ApcState); ObfDereferenceObject(Process); } LODWORD(v4) = ObfDereferenceObject(Object); } } } } return v4; }
通过调试可以发现是使用间接调用调用了PsGetThreadTeb获取发起移动指令IOCTL的线程的TEB结构, 根据第 2 个参数修改了其中的LastErrorValue字段
通过匹配指令找到的最后一处指令位于第一问分析得到的关键移动逻辑之后对移动结果的判断, 通过调试可以发现其比较的值虽然对比目标是2, 但实际结果依然是一个二值信号, 结合上面所有泄露的信道以及调试测试(测试得到的日志存放在log.txt中)可以明显感知到, 这个值, 包括上面所有的二值信号高度相关, 和进入那些泄露信道函数的第 2 个参数高度一致, 有着如下对应关系:
1 2 MazeMoveOK==c0de0001==\BaseNamedObjects\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}==ObjectHandleFlagInformationVal_100==0 MazeMoveWall==c0de0000==\BaseNamedObjects\{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}==ObjectHandleFlagInformationVal_000==1
编写可工作的检测工具 工具1 WinDBG断点脚本 最简单的获取以上隐藏的通信信息的方法便是在调试环境下, 在发出信号的关键位置下断点, 然后通过WinDBG内置的断点指令功能直接输出关键信息, 断点脚本如下:
1 2 3 4 5 6 7 8 r @$t0 = Base of ShadowGate.sys bp @$t0+31755F ".printf \"[IOCTL MOVE] seq=%x dir=%x\\n\", poi(@r14+4), poi(@r14); gc" bp @$t0+316D05 ".printf \"[Set LastError] LastError=%x\\n\", @ebx; gc" bp @$t0+319C07 ".printf \"[CALL KeReleaseSemaphore] Semphore: %mu\\n\", @rdx; gc" bp @$t0+318750 ".printf \"[Set ObjectHandleFlagInformation] val=%04x\\n\", wo(@r8); gc" bp @$t0+2306h ".printf \"[Set Event] MazeMoveWall\\n\"; gc" bp @$t0+2313h ".printf \"[Set Event] MazeMoveOK\\n\"; gc" bp @$t0+317607h ".printf \"[Final Result] %04x\\n\", @rsi; gc"
使用效果:
工具2 C++编写的信号量主动探测脚本 全局事件监测: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Handle evtOk; Handle evtWall; static bool openFirstEvent (const std ::vector <std ::wstring >& names, Handle& out) { for (const auto & n : names) { HANDLE h = OpenEventW(EVENT_MODIFY_STATE | SYNCHRONIZE, FALSE, n.c_str()); if (h) { out.reset(h); std ::wprintf(L"[probe] opened event %ls\n" , n.c_str()); return true ; } } return false ; } (void )openFirstEvent({ L"Global\\MazeMoveOK" , L"MazeMoveOK" }, evtOk); (void )openFirstEvent({ L"Global\\MazeMoveWall" , L"MazeMoveWall" }, evtWall);
监测信号量释放 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Handle semA; Handle semB; static bool openFirstSemaphore (const std ::vector <std ::wstring >& names, Handle& out) { for (const auto & n : names) { HANDLE h = OpenSemaphoreW(SYNCHRONIZE | SEMAPHORE_MODIFY_STATE, FALSE, n.c_str()); if (h) { out.reset(h); std ::wprintf(L"[probe] opened semaphore %ls\n" , n.c_str()); return true ; } } return false ; } (void )openFirstSemaphore({ L"Global\\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}" , L"{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}" }, semA); (void )openFirstSemaphore({ L"Global\\{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}" , L"{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}" }, semB);
监测线程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 Handle threadHandle; static bool installThreadHandleLeakAnchor (Handle& outThreadHandle) { HANDLE th = OpenThread(THREAD_QUERY_INFORMATION, FALSE, GetCurrentThreadId()); if (!th) { std ::printf ("[probe] OpenThread failed: %lu\n" , GetLastError()); return false ; } outThreadHandle.reset(th); auto * teb = reinterpret_cast<uint8_t *>(NtCurrentTeb()); *reinterpret_cast<HANDLE*>(teb + 0x1748 ) = outThreadHandle.get(); return true ; } (void )installThreadHandleLeakAnchor(threadHandle); MoveOnce(Handle threadLeakHandle...){ ... DWORD flags = 0 ; bool objValid = false ; bool objNowOk = false ; if (objChannelEnabled) { if (GetHandleInformation(threadLeakHandle, &flags)) { objNowOk = ((flags & HANDLE_FLAG_PROTECT_FROM_CLOSE) != 0 ); objValid = (objNowOk != baselineObjOk); } } ... } MoveOnce(..., threadHandle, ...);
监测线程LastError设置 因为是 ring0 直接对指令来源线程设置的, 所以可以直接用一条指令拦截:
DWORD le = GetLastError();
该工具在探测迷宫地图的阶段编写, 故内嵌在了迷宫探测过程中, 完整实现见附件源码, 使用效果:
3. 探索出「宫殿」系统完整的迷宫墙壁布局,需提交迷宫地图和自动化探索脚本 由于目前泄露的信道仍然不能保证每次移动之后都能泄露出一个判断是否撞墙的信号量, 所以我采取成功率最高的方法, 分析泄露的信道时提到, 每次移动操作结束后都会比较移动结果, 而这个结果的值和上面泄露的所有信号量完全相关, 所以可以将 WinDBG的输出都存放到一个日志文件中, 在每次移动操作结束后打印ESI的值(第 2 问中的WinDBG脚本的最后一句), 然后通过 VMware 将物理机的日志文件共享到虚拟机中, 让地图探测程序读取并根据日志最后一行的ESI来当作本次移动的结果(Wall or Ok), 借此进行DFS描述完整迷宫
WinDBG脚本如下:
1 2 3 4 5 bc * .logclose .shell cmd /c type nul > F:\Learn\REprac\TencentGameSecurity\2026\VMShare\maze_bridge\maze_leak.txt .logopen F:\Learn\REprac\TencentGameSecurity\2026\VMShare\maze_bridge\maze_leak.txt bp @$t0+317607h ".printf \"[Final Result] %04x\\n\", @rsi; gc"
在虚拟机根目录下创建共享文件夹的符号链接即可让探测程序读取到日志信息
最终得到的地图如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 S......#..... ######.###.#. .....#.....#. .###.#######. .#.........#. .#.#.#####.#. .#.#.#...#.#. .#.###.#.###. .#.....#.#... .#######.#.## ...#...#.#.#. ##.#.#.#.#.#. .....#.#....E
其中S代表起点, E代表终点, #代表墙壁, .代表可行走的节点, 探测脚本见附件
4. 在还原的迷宫地图上求解起点到终点的最短路径。需提交求解算法和路径结果 使用BFS算法求解最短路径, 脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 from collections import dequeMAZE_TEXT = """\ S......#..... ######.###.#. .....#.....#. .###.#######. .#.........#. .#.#.#####.#. .#.#.#...#.#. .#.###.#.###. .#.....#.#... .#######.#.## ...#...#.#.#. ##.#.#.#.#.#. .....#.#....E """ def load_maze_from_text (text: str ): lines = [line.rstrip("\n" ) for line in text.splitlines() if line.strip()] if not lines: raise ValueError("Maze file is empty" ) width = len (lines[0 ]) for i, row in enumerate (lines): if len (row) != width: raise ValueError(f"Row {i} has inconsistent width" ) start = None end = None for r, row in enumerate (lines): for c, ch in enumerate (row): if ch == "S" : start = (r, c) elif ch == "E" : end = (r, c) if start is None or end is None : raise ValueError("Maze must contain both S and E" ) return lines, start, end def shortest_path (maze, start, end ): dirs = [(-1 , 0 , "W" ), (1 , 0 , "S" ), (0 , -1 , "A" ), (0 , 1 , "D" )] rows = len (maze) cols = len (maze[0 ]) q = deque([start]) prev = {start: None } move_to = {} while q: r, c = q.popleft() if (r, c) == end: break for dr, dc, mv in dirs: nr, nc = r + dr, c + dc if not (0 <= nr < rows and 0 <= nc < cols): continue if maze[nr][nc] == "#" : continue nxt = (nr, nc) if nxt in prev: continue prev[nxt] = (r, c) move_to[nxt] = mv q.append(nxt) if end not in prev: return None , None path = [] moves = [] cur = end while cur is not None : path.append(cur) if cur in move_to: moves.append(move_to[cur]) cur = prev[cur] path.reverse() moves.reverse() return path, "" .join(moves) def main (): maze, start, end = load_maze_from_text(MAZE_TEXT) path, moves = shortest_path(maze, start, end) if path is None : print ("No path found from S to E" ) return print (f"Start: {start} " ) print (f"End: {end} " ) print (f"Shortest steps: {len (path) - 1 } " ) print (f"Path (WASD): {moves} " ) print ("Path (coordinates):" ) print (path) if __name__ == "__main__" : main()
得到最短路径: DDDDDDSSDDDDWWDDSSSSSSSSAASSSSDD
提交正确的 Flag 得 1.5 分;到达终点但未能正确解密得 0.5 分 输入最短路径后获得: flag{SHAD0WNT_HYPERVMX}