前言
第一次复现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:

不以管理员启动:

然后进行具体分析:由于R0有tvm的混淆,很难看,因此先看R3为好。
R3
直接看main函数
检查操作

加密操作

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

这里发送最终的字符串给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=0x1000000000 {00000000 struct ACESDK_vtbl *lpVtbl;00000008 HANDLE hPort;00000010 };
00000000 struct /*VFT*/ ACESDK_vtbl // sizeof=0x6800000000 {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 };
在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加密。大概是这样:

Tea函数:

是和encdata进行判断。因此tea函数是具体加密,同时也拿到了key:ACE6
按顺序来说不会这么顺利,因为全都要解决一下混淆的问题,所以我一开始是从上到下全部看一遍的。
结果发现了一个SMC:


跟一下patchData的来源:

但是直接计算还原有点麻烦,我感觉直接dump更方便。使用windbg查看对应内存然后dump:
mov rax, rspmov [rax+8], rbxmov [rax+10h], rbpmov [rax+18h], rsimov [rax+20h], rdipush r13mov r13, rdxmov ebx, [rdx]xor r11d, r11dmov edi, [rdx+4]mov r8, rcxmov esi, [rdx+8]mov ebp, [rdx+0Ch]mov r9d, [rcx]lea edx, [r11+20h]mov r10d, [rcx+4]
BACK:mov ecx, r10dlea r11d, [r11-61C88647h]shr ecx, 5mov eax, r10dadd ecx, edishl eax, 4add eax, ebxxor ecx, eaxlea eax, [r11+r10]xor ecx, eaxadd r9d, ecxmov ecx,r9dmov eax,r9dshl eax,4shr ecx,5xor ecx,eaxmov rax,r11dshr rax,0bhadd ecx,r9dand eax,3mov eax,dword ptr [r13+rax*4]add eax,r11dxor ecx,eaxadd r10d,ecxsub rdx,1jne Back
pop r13mov rbx,[rsp+8]mov rbp, [rsp+10h]mov rsi, [rsp+18h]mov rdi, [rsp+20h]mov [r8], r9dmov [r8+4], r10dretn所以直接逆:
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部分信息可能已经过时









