Ikoct的饮冰室

你愿意和我学一辈子二进制吗?

0%

2026腾讯游戏安全竞赛 PC客户端安全 初赛题解

这比赛也太难了

1. 成功加载驱动并与之正确通信,理解题目基本机制,识别并排除干扰信息。需在 writeup 中说明分析过程

成功加载驱动并与之通信

image-20260411154120692

关闭强制驱动签名后即可手动加载驱动并运行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; // r12
__int64 v4; // r14
__int64 v6; // rcx
unsigned int op; // edi
unsigned int jmp_code; // edi
maze_info mazeinfo; // [rsp+28h] [rbp-180h] BYREF
_BYTE p_ContextRecord[256]; // [rsp+80h] [rbp-128h] BYREF
__int64 v11; // [rsp+190h] [rbp-18h]
__int64 v12; // [rsp+198h] [rbp-10h]

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; // [rsp+40h] [rbp-18h] BYREF

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

交叉引用DeviceIoControl还可以找到另外两处调用分别在14000128014021B64C中, 通过调试可以发现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; // rdi
char v6; // al
unsigned int v7; // esi
int direction; // eax
unsigned int Size; // ecx
__int64 Size_1; // rdi
_OWORD OutBuffer[9]; // [rsp+20h] [rbp-C8h] BYREF
DWORD lpBytesReturned; // [rsp+B0h] [rbp-38h] BYREF
mov_info mov_infomation; // [rsp+B8h] [rbp-30h] BYREF
__int64 v17; // [rsp+D0h] [rbp-18h]

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, 8Bh }
*(_QWORD *)&mov_infomation.direction = (unsigned __int8)direction;
lpBytesReturned = 0;
mov_infomation.key = step ^ direction ^ 0xDEAD1337;
if ( DeviceIoControl(hDevice, 0x80012004, &mov_infomation, 0xCu, OutBuffer, 0x84u, &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, 0x84u);
return v7;
}

这是 3 中控制指令中唯一一个需要传入数据给 ring 0 的, 一共是 3 个 DWORD 数据共0xC字节, 多次调试可以观察出这 3 个数据的含义分别为当前这一步的方向, 当前这一步是第几步, 一个校验值:

1
2
3
4
5
6
7
8
9
struct mov_info
{
_DWORD direction; ///< 'w' : 0x52
///< 'a' : 0x53
///< 's' : 0xd3
///< 'd' : 0xd0
_DWORD step;
_DWORD key; ///< key = direction ^ step ^ 0xdead1337
};

至此可以分析出 3 个基本操作对应的控制码:

1
2
3
move    : 0x80012004
reset : 0x80012008
mazeinfo: 0x8001200C

ring 0 对IOCTL分发处理

通过搜索控制码的立即数可以定位到 ring 0 的分发器:

image-20260411155716734

分发器伪代码:

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; // r11
__int16 v5; // r13
__int16 v6; // di
struct _IO_STACK_LOCATION *CurrentStackLocation; // rax
__int16 _DI; // di
unsigned int v10; // ebp
ULONG_PTR n132; // r8
DWORD ioctl_code; // edx
unsigned int in_len; // edi
unsigned int out_len; // eax
mov_info *in_buf; // r14
unsigned int v16; // edx
KIRQL NewIrql_1; // al
KSPIN_LOCK *SpinLock; // rcx
unsigned __int8 v19; // bl
KIRQL NewIrql; // al
KSPIN_LOCK *P; // rcx
__int64 v22; // rdx

v6 = __ROR2__(v5, 1);
LOBYTE(v6) = a4 - v6;
CurrentStackLocation = a2->Tail.Overlay.CurrentStackLocation;
_DI = v6 - v4;
__asm { rcr di, 0AFh }
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 )// do mov operation
{
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, 0x84u);
sub_140002038(&in_buf->direction);
n132 = 132;
goto LABEL_18;
}
break;
case 0x80012008:
sub_140001A7C((KSPIN_LOCK *)::P); // reset
sub_14031A53E();
n132 = 0;
goto LABEL_18;
case 0x8001200C:
if ( in_buf && out_len >= 0x18 ) // get maze info
{
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 )// do mov operation
{
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, 0x84u);
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);
// Final result
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, 0BEh }
NewIrql_3 = NewIrql_5;
LOBYTE(_R8D) = _DI;
__asm { rcr r8d, 22h }
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, 0x84u);
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; // rax

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; // rdx
int result; // eax
struct _UNICODE_STRING DestinationString; // [rsp+20h] [rbp-40h] BYREF
_OBJECT_ATTRIBUTES ObjectAttributes; // [rsp+30h] [rbp-30h] BYREF
void *EventHandle; // [rsp+80h] [rbp+20h] BYREF

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

的指令可以再找到以下几个泄露的信道

ring0 14031857E set_ObjectHandleFlagInformation 设置当前线程的ObjectHandleFlagInformation

函数如下:

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; // rax
void **v6; // rbx
void *ObjectHandle; // r10
bool v8; // r8
char ObjectInformation_[2]; // [rsp+50h] [rbp+18h] BYREF

if ( PsGetThreadTeb )
{
if ( qword_140005090 )
{
LODWORD(v5) = idirect_call(
KeGetCurrentThread(),
(OBJECT_INFORMATION_CLASS)idirect_call,
ObjectInformation,
Length); // PsGetThreadTeb
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);// ZwSetInformationObject
}
}
}
}
}

使用间接调用隐藏调用的API, 可以调试得到第一个间接调用调用的是PsGetThreadTeb, 第二个是ZwSetInformationObject, 写入了两个字节的数据, 调试可以发现依旧是一个二值信号, 这两个字节只会是00 0000 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; // ax
unsigned __int64 v4; // rax
__int64 _RCX_1; // rbx
void *ThreadId; // rcx
OBJECT_INFORMATION_CLASS ObjectInformationClass; // edx
PVOID ObjectInformation; // r8
ULONG Length; // r9d
TEB *TEB; // rax
unsigned int *Address; // rsi
unsigned int walk_back; // ebx
__int64 v15; // [rsp-20h] [rbp-78h] BYREF
PEPROCESS Process; // [rsp+0h] [rbp-58h] BYREF
PVOID Object; // [rsp+8h] [rbp-50h] BYREF
_KAPC_STATE ApcState; // [rsp+10h] [rbp-48h] BYREF

_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);// Thread which send MOVE IOCTL
if ( (v4 & 0x80000000) == 0LL )
{
Process = 0;
if ( PsLookupProcessByProcessId(*(HANDLE *)(_RCX_1 + 0x1C8), &Process) >= 0 )
{
KeStackAttachProcess(Process, &ApcState);// Process which send MOVE IOCTL
LODWORD(TEB) = idirect_call(Object, ObjectInformationClass, ObjectInformation, Length);// PsGetThreadTeb
if ( TEB )
{
Address = &TEB->LastErrorValue;
if ( n2 )
{
walk_back = 0xC0DE0002;
if ( n2 != 2 )
walk_back = 0xC0DE0000;
}
else
{
walk_back = 0xC0DE0001; // walk back
}
ProbeForWrite(Address, 4u, 4u);
*Address = walk_back;
}
KeUnstackDetachProcess(&ApcState);
ObfDereferenceObject(Process);
}
LODWORD(v4) = ObfDereferenceObject(Object);
}
}
}
}
return v4;
}

通过调试可以发现是使用间接调用调用了PsGetThreadTeb获取发起移动指令IOCTL的线程的TEB结构, 根据第 2 个参数修改了其中的LastErrorValue字段

image-20260411131928346

通过匹配指令找到的最后一处指令位于第一问分析得到的关键移动逻辑之后对移动结果的判断, 通过调试可以发现其比较的值虽然对比目标是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"

使用效果:

image-20260411163815978

工具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());
// Based on reversing notes: driver reads a handle from TEB+0x1748.
*reinterpret_cast<HANDLE*>(teb + 0x1748) = outThreadHandle.get();
return true;
}
(void)installThreadHandleLeakAnchor(threadHandle);
MoveOnce(Handle threadLeakHandle...){
...
DWORD flags = 0; // target leak communication info
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();

该工具在探测迷宫地图的阶段编写, 故内嵌在了迷宫探测过程中, 完整实现见附件源码, 使用效果:

image-20260411164808528

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 deque


MAZE_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):
# dr, dc, move-char
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

# Rebuild path from end to start.
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}