Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4Mobile wallpaper 5Mobile wallpaper 6
2420 字
12 分钟
2025腾讯游戏安全大赛PC端初赛复现

前言#

第一次复现TX的题,没做之前感觉很难,但是做完之后还是很简单的。

本题的考点也许是:

  • minifilter的注册和通信

  • Base58+xor+Tea

  • SMC

  • junkcode

可能还有反调试,但是因为driverEntry里面的混淆太强了,没分析出来。不过也不需要分析这里就可以得出最后的内容。

题目描述#

小Q是一位热衷于PC客户端安全的技术爱好者,为了不断提升自己的技能,他经常参与各类CTF竞赛。某天,他收到了一封来自神秘人的邮件,内容如下:
“我可以引领你进入游戏安全的殿堂,但在此之前,你需要通过我的考验。打开这扇大门的钥匙就隐藏在附件中,你有能力找到它吗?”
### 评分标准:满分5分
(1)**在64位Windows10+系统**上运行exe, 找到正确的flag,作为答案提交(2分)。
(2)文档编写,详细描述解题过程,详述提供的解题程序的演示方法。(满1分)
(3)提供解题演示所用的源代码。编码工整风格优雅(1分)、注释详尽(1分)。
### 解题要求:
(1)**关闭Windows Defender等杀软、VBS、hyper-v后**,管理员运行exe(驱动需要自己解决签名问题)。
(2)解题得到正确的flag,以“flag{xxxxxxxxxxxxxxxxxxxxxx}”的形式提交。
(3)不得以任何方式暴力破解得到flag,解题注重分析过程。
(4)不得删改exe/sys的文件本身;不得使用任何文件和磁盘相关手段(比如同名文件占坑等方式)影响程序运行。
(5)必须使用64位Win10-Win11 22H2系统解题。
### 提交内容:
(1)flag(txt格式),可执行文件和运行说明(exe/dll/sys),文档(pdf或docx格式),代码(不限语言)打包压缩到一个zip文件中。
(2)zip文件命名格式:初赛-PC客户端+姓名+学校+手机号.zip。

总览#

该题目文件分为R3层和R0层。R3是和用户交互的CLI程序。R0是一个minifilter,其中对flag进行了最终校验。

先尝试打开看看具体的情况,以管理员启动exe:

446b34d4-b2bc-4aeb-878c-e5e082944587

不以管理员启动:

37646126-ce90-4d4b-8feb-97cb83f28d7a

然后进行具体分析:由于R0有tvm的混淆,很难看,因此先看R3为好。

R3#

直接看main函数

检查操作#

340810b2-520c-41a4-a931-1c8c458879a6

加密操作#

4137d3fd-0484-4f76-bb98-a626fa790229

在检查完ACE_开头之后,将Data异或然后放入Block,Block作为key在inputbase58后异或,base58函数会先进行换表base58加密然后加@并反转。以上过程都可以动态调试直接分析到,就不详细写了。

校验部分#

ae889741-9a71-4a52-b0ae-4b364083d7c0

这里发送最终的字符串给R0层做最终的校验。

因此R3层的解密为:

def solve():
cipher = [0x33, 0x1B, 0x4F, 0x4B, 0x32, 0x34, 0x3E, 0x20, 0x41, 0x25, 0x33, 0x0D, 0x20, 0x21, 0x29, 0x44, 0x4F, 0x1B, 0x11, 0x2B, 0x0D, 0x46, 0x16, 0x01, 0x21, 0x35, 0x2D, 0x22]
key = b"sxx"
xor_result = ""
for i in range(len(cipher)):
xor_result += chr(cipher[i] ^ key[i % len(key)])
print(f"[*] 1. 异或还原后结果: {xor_result}")
reversed_str = xor_result[::-1]
print(f"[*] 2. 翻转后结果: {reversed_str}")
if reversed_str.startswith('@'):
base58_str = reversed_str[1:]
else:
base58_str = reversed_str[:-1]
print(f"[*] 3. 移除后缀后的 Base58 串: {base58_str}")
ALPHABET = "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ123456789"
n = 0
for char in base58_str:
if char not in ALPHABET:
print(f"[!] 警告:字符 '{char}' 不在 Base58 表中,请检查表顺序或密文!")
return
n = n * 58 + ALPHABET.index(char)
res = []
while n > 0:
res.append(n % 256)
n //= 256
flag_body = bytes(res[::-1]).decode('ascii', errors='ignore')
print(f"\n[+] 最终 Flag: ACE_{flag_body}")
if __name__ == "__main__":
solve()

驱动初始化部分#

在main函数开头有一个ACEDriverSDK::`vftable’,打开之后发现是一个虚表

进行分析之后可得ACEDriverSDK结构:

00000000 struct ACESDK // sizeof=0x10
00000000 {
00000000 struct ACESDK_vtbl *lpVtbl;
00000008 HANDLE hPort;
00000010 };
00000000 struct /*VFT*/ ACESDK_vtbl // sizeof=0x68
00000000 {
00000000 __int64 (__fastcall *ScalarDeletingDestructor)(ACESDK *this, char flags);
00000008 __int64 (__fastcall *SetupAndConnectDriver)(ACESDK *this, const wchar_t *sysName, const wchar_t *svcName);
00000010 __int64 (__fastcall *FullDriverUninstall)(ACESDK *this, __int64);
00000018 _BOOL8 (__fastcall *startDriverService)(ACESDK *this, SC_HANDLE hSCManager, const WCHAR *svcName);
00000020 SC_HANDLE (__fastcall *stopService)(ACESDK *this, SC_HANDLE hSCManager, const WCHAR *svcName);
00000028 __int64 (__fastcall *InstallMinifilterService)(ACESDK *this, SC_HANDLE hSCManager, const WCHAR *svcName, const WCHAR *dbPath);
00000030 __int64 (__fastcall *UninstallService)(ACESDK *this, SC_HANDLE hSCManager, const WCHAR *svcName);
00000038 HRESULT (__fastcall *ConnectToDriverPort)(ACESDK *this);
00000040 __int64 (__fastcall *SendDriverCommand)(ACESDK *this, int cmdID, const void *inBuf, unsigned int inSize, LPVOID outBuf, DWORD outSize, DWORD *pRetSize);
00000048 __int64 (__fastcall *Unknown_48)(ACESDK *this);
00000050 int (__fastcall *Unknown_50)(ACESDK *this);
00000058 __int64 (__fastcall *Unknown_58)(ACESDK *this);
00000060 __int64 (__fastcall *sendData)(ACESDK *this, unsigned int dataLen, const void *data);
00000068 };

2bb1947c-c82d-408d-b88b-b5e4c72fd698

在testSend中和sendData中可以看到R3是如何跟R0交互的。

__int64 __fastcall testSend(ACESDK *a1)
{
__int64 v2; // rax
int v5; // [rsp+40h] [rbp-148h] BYREF
char v6[40]; // [rsp+48h] [rbp-140h] BYREF
_BYTE v7[256]; // [rsp+70h] [rbp-118h] BYREF
v5 = 0;
strcpy(v6, "This is TestHello from r3");
memset(v7, 0, sizeof(v7));
v2 = -1;
while ( v6[++v2] != 0 )
;
return a1->lpVtbl->SendDriverCommand(a1, 0x154000, v6, v2 + 1, v7, 256, (DWORD *)&v5);
}
__int64 __fastcall sendData(ACESDK *a1, unsigned int a2, const void *a3)
{
unsigned int v7; // [rsp+40h] [rbp-438h] BYREF
_BYTE v8[1036]; // [rsp+44h] [rbp-434h] BYREF
memset(v8, 0, 0x400u);
v7 = a2;
memmove(v8, a3, a2);
return a1->lpVtbl->SendDriverCommand(a1, 0x154004, &v7, 1028u, 0, 0, 0);
}
__int64 __fastcall SendDriverCommand(
ACESDK *a1,
int a2,
const void *a3,
unsigned int a4,
LPVOID lpOutBuffer,
DWORD dwOutBufferSize,
DWORD *a7)
{
DWORD inputBufferSize; // r15d
_DWORD *inputBuffer; // rsi
DWORD *v13; // rbx
unsigned int v14; // edi
DWORD BytesReturned; // [rsp+68h] [rbp+10h] BYREF
BytesReturned = 0;
inputBufferSize = a4 + 4;
inputBuffer = operator new(a4 + 4);
memset(inputBuffer, 0, inputBufferSize);
*inputBuffer = a2;
if ( a3 && a4 )
memmove(inputBuffer + 1, a3, a4);
v13 = a7;
if ( a7 )
*a7 = 0;
v14 = FilterSendMessage(a1->hPort, inputBuffer, inputBufferSize, lpOutBuffer, dwOutBufferSize, &BytesReturned);
if ( v13 )
*v13 = BytesReturned;
j_j_free(inputBuffer);
return v14;
}

可以对照看出senddata中a2是具体内容。同时最终将一个数字(实际是R0判断输入是不是测试的依据)复制到了FilterSendMessage函数的输入前。FilterSendMessage是和minifilter交互的函数。

至此,R3分析完毕。

R0#

driverentry里面一堆混淆,看不懂,所以尝试从MessageNotifyCallBack来看,这里是传入消息的回调。

有一些花指令,可以尝试写个插件patch掉,:

rules:
- name: advanced_lea_math_junk
pattern:
- "push $reg1"
- "lea $reg1, [$mem1]"
- "{lea $reg1, [$reg1 $op1 $mem2] | lea $reg1, [$reg1]}"
- "jmp_inst: jmp $reg1"
- "{E9|E8}"
- "target: pop $reg1"
condition:
- "$op1 is None or $op1 in ['+', '-']"
- "($target == $mem1 $op1 $mem2) if $mem2 is not None else ($target == $mem1)"
replace:
- "nop"
- name: advanced_lea_math_junk_2
pattern:
- "push $reg1"
- "lea $reg1, [$mem1]"
- "lea $reg1, [$reg1 $op1 $imm2]"
- "jmp_inst: jmp $reg1"
- "{E9|E8}"
- "target: pop $reg1"
condition:
- "$op1 in ['+', '-']"
- "$target == $mem1 $op1 $imm2"
replace:
- "nop"
- name: jmp_junk_target
pattern:
- "jmp_inst: jmp $target"
- "{E9|E8}"
- "target_loc: $any_inst"
condition:
- "$target == $target_loc"
replace:
- "nop"
- name: pushfq_add_popfq_jmp_e8_pop
pattern:
- "push $reg1"
- "mov $reg1, $imm1"
- "pushfq"
- "add $reg1, $imm2"
- "popfq"
- "jmp_inst: jmp $reg1"
- "{E9|E8}"
- "target: pop $reg1"
condition:
- "$target == ($imm1 + $imm2)&0xFFFFFFFFFFFFFFFF"
replace:
- "nop"
- name: not_xchg_not
pattern:
- "not $reg1"
- "{xchg $reg1, $reg2 | xchg $reg2, $reg1}"
- "not $reg2"
replace:
- "xchg $reg1, $reg2"
- name: mov_add_jmp
pattern:
- "push $reg1"
- "mov $reg1, $imm1"
- "pushfq"
- "add $reg1, $imm2"
- "popfq"
- "jmp_inst: jmp $reg1"
- "{E9|E8}"
replace:
- "push $reg1"
- "mov $reg1, imm(($imm1 + $imm2)&0xFFFFFFFFFFFFFFFF)"
- "jmp imm(($imm1 + $imm2)&0xFFFFFFFFFFFFFFFF)"

patch掉之后可能还有一些没匹配到的,尝试修复函数:

__int64 __fastcall sub_140009F83(__int64 a1, volatile void *a2, unsigned int a3, __int64 a4)
{
__int64 result; // rax
__int64 v5; // rsi
__int64 v8; // rdx
__int64 v10; // rt1
unsigned __int64 v11; // rcx
int v12; // r8d
void *v13; // rbx
int v14; // edx
v10 = v5;
v8 = v10;
*(_QWORD *)(result - 1869574000) = v10;
if ( v10 )
{
v11 = *(_QWORD *)result;
result = a3 + v8;
if ( result < v11 )
{
result += a4;
if ( result < v11 )
{
result = (__int64)ExAllocatePoolWithTag(NonPagedPool, a3, ~a3);
v13 = (void *)result;
if ( result )
{
ProbeForRead(a2, a3, ~v12);
memmove(v13, (const void *)a2, a3);
doSomething((__int64)v13, a3, a4);
ExFreePoolWithTag(v13, ~v14);
}
}
}
}
return result;
}

基本上重要的就是doSomething这个函数了。进去之后是核心加密函数。其中进行了判断,如果是0x154000就xor并返回,如果不是,就进行Tea加密。大概是这样: 06506bbe-41d7-4abc-b9c6-c1feea651c66

Tea函数: 21465a71-ca32-4660-8e77-24322ffacda2

是和encdata进行判断。因此tea函数是具体加密,同时也拿到了key:ACE6

按顺序来说不会这么顺利,因为全都要解决一下混淆的问题,所以我一开始是从上到下全部看一遍的。

结果发现了一个SMC: 63c4f62d-57ab-4998-88be-1f596170e222

8b044c17-05c8-4edc-8020-0e8747ac408f

跟一下patchData的来源:

811de51a-7db9-41fc-b513-d105b9056643

但是直接计算还原有点麻烦,我感觉直接dump更方便。使用windbg查看对应内存然后dump:

mov rax, rsp
mov [rax+8], rbx
mov [rax+10h], rbp
mov [rax+18h], rsi
mov [rax+20h], rdi
push r13
mov r13, rdx
mov ebx, [rdx]
xor r11d, r11d
mov edi, [rdx+4]
mov r8, rcx
mov esi, [rdx+8]
mov ebp, [rdx+0Ch]
mov r9d, [rcx]
lea edx, [r11+20h]
mov r10d, [rcx+4]
BACK:
mov ecx, r10d
lea r11d, [r11-61C88647h]
shr ecx, 5
mov eax, r10d
add ecx, edi
shl eax, 4
add eax, ebx
xor ecx, eax
lea eax, [r11+r10]
xor ecx, eax
add r9d, ecx
mov ecx,r9d
mov eax,r9d
shl eax,4
shr ecx,5
xor ecx,eax
mov rax,r11d
shr rax,0bh
add ecx,r9d
and eax,3
mov eax,dword ptr [r13+rax*4]
add eax,r11d
xor ecx,eax
add r10d,ecx
sub rdx,1
jne Back
pop r13
mov rbx,[rsp+8]
mov rbp, [rsp+10h]
mov rsi, [rsp+18h]
mov rdi, [rsp+20h]
mov [r8], r9d
mov [r8+4], r10d
retn

所以直接逆:

import struct
def solve(cipher):
key = b"sxx"
xor_result = ""
for i in range(len(cipher)):
xor_result += chr(cipher[i] ^ key[i % len(key)])
reversed_str = xor_result[::-1]
if reversed_str.startswith('@'):
base58_str = reversed_str[1:]
else:
base58_str = reversed_str[:-1]
ALPHABET = "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ123456789"
n = 0
for char in base58_str:
if char not in ALPHABET:
print(f"[!] 警告:字符 '{char}' 不在 Base58 表中,请检查表顺序或密文!")
return
n = n * 58 + ALPHABET.index(char)
res = []
while n > 0:
res.append(n % 256)
n //= 256
flag_body = bytes(res[::-1]).decode('ascii', errors='ignore')
print(f"\n[+] 最终 Flag: ACE_{flag_body}")
def encrypt_hybrid(pt, key):
delta = 0x9E3779B9
m = 0xFFFFFFFF
v0, v1 = pt[0] & m, pt[1] & m
s = 0
for _ in range(32):
s = (s + delta) & m
v0 = (v0 + ((s + v1) ^ (key[0] + (v1 << 4)) ^ (key[1] + (v1 >> 5)))) & m
v1 = (v1 + ((s + key[(s >> 11) & 3]) ^ (v0 + ((v0 << 4) ^ (v0 >> 5))))) & m
return (v0, v1)
def decrypt_hybrid(ct, key):
delta = 0x9E3779B9
m = 0xFFFFFFFF
v0, v1 = ct[0] & m, ct[1] & m
s = (delta * 32) & m
for _ in range(32):
v1 = (v1 - ((s + key[(s >> 11) & 3]) ^ (v0 + ((v0 << 4) ^ (v0 >> 5))))) & m
v0 = (v0 - ((s + v1) ^ (key[0] + (v1 << 4)) ^ (key[1] + (v1 >> 5)))) & m
s = (s - delta) & m
return (v0, v1)
if __name__ == "__main__":
key = [0x41, 0x43, 0x45, 0x36]
ct = (0x0EC367B8, 0xC9DA9044, 0xDA6C2DEB, 0x88DDC9C3, 0x32A01575, 0x231DD0B4, 0x4B9E8A74, 0xD75D3E74, 0xEAAB8712, 0xE704E888, 0xE01A31AC, 0xECAE205C, 0xA7BE7467, 0x0C6252A3, 0x1AEFEC4E, 0xC40DED44, 0xC3C842CC, 0xDE4A0C0E, 0x7C24F3FC, 0x8FB8D001, 0x11153E6E, 0x530ED15C, 0xF4214811, 0xBEB517E0, 0x63F91634, 0x4D96F8A5, 0xFE23EAC8, 0x2C607ADF, 0xCC43D85C, 0xFF186C5B, 0x8763E1A5, 0x9187BD58, 0x87D1069B, 0xD7878D7B, 0x836E6B68, 0x55A0C63F, 0xD979FDB3, 0x3E524DEE, 0x7AB35C82, 0xA2F4DA8D, 0x1708BA4C, 0x710653E6)
result = bytearray()
for i in range(0, len(ct), 2):
dt = decrypt_hybrid((ct[i], ct[i + 1]), key)
result.append(dt[0] & 0xFF)
result.append(dt[1] & 0xFF)
result = bytes(result).rstrip(b'\x00')
printable = ''.join(chr(b) if 0x20 <= b < 0x7f else '.' for b in result)
solve(list(result))
# [+] 最终 Flag: ACE_We1C0me!T0Z0Z5GamESecur1t9*CTf
2025腾讯游戏安全大赛PC端初赛复现
https://pri87.vip/posts/2025腾讯游戏安全大赛pc端初赛复现/
作者
pRism
发布于
2026-03-29
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00