YuHuanTin's Blog

2026 腾讯游戏安全 PC 初赛 WP

2026/04/30

驱动加载

使用 EfiGuard 进行 PG 和 DSE 修复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 挂载 EFI 到 M:,注意在 explorer 中是看不到的
mountvol M: /S
M:
mkdir \EFI\EfiGuard

copy C:\EfiGuard\EFI\Boot\Loader.efi M:\EFI\EfiGuard\
copy C:\EfiGuard\EFI\Boot\EfiGuardDxe.efi M:\EFI\EfiGuard\

// 创建启动项,生成一个 GUID,下面要用
bcdedit /copy {bootmgr} /d "EfiGuard"
bcdedit /set {上面的GUID} path \EFI\EfiGuard\Loader.efi
bcdedit /set {fwbootmgr} displayorder {上面的GUID} /addfirst

// 取消第一启动,恢复
bcdedit /set {fwbootmgr} displayorder {bootmgr} /addfirst

绿字进系统后关闭 DSE 就可以加载驱动了

1
2
3
4
5
EfiDSEFix.exe -d

Disabling DSE...
CI!g_CiOptions at 0xFFFFF800648391B0.
Successfully disabled DSE. Original g_CiOptions value: 0xE

启动驱动后运行程序

image.png

exe 分析

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
int __fastcall main_0(int argc, const char **argv, const char **envp)
{
__int64 v4; // rcx
int inputPos; // ebx
__int64 wirtePos; // rsi
unsigned int inputChar; // edi
__int64 code; // rdx
char _AL; // al
__int64 _RCX; // rcx
char theSend; // al
int v13; // r8d
int status; // eax
DWORD LastError; // edx
char *seq; // rdx
_DWORD moveBufOut[2]; // [rsp+20h] [rbp-188h] BYREF
ioctl_8001200c_out map; // [rsp+28h] [rbp-180h] BYREF
_OWORD buf[4]; // [rsp+40h] [rbp-168h] BYREF
int (__stdcall *send)(SOCKET, const char *, int, int); // [rsp+80h] [rbp-128h] BYREF
_BYTE Buf[176]; // [rsp+E0h] [rbp-C8h] BYREF

InitTips();
printf("[*] Connecting to Shadow gate driver...\n");
hDevice = (HANDLE)OpenDriver();
if ( hDevice == (HANDLE)-1LL )
{
printf("[!] Cannot continue without driver.\n");
printf("[*] Press any key to exit...\n");
getch();
return 1;
}
printf("[+] Gate module online.\n\n");
sub_140001670();
memset(&map, 0, sizeof(map));
if ( IO_8001200C(v4, &map) )
printf("[*] Maze grid: %ux%u, Entry=(%u,%u), Exit=(%u,%u)\n", map.x, map.y, map.ex, map.ey, map.exX, map.exY);
PrintOperatorTips();
memset(&send, 0, 0x100u);
inputPos = 0;
wirtePos = 0;
while ( 2 )
{
printf("[op #%d] > ", inputPos);
inputChar = getch();
printf("%c\n", inputChar);
inputChar -= 0x1B;
_AL = inputChar;
_RCX = (unsigned __int16)Buf;
LOBYTE(_RCX) = (unsigned __int8)Buf + 11;
switch ( inputChar )
{
case '\0':
case '6': // Q
case 'V': // q
goto Exit;
case '$': // ?Hh
case '-':
case 'M':
PrintOperatorTips();
continue;
case '&': // AJaj left
case '/':
case 'F':
case 'O':
LOBYTE(code) = 0x30;
theSend = 'L';
goto Send;
case ')': // DLdl right
case '1':
case 'I':
case 'Q':
LOBYTE(code) = 0x40;
__asm { rcl al, cl; Rotate Through Carry Left }
theSend = 'R';
goto Send;
case '.':
case '<':
case 'N':
case '\\':
LOBYTE(code) = 0x10;
theSend = 'U';
goto Send;
case '0':
case '8':
case 'P':
case 'X':
LOBYTE(code) = 0x20;
theSend = 'D';
Send:
if ( inputPos < 255 )
{
*((_BYTE *)Buf + wirtePos + 80) = theSend;
++inputPos;
++wirtePos;
if ( (unsigned int)inputPos >= 0x100 )
_report_securityfailure_w(_RCX);
*((_BYTE *)Buf + wirtePos + 80) = 0;
}
v13 = dword_140005668;
memset(buf, 0, sizeof(buf));
moveBufOut[0] = 0;
++dword_140005668;
status = IO_80012004(hDevice, code, v13, buf, moveBufOut);
if ( status != 1 )
{
if ( status == -1 )
{
LastError = GetLastError();
printf("[ERROR] DeviceIoControl failed: %lu\n", LastError);
}
continue;
}
printf(L"\n");
printf("=============================================\n");
printf(" ACCESS GRANTED - Credential extracted!\n");
printf("=============================================\n");
if ( moveBufOut[0] )
{
printf("[CREDENTIAL] %s\n", (const char *)buf);
}
else
{
printf("[*] You reached the exit, but the credential could not be decrypted.\n");
printf("[*] Hint: Only the shortest path unlocks the credential.\n");
}
printf("[*] Your input (%d ops sent): %s\n", inputPos, (const char *)&send);
printf("[*] Note: blocked ops are included above; the driver only counts successful ones.\n");
printf(L"\n");
memset(buf, 0, sizeof(buf));
printf("[*] Press any key to exit...\n");
getch();
Exit:
Close3Func(_RCX);
if ( hDevice != (HANDLE)-1LL )
CloseHandle(hDevice);
printf("[*] Mission aborted. ACE HQ standing by.\n");
return 0;
case '7':
case 'W':
IO_80012008_Reset();
inputPos = 0;
dword_140005668 = 0;
LOBYTE(send) = 0;
wirtePos = 0;
printf("[*] Reset to entry point.\n");
continue;
case '9':
case 'Y':
printf("[*] Operations sent: %d\n", inputPos);
seq = (char *)&send;
if ( inputPos <= 0 )
seq = "(none)";
printf("[*] Sequence: %s\n", seq);
continue;
default:
printf("[?] Unknown command. 'h' for help.\n");
continue;
}
}
}

重点在驱动交互,其余均为负责输入的逻辑

IO_8001200C

初始化

1
2
3
4
5
6
7
BOOL __fastcall IO_8001200C(__int64 a1, void *lpOutBuffer)
{
DWORD BytesReturned; // [rsp+40h] [rbp-18h] BYREF

BytesReturned = 0;
return DeviceIoControl(hDevice, 0x8001200C, 0, 0, lpOutBuffer, 0x18u, &BytesReturned, 0);
}
1
2
3
4
5
6
7
8
9
struct __unaligned ioctl_8001200c_out
{
DWORD x;
DWORD y;
DWORD ex;
DWORD ey;
DWORD exX;
DWORD exY;
};

IO_80012004

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 IO_80012004(void *hDevice, __int64 _RDX, int a3, _BYTE *buf, DWORD *out)
{
__int64 v5; // rdi
char v6; // al
unsigned int rest; // esi
int lpInBuffer__1; // eax
DWORD Size; // ecx
__int64 Size_1; // rdi
MoveOutBuf IOOutBuf; // [rsp+20h] [rbp-C8h] BYREF
DWORD lpBytesReturned; // [rsp+B0h] [rbp-38h] BYREF
ioctl_80012004_in lpInBuffer_; // [rsp+B8h] [rbp-30h] BYREF
__int64 v17; // [rsp+D0h] [rbp-18h]

v17 = v5;
v6 = _RDX ^ 0xFA;
rest = 0;
memset(&IOOutBuf, 0, sizeof(IOOutBuf));
LOBYTE(_RDX) = (unsigned __int8)(_RDX ^ 0x40) >> 5;
lpInBuffer__1 = (unsigned __int8)(_RDX | (8 * v6));
__asm { rcr rdx, 8Bh; Rotate Through Carry Right }
*(_QWORD *)&lpInBuffer_.code = (unsigned __int8)lpInBuffer__1;
lpBytesReturned = 0;
lpInBuffer_.check = a3 ^ lpInBuffer__1 ^ 0xDEAD1337;
if ( DeviceIoControl(hDevice, 0x80012004, &lpInBuffer_, 0xCu, &IOOutBuf, 0x84u, &lpBytesReturned, 0) )
{
if ( *(_DWORD *)IOOutBuf.msg == 'WIN!' )
{
rest = 1;
if ( buf )
{
if ( out )
{
Size = IOOutBuf.size;
*out = IOOutBuf.size;
if ( Size - 1 > 0x3E )
{
*buf = 0;
}
else
{
Size_1 = Size;
memcpy(buf, IOOutBuf.p2, Size);
buf[Size_1] = 0;
}
}
}
}
}
else
{
rest = -1;
}
memset(&IOOutBuf, 0, sizeof(IOOutBuf));
return rest;
}

负责移动,发送给驱动的数据结构见下

1
2
3
4
5
6
struct ioctl_80012004_in
{
DWORD code;
DWORD pos;
DWORD check;
};
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
from struct import pack

def calcMoveCode(_RDX):
'''
计算键盘映射到驱动键盘映射
0x10 up
0x20 down
0x30 left
0x40 right
:param _RDX:
:return:
'''
v6 = (_RDX ^ 0xFA) & 0xff
_RDX = ((_RDX ^ 0x40) >> 5) & 0xff
res = (_RDX | (8 * v6)) & 0xff
print(hex(res))
return res

def IO_MOVE_GenBuf(code, index):
code = calcMoveCode(code)
check = (code ^ index ^ 0xDEAD1337) & 0xffffffff
print(hex(check))

b = pack('<III', code, index, check)
print(f'{b.hex()}')

IO_MOVE_GenBuf(0x30, 0)
IO_MOVE_GenBuf(0x20, 1)
IO_MOVE_GenBuf(0x10, 2)
IO_MOVE_GenBuf(0x40, 3)

IO_80012008_Reset

重置迷宫

1
2
3
4
5
6
7
BOOL IO_80012008_Reset()
{
DWORD BytesReturned; // [rsp+40h] [rbp-18h] BYREF

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

sys 分析

有一些难以分析的 call,nop 掉看整体流程

image.png

载入点

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
__int64 __fastcall sub_140003208(struct _DRIVER_OBJECT *DriverObject)
{
void *Pool2; // rax
NTSTATUS Pool2_1; // edi
struct _UNICODE_STRING DestinationString; // [rsp+40h] [rbp-18h] BYREF

Pool2 = (void *)ExAllocatePool2(64, 472, 0x657A614D);
MemoryPool = Pool2;
if ( !Pool2 )
return 0xC000009ALL;
Pool2_1 = (int)Pool2; // 混淆
if ( (int)Pool2 < 0
|| (KeInitializeSpinLock(&SpinLock),
KeInitializeSpinLock(&SpinLock_),
((void (*)(void))loc_140001E60)(),
DestinationString = 0,
RtlInitUnicodeString(&DestinationString, L"\\Device\\ShadowGate"),
Pool2_1 = IoCreateDevice(DriverObject, 0, &DestinationString, 0x22u, 0x100u, 0, &DeviceObject),
Pool2_1 < 0) )
{
_mm_lfence();
ExFreePoolWithTag(MemoryPool, 0x657A614Du);
LABEL_5:
MemoryPool = 0;
return (unsigned int)Pool2_1;
}
RtlInitUnicodeString(&SymbolicLinkName, L"\\??\\ShadowGate");
Pool2_1 = IoCreateSymbolicLink(&SymbolicLinkName, &DestinationString);
if ( Pool2_1 < 0 )
{
_mm_lfence();
IoDeleteDevice(DeviceObject);
ExFreePoolWithTag(MemoryPool, 0x657A614Du);
DeviceObject = 0;
goto LABEL_5;
}
DriverObject->DriverUnload = (PDRIVER_UNLOAD)sub_140001840;
DriverObject->MajorFunction[0] = (PDRIVER_DISPATCH)sub_1400014B0;
DriverObject->MajorFunction[2] = (PDRIVER_DISPATCH)sub_140001410;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = (PDRIVER_DISPATCH)j_IRP_Control_Func;
DeviceObject->Flags |= 4u;
DeviceObject->Flags &= ~0x80u;
return 0;
}

Contorl 里面也有两处难以分析 call,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
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
__int64 __fastcall IRP_Control_Func(__int64 a1, IRP *IRP, __int64 a3, char a4)
{
__int16 v4; // r11
__int16 v5; // r13
__int16 v6; // di
struct _IO_STACK_LOCATION *CurrentStackLocation; // rax
unsigned int Status_1; // ebp
ULONG_PTR info; // r8
ULONG IoControlCode; // edx
unsigned int inputLen; // edi
unsigned int outLen; // eax
buf_u *SystemBuffer; // r14
unsigned int Status; // edx
KIRQL NewIrql_1; // al
KSPIN_LOCK *SpinLock; // rcx
KIRQL NewIrql; // al
struct_MemoryPool *P; // rcx
unsigned int _____1; // eax
unsigned int ____; // esi
KIRQL NewIrql_4; // al
struct_MemoryPool *MemoryPool; // rdi
KIRQL NewIrql_2; // bl
HANDLE CurrentThreadId; // rax
KSPIN_LOCK *p_kspin_lock1C0; // rcx
KSPIN_LOCK *p_kspin_lock1C0_1; // rcx
KIRQL NewIrql_5; // al
char _DI_1; // cl
KIRQL NewIrql_3; // si
__int64 n255; // rdi
KSPIN_LOCK *p_kspin_lock1C0_2; // rcx
DWORD v37; // [rsp+0h] [rbp-158h] BYREF
__int64 v38; // [rsp+8h] [rbp-150h]
__m128 buf[16]; // [rsp+20h] [rbp-138h] BYREF

v6 = __ROR2__(v5, 1);
LOBYTE(v6) = a4 - v6;
CurrentStackLocation = IRP->Tail.Overlay.CurrentStackLocation;
_DI = v6 - v4;
__asm { rcr di, 0AFh }
Status_1 = 0;
info = 0;
IoControlCode = CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode;
inputLen = CurrentStackLocation->Parameters.DeviceIoControl.InputBufferLength;
outLen = CurrentStackLocation->Parameters.DeviceIoControl.OutputBufferLength;
SystemBuffer = (buf_u *)IRP->AssociatedIrp.SystemBuffer;
if ( ::MemoryPool )
{
switch ( IoControlCode )
{
case 0x80012004:
if ( SystemBuffer && inputLen >= 0xC && outLen >= 0x84 )// move?
{
v38 = *(_QWORD *)&SystemBuffer->_12004.code;
if ( SystemBuffer->_12004.check == ((unsigned __int8)v38 ^ HIDWORD(v38) ^ 0xDEAD1337) )
{
NewIrql = KeAcquireSpinLockRaiseToDpc(&::MemoryPool->kspin_lock1C0);
P = ::MemoryPool;
++::MemoryPool->dwordBC;
KeReleaseSpinLock(&P->kspin_lock1C0, NewIrql);
____ = _____1; // 可能混淆
customMemset(SystemBuffer, 0, 0x84u);
NewIrql_4 = KeAcquireSpinLockRaiseToDpc(&::MemoryPool->kspin_lock1C0);
MemoryPool = ::MemoryPool;
NewIrql_2 = NewIrql_4;
CurrentThreadId = PsGetCurrentThreadId();
p_kspin_lock1C0 = &::MemoryPool->kspin_lock1C0;
*(_QWORD *)MemoryPool[1].gap8 = CurrentThreadId;
KeReleaseSpinLock(p_kspin_lock1C0, NewIrql_2);
((void (__fastcall *)(struct_MemoryPool *, _QWORD))loc_14040305A)(::MemoryPool, ____);
if ( ____ == 2 )
{
p_kspin_lock1C0_1 = &::MemoryPool->kspin_lock1C0;
SystemBuffer[2]._1200c.enY = 'WIN!';
NewIrql_5 = KeAcquireSpinLockRaiseToDpc(p_kspin_lock1C0_1);
_DI = _DI_1;
__asm { rcl dil, 0BEh }
NewIrql_3 = NewIrql_5;
LOBYTE(_R8D) = _DI;
__asm { rcr r8d, 22h }
n255 = *(unsigned int *)&::MemoryPool->gap8[172];
if ( (unsigned int)n255 > 0xFF )
n255 = 255;
sub_140003600(buf, (__m128 *)::MemoryPool->buf, (unsigned int)n255);
_mm_lfence();
p_kspin_lock1C0_2 = &::MemoryPool->kspin_lock1C0;
buf[0].m128_i8[n255] = 0;
KeReleaseSpinLock(p_kspin_lock1C0_2, NewIrql_3);
v37 = 0;
sub_1403F7C6D(buf, (unsigned int)n255, (__int64)&SystemBuffer[2]._12004 + 16, (__int64)&v37);
SystemBuffer[5]._1200c.enX = v37;
memset(buf, 0, sizeof(buf));
}
sub_1400026B4(SystemBuffer);
}
else
{
customMemset(SystemBuffer, 0, 0x84u);
sub_140002038(SystemBuffer);
}
info = 132;
goto LABEL_23;
}
break;
case 0x80012008:
sub_140001A7C((__int64)::MemoryPool); // reset
sub_14031A53E();
info = 0;
goto LABEL_23;
case 0x8001200C:
if ( SystemBuffer && outLen >= 0x18 ) // init
{
NewIrql_1 = KeAcquireSpinLockRaiseToDpc(&::MemoryPool->kspin_lock1C0);
SystemBuffer->_1200c.enY = 0;
SystemBuffer->_1200c.x = 13;
*(_QWORD *)&SystemBuffer->_1200c.y = 13;
SpinLock = &::MemoryPool->kspin_lock1C0;
SystemBuffer->_1200c.exitX = 12;
SystemBuffer->_1200c.exitY = 12;
KeReleaseSpinLock(SpinLock, NewIrql_1);
info = 0x18;
goto LABEL_23;
}
break;
default:
Status_1 = 0xC0000010;
LABEL_23:
Status = Status_1; // 可能混淆
return IO_CompleteReq(IRP, Status, info);
}
Status_1 = 0xC0000023;
goto LABEL_23;
}
Status = 0xC00000A3;
// 可能混淆
return IO_CompleteReq(IRP, Status, info);
}
/* Orphan comments:
可能混淆
*/

CODE 0x8001200C

传递迷宫大小,位置信息

CODE 0x80012008

重置

CODE 0x80012004

移动,主要逻辑

移动延迟

KeDelayExecutionThread 对每步移动都进行一点延迟,所以算法爆破特别慢,需要 patch

image.png

VA 0x1400026BD,RVA 0x26BD 全 nop (5字节)

1
eq ShadowGateSys+0x26bd c483489090909090

五个泄漏点

sys data 段起始有一个花指令魔数,通过该魔数可以定位四个泄漏点

image.png

1. Event 泄漏

exe 0x140001340 创建 Global Event

image.png

sys 0x1400022B0 中 event 泄漏:n2==0/2 置 MazeMoveOK,否则置 MazeMoveWall

image.png

2. 信号量 泄漏

exe 0x14021B91F 创建信号量,异或 0x4B 解密字符串

image.png

image.png

得到

1
2
Global\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}
Global\{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}

sys 0x140319A37 中 semaphore 泄漏,_EDX ==0/2 释放 {A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D} ,否则释放 {B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}

image.png

3. TEB.LastError 泄漏

sys 0x140316ADF 中修改 exe LastErrorValue ,n2 为 0 时设置 0xC0DE0001, 2 时设置 0xC0DE0002, 其他时设置 0xC0DE0000

image.png

4. TEB + 0x1748 泄漏

exe 0x14021BC88 在释放时有部分提示

image.png

sys 0x14031857E 中,使用 PsGetThreadTeb 获取 TEB 后使用 ZwSetInfomationObjectteb + 0x1748 处进行设置

在 icall 分发处能够看到实际调用的 call 为 PsGetThreadTebZwSetInfomationObject

image.png

两个 icall 都清理为 call rax 不然反编译有问题

image.png

n2 = 0/2,设置 1,否则设置 0

image.png

5. 内存属性 泄漏

_guard_dispatch_icall_nop 下断点,当输入断在 rax=ZwProtectVirtualMemory 时就是触发第五个漏洞

调用参数为

rdx = ShadowGateApp.exe.data

rcx = ShadowGateApp.exe.data 页长度

.data 页的属性修改为 RWERW

1
2
3
4
3: kd> dq rdx L1
ffffab06`6e6a75a0 00007ff6`0eb55000
3: kd> dq r8 L1
ffffab06`6e6a75a8 00000000`00001000

image.png

找了下没找到,看堆栈这个是真的在 vm 里面了,该泄漏通过观察页得到,猜测 sys 会修改名为 .data 的页

image.png

image.png

根据上面规则猜测 n2 = 0/2 时,页属性变成 PAGE_EXECUTE_READWRITE 其他时,保持 PAGE_READWRITE

查询迷宫 & 探索迷宫

方法一:r3 下通过泄漏探索迷宫

伪代码,完整见附件

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
Init():
打开驱动设备
创建 2 个事件对象
创建 2 个命名信号量
创建 1 个 probe handle
找到当前 exe 的 .data 首页
把 probe handle 写入 TEB+0x1748
发送 QUERY,得到迷宫大小、起点、终点

EncodeMove(dir):
将 U/D/L/R 编码成驱动需要的 dir 值

DoMove(dir):
packet.dir = EncodeMove(dir)
packet.counter = global_counter
packet.check = dir ^ counter ^ 0xDEAD1337
global_counter++
发送 IOCTL_MOVE
返回 reply

PrepareDetectors():
ResetEvent(ok)
ResetEvent(wall)
DrainSemaphore(A)
DrainSemaphore(B)
SetLastError(哨兵值)
将 .data 首页改回 RW
将 probe handle flags 改回基线
重新确保 TEB+0x1748 = probe handle

ReadVerdict(reply):
同时读取:
Event 状态
Semaphore 状态
LastError
HandleFlags
.data 页保护
if Event 有效:
返回 open / wall / win
else if Semaphore 有效:
返回 open / wall / win
else if LastError 有效:
返回 open / wall / win
else if HandleFlags 相对基线发生变化:
返回 open / wall / win
else:
根据 .data 页保护返回 open / wall / win

ReplayPath(path):
RESET 驱动
for step in path:
PrepareDetectors()
verdict = ReadVerdict(DoMove(step))
若 replay 过程中出现 wall,则说明路径失效,直接报错

ProbeOneStep(path_to_cur, dir):
ReplayPath(path_to_cur)
PrepareDetectors()
reply = DoMove(dir)
return ReadVerdict(reply)

DFS(x, y):
标记 (x, y) 已访问
for dir in [R, D, L, U]:
nx, ny = 邻格
若越界或该格已判定,continue

verdict = ProbeOneStep(path[x][y], dir)

if verdict == wall:
maze[ny][nx] = '#'
else:
maze[ny][nx] = '.'
path[nx][ny] = path[x][y] + dir
if 未访问:
DFS(nx, ny)

BuildShortestPath():
对已恢复出的 '.' 地图做 BFS
从起点求到终点的最短路径

得到以下迷宫图

1
2
3
4
5
6
7
8
9
10
11
12
13
.......#.....
######.###.#.
.....#.....#.
.###.#######.
.#.........#.
.#.#.#####.#.
.#.#.#...#.#.
.#.###.#.###.
.#.....#.#...
.#######.#.##
...#...#.#.#.
##.#.#.#.#.#.
.....#.#.....

方法二:可以通过 sys 直接读取分配内存读取到迷宫

image.png

image.png

最短路径求解

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
from __future__ import annotations

from collections import deque
import re
from pathlib import Path

INPUT_PATH = Path("Input.txt")
OPEN_TILES = {".", "*"}

def parse_point(text: str) -> tuple[int, int] | None:
match = re.search(r"\((\d+),\s*(\d+)\)", text)
if not match:
return None
return int(match.group(1)), int(match.group(2))

def load_maze(path: Path) -> tuple[list[str], tuple[int, int], tuple[int, int]]:
lines = [line.strip() for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
start: tuple[int, int] | None = None
goal: tuple[int, int] | None = None
maze: list[str] = []

for line in lines:
if line.startswith("start="):
left, _, right = line.partition("goal=")
start = parse_point(left)
goal = parse_point(right)
continue
if set(line) <= OPEN_TILES | {"#"}:
maze.append(line)

if not maze:
raise ValueError("Input.txt 中没有找到迷宫数据")

width = len(maze[0])
if any(len(row) != width for row in maze):
raise ValueError("迷宫每一行长度必须一致")

if start is None:
start = (0, 0)
if goal is None:
goal = (width - 1, len(maze) - 1)

return maze, start, goal

def is_open(maze: list[str], x: int, y: int) -> bool:
return maze[y][x] in OPEN_TILES

def solve(maze: list[str], start: tuple[int, int], goal: tuple[int, int]) -> str:
width = len(maze[0])
height = len(maze)
sx, sy = start
gx, gy = goal

if not (0 <= sx < width and 0 <= sy < height and is_open(maze, sx, sy)):
raise ValueError("起点不可达或越界")
if not (0 <= gx < width and 0 <= gy < height and is_open(maze, gx, gy)):
raise ValueError("终点不可达或越界")

moves = [
(1, 0, "R"),
(0, 1, "D"),
(-1, 0, "L"),
(0, -1, "U"),
]

queue = deque([start])
prev: dict[tuple[int, int], tuple[tuple[int, int], str] | None] = {start: None}

while queue:
x, y = queue.popleft()
if (x, y) == goal:
break

for dx, dy, step in moves:
nx = x + dx
ny = y + dy
if not (0 <= nx < width and 0 <= ny < height):
continue
if not is_open(maze, nx, ny):
continue
if (nx, ny) in prev:
continue
prev[(nx, ny)] = ((x, y), step)
queue.append((nx, ny))

if goal not in prev:
raise ValueError("找不到从起点到终点的路径")

path: list[str] = []
cur = goal
while cur != start:
parent, step = prev[cur] # type: ignore[misc]
path.append(step)
cur = parent
path.reverse()
return "".join(path)

def main() -> None:
maze, start, goal = load_maze(INPUT_PATH)
path = solve(maze, start, goal)
print(f"start={start} goal={goal}")
print(f"length={len(path)}")
print(path)

if __name__ == "__main__":
main()

Flag

flag{SHAD0WNT_HYPERVMX}

1
2
3
4
5
6
7
8
9
=============================================
ACCESS GRANTED - Credential extracted!
=============================================
[CREDENTIAL] flag{SHAD0WNT_HYPERVMX}
[*] Your input (32 ops sent): RRRRRRDDRRRRUURRDDDDDDDDLLDDDDRR
[*] Note: blocked ops are included above; the driver only counts successful ones.

[*] Press any key to exit...
[*] Mission aborted. ACE HQ standing by.

内网 ss 服务端搭建

2026/04/08

在某某内网环境下,手机需要通过某内网设备来连接到互联网(内网设备已连接外网),且为了安全考虑,所以采用 ss 的加密,故有此篇记录

下载&启动 ss 服务端

选取 https://github.com/shadowsocks/shadowsocks-rust

笔者使用 shadowsocks-v1.24.0.x86_64-pc-windows-gnu.zip 进行测试

1
2
3
4
5
6
7
8
9
10
\shadowsocks-v1.24.0.x86_64-pc-windows-gnu 的目录

2026/04/08 05:56 <DIR> .
2026/04/08 05:56 <DIR> ..
2026/04/08 04:51 9,754,112 sslocal.exe
2025/12/11 08:22 7,732,224 ssmanager.exe
2025/12/11 08:21 7,635,456 ssserver.exe
2026/04/08 04:51 10,604,032 ssservice.exe
2025/12/11 08:20 2,496,000 ssurl.exe
2026/04/08 04:51 10,598,912 sswinservice.exe

生成密钥

1
ssservice.exe genkey -m "aes-256-gcm"

编写 config.json

1
2
3
4
5
6
7
8
9
{
"server": "0.0.0.0",
"server_port": 8388,
"password": "你生成的密钥或自定义密码",
"method": "aes-256-gcm",
"timeout": 300,
"fast_open": true,
"mode": "tcp_and_udp"
}

启动服务端(这里不做服务安装,仅作为 cli server)

1
ssserver.exe -c config.json

导出 ss 链接

1
ssurl.exe -e config.json

注意

由于服务端接收 0.0.0.0 也就是所有 ip 请求,所以生成的 ss 链接也是 0.0.0.0 所以需要改成目标设备 ip,例如 192.168.1.2@8388

注意最后要开启对应端口的防火墙,例如 8388


1+13T Root 记录

2026/03/23

1+13T 刷机记录

目标:SukiSU-Ultra


准备

https://yun.daxiaamu.com/OnePlus_Roms/一加OnePlus 13T/

新机搭载 ColorOS PKX110_15.0.2.602(CN01) ,上面没有

1
2
3
4
5
> getprop | grep ro.build.display
[ro.build.display.full_id]: [PKX110domestic_11_15.0.2.602(CN01)_2025090420480208]
[ro.build.display.id]: [PKX110_15.0.2.602(CN01)]
[ro.build.display.id.show]: [PKX110_15.0.2.602(CN01)]
[ro.build.display.ota]: [PKX110_11_A.53]

https://optool.daxiaamu.com/install_adb_drivers?src=pctool 下载并安装 fastboot 驱动

解锁BL

开发者 → OEM 解锁 开启

1
2
3
> adb devices
List of devices attached
<hide> device
1
adb reboot bootloader
1
2
> fastboot devices
<hide> fastboot
1
2
3
> fastboot flashing unlock
OKAY [ 0.016s]
Finished. Total time: 0.016s

按音量下键并确认,出现黄字即成功

Root

本来想要采用 DSU + GSI Sideload 的方式启动下载本机的 init_boot.img 的,但是没找到编译好的,要不就是版本不对,所以不采用 DSU + GSI Sideload 的方式

更新系统到 603

https://yun.daxiaamu.com/OnePlus_Roms/一加OnePlus 13T/ColorOS PKX110_15.0.2.603(CN01) A.54/

系统 → 软件更新 → 本地更新,使用全量包安装

提取 img

https://yun.daxiaamu.com/files/tool/Fastboot Enhance/

加载全量包的 payload.bin 使用工具提取 init_boot.imgboot.img

image.png

刷入 img

首先安装 SukiSu.apk,将刚才提取的 init_boot.img 进行修补,下载修补完成的 img

eg. kernelsu_patched_20241231_195134.img

进入 bootloader

1
adb reboot bootloader

注意这是持久化刷入

1
fastboot flash init_boot path\to\kernelsu_patched_20241231_195134.img
1
fastboot reboot

刷入 TWRP(可不刷)

https://yun.daxiaamu.com/files/TWRP/OnePlus 13T/kmiit/

1
adb reboot bootloader

fastboot 下

1
fastboot getvar all

slot-count: 如果显示 2,说明是 A/B 分区;如果显示 1 或找不到,则是传统的 A-only 分区

current-slot: 显示当前活跃的分区(如 a 或 b)

has-slot:recovery: 如果显示 yes,说明你有独立的 recovery 分区;如果显示 no,说明你的 Recovery 是集成在 boot 分区(或 Android 13+ 的 init_boot / vendor_boot)里的

1
fastboot flash recovery_? TWRP.img

在拥有全量包的情况下其实无需安装 TWRP,如果出现故障,fastboot boot 到 TWRP 临时用即可

备份

字库备份

吐槽:这是哪个大聪明发明的词,就是备份分区 :(

使用多系统工具箱 / TWRP 直接备份均可,super 可不用备份

保留 root 升级

见视频

不可禁用/删除 app

名称 解决方案
软件包安装程序 使用 CrossProfileTestApp.apk 禁用 or CtsPermissionApp(未测试) 应该卸载 oplus.appdetail

需要删除的

com.coloros.phonemanager

com.oplus.appdetail

基本模块

名称 功能
https://github.com/Dr-TSNG/ZygiskNext
https://t.me/lsposed_irena
https://github.com/re-zero001/LSPosed-Irena
https://github.com/KOWX712/PlayIntegrityFix https://github.com/osm0sis/PlayIntegrityFork 谷歌商店修复
https://github.com/5ec1cff/TrickyStore 隐藏 tee 损坏
https://github.com/KOWX712/Tricky-Addon-Update-Target-List 同上,配置
shamiko? 隐藏 root?

ref

https://optool.daxiaamu.com/

https://github.com/eritpchy/FingerprintPay/releases(指纹)

https://www.bilibili.com/video/av115547337459255(晨钟酱)

https://www.bilibili.com/video/av115042041206760

https://www.bilibili.com/video/av616455200/ or https://www.bilibili.com/video/av616455200/(CrossProfileTestApp)


SU_Protocol-WriteUp

2026/03/17

前言

此题采取 Themida 静态压缩 + 动态修改密钥 + 预期多解的思路防止 ai 一把梭(被一些师傅骂 misc 了 😭

WP

密钥写在段名中,预期解法是 x64dbg 运行时进行 dump,由于 Themida 的原因一定会查看段内存(下断点),所以人类选手(应该)能找到此 key

1
su2026-keysecret

image.png

有个别师傅被 ida 坑了,见下图

image.png

流程

注册 /flag POST 路由来接收用户输入

image.png


整体字段(感谢 r3 的师傅图表做的这么好 :D)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Offset  Value         Meaning
------ ----- -------
0 0x60 Magic byte
1-2 0x00 0x7C Big-endian length = 124
3 0x80 Header byte (from hint)
4 0x55 Type byte (abs(0x55) = 0x55 = type 85)
5 0x00 Default Padding

--- payload ---
6 0x80 Magic byte (must be 0x80)
7-110 [104 bytes] TEA-encrypted data (13 blocks × 8 bytes)
111-126 [16 bytes] TEA key = "su2026_keysecret"
---------------

127 [checksum] (byte3 + byte4 + byte5 + sum(bytes 6..126)) & 0xFF
128 0x16 Trailing byte (from hint)

构造 payload (使用正确的 key 和 delta 进行 tea 加密)

1
802ba5e6806f7dd07b988241146e350f481ec220fe1536b67193671193ca08060fd065ddf9c197a119d2f732d8c574e7fc8ca862a2a15e3e7312df0fe81b0f810bf27f7f8982b9a1880ac3d3fd128acabe866e82655cb2b536edf8714ec03162c91ed2c534c132a3347375323032362d6b6579736563726574

构造整体

1
2
60 00 7c 80		           55 00 <payload> 45 16
^len of payload + 3 ^opcode ^check sum of(payload + index 3,4,5)
1
60007c805500802ba5e6806f7dd07b988241146e350f481ec220fe1536b67193671193ca08060fd065ddf9c197a119d2f732d8c574e7fc8ca862a2a15e3e7312df0fe81b0f810bf27f7f8982b9a1880ac3d3fd128acabe866e82655cb2b536edf8714ec03162c91ed2c534c132a3347375323032362d6b65797365637265744516

补上外层 #\n 之后再做一次 hex string

1
#60007c805500802ba5e6806f7dd07b988241146e350f481ec220fe1536b67193671193ca08060fd065ddf9c197a119d2f732d8c574e7fc8ca862a2a15e3e7312df0fe81b0f810bf27f7f8982b9a1880ac3d3fd128acabe866e82655cb2b536edf8714ec03162c91ed2c534c132a3347375323032362d6b65797365637265744516\x0a
1
233630303037633830353530303830326261356536383036663764643037623938383234313134366533353066343831656332323066653135333662363731393336373131393363613038303630666430363564646639633139376131313964326637333264386335373465376663386361383632613261313565336537333132646630666538316230663831306266323766376638393832623961313838306163336433666431323861636162653836366538323635356362326235333665646638373134656330333136326339316564326335333463313332613333343733373533323330333233363264366236353739373336353633373236353734343531360a

对该字符串做 md5 即为 flag

flag: SUCTF{ad1b51464c1b679fe731c7d718af241f}

反调试

image.png

image.png

image.png

image.png

当检测父进程为 explorer.execmd.exe 时,对程序进行修补

delta = 0x9E3779B0

Other

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
struct ranges // sizeof=0x10
{
void *start;
void *end;
};
struct ranges2 // sizeof=0x10
{ // XREF: sub_7FF7C5D5F380/r
// sub_7FF7C5D5F3E0/r
void *start; // XREF: sub_7FF7C5D5F380+25/w
// sub_7FF7C5D5F380+38/w ...
_QWORD len; // XREF: sub_7FF7C5D5F380+2E/w
// sub_7FF7C5D5F3E0+2E/w
};
struct __unaligned vec // sizeof=0x18
{ // XREF: unpackData/r
// unpackData/r
_QWORD start; // XREF: unpackData+23/r
// unpackData+36/r ...
_QWORD end; // XREF: unpackData+28/r
// unpackData+3B/r ...
_QWORD len;
};
struct __unaligned ProtocolData // sizeof=0x79
{
_BYTE flag;
_DWORD data[26];
_DWORD key[4];
};

unpackData

编译器做了向量优化,这里可以看出结构体总大小为 121 字节,10523 * 4 + 3 处有部分重合

image.png

反序列化

将函数从十六进制字符串转换为 bytes,要求第一个字节为 0x60,然后

image.png

可以看出将 payload 写入结构体

image.png

补码计算,约束 opcode = 0x55 走正确的 unpackData 逻辑