星野安全
返回首页
· 17 分钟

某 Unity 枪战手游的四层防护——腾讯 ACE 反作弊 + 加固 metadata 的内存取证

从一款 Unity(IL2CPP) 海外枪战手游的静态逆向出发,逐层拆解 Google PairIP、腾讯 ACE(GameSafe)、IL2CPP metadata 混淆与 CodeStage ACTk;重点复盘如何用被动内存取证绕过 ACE 反注入、还原被加固抹零的 metadata,并逆出其 XOR-0x66 交错块混淆算法,最终从解密 metadata 直接重建反作弊层的 C# 类结构、检测点枚举与设备指纹采集面。

target · 某 Unity 枪战手游(Unity 2020.3.49f1 / IL2CPP / arm64)

一款 Unity(IL2CPP, Unity 2020.3.49f1) 海外枪战手游,端上叠了四层防护。本文按”先摸清防护栈 → 再攻破最硬的两层”的顺序,重点复盘两件有看点的事:

  1. 绕过腾讯 ACE 的反注入:直接 attach frida 会被 ACE 秒杀,改用纯 root 的被动内存取证拿到解密后的 IL2CPP metadata;
  2. 逆加固对 metadata 的混淆:还原它 XOR-0x66 交错块 + 内存抹零 magic 的手法,把静态 dumper 认不了的 metadata 重建成可解析格式,进而直接重建反作弊层的 C# 类结构。

防护栈总览

层组件载体职责
L0 应用壳Google PairIP(license-check)com.pairip.*、res/raw/license、CHECK_LICENSE防重打包/防盗版/来源完整性
L1 反作弊腾讯 ACE / GameSafe v8.1.18libanogs.so+libanort.so、Java 桥 com.gamesafe.ano.AnoSdk、C# AntiCheatACE反调试/模拟器/多开/内存扫描/设备风险数据
L2 反静态IL2CPP metadata 混淆global-metadata.dat(XOR-0x66 交错块 + 内存抹零 magic)阻断静态 Il2CppDumper
L3 内存反作弊CodeStage ACTkC# SpeedHackDetector/WallHackDetector/Obscured*抗内存改值/变速/透视

顺带纠一个易混点:包内 libpglarmor.so/*_pgl.so 是字节 Pangle 广告 SDK 的加密组件(Java_com_bytedance_sdk_component_pglcrypt_PglCryptUtils_bc),不是加固,与反作弊无关。

L1:腾讯 ACE 的身份与命令面

libanogs.so 的字符串直接暴露了它的出身——ACE(Anti-Cheat Expert) 8.1.18,含 TerSafe2 模块,导出 AnoSDK* 全套 C-API;命令字里能看到检测用的 IPC 管道(ilc_*_pipe)、云端后台配置(SetCS6*)、在线策略下发(SetDownloadConfig)、装机扫描(apk_path)、inline-hook 检测(set_inline_hook_error)与签名校验(sig.dat):

ACE identity and command vocabulary in libanogs.so

这些命令字在 native 侧进入 AnoSDKIoctl 分发。用 IDA F5 反编译它,出来的不是干净的分支,而是一坨状态机:一个 32-bit 状态变量、满屏魔数常量、while(2) + goto LABEL_* 的循环,中间还夹着 v17-(v17&~v17)!=v17 这类恒假的不透明谓词。这不是字节码虚拟机,而是 控制流平坦化(CFF) + 混合布尔算术(MBA) —— OLLVM 家族的 fla + bcf + sub 那一类编译期 pass(判据:所有 ldr/uxtw/br 分发的索引全部由 csel 供给、由内存 load 供给 0 次,不存在”取 handler 指针→间接跳”的解释器环):

AnoSDKIoctl decompiled — control-flow flattening + MBA (not a bytecode VM)

把这层平坦化”去平坦化”回去后真相很朴素:AnoSDKIoctl 是个恒等 trampoline——全函数没有一条指令写 w0/x0/w1/x1,魔数状态序列只往废弃栈槽里涂写做诱饵,最后 mov sp,x29; ldp; b sub_1CF108 尾调真正的处理器 sub_1CF108(同族 CFF)。两个 MBA 谓词都能常量折叠证明恒定(x&~x=0 系)。命令分发本身是明文 strcmp 比对运行时解密的字面量(滚动 XOR 字符串保险库),不是 hash、不是跳转表。ACE 真正意义上的”VM”是另一回事:功能层的 mvm_* 扫描脚本解释器(字符串 Invalid scan script at entry %d),用来跑云端下发的检测脚本——那是”内置脚本 VM”,和”用 VM 保护自身代码”要分开看。

Java 侧桥类 com.gamesafe.ano.AnoSdk 负责 loadLibrary("anogs") 与全部 native 调用。它的 ioctl 命令字用 +5 凯撒做了轻混淆——a.a("bzo_mzkjmo_yvov") → get_report_data、a.a("vkk_fzt:") → app_key:、dec_tss_info(tss = TenProtect Security):

AnoSdk — the Java↔native bridge of Tencent ACE

native 侧的引导落在 JNI_OnLoad(F5):取 JavaVM → GetEnv → 两次 FindClass + RegisterNatives(分别注册 3 个和 11 个 native,即上面 AnoSdk 那批),随后启动 ACE:

JNI_OnLoad decompiled — FindClass + RegisterNatives(3) then (11)

检测结果经 AnoInfoPublisher 这条异步管道回吐给游戏:后台线程开一条到 native 的 IPC 管道(ilc_open_pipe/ilc_recv_pipe/ilc_close_pipe),循环 recv 检测报文,type=1 为命中、2 为心跳,派发给注册的 AnoInfoReceiver,最终触发踢人:

AnoInfoPublisher — the async detection/kick channel

反调试与反注入放在 libanort.so。它 import 了一整套原语:ptrace+fork+kill(子进程 ptrace 看护父进程,堵住调试器)、syscall+tp_syscall_imp(直系统调用,绕开 libc/PLT 上的 hook)、prctl(PR_SET_DUMPABLE 反 dump)、sigaction(SIGTRAP 陷阱)、strcasestr 配合 maps 扫描找注入库,并显式检测虚拟空间/多开(com.excean.gspace):

libanort.so anti-debug / anti-injection toolkit

libanort 里 ptrace/fork/syscall 这些 import 也全包在同族 CFF+MBA 里。把它反混淆开,反调试设计浮出水面:一个手写的 svc 系统调用门 tp_syscall_imp(自己摆参数直接 svc #0,绕开 libc/PLT 上的 inline hook,专治 Frida/xHook)+ fork/execv 拉起看门狗子进程 + prctl(PR_SET_DUMPABLE, 1)(注意值是 1,是放行同族进程 ptrace 本进程,不是反 dump)+ sigaction 装 SIGSEGV/SIGSYS(没有 SIGTRAP handler)+ 完整性校验值不符即 kill(SIGKILL) 自杀。真正静态到不了的只剩:字符串保险库的明文、ptrace 的实际 request(数据驱动的间接调用,静态判不了 TRACEME vs ATTACH)、svc 走的具体 syscall 号——这几项要在设备上对 tp_syscall_imp 和字符串解密器挂只读 hook 才能收口。

L2:加固对 metadata 做了什么,以及怎么绕过

游戏是 IL2CPP,真正的业务/反作弊 C# 逻辑在 libil2cpp.so + global-metadata.dat 里。但静态 Il2CppDumper 直接失败——global-metadata.dat 头部 magic 是 0x12724394,不是标准的 0xFAB11BAF。

被动内存取证绕过反注入

直接 attach frida 会被 ACE 检测并秒杀进程(预期的反注入)。换思路:以 root 直接读 /proc/<pid>/mem——这是被动读取,不 ptrace、不注入,ACE 难以感知。

在内存里解密后的 metadata 独占一个 rw-s /dev/zero、大小正好等于磁盘 metadata 页对齐后尺寸(19468288)的 mmap。dump 出来一看:magic 被抹成了 0,但 version=24、各 section offset 完全连续(off_{n+1}=off_n+size_n)——这是完好的 v24.5 metadata,加固只是校验后把 magic 抹零来防 dump:

Decrypted metadata recovered from memory — magic zeroed as anti-dump

逆 XOR-0x66 交错块混淆

把磁盘密文和内存明文逐字节对比,混淆算法就现形了:

  • 差异字节的 enc^dec 99.99% 是 0x66 —— 即 XOR 0x66;
  • 只有约 25% 的字节被 XOR,其余是明文(所以 strings 一直能读到方法名/类名);
  • 差异位置不是稀疏散布,而是连续块:一段 XOR 块、跳过约 0xC000(49152) 字节明文、再一段——交错块混淆;
  • magic 则在内存里单独抹零。

手法轻量,但足以让 Il2CppDumper 的头部识别与结构解析全线崩掉。

重建为可解析布局

这颗 metadata 是个非标 64-int(256B) header(没有 rgctx、也没有 exportedTypeDefinitions),Il2CppDumper 认不了。做法是把它重构成标准 24.2 布局:在 header 末尾补上 exportedTypeDefinitions 对——这样 stringLiteralOffset 从 256 变 264,正好触发 Il2CppDumper 的 24.2 识别;所有 section offset 统一 +8,数据整体后移 8 字节;再把加密的 metadataUsage/customAttribute 计数清零跳过。

之后 metadata 完整解析:21567 类 / 183271 方法 / 102255 字段(结构体尺寸用”段大小整除”验证:TypeDef=88B、MethodDef=32B、FieldDef=12B)。二进制侧 MetadataRegistration 因加固仍加密,但类/方法/字段结构不依赖它——写个 60 行的解析器直接从 metadata 提取即可。

L1(C# 层):从解密 metadata 重建反作弊逻辑

反作弊抽象基类 AntiCheatSdkBase 唯一被实现的子类是 AntiCheatACE(49 方法/21 字段)。它的方法名把 ACE 的检测/采集面铺得清清楚楚——模拟器检测、装机列表采集、安全扫描、自动点击器检测、触摸轨迹录制,以及 MrData(设备风险数据) 的采集/缓存/上报:

AntiCheatACE — runtime-dumped C# wrapper of Tencent ACE

对局内检测由 AntiCheatMgr 负责,其 cheat-reason 枚举就是一份检测点清单:

  • 穿墙 ShootCrossWall/ValidCrossWall、变速 InvalidSpeed/ClientSpeedCheat、飞天 InvalidAirFly/GravityException
  • 内存修改 MemoryCheating、弹药 AmmoCheat/AmmoFreeze/InvalidInfiniteAmmo
  • 伤害/位置/技能 DamageOverDist/InvalidHitPosition/SkillDamageException …命中即 onServerKickOutPlayer 踢人。

一个有意思的细节:枚举里 YD* 与 ACE* 成对出现(YDTokenTimeOut↔ACETokenTimeOut、YDData2IsNull↔ACEData2IsNull),并且存在数据模型类 ModelAntiCheatYD——说明这套反作弊框架在架构上按”某盾 or 腾讯 ACE 双后端”设计,但本包里只有 AntiCheatACE 是被实现、被网络协议(NetClientMrDataACE)接上的实活后端;某盾一侧只剩休眠的模型与枚举码路,端上没有它的任何 SDK。

设备指纹采集面

C# 侧的指纹采集集中在 Sdk.SDKHelper(108 方法):GetAndroidID、GetMacAddress、GetAndroiOaid(OAID);此外 ThinkingAnalytics 取 GetDeviceId、CodeStage 的 DeviceIdHolder/AndroidRoutines.GetPackageInstallerName(校验安装来源)。底层还叠了 MSA OAID(miitmdid) 与 GAID。这些指纹一路喂给 ACE 的 MrData 做设备风险核验,以及埋点上报。

小结

  • 端上真实防护 = *PairIP(壳) + 腾讯 ACE(反作弊,CFF+MBA 自混淆 + 功能层 mvm_ 脚本 VM) + metadata 混淆(反静态) + CodeStage ACTk(内存)**,四层职责不重叠。
  • ACE 的强项是反注入 + 自身代码 CFF+MBA 混淆:frida 直连即被 svc 门 + 看门狗秒杀,自身逻辑用控制流平坦化藏住(不是字节码 VM,可去平坦化);但它挡不住被动的 /proc/mem 读取——解密后的 metadata 在内存里是现成的。
  • 加固对 metadata 的保护是轻量但有效的:XOR-0x66 交错块 + 抹零 magic + 非标 header,三招合起来废掉现成 dumper,却挡不住”对比密文明文还原变换 + 重构标准布局”这条路。
  • 方法学上:先广度铺清防护栈,再对最硬的层做定点深挖;能被动取证就不硬刚反注入,能离线还原就不在设备上和反作弊拉锯。

本文为对外技术稿,已脱敏——目标匿名、隐去业务动机与运行环境细节,只讲可公开考证的防护机制。