首页
社区
课程
招聘
10
[原创]2025腾讯游戏安全技术竞赛决赛题解
发表于: 2025-4-14 00:29 5559

[原创]2025腾讯游戏安全技术竞赛决赛题解

2025-4-14 00:29
5559

(1)在intel CPU/64位Windows10系统上运行sys,成功加载驱动(0.5分)

(2)能在双机环境运行驱动并调试(1分)

(3)优化驱动中的耗时算法,并给出demo能快速计算得出正确的key(1分)

(4)分析并给出flag的计算执行流程(1.5分),能准确说明其串联逻辑(0.5分)

(5)正确解出flag(1分)

(6)该题目使用了一种外挂常用的隐藏手段,请给出多种检测方法,要求demo程序能在题目驱动运行的环境下进行精确检测,方法越多分数越高(3分)

(7)文档编写,详细描述解题过程,详述提供的解题程序的演示方法。做到清晰易懂,操作可以复现结果;编码工整风格优雅、注释详尽(1.5分)

驱动带反调,且目测有 VMP 壳,于是选择 dump+Fix,由于驱动带反调,会蓝屏,于是 hook 蓝屏代码,选择该时机去 dump 内存,找到 Entry。

随后跟进,遇到一些立即数的赋值,且有函数加密,直接选择模拟执行。

最后得到一个注册表字符串 \\Machine\\System\\CurrentControlSet\\Services\\ACEDriver\\2025ACECTF

正常直接加载驱动会返回 31 错误,猜测判定了注册表的某些东西,继续往下模拟可得一个字符串 Key。

模拟执行可以尽量挑不依赖外部函数,且立即数比较多的片段,这样可以省略计算的过程。

下面也可以模拟,但是根据题目描述也能猜个大概,有一个 Key,有一个 Flag。再结合该函数的定义和调用

不难得到 Key 应该是一个 __int64 的值,Flag 是一个字符串,保存到全局变量当中,创建对应的注册表项,成功加载驱动。

前面说过,有反调试,观察导入表遍历了 NtQuerySystemInformation,于是想到可能是检测到了 kdcom.dll 模块(因为之前有游戏做过类似的检测),那么直接 hook 把 kdcom.dll 改名。

因为保护了 IAT,因此不能使用常规的 IAT hook,还是选择使用 inline hook

绕过之后加载驱动不会蓝屏,但是会出现另一个错误。

随后查看 DbgView 发现似乎是 vmp 自带的,手上有 3.8 版本,尝试编译放进去加载,果然如此,一摸一样的错误代码。

这个可以通过字符串定位,也可以由上面注册表继续往后分析得到。

当输入的 Key0 时,尝试使用算法生成。通过分析该函数,结合一些一些字符串可知,该算法自己实现了一个双端队列(deque),但是实际使用的时候是把它当成栈来用了,实现了一个深度优先搜索算法。

第一步恢复 deque 结构体,第一个 8 字节是一个指向自身的指针,但是似乎没有用过,正常来说应该是虚表。双向队列会有全队列大小(队列最多容纳的元素个数),头指针还有尾指针,而通常情况下,后两者可以使用头指针 + 有效元素个数来实现,因此最后得到以下定义:

应用到 IDA 之后,配合注释,算法一目了然。

深入阅读它实现 deque 的源码其实可以明白,第一,它的 MAX_SIZE 一定是 2 的整数幂,并且它是环形队列。第二,在取模的时候更加高效(即 &(MAX_SIZE-1))。

循环开头压入了 (44,22) 元素。

每次循环开始,取得尾部的元素,判断 x1 是否为 0,或者说 x==y,如果是则删除该元素。

否则尝试先往左走(即 x-1)并立刻将往左走的点压入栈中重新循环,经典的 DFS。

往左走之后会将当前点标记为已经往左走过,这里 x4 的值有以下三种情况:

x4==2 时,该点也会被删除,并将,结合图中的注释大概也能看懂这个算法了,这里画了一个图更好理解

从黑色格子出发,只能向左或者向左上(y轴往下的情况下)。红色格子不能继续走,价值为1,同样在 y 0层也有一行红色格子价值为 1,其余格子价值均为 x%5,最后应该是计算黑色格子到红色格子的所有不同的路径的价值之和。

优化可以使用记忆化搜索,或者直接使用动态规划,记忆化搜索简单无脑,三行搞定。

反调试检测:

绕过:

尝试 hook NtQuerySystemInformationKeBugCheckEx,找到蓝屏的函数在 0x74F0,于是考虑在 hook NtQuerySystemInformation 的某个节点,把该函数 hook 直接返回,不会蓝屏,但是调试器被剥离。

调试发现是调用了 KdDisableDebugger 函数。

同样也是直接返回,操作完成后,可以发现驱动已经可以正常运行,且调试器正常工作。

这里代码实现仅仅变动了 hook 的 NtQuerySystemInformation 函数,因为有 vmp 壳,所以在加载的时候去 hook 是不明智的,直接在调用 NtQuerySystemInformation 的某一刻过掉即可。

因为壳似乎有 API 防 hook 的检测,如果不及时下掉钩子则会加载失败,因此选择在第三次调用之后下掉钩子并做反调试的相关 hook。

结论:

从后续的逻辑来看,生成的 key 就是 flag 做某种加密的密钥。

这里的 v10,经过动态调试,记录了最高有效位,例如我现在输入的 key=0x25312620c4fe,占用 6 字节,所以最高有效位为第五位(从零开始),如图所示

因此第一步就是实现一个简单的异或加密,根据密钥的长度而定。

紧随其后的是 TEA 加密,和初赛一样,每两个字符零扩展成 int 之后放入 TEA 加密。

乍一看这里居然用了 key 的地址进行运算,实则不需要被他吓到,这么玩确实会导致每次加密的结果不一样,但是不代表就不可逆(后来嘎嘎被打脸),逆了一下发现逆推到第一个式子的时候推不动了。

经过调试,发现是代码被 VT hook 了,联想到之前要求一定是 Intel CPU

可以看到单步执行得到的指令结果不符合预期,题目在此处开启 VT 环境。

+0x5150 处的函数实现 hook 的分发。

答案:flag: flag{ACE_C0n9raTs0nPA55TheZ02S9AmeScTf#}

由于分发函数过于庞大,且 VT 的hook是无痕的,因此考虑能否使用加密的弱点去实现 flag 的解密,由于 TEA 加密的输入是被零扩展的,因此实际 8 字节的分组只有 2 字节是有效的。

可以计算两字节的所有组合,获得它的密文结果,实施这个方法之前,需要确定,相同的密文,相同的 key,得到的一定是相同的输出,断 TEA 加密的 call,选中 RCX 的内存,改成全 0,得到 A9 59 CF AB EB 9D A3 0A,多个位置尝试发现得到的始终是这个结果,因此判定该方法可行。

这里方法就多起来了,第一可以把注册表写满 0000-FFFF,然后指定 Key0xFF,就可以 dump 得到一份表,或者可以直接写一个驱动去调用那个功能,这里我选择了后者。

观察 windbg 的输出,得到了正确的运行结果:

理论可行,那就直接 for 爆一遍,然后存到内存里面,最后 windbg 直接 dump 出来。

但是发现直接 dump 无法直接查找得到,经检查,原来是 rdmsrVT hook,做了一次异或加密,并且根据长度生成异或的密钥,很简单,直接把内存都置 0 就能直接拿到异或的密钥,并且密钥由输入的 flag 长度决定,34 长度的密钥为 03 39 49 26 2F F6 F8 4E

做完异或加密之后,就可以配合 dump 的密文查表。

用如下脚本查表:

得到结果

为了验证 TEA,选择在做完异或加密之后直接把以上密文贴到 check 的内存中,结果返回正确。

随后拿 Key 异或还是无法得到正确的结果,经查在 +0x95DF 处的指令,读取 flag 的时候存在 VT hook

可以发现内存实际是 A 但是读取结果为 0x24,存在类似的单表替换,而刚刚好,TEA 解密得到的信息异或 key 之后得到的值刚好是 0x24

结合初赛的 flag 格式,A 应该是正确的明文了。

这里就是动调大法,按了三个小时调试器,在取内存的地方下断点,然后每次给内存自增 1,观察 ax 寄存器是否符合预期,最终得到正确的结果

题目明显使用 VT 技术实现对内存某些部分的无痕 hook,因此我们的做法就是去检测自身运行是否处于 VT 环境。

但是题目做了 hook,题目运行的时候,我们读取的 MSR 显示是没开启 VT 的。

如图所示,前者开启题目驱动,VT1 提示 not enable,卸载题目驱动后又能够检测到 VT 处于开启状态。

于是这里我想到,如果题目要 hook,那么在读取 MSR 的时候必然要运行很多额外代码,这里选择对比开启题目驱动和关闭题目驱动之间的时间差。

运行结果:

可以发现,我测试了 100000 条 MSR 指令,正常虚拟机运行 132ms,而开启题目驱动之后来到了 2079ms,接近 20 倍的差距。那么第一种方法可以是找一条跟 MSR 指令差不多周期的指令,但是这个指令不会被 VT 影响,最后计算两者的偏差率,超过很多则时间判定不通过,当然它如果不 hook MSR,就可以选择直接判定对应的 VT 位是否处于开启状态,两者联合检测怎么都是可以检测到的。

这里我使用 inc 指令去比较。

这里使用指针来保证每次循环的结果均写入内存中。

结果也很完美,那么最终检测 VT 环境可以判断 elapsed_us2/elapsed_us1>5,如果是则说明处于 VT 环境中被 hook。

最终代码:

运行效果:

能够在开启题目驱动的情况下检测到。

伪造虚假的 cpuid 参数,通常情况下真机会返回 0,而开启了 VT 则会接管返回正常的值。参考文章:https://secret.club/2020/04/13/how-anti-cheats-detect-system-emulation.html。

该代码可以运行在用户层,经测试,该代码在开启题目驱动的真机上返回为 true,未运行题目的真机返回为 false,虚拟机中则一律返回 true

import idaapi
import idc
from unicorn import *
from unicorn.x86_const import *
import ida_name
import mmap
import sys
import idautils
import struct
base_addr = idaapi.get_imagebase()
fix_function_start=0xFFFFF806FF8D9F0C
fix_function_end=0xFFFFF806FF8DA05F
PAGE_SIZE=0x1000
RSP=0xdead0000
RBP=0xdead0000
map_addr=idaapi.get_imagebase()
offset=base_addr-map_addr
def hook_mem_unmapped(uc, access, address, size, value, user_data):
    aligned_addr = address&0xFFFFFFFFFFFFF000
    try:
        uc.mem_map(aligned_addr, PAGE_SIZE)
        data=idaapi.get_bytes(aligned_addr,PAGE_SIZE)
        uc.mem_write(aligned_addr,data)
        return True  # 表示错误已处理,继续执行
    except Exception as e:
        print(f"[-] 动态映射内存页失败: {e}")
        return False 
 
instr_count = 0
cnt=0
def hook_code(uc, address, size, user_data):
    global instr_count,cnt
    instr_count += 1
    rax=uc.reg_read(UC_X86_REG_RAX)
    rcx=uc.reg_read(UC_X86_REG_RCX)
    rdx=uc.reg_read(UC_X86_REG_RDX)
    r8=uc.reg_read(UC_X86_REG_R8)
    r9=uc.reg_read(UC_X86_REG_R9)
    r10=uc.reg_read(UC_X86_REG_R10)
    rbp=uc.reg_read(UC_X86_REG_RBP)
    rsp=uc.reg_read(UC_X86_REG_RSP)
    rip=uc.reg_read(UC_X86_REG_RIP)
    if rip==0xFFFFF806FF8DA05C:
        st=b''
        offset=0xE0
        while True:
            if uc.mem_read(rsp+offset,2)==b'\x00\x00':
                break
            st+=uc.mem_read(rsp+offset,1)
            offset+=2
        print(st)
        #print(uc.mem_read(rsp+offset))
     
mu = Uc(UC_ARCH_X86, UC_MODE_64)
mu.reg_write(UC_X86_REG_RIP, fix_function_start)  # 设置执行起始地址
mu.reg_write(UC_X86_REG_R13, 0xFF)
mu.reg_write(UC_X86_REG_RSP, RSP)
mu.reg_write(UC_X86_REG_RBP, RBP)
mu.mem_map(RSP-PAGE_SIZE,PAGE_SIZE*2)
mu.hook_add(UC_HOOK_MEM_FETCH_UNMAPPED, hook_mem_unmapped)
mu.hook_add(UC_HOOK_MEM_READ_UNMAPPED, hook_mem_unmapped)
mu.hook_add(UC_HOOK_MEM_WRITE_UNMAPPED, hook_mem_unmapped)
mu.hook_add(UC_HOOK_CODE, hook_code)
instr_count=0
print(hex(mu.reg_read(UC_X86_REG_RIP)))
try:
    mu.emu_start(fix_function_start,fix_function_end)
except UcError as e:
    print(e)
    pass
import idaapi
import idc
from unicorn import *
from unicorn.x86_const import *
import ida_name
import mmap
import sys
import idautils
import struct
base_addr = idaapi.get_imagebase()
fix_function_start=0xFFFFF806FF8D9F0C
fix_function_end=0xFFFFF806FF8DA05F
PAGE_SIZE=0x1000
RSP=0xdead0000
RBP=0xdead0000
map_addr=idaapi.get_imagebase()
offset=base_addr-map_addr
def hook_mem_unmapped(uc, access, address, size, value, user_data):
    aligned_addr = address&0xFFFFFFFFFFFFF000
    try:
        uc.mem_map(aligned_addr, PAGE_SIZE)
        data=idaapi.get_bytes(aligned_addr,PAGE_SIZE)
        uc.mem_write(aligned_addr,data)
        return True  # 表示错误已处理,继续执行
    except Exception as e:
        print(f"[-] 动态映射内存页失败: {e}")
        return False 
 
instr_count = 0
cnt=0
def hook_code(uc, address, size, user_data):
    global instr_count,cnt
    instr_count += 1
    rax=uc.reg_read(UC_X86_REG_RAX)
    rcx=uc.reg_read(UC_X86_REG_RCX)
    rdx=uc.reg_read(UC_X86_REG_RDX)
    r8=uc.reg_read(UC_X86_REG_R8)
    r9=uc.reg_read(UC_X86_REG_R9)
    r10=uc.reg_read(UC_X86_REG_R10)
    rbp=uc.reg_read(UC_X86_REG_RBP)
    rsp=uc.reg_read(UC_X86_REG_RSP)
    rip=uc.reg_read(UC_X86_REG_RIP)
    if rip==0xFFFFF806FF8DA05C:
        st=b''
        offset=0xE0
        while True:
            if uc.mem_read(rsp+offset,2)==b'\x00\x00':
                break
            st+=uc.mem_read(rsp+offset,1)
            offset+=2
        print(st)
        #print(uc.mem_read(rsp+offset))
     
mu = Uc(UC_ARCH_X86, UC_MODE_64)
mu.reg_write(UC_X86_REG_RIP, fix_function_start)  # 设置执行起始地址
mu.reg_write(UC_X86_REG_R13, 0xFF)
mu.reg_write(UC_X86_REG_RSP, RSP)
mu.reg_write(UC_X86_REG_RBP, RBP)
mu.mem_map(RSP-PAGE_SIZE,PAGE_SIZE*2)
mu.hook_add(UC_HOOK_MEM_FETCH_UNMAPPED, hook_mem_unmapped)
mu.hook_add(UC_HOOK_MEM_READ_UNMAPPED, hook_mem_unmapped)
mu.hook_add(UC_HOOK_MEM_WRITE_UNMAPPED, hook_mem_unmapped)
mu.hook_add(UC_HOOK_CODE, hook_code)
instr_count=0
print(hex(mu.reg_read(UC_X86_REG_RIP)))
try:
    mu.emu_start(fix_function_start,fix_function_end)
except UcError as e:
    print(e)
    pass
NTSTATUS gh_NtQuerySystemInformation(...)
{
    unhook();
    auto ret = ((NtQuerySystemInformation_t)(TargetFunction))(...);
    if (SystemInformationLength&& SystemInformationClass== SystemModuleInformation) {
        PSYSTEM_MODULE_INFORMATION pModInfo = (PSYSTEM_MODULE_INFORMATION)SystemInformation;
        for (int i = 0; i < pModInfo->ModulesCount; i++) {
            PSYSTEM_MODULE_INFORMATION_ENTRY pEntry = &pModInfo->Modules[i];
            if (strcmp(pEntry->Name + pEntry->NameOffset, "kdcom.dll")) {
                (pEntry->Name + pEntry->NameOffset)[0] = 'x';
            }
        }
    }
    rehook();
    return ret;
}
NTSTATUS gh_NtQuerySystemInformation(...)
{
    unhook();
    auto ret = ((NtQuerySystemInformation_t)(TargetFunction))(...);
    if (SystemInformationLength&& SystemInformationClass== SystemModuleInformation) {
        PSYSTEM_MODULE_INFORMATION pModInfo = (PSYSTEM_MODULE_INFORMATION)SystemInformation;
        for (int i = 0; i < pModInfo->ModulesCount; i++) {
            PSYSTEM_MODULE_INFORMATION_ENTRY pEntry = &pModInfo->Modules[i];
            if (strcmp(pEntry->Name + pEntry->NameOffset, "kdcom.dll")) {
                (pEntry->Name + pEntry->NameOffset)[0] = 'x';
            }
        }
    }
    rehook();
    return ret;
}
struct deque
{
    void *vtable;
    data **map;
    __int64 MAX_SIZE;
    __int64 begin_idx;
    __int64 size;
};
struct data
{
    int x1;
    int y1;
    _QWORD data2;
    _QWORD data3;
    int x4;
    int y4;
};
struct deque
{
    void *vtable;
    data **map;
    __int64 MAX_SIZE;
    __int64 begin_idx;
    __int64 size;
};
struct data
{

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 10
支持
分享
赞赏记录
参与人
雪币
留言
时间
西贝巴巴
+1
你的帖子非常有用,感谢分享!
2025-4-17 14:14
LovelyWei
+1
谢谢你的细致分析,受益匪浅!
2025-4-17 11:22
tlsn
你的分享对大家帮助很大,非常感谢!
2025-4-16 11:25
moshuiD
你的分享对大家帮助很大,非常感谢!
2025-4-15 22:06
mb_ripshzdx
期待更多优质内容的分享,论坛有你更精彩!
2025-4-15 19:45
咕咕咕下来
为你点赞!
2025-4-14 17:33
大笨钟
你的分享对大家帮助很大,非常感谢!
2025-4-14 16:32
Bombs
非常支持你的观点!
2025-4-14 11:39
hkdong
谢谢你的细致分析,受益匪浅!
2025-4-14 09:55
5m10v3
+10
非常支持你的观点!
2025-4-14 00:57
最新回复 (6)
雪    币: 393
活跃值: (1880)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
2
太强了????,学习一波
2025-4-14 00:57
0
雪    币: 541
活跃值: (669)
能力值: ( LV12,RANK:250 )
在线值:
发帖
回帖
粉丝
3
 学习了
2025-4-14 17:14
0
雪    币: 220
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
学习了
2025-4-14 17:33
0
雪    币: 165
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5
666,收藏学习了
2025-4-15 19:45
0
雪    币: 215
活跃值: (656)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
希望上传附件 学习
2025-4-16 13:55
0
雪    币: 3221
活跃值: (5809)
能力值: ( LV4,RANK:55 )
在线值:
发帖
回帖
粉丝
7
牛逼哥
6天前
0
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册