2025腾讯游戏安全技术竞赛-PC客户端安全-初赛(复现)
复现题目旨在了解题目的逻辑~~~~,可能与真正的解题流程有出入
先看main函数
首先,加载了ACEDriverSDK,里面有许多函数,我们先标出函数的作用。

在虚表中我们可以看到下面这两个函数,一个是用于后面base58表的初始化的,还有一个就是反调试函数。

根据作用大致恢复一下结构体(命名函数时会自动修改)。
1 | struct ACEDriverSDK_vftable |
这里我们主要注意一下initserver,Filter_install,FilterConnectCommunicationPort_wrp,FilterSendMessaged,send_test,send_message。其他的函数也都是字面意思。

initserver函数注册并启动驱动服务,同时初始化了Windows 文件系统过滤驱动(File System Filter Driver)。

FilterConnectCommunicationPort_wrp()创建了一个端口用于用户层和内核层的通信。
FilterSendMessaged,send_test,send_message函数把信息发送给内核层。其中send_test发送了一个测试信息,”This is TestHello from r3”。
接下来我们继续分析main函数。
regDriverFunc_(sdk)加载sdk,加载驱动后,题目要求我们输入flag,并下下面规定了flag前四位为”ACE_“。

接下来用异或的方式获取了一个密钥。下面还有一个类似strcpy函数把获取的key赋值到了Block中。后面把我们的输入放到了base58enc_and_re()函数处理。

base58加密特征,output是表,看一下引用,是来自一个虚表的函数初始化的,到时候我们动调获得就行。

在末尾还有一个_std_reverse_trivially_swappable_1(v44, v45)会把结果反转(后续调试得来,看名字也可以知道)。

接下来把base58enc_and_re()处理的结果与上面得到的异或密钥进行异或加密,并把加密结果存入res中。

与把数据发送给内核,进行flag的check。

大概逻辑清晰了,接下来动调获取数据。发现有反调试,推测是在main函数前加载,对main交叉引用可以找到__scrt_common_main_seh()函数。在虚表中我们可以看到反调试函数。

在反调试函数中我们能够看到checkdebug()函数被传入beginthreadex()创建了一个反调试的线程,把线程信息保存到了全局变量&byte_7FF7F4D78C40中。

仔细查看checkdebug()函数能够发现调用了CheckRemoteDebuggerPresent()进行调试检测。同时通过Query_perf_frequency()函数获取CPU的时钟频率,调用QueryPerformanceCounter()精确计算程序运行的时间,如果程序的运行时间超过预定时间8.64e14s就会terminate()终止进程。

绕过反调试,我们只需要在函数里修改一下让函数直接返回就行。还可以用frida把checkdebug函数替换掉,这样才遵守了不修改原程序的规则。不过为了分析,我们先把函数ret掉。

去掉反调试后发现程序在调试的时候会直接闪退在驱动加载阶段。推测在驱动内还有反调试的检测,于是我们直接跳过驱动的加载先分析r3层,毕竟只有在最后checkflag的时候才需要用到驱动。

在base58函数里面拿到表

base58

倒转

异或”sxx”

发送到内核check

现在来分析驱动,我们先来了解一下在这题中r3与r0的通信方式。
查找我们发现的那些通信函数,我们可以发现,本题采用的通信方式是minifilter的port端口通信。参考这篇文章[内核驱动] miniFilter 内核层与应用程序通信,和mapiFltCreateCommunicationPort 函数 (fltkernel.h) - Windows drivers | Microsoft Learn。
在驱动中,首先要在DriverEntry中用FltRegisterFilter函数进行注册,通过FltCreateCommunicationPort创建一个交互端口,并创建3个回调函数分别在连接,关闭连接,接收消息时调用,我们需要重点关注消息接收函数的作用。
由于混淆的不是特别强烈,在函数表中我们就能看到FltRegisterFilter函数,交叉引用过去,我们就能看到minifilter的初始化函数

我们主要看一下MessageNotifyCallback。里面有一些混淆,大概就是以一对push和pop为一组混淆或者花指令。去掉混淆和花指令。没有看到明显加密特征,继续跟函数调用。
发现sub_1400021C0里面有一堆_mm_stream_ps系统调用,看着像系统函数,(这里可以通过windbg动调给输入的数据下硬件断点找到数据被操作的位置)猜测是memcpy(),继续往下看可以看到一个被混淆的函数sub_140001448,跟进去看看。进去后在第一个call的函数里call了一个地址为0x140001000的函数,看到有加密特征,去一下花指令,恢复函数,可以看到一个tea加密以及密钥。加密后对密文进行了比较,所以这里就是最后的加密逻辑,把两个一字节数据传入加密。对tea交叉引用可以发现有一个函数对tea进行了动态修补。而且还不清楚输入数据是否是没有修改的被传递传递到这里来的,打算采用动调的方式解决。

用windbg动调,先找到基地址我这里是0xfffff803`31db0000

找到位置基地址+1000就是tea加密函数的位置,直接在0xfffff803`31db1000处下断点后输入”ACE_1111111”运行到这里,在ida上看到,加密传进来的数据在rcx,密钥在rdx,直接查看内存就可以发现。左边两位”3”,”=”是要加密的输入,’’A,C,E,6’就是key。那么逻辑的确就是把r3层传入的数据按两个字节为一组进行加密。

但是tea函数被动态patch了,打算动调拿出patch之后函数的字节码。

从刚刚的断点往下看,现在这就是已经被patch掉的函数了。往下分析,

发现中间多出一个jmp跳转到l0x0FFFFB98A61565000,跟进去看看

发现执行完这一段后又会跳转回原来的加密函数,看一下跳转的地址我们就可以发现。第一个jmp是直接跳转到了原来加密函数的末尾,也就是加密结束。第二个jmp跳转到了加密函数前面的位置执行下一轮加密。也就是说原来加密函数的后面部分是执行不到的,原函数直接跳转到了0x0FFFFB98A61565000执行。如下面的汇编
1 | fffff803`31db1000 488bc4 mov rax, rsp |
我们到对应的地址去把对应的十六进制dump下来,删掉中间的无效部分,直接进行拼接。拼接完成后保存为文件,用ida打开,修复一下跳转地址就可以看到伪代码。
1 |
|
修复

看伪代码

现在逻辑就很清晰了,就是一个魔改的tea加密,密钥和加密数据是一样的。
再动调往后看看。
在加密函数的ret处下断点运行到这里,单步运行返回到0xFFFFF80331B19C5B处,紧接着的就是一个比较函数,的确这就是最后的加密了。

发现数据来源是rsi-4也就是0xFFFFF80331B14064 - 4,而且每次比较完后都会把rsi+8,也就是2个四字节数据为一组进行比较。查看所在内存,把密文提取出来(当然也可以静态提取密文)。

1 | B8 67 C3 0E 44 90 DA C9 EB 2D 6C DA C3 C9 DD 88 |
解密方式有两种,一是写常规解密脚本。二是爆破,因为他每次只对两字节加密,进行比较,我们爆破这两个字节也很容易得到结果。
下面是解密脚本,base58也可以用赛博厨师解密,记得去掉”@”。
1 |
|
