2025腾讯游戏安全PC初赛

2025腾讯游戏安全PC初赛

之前都是复现,今年第一次参赛,由于题目回归CTF赛制,所以题目难度不是很大,在第一天就成功解出来了,但是强度也挺大的,一直到凌晨三点才写完题解,深夜写完题解,写完的时候已经困傻了….

ACEFirstRound.exe分析

老规矩,先查壳

image-20250329012004388

无壳,ida64位打开,main函数先调用ACEDriverSDK成员函数来loadDriver,然后cout,cin等待用户输入

image-20250329012811573

判断输入的长度和头部字符串,要求输入长度大于0x10,头部字符串为“ACE_”

image-20250329013012501

然后发现了一个奇奇怪怪的加密算法,发现和输入无关,动调发现是生成固定的string字符串:sxx

image-20250329013130055

image-20250329014011194

拷贝一份输入,对输入进行base58加密

image-20250329013544819

image-20250329013752834

猜测是常规码表,随意输入ACE_0123456789ABCDEF,动调得到的是@iCZKJxoTR3SicbazBLej7f,跟预测不符。

image-20250329014043276

IDA找到码表,交叉引用找到赋值码表处

image-20250329014145625

image-20250329014251240

好吧,全是浮点数寄存器的计算,还是继续动调找到真是码表,abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ123456789,根据该码表进行base58加密,发现该函数还将加密后的逆序了一下

image-20250329013915199

image-20250329013915199

image-20250329015051976

sxx作为异或密钥再进行一层加密

image-20250329014612644

OK,三环加密分析结束,发送消息调用驱动接口进行输入验证

image-20250329015151461

此时输入为:ACE_0123456789ABCDEF
传入驱动的密文:33 11 3B 29 33 32 0B 17 2C 21 4B 2B 1A 1B 1A 12 02 3A 3F 1D 12 44 1E 00

ACEDriver.sys分析

查壳,发现有tvm,丢垃圾桶(

但是只有几十kb,相较于去年的十几mb还算仁慈

IDA打开定位到DriverEntry,果然v掉了

image-20250329015932175

左边函数比较少,符号也有一些,找到PfltMessageNotify函数,查阅文档发现是用于通信的

image-20250329020510385

刚好三环是通过filterSendMessage 发送消息的,这个应该就是0环通信函数了。windbg对FltCreateCommunicationPort下断点,从参数定位到MessageNotifyCallback地址

image-20250329021610926

在该处断点,运行程序,输入ACE_0123456789ABCDEF,成功断下

image-20250329021813205

观察参数发现确实是预想的传参

image-20250329022114378

接着对MessageNotifyCallback函数进行分析,尝试找到验证逻辑

发现存在混淆

image-20250329022249324

混淆的方式和ACE-BASE很像,对push和pop之间的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
def getAsmInfo(start,end):
dic = {}
pointer = start
while(pointer <= end):
opcode = print_insn_mnem(pointer)
operand1 = print_operand(pointer,0)
operand2 = print_operand(pointer,1)
size = get_item_size(pointer)
dic[pointer] = [opcode,operand1,operand2,size]
pointer += size
return dic

def deObfuscation(start , end, jmp_push_gapMax = 25, jmp_pop_gapMax = 5):
asmInfo = getAsmInfo(start,end)
obfuscated_blocks = []

for addr in sorted(asmInfo.keys()):
processed_jmp = set()
processed_pop = set()
opcode, op1, _ , _ = asmInfo[addr]
if opcode == 'push':
reg = op1
# 在合理距离内寻找jmp指令
for jmp_addr in sorted(filter(lambda x: x > addr, asmInfo.keys())):
if jmp_addr - addr > jmp_push_gapMax:
break
jmp_op, jmp_op1, _ , _ = asmInfo[jmp_addr]
if jmp_op == 'jmp' and jmp_op1 == reg and jmp_addr not in processed_jmp:
processed_jmp.add(jmp_addr)
for pop_addr in sorted(filter(lambda x: x > jmp_addr, asmInfo.keys())):

if pop_addr - jmp_addr > jmp_pop_gapMax:
break
pop_op, pop_op1, _ , size = asmInfo[pop_addr]
if pop_op == 'pop' and pop_op1 == reg:
processed_pop.add(pop_addr)
obfuscated_blocks.append({
'start': addr,
'end': pop_addr + size,
})
break

return obfuscated_blocks

还有些没识别出来的,手搓一下,去掉混淆后函数大概长这样

image-20250329023212261

大致能猜出来是sub_140001448函数处理主要逻辑,但是也被混淆了,继续还原

image-20250329023353934

image-20250329023513784

image-20250329023558336

很明显第三个是一个tea加密,而且存在验证逻辑,提取dword_140004064和keyACE6,尝试直接解密,发现结果不对,看来没有那么简单,xref一下tea加密函数,发现还有一处调用

image-20250329023824200

该函数对tea加密存在内存修改行为,怀疑是动态调整加密逻辑,windbg动调看看

image-20250329024029891

发现异常,确实如意料之中,程序加载时对该处内存进行了修改,那dword_140004064呢?查看内存,发现这个没有修改

OK,逆一下这个修改后的tea加密

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
原片段: 
mov ecx, r9d
.text:0000000140001059 41 8B C1 mov eax, r9d
.text:000000014000105C
.text:000000014000105C C1 E9 05 shr ecx, 5
.text:000000014000105F C1 E0 04 shl eax, 4
.text:0000000140001062
.text:0000000140001062 03 CD add ecx, ebp
.text:0000000140001064 03 C6 add eax, esi
.text:0000000140001066 33 C8 xor ecx, eax
.text:0000000140001068 43 8D 04 0B lea eax, [r11+r9]
.text:000000014000106C 33 C8 xor ecx, eax
.text:000000014000106E 44 03 D1 add r10d, ecx

v9 += result ^ (v5 + v7 << 4) ^ (v6 + (v7 >> 5));

修改后的片段
ffffd90f`52316001 418bc9 mov ecx, r9d
ffffd90f`52316004 418bc1 mov eax, r9d
ffffd90f`52316007 c1e004 shl eax, 4 v7 << 4
ffffd90f`5231600a c1e905 shr ecx, 5 v7 >> 5
ffffd90f`5231600d 33c8 xor ecx, eax (v7 >> 5) ^ (v7 << 4)
ffffd90f`5231600f 418bc3 mov eax, r11d v3
ffffd90f`52316012 48c1e80b shr rax, 0Bh (v3 >> 0x0B)
ffffd90f`52316016 4103c9 add ecx, r9d (v7 >> 5) ^ (v7 << 4) + v7
ffffd90f`52316019 83e003 and eax, 3 (v3 >> 0x0B) & 3
ffffd90f`5231601c 418b448500 mov eax, dword ptr [r13+rax*4] [((v3 >> 0x0B) & 3 ) * 4 + key]
ffffd90f`52316021 4103c3 add eax, r11d [((v3 >> 0x0B) & 3 ) * 4 + key] + v3
ffffd90f`52316024 33c8 xor ecx, eax ([((v3 >> 0x0B) & 3 ) * 4 + key] + v3 ) ^ ((v7 >> 5) ^ (v7 << 4) + v7)
ffffd90f`52316026 4403d1 add r10d, ecx
v9 += ([((v3 >> 0x0B) & 3 ) + key] + v3 ) ^ (((v7 >> 5) ^ (v7 << 4) + v7))

好了,发现就是对v9的代码行进行了修改,还原后可以进行测试了,先获取ACE_0123456789ABCDEF加密后的密文,用作加密算法的验证

image-20250329024532748

得到33 11加密后的密文965f5996 866e20c2

编写tea解密脚本

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
VOID __fastcall DEC(unsigned int* a1, int* a2)
{
int v2; // ebx
unsigned int v3; // r11d
int v4; // edi
int v5; // esi
int v6; // ebp
unsigned int v7; // r9d
__int64 v8; // rdx
unsigned int v9; // r10d
__int64 result; // rax

v2 = *a2;
v3 = -0x61C88647*32;
v4 = a2[1];
v5 = a2[2];
v6 = a2[3];
v7 = *a1;
v8 = 32i64;
v9 = a1[1];
do
{
v9 -= (a2[((v3 >> 0x0B) & 3)] + v3) ^ (((v7 >> 5) ^ (v7 << 4)) + v7);

//v9 -= (v3 + v7) ^ (v5 + 16 * v7) ^ (v6 + (v7 >> 5));
v7 -= (v3 + v9) ^ (v2 + 16 * v9) ^ (v4 + (v9 >> 5));
v3 += 0x61C88647;
--v8;


} while (v8);
*a1 = v7;
a1[1] = v9;
}

运行结果

image-20250329024803496

跟密文相符,验证成功

解密思路

提取dword_140004064,注意从dword_140004060开始提取,然后tea解密,循环异或sxx,逆序,base58解码,得到flag

image-20250329025743103