2025腾讯游戏安全竞赛决赛题解(安卓方向)
外挂功能分析与检测方案
一、外挂功能分析
1. 范围伤害
样本通过修改游戏中子弹的碰撞体积参数,实现了范围伤害的效果。正常情况下,子弹只能在直接命中目标时造成伤害,而修改后的子弹拥有更大的碰撞检测范围,使玩家即使瞄准不准确也能造成伤害。
该功能通过一个zygisk模块实现,ACEInject为一个标准的zygisk插件包。在customize.sh中将库文件libGame.so解压到游戏目录下。同时在zygisk Module模块的二进制文件中实现加载逻辑。
逻辑比较简单,在preAppSpecialize回调判断进程niceName是否为游戏包名并保存到全局变量。在postAppSpecialize回调根据前面的判断结果使用dlopen加载libGame。
libGame.so在init_array中注册了构造函数,使其在dlopen后直接获得执行时机。在其构造函数中创建了新线程(pthread_create)来执行修改逻辑。
通过findModuleBase函数定位"libUE4.so"模块基地址,这通常是游戏使用的Unreal Engine 4引擎库,在确定模块基址后,patch偏移量ModuleBase + 0x6711AC4位置的指令,通过mprotect修改目标指令地址的页权限,使用memcpy_chk将patch数据写入。
修改指令:
原始指令:MOV W8, #0x40A00000(浮点数5.0f的十六进制表示)
修改后:MOV W8, #0x42C80000(浮点数100.0f的十六进制表示)
该指令用于为调用函数准备参数,CollisionComp->InitSphereRadius(5.0f);
可知该patch修改了发射物属性初始化时的范围值将其调大以实现范围伤害
InitSphereRadius:设置球体半径而不触发渲染或物理更新
该库还具备一些检测能力:在完成patchCode之后,这个线程不会立即结束会进行一个无限循环的检测
检测内容包括:
l 判断父进程id是否为1(调试器导致的僵尸进程?)
l 在/proc/net/tcp中读取连接信息判断是否存在frida端口(0x69a4,0x69a3)
l 判断status文件中TracerPid是否不为0(被附加)
检测到这三项后会通过kill自己的pid以及产生SIGSEGV信号使进程退出
l 遍历procfs中的task comm检测有无frida线程
l 检测maps中是否有frida相关库
l 获取fopen、dlsym和dlopen函数地址并通过指令操作码检测是否被inlinehook
检测到上述内容后通过exit_group 系统调用及exit退出。
2. 方框透视、人物骨骼和人物射线
样本的透视功能通过高级内存操作实现了多种视觉作弊功能,具体包括方框透视、骨骼透视和人物射线,其实现大体如下图。
绘制UI及透视
绘制部分,该进程创建一个名为Louis的NativeWindow并使用imgui绘制内容
该窗口被设置为INPUT_FEATURE_NO_INPUT_CHANNEL 不处理触摸事件,通过后续的触摸劫持路径获取触摸信息并传递给imgui进行响应处理。
获取视图矩阵
使用findModuleBase函数(sub_24220C)查找并获取"libUE4.so"模块的基址后:
计算重要偏移量: ue4_base + 0xAF7C8C8] + 0x20] + 0x280
使用syscall(process_vm_readv)系统调用直接读取游戏进程的内存,获取了偏移ue4_base + 0xAF7C8C8处的数据,这是视图矩阵的地址,将结果保存在变量matrix(qword_2B65B8)中,显然这是用于后续3D到2D坐标转换的关键数据。
获取对象数组
获取对象数组的起始地址:qword_2B65D0 = ue4_base + 0xAF75B08
使用process_vm_readv系统调用读取对象数组地址,然后读取数组大小实现对象迭代器,遍历游戏中的所有对象。通过调用sub_242C5C函数并传递"MyProjectCharacter"参数识别玩家角色。
3. 开火自瞄
这款外挂的开火自瞄采用了虚拟输入设备的方法,通过创建虚拟触摸事件来实现自动瞄准功能。作弊文件中的sub_24C958方法获取了真实触摸设备的句柄,使用EVIOCGRAB(很可能对应ioctl中的某个命令)来"抓取"(grab)真实触摸设备,这种做法会阻止真实触摸事件被系统处理,防止用户输入与外挂输入冲突。助前面分析的透视功能获取敌人位置,然后生成对应的瞄准事件。
二、检测方案设计
1. 系统级注入外挂检测方案
1.1 基本原理
系统级注入外挂因其深度集成到系统层面,通常具有较强的隐蔽性和对抗性,使传统的检测手段难以发挥作用。特别是Zygisk模块,它允许在Zygote进程初始化时注入代码,使得外挂能够在应用程序启动初期就获得控制权。本次样本使用了动态库注入,将包含外挂功能的.so文件注入到游戏进程。
对于这种注入方式,我们可以在各个流程中进行检测,例如:
l Zygisk特征(取决于使用的实现,检出因使用方案而异)如jit_cache, nativebridge,app_process修改
l 注入文件特征:由于文件被注入到应用目录,可以根据其文件特征进行检测,例如文件名,错误的owner,group,selinux context等
l 注入库内存特征:异常动态库(linker, maps读取),异常线程
1.2 JIT缓存分析
如图所示,我检查了系统的JIT缓存,
虽然存在JIT缓存记录,但分析表明这些是正常的系统JIT行为,并没有发现与Zygisk注入明确相关的可疑痕迹。这可能表明该样本在完成注入后自动卸载了Zygisk模块,从而规避了这种常规的检测方法。
1.3 内存映射分析
如图所示,在/proc/self/maps及/proc/self/smaps中发现可疑的动态链接库/data/data/com.ACE2025.Game/libGame.so,这表明外挂程序没有隐藏其在/proc/self/maps中的记录,我们可以通过检测该文件中的可疑路径来识别注入的动态库。
1.4 内存映射检测
实现方式:解析/proc/self/maps文件,识别可疑的内存区域和动态链接库。
代码示例:
有效性:该方法能够检测未隐藏其maps记录的注入库,实现简单且效率高。
1.5 链接器SO列表检测
实现方式:利用Android链接器内部的solist结构,遍历所有已加载的共享库。
代码示例:
有效性:相比maps检测,这种方法更加底层,即使外挂隐藏了maps文件中的信息,也难以规避这种检测。
1.6 动态链接器遍历检测
实现方式:使用dl_iterate_phdr函数遍历所有已加载的程序头表。
有效性:此方法利用标准API,实现简单且适用于各种Android版本。
2. patchCode检测
2.1 基本原理
系统级注入外挂因其深度集成到系统层面,通常具有较强的隐蔽性和对抗性,使传统的检测手段难以发挥作用。本次分析的样本针对基于UE4引擎的游戏,通过注入动态库(libUE4.so)实现功能修改。外挂主要通过以下步骤工作:
定位并加载目标模块(libUE4.so)
通过偏移量(0x6711AC4)定位关键函数
修改内存中的代码实现功能劫持
使用memcpy_chk函数替换原始代码
可以针对如下内容进行检测
l Patch内存特征:由于该库patch了内存,会导致内存maps文件产生额外分段,也可以针对mprotect进行hook,或对内存进行crc,将内存内容与文件进行比对
2.2 内存特征分析
从之前的样本代码可以看出,该外挂有明显的特征:
1 使用findModuleBase函数寻找"libUE4.so"模块
2 关注特定内存偏移量0x6711AC4
3 使用memcpy_chk函数修改内存
4 通过设置dword_4758 = 0终止循环
5 写入特定值0x52A85908到v3变量
2.3 CRC32校验检测
实现方式:计算关键函数的CRC32值,与预期值比对,发现差异则表明函数被修改。
有效性:这种方法能够有效检测代码修改,但如果外挂先修改检测代码本身,可能导致检测失效。
2.3.1 扩展:模块完整性校验
基本原理:对整个libUE4.so模块进行完整性校验,而不仅限于单个函数。
2.4 内存中库text段与本地文件比较
实现方式:解析内存中text段映射范围,并打开文件与文件中的text段内容进行比对判断是否被修改
2.5 vma断开检测
由于patch需要修改页权限,会导致原本的vma分段,可以通过ue4库在内存中范围内的vma数量判断是否被修改过,一个典型的内存段结构应该如下图
可以判断该库在内存中是否存在多个可执行段确定有没有被修改过页权限
3. 内存读取型外挂检测
3.1 基本原理
使用process_vm_readv读取游戏内存,样本cheat可执行文件利用Linux系统提供的高级API来访问其他进程的内存空间,无需注入代码到目标进程中。虽然破坏性相对较小(只读不写),但仍会破坏游戏平衡,因为能提供额外的信息优势,比如透视效果(通过读取敌人位置信息)或自动瞄准的数据基础。
3.2 样本分析
从分析cheat样本可以知道,这是一种基于UE4引擎游戏的内存读取型外挂。内存读取型外挂,特别是那些使用process_vm_readv等系统API的外挂,由于其非侵入式特性,传统的代码完整性校验无法有效识别。本方案将通过设置"陷阱内存"并监控其访问状态,能够有效捕获这类外挂的行为。本方案的核心检测机制基于"陷阱内存"(Memory Trap)技术,具体工作原理为:
在游戏主循环中临时修改关键游戏对象的内存结构
将角色的根组件指针替换为指向特制的陷阱内存区域
通过mincore系统调用检测该内存页是否被访问
如被访问(进入present状态),意味着存在外部进程尝试读取该内存区域
3.3 钩子实现
检测方案通过hook游戏引擎的核心循环函数FEngineLoop::Tick来实现:
3.4 陷阱实现
有效性:对于没有缺页判断的外挂检测成功率相当高,如果有缺页判断例如使用内核模块使用物理地址读取内存则没有显著效果。
3.5 改进方案
多对象陷阱:除了角色根组件外,可以扩展到更多游戏对象:
3.6 基于CPU缓存的旁道攻击检测
针对使用内核模块直接读取物理内存等高级内存读取技术,可以采用基于CPU缓存的旁道攻击检测方法。此方法利用CPU缓存机制来判断内存是否被访问,具体原理如下:
首先清空CPU缓存中特定内存区域的缓存行
设置计时器并读取该内存区域
如果读取速度异常快(说明已被加载至缓存),则表明在清空缓存后、检测前有其他程序访问了该内存
这种方法的优势在于:
1 可以检测到通过物理内存地址访问的读取行为
2 可以发现未通过常规系统调用访问内存的外挂
3 对使用内核模块进行内存读取的外挂有较好的检测效果
缺点是:
1 受CPU架构和具体硬件性能影响较大,阈值需要针对不同设备调整
2 可能存在误报情况,需要结合其他检测手段综合判断
3 在某些平台上可能缺乏精确控制缓存的指令支持
4. 外挂覆盖游戏绘制的检测
4.1 基本原理
Java层绘制:使用WindowManager添加TYPE_APPLICATION_OVERLAY类型的View
Native层绘制:直接使用SurfaceFlinger的API创建Surface并绘制
SurfaceFlinger是Android系统的核心组件,负责管理和合成所有应用程序的图形缓冲区。通过直接操作SurfaceFlinger创建的覆盖层具有以下特点:
1 绕过常规WindowManager,无需特殊权限即可创建全屏覆盖
2 难以通过Java层API检测
3 性能开销小,适合实时绘制
4.2 基于FLAG_WINDOW_IS_PARTIALLY_OBSCURED的检测(受限)
通过监控输入事件的flags来检测窗口是否被遮挡:
这种方法适用于检测通过Java层WindowManager创建的覆盖层,但对于SurfaceFlinger直接创建的覆盖层无效。
4.3 基于dumpsys surfaceflinger的检测方案(需要root)
通过dumpsys surfaceflinger命令可以发现作弊软件使用SurfaceFlinger创建的覆盖层。这些层通常具有特殊特征:
覆盖整个屏幕的大小
特定的命名模式或标志
在具有DUMP权限时可以检测窗口是否有上文分析中的特征判断是否为外挂绘制,例如触摸flag,窗口名及格式,全屏覆盖的大小,ownerUid以及ownerPid (如果有root权限可以通过该PID直接对进程进行检测imgui特征等)
4.4 基于SurfaceComposerClient__CreateSurface创建同名layer检测(无效)
在测试的时候发现,如果创建一个同名的layer(也就是创建名字一样的surface)的话,会比如说外挂创建的叫Test#0,再创建一个叫Test的名字就是Test#1,但是实测的时候发现,无法获取surface的名称,故放弃!
4.5 /sys/devices/virtual/kgsl/kgsl/proc/***可疑特征(待定)
在外挂创建悬浮窗之后,这里会新增一个名称外挂pid的文件夹,可以用来判断谁创建了窗口调用了gpu什么的,可以配合其它方案打出组合拳!
5. 游戏触摸自瞄外挂检测
通过创建虚拟输入设备(使用uinput)并劫持真实触摸事件来实现自动瞄准功能。检测方案主要通过分析输入设备的特征和行为模式来识别可疑的虚拟触摸设备。
5.1 基本原理
典型的自瞄外挂通过以下步骤工作:
1 劫持真实的触摸设备事件(通过抓取/dev/input下的设备节点)
2 使用uinput创建一个虚拟触摸设备
3 读取游戏内存,获取敌人位置信息
4 计算瞄准角度和移动距离
5 通过虚拟触摸设备发送修改后的触摸事件,实现自动瞄准
这里的检测方案是通过劫持虚幻引擎4的HandleInputCB(虚幻引擎在安卓负责处理触摸的入口函数)获取到触摸设备ID然后调用安卓API获取输入设备信息去检测。
5.2 虚拟设备检测((无效)
过Android的InputDevice API检测虚拟输入设备:
5.3 设备名称异常检测
许多自瞄外挂创建的虚拟设备有特殊的命名模式,样本中创建的触摸设备具有名称全大写的情况(正常情况下应该把名称和设备信息上传到云端去判断触摸设备是否合法,如果是触摸笔会有别的信息可以防止误判):
5.4 设备ID异常检测
代码检测设备ID的变化,这可以发现在游戏过程中切换触摸设备的情况,正常情况下,一次游戏会话中设备ID应该保持稳定,除非用户物理切换了输入设备:
5.5 设备ID序列检测
这种检测基于一个假设:正常的Android设备ID应该是连续分配的,如果设备ID-1不存在,可能表明该设备是异常创建的。
因为外挂每次启动都会尝试用新的名称去分配虚拟触摸设备,导致触摸设备id一直改变!
5.6 输入设备属性详细比对(无效)
在某些设备中获取到的设备属性可能是空白的,即使他是真实的触摸设备,这种异常的行为不应该被检测,误判概率很大。
5.7 针对触摸特征的检测(未实现)
由于机器产生的触摸轨迹,刷新频率,瞄准精度,触摸位置热度等存在一些固有特征,可以编写规则或者使用模型进行检测,工程量比较大就不做了。
三、测试环境
MODEL:23013RK75C
Manufacturer: Xiaomi
Android Version: 14
UNAME: Linux localhost 5.10.177-android12-9-g6e14cdf13edc #1 SMP PREEMPT Thu Jan 18 14:29:34 UTC 2024 aarch64 Toybox4.5