This page looks best with JavaScript enabled

2024游戏安全技术竞赛-PC客户端安全 出题背景与思路

 ·  ☕ 8 min read · 👀... views

“屠龙勇士终成恶龙”,昔日参赛选手变成出题人。

这次竞赛的出题确实是耗费了非常多的时间和精力,虽然以前也经常给别的CTF比赛出题,但还从来没有给这种级别的比赛出过。给到我的竞赛的主题是shellcode相关,我需要以shellcode为主要题材设计出初赛和决赛两道题目。
怎么样才能不拘泥于传统的CTF算法比拼,但又能以客观的标准判定成绩;怎么样才能让赛题设计的更加偏向于游戏安全实战环境,但又能让基础比较好的CTFer努努力也能出线;怎么样才能让各个层次水平的参赛选手都能玩一部分,但最终成绩上又要有区分度。希望CTF、游戏安全、破解脱壳中任何一项有较强能力水平的选手都能解出题目。

初赛

Ring3

初赛赛题设计的原型是shellcode类型外挂的分析。赛题分为两个部分:三环的hack.exe本质上是一个注入器,会随机挑选一个白进程去注入,注入手法用的是最传统的远线程+manualmap,同时为了降低题目难度,在注入过程中的DLL是没有被加密的。注入器模拟外挂的启动器,添加了VMP;注入的DLL没有添加外壳,只保护了少数函数,并且注入的DLL里具有多项反黑工具的能力。初赛整道赛题花费我最多时间的部分就是反黑工具开发这部分。其实CTF和真实游戏安全实战环境中很大一个区别就是程序的反分析对抗强度,CTF中的题目对抗调试器往往也就是一些传统的反调试手法,但是在实战环境中,常用工具的特征检测会更厉害。所以这次比赛收集了三十几种常用二进制分析工具的特征,检测维度包括进程名、窗体名、驱动模块名以及CE专项检测。我希望每个出线的选手都能有比较强的动手能力,能够在强对抗的情况下自己编写代码来进行赛题的分析,而非依赖现有的工具。

预期解大概如下:

  1. 挂钩OpenProcess/WriteProcessMemory 或使用其他未在黑名单内的分析工具或魔改工具将DLL Dump出来
  2. DLL未加外壳,直接找到CreateFileA API调用,或者根据hack.exe的行为找到被注入进程,监控被注入进程文件操作行为定位到关键点
  3. 修改CreateFileA参数OPEN_EXISTING为OPEN_ALWAYS获得Flag,只需patch一字节
  4. 修改xorstr异或前的密文 或者 调用函数前inlinehook替换字符串 或者hook CreateFileA函数指针来实现任意位置输出

还看到有些同学直接上沙箱,沙箱或者杀软能直接识别出来hack.exe是一个注入器,甚至一些沙箱可以把里面的shellcode直接dump出来,然后根据PE特征抠出DLL直接分析。当时因为考虑难度没有对DLL加密,也算是被非预期偷鸡了。

Ring0

初赛的Ring0其实非常简单,整体逻辑比Ring3简单太多了,其核心考点就是:shellcode与线程之间的关系。驱动模块加载失败,说明DriverEntry返回非零值,但是检测到分析工具时弹蓝屏0x00000ace这一定不是三环可以实现的,说明内核一定是有东西在工作的。那唯一的解释就是:ace.sys在DriverEntry中注入了一段内核shellcode,并且一定是在上面创建了一条线程,然后通过DriverEntry返回FALSE来自卸载。我们很难在这么大的内核地址空间中找到这段shellcode,但是由于上面跑着一条线程,我们很容易可以从线程的一些特征来定位出这段shellcode。

预期解:

  1. 遍历所有内核线程(线程pid=4),找到线程入口不处于模块内的线程
  2. 将该块内存dump下来,分析线程入口所在函数,发现调用了三次DbgPrintEx,并且参数Level=5,导致没有输出
  3. patch level=0,token2自动输出,只需patch共三个字节

当然上面只是最为简便的预期解,还看到有大佬直接还原了VMP,甚至从中拿到了原始key(token是由原始key哈希得到),确实牛逼

最后说一下通信,很多同学都分析出来了反分析工具是在三环,也就是被注入到其他进程的shellcode里,并且一旦检测到分析工具后会弹框+蓝屏,如果没有加载驱动触发蓝屏,也会在系统上留下特征导致hack.exe不能再继续运行。这里的通信是系统白进程的内存做的。hack.exe在完成注入后会将winlogon.exe的 [PEB+0xAD0] 设置为被注入进程的pid,三环shellcode初始化完成后会将 [PEB+0xACE] 设置为1;hack.exe会循环检测0xAD0处的进程是否结束,如果结束会重新注入;三环shellcode检测到黑工具后会将 [PEB+0xACE] 设置为0xF;驱动shellcode持续检测 [PEB+0xACE],如果为0xF则蓝屏;hack.exe在启动时如果 [PEB+0xACE] 不为0则不会启动,ace.sys在启动时如果 [PEB+0xACE] 为0则认为三环程序还没有启动,则会返回错误码STATUS_UNSUCCESSFUL。通过这样设计,一旦被检测到分析工具,三环shellcode留下标志位,只有重启winlogon进程,也就是注销用户操作,才可以恢复状态。通过一些白进程的unuse内存来实现通信也是外挂常用的手法。

决赛

前三题

决赛主要考察shellcode的对抗,也就是如何通过一些方法从内存中寻找到shellcode。同时,加入了一些CTF部分的内容,让大家都能玩玩。题目同样是加了部分VM,并通过特征反了很多黑工具,但是故意设计让几个关键字符串在运行时解密到data段上,比如 C:/card.txt。参赛者只需要成功加载题目程序,自己写一个驱动去把loader.sys的内存dump下来,就可以搜到这个字符串,然后交叉引用找到访问函数和算法函数,完成前三问。

第四题

第四问开始会上难度,Loader.sys会像初赛一样把内核shellcode加载起来,并且每次加载的shellcode都是不一样的,以模拟外挂的云编译。这一问的主要难点是找不到内核shellcode,因为Loader.sys加载内核shellcode的部分加了VM,除非分析VMP否则没法直接拿到shellcode文件;再加上非常强的反黑工具+反调试+反双机;模拟执行了函数头部,不能直接用库去Hook函数头。预期解主要两个:

  1. 绕过反调试检测挂调试器分析。因为题目直接特征了kdcom,所以传统的Vmware+kdstub+windbg的双机调试方法行不通了,可以使用 Vmware+gdbstub+IDA 的方法来挂调试器然后分析
  2. 分析Loader的行为,或者Hook KeDelayExecutionThread API,可以dump出shellcode,去混淆后分析。shellcode的混淆是自己写的,没有任何现成的工具可以用,这种方法的做题成本非常高。R0g就是这样做的:https://bbs.kanxue.com/thread-281459.htm

当然,如果思路特别清晰也可以通过Hook来分析出此题:题目说找到shellcode在持续访问哪个内存地址,这个过程中几乎不可避免的要调用一些api,hook相关的API并且堆栈上层是来自shellcode的访问即可确认行为。内存地址分为物理地址和虚拟地址,Hook MmCopyMemory等API能够很快的排除物理地址的可能;而虚拟地址则可能是某个内核地址或者某个Ring3进程地址。Hook MmCopyVirtualMemory, MDL相关函数都没有调用就应该思考是读取某个Ring3进程地址的可能。下一步就是Hook KeStackAttachProcess,PsLookupProcessByProcessId,PsGetProcessImageFileName等进程相关的函数,到这一步是可以发现shellcode的调用的。然后用DBVM或者手动扫等方式看一下堆栈,能够很容易发现GameSec的进程名,创建进程后抓到MmCopyVirtualMemory的内存访问,最终完成此题。在审阅WriteUp的时候也确实发现有同学是通过这种方法解得的,非常佩服他们对此类题目的理解程度。

当然还发现一些通过其他姿势绕过的,比如就看到有同学直接Hook KeBugCheckEx拦截参数为0x00000ace的调用,并直接Sleep该线程,然后快乐调试。之前设计赛题的时候,为了防止选手从蓝屏回溯找到关键检测和shellcode,用了PatchGuard的蓝屏方式,即清栈后jmp到KeBugCheckEx上,但确实没考虑到直接Hook+过滤+Sleep的绕过。应该用IPI或者DPC等方式来触发蓝屏的。

第五题

和传统一样,决赛的最后一题是开放题,话题也是耳熟能详的:扫描内核中的shellcode。比较正统的方法其实就两大类,插中断扫栈和特征码扫内存。中断有DPC,NMI,IPI,PMI等;扫内存有扫描内核Pool,扫描系统进程页表,扫描全局内核内存,扫描全局物理内存等。当然方法与方法之间亦有差别,比如扫描全局内核内存在实际中是不现实的,空间太大了;扫描全局物理内存会出现误判,比如其他程序map了shellcode的文件,那特征码也会出现在物理内存中。即,方法很多,但根据实现的手段优劣之分,得分也会有所不同。具体的一些方法实现R0g写的很全了:https://bbs.kanxue.com/thread-281459.htm

这里还要补充一点,APC对于这题是无效的,也是故意留下的一个坑,审阅WriteUp的时候发现好多同学都踩了这个坑,一直研究APC但就是插不进去。实际上shellcode是跑在IRQL=APC_LEVEL上的,或者说shellcode的loop函数本身就是通过APC插入进去的,所以不可以被APC中断。

Share on

Qfrost
WRITTEN BY
Qfrost
CTFer, Anti-Cheater, LLVM Committer