-
-
[原创][求助]某环逆向 TOP3:反作弊与反调试
-
发表于: 2026-9-26 17:47 325
-
前两篇解决了资源和原生库。这篇讲反作弊、反调试,顺带补上 SSL、登录链和资源清单。
所有结论都来自本地静态分析和模拟,真机没上。跑不通、抓不到、或者报告之间有分歧的地方,我不另作标记,直接在正文里说清楚哪块没把握。
速查:要干什么 / 怎么办 / 用什么工具
| 我要干什么 | 该怎么办 | 用什么工具 |
|---|---|---|
| 摸清一个 so 是干嘛的 | 扫导出符号 / 字符串 / 熵 | rz-bin · strings · scripts/scan_so*.py |
| 认出反作弊是谁家的 | 扫厂商串(tencent/netease/…) | strings · scripts/scan_so*.py |
| 看函数内部逻辑 | 反编译(破控制流平坦化) | IDA idat + Hex-Rays |
| 追谁调用谁 / 常量被谁引用 | 交叉引用 | capstone · scripts/xref_*.py |
| 抠出隐藏字符串 | 找取串函数 → 枚举 id | unidbg Module.callFunction |
| 看库运行时读什么 / 调什么 | 补环境跑 init,hook syscall | Qiling(补 GOT + auto-map) |
| 调 native 方法 / 触真分支 | 补 JNI,跑 callJNI_OnLoad |
unidbg(补 MyJni) |
找内联 svc / 反调试探针 |
反汇编数 svc |
capstone · scripts/find_svc.py · themis_svc_list.py |
| 判检测走哪层 | 看它读环境走 Java / libc / svc / 自带引擎 | —(判据见 §五) |
| 堵检测 | 堵漏斗(汇总通道),不堵点 | Frida |
| 拿网络明文 | hook SSL_read / SSL_write |
Frida(frida_ssl_hook.js) |
| 保稳注入 | 三层 hook + patch svc + spawn 改名 |
Frida(frida_stable.js / frida_svc_patch.js) |
| 模拟跑 native | 补环境(GOT / JNI / auto-map) | Qiling / unidbg |
| 解资源 | retoc -a <key> unpack |
retoc |
| 看登录 / 支付链 | 反编译 dex | jadx |
| 看资源里有什么 | 解包后分类(UE5 需 .usmap) |
retoc + FModel |
详细做法、补环境步骤、脚本清单在 §十。
换个角度——对象 / 库 对应哪个工具:
| 什么 | 用什么 |
|---|---|
| 认反作弊是哪家 | strings / scan_so*.py |
libtprt.so(ACE) |
IDA idat + unidbg + Qiling |
libsecsdk.so(易盾 VMP) |
IDA + unidbg + androguard |
libthemis.so(内联 svc) |
capstone + Qiling + Frida |
libturingmfa.so(图灵盾) |
Qiling |
libUnreal.so(引擎/密钥) |
IDA + retoc |
libmxcore.so(IM/SSL) |
Qiling + Frida |
| dex(登录/支付) | jadx |
一、反作弊是多家叠加
包里不止一家,是六家叠在一起:
| 厂商 | 库 | 特征 |
|---|---|---|
| 腾讯 ACE | libtprt.so |
控制流平坦化,检测绕 libc |
| 腾讯图灵盾 | libturingmfa.so |
原生扫描(文件 / 正则 / popen / syscall) |
| 完美 DFGA | libDfga_Catch.so |
反外挂 |
| 网易易盾 | libsecsdk.so |
DEX-VMP |
| 腾讯 | libthemis.so |
8 条内联 svc;JNI 名混淆 |
libclient.so |
Crashpad 崩溃上报 |
上报走腾讯 h.trace.qq.com、完美 clientlog.wanmei.com。
这六家里有两家我心里没底。libturingmfa.so 具体扫什么没验出来——Qiling 一跑它就崩在未映射页,只能从导入表看出它会开文件、popen、跑正则、发网络。libthemis.so 的归属也有分歧:报告 01 写的是"腾讯加密",报告 09/11 又写成"TapTap SDK",这个我暂时没定论。
识别这六家靠 strings 加 scripts/scan_so*.py 扫各 so 的厂商串。
二、腾讯 ACE(libtprt.so)
六家里最难的。
控制流平坦化。 libtprt.so 的核心函数 JNI_OnLoad、initialize、unwind_xx_* 全被 OLLVM 拉平成状态机。直接读汇编读不动,用 IDA 反编译:
idat.exe -A -S"scripts/ida_libtprt.py" libtprt.so
F5 后还原成可读 C。
字符串。 不明文。libtprt.so 里有个分发器:
sub_111740(int a1) → return off_163750[a1 % 100]() // 100 个取串函数
用 unidbg 的 Module.callFunction 枚举 id 0..2000,dump 出 101 条明文。
检测点。 这些路径都从 sub_111740 取出的字符串表里还原:
| 查什么 | 读哪里 |
|---|---|
| 调试器 | /proc/%u/status 的 TracerPid、/proc/self/status |
| 进程名 / 命令行 | /proc/%d/cmdline、/proc/self/cmdline |
| 内存映射 | /proc/self/maps |
| 打开的 fd | /proc/self/fd |
| 模拟器 | /init.vbox86.rc、/dev/socket/genyd(Genymotion) |
| ADB / USB | ADB_ENABLED、android.hardware.usb.action.USB_STATE |
| 注入痕迹 | /data/data/com.tencent.tinput、/data/app/、/data/user/0/ |
| 自身文件完整性 | assets/__tpinfo.tsc / __acinfo.tsc / __tpinfo.tsb / __tp_unity.dat |
| 开关标志 | DetailSecurityCheck、__acignore.acc |
自退。 命令串 initialize / start_service / start_worker / stop / enable_gp7_exit_group。stop 跟 enable_gp7_exit_group 一起走到,进程就 exit_group 退出——unidbg 里看到 libtprt.so 在 0x2603c 跑了条内联 svc #0。把这处 patch 掉就能拦住。
协议。 libtprt.so 的 ioctl(0x136d7c)没被平坦化,能直接读:按整数命令号分发,消息头魔数 0x87920101,字段 0x22080808 / 0x22080809,构建在 0x136f24、解析在 0x137018。
不走 libc。 文件检测不调 libc 的 open/stat,走 libtprt.so 自带的系统调用引擎 tp_syscall_imp(0x146be0),用内联 svc 进内核。所以 libc 上挂 open hook 抓不到它。
这一步用的工具:idat(IDA 脚本模式)反编译破平坦化,unidbg 的 Module.callFunction 枚举取串,Qiling / unidbg hook tp_syscall_imp 看行为。
三、网易易盾(libsecsdk.so):DEX-VMP
易盾不查环境,把 DEX 字节码抽出来自己解释执行,Java 层看到的方法都是桩。
入口。 Java 侧只有一个:
com.netease.nis.sdkwrapper.Utils
static native Object rL(Object[]);
桩方法把参数打包成 Object[] 交给 rL,native 实现是 libsecsdk.so 的 rL(0xaddc),内部再转 worker(0xfcfc)。
解释器。 libsecsdk.so 的解释器在 0x30374,主循环 0x307b8:取一个字节作 opcode,查 0x48b30 的跳转表跳 handler。表是 256 个 int32,用到 212 个 handler(0x30c00–0x34458)。op 0x00 是 nop,说明 opcode 编号就是标准 DEX。
SepData 格式。 解析器是 libsecsdk.so 的 0xc1c8(out, buf, len):
[count:u32] + count × { v1:u32, header:u32(id|flags), len:u32, bytecode }
解析后填进 vector[id],元素是 methodInfo(0x50 字节),+0x40 是字节码指针。VM 上下文单例在 0x64d30,vtable 在 0x60c78,vtable[4] = lookup(id)(0xc80c)。
这份 SepData 抓不到:5 个 dex、so、还有运行时 dump 出来的解密段都搜遍了,没有,它只在运行时内存里拼。想拿到只能真机上动态截。
rL 调用约定。 androguard 查 xref:106 个方法调 rL,全是 MSA OAID SDK(com.bun.miitmdid.*)——设备标识获取链被整个虚拟化了。
rL([ receiver, *args, Integer(methodId), Long(salt) ])
salt 约 1606976968xxx,是易盾 2020-12-03 的构建戳。
ClassLoader 链。 VM 解释前要先拿 DEX,走标准链:
getClassLoader → BaseDexClassLoader → pathList → DexPathList → dexElements[0]
→ Element.dexFile → DexFile.mFileName("/data/app/.../base.apk")
→ String.getBytes("GB2312")
unidbg 里补这条链修了 4 处(MyJni.java):getClassLoader 要返回 BaseDexClassLoader 对象、字段名按签名解析、补 mFileName、补 String.getBytes。修完 rL 能进 VM 入口,但再往下要真实的 DEX 内容,我手上是空 stub,所以停在解析器门口——VM 里到底怎么执行,没走通。
字符串加密。 解密器是 libsecsdk.so 的 ctor 0xa868(init_array[0]):
明文 = ((K − 密文) ^ K) & 0xff // 每串一个 7-bit K
这一步用的工具:androguard 查 rL 的 xref,IDA 逆解释器和 opcode 表,unidbg 补 MyJni 走 ClassLoader 链。
四、其余的
libthemis.so:.text里 8 条内联svc。4 条getuid、2 条close无害,2 条openat(0x33d38/0x356a8)绕 libc 读文件,patch 成movn x0,#0。反调试prctl(PR_SET_DUMPABLE=21)在0x71f0c,走 libc。libturingmfa.so:图灵盾原生,导入open/access/stat/readlink/opendir、popen(跑 shell)、regcomp/regexec(正则扫)、syscall、传感器接口、socket 上报。它具体扫什么没验出来。libDfga_Catch.so:init 和 JNI_OnLoad 里没检测,其实是个崩溃/信号捕获库。libclient.so:ptrace来自 Crashpad,不是反调试。
这一步用的工具:scripts/themis_svc_list.py 数 svc,Qiling 跑 libDfga_Catch.so 的 init / JNI_OnLoad。
五、怎么堵:看路,不看点
一条经验:看它读环境走哪条路,就往那条路堵。
| 它走哪 | 在哪堵 | 本样本 |
|---|---|---|
| Java API | 改返回值 | 图灵盾 / DFGA |
libc / syscall() |
hook libc | libsecsdk.so、libturingmfa.so |
内联 svc |
patch svc |
libthemis.so(2 条 openat) |
| 自带 syscall 引擎 | hook 该引擎 | libtprt.so 的 tp_syscall_imp |
另一条是堵漏斗,不堵点。ACE 的汇总通道:
libtprt 检测 ──(ioctl/gp*ioctl)──> Java AceManager ──> h.trace.qq.com
堵 libtprt.so 的 ioctl,或在 Java 层把 AceApplication.initialize 改返回 0。
六、Frida
分三层写:
- Java 层:改检测方法返回,堵漏斗(
AceApplication.initialize→0,图灵盾 / DFGA / Alipay / Bugly / Debug)。 - libc 层:
/proc/self/{maps,status}洗白(TracerPid→0)、prctl/ptrace→0、dl_iterate_phdr过滤注入模块、syscall()拦截。 - raw svc 层:hook
libtprt.so的tp_syscall_imp,patch 内联svc(libtprt.so的0x2603c、libthemis.so的0x33d38/0x356a8)。
注入用 spawn(-f) + --no-pause,frida-server / gadget 改名。自检看 /proc/self/maps 有没有 frida、TracerPid 是不是 0。
这套方案只在模拟里过过——frida_stable.js 里只有 libthemis.so 那条(prctl)验过会被绕过,Java 层和 libturingmfa.so 都没验,真机也没上。
这一步用的工具:frida-tools(frida -U -f ... -l frida_stable.js -l frida_svc_patch.js),配 node 校验 .js。
七、SSL:没有 pin
5 个 dex、所有带 SSL 的库、libUnreal.so 全流式搜 sha256/<base64> 形式的 pin,一个都没有。dex 里的 sha256/ 是 okhttp 格式串,libmxcore.so 的 verify 回调只打日志。所以抓包不用清 pin,直接 hook SSL_write/SSL_read。
SSL 栈分布:libmxcore.so、libprimekit.so、libtapsdkcore.so、libliteavsdk.so(BoringSSL)、libUnreal.so(UE 自带 OpenSSL)。
这一步用的工具:strings + 全库流式搜(scripts/scan_all_ssl.py / find_realpins.py)。
八、登录 / 支付链
账号和支付全在虎鱼 laohu SDK(com.laohu.sdk.LaohuPlatform),纯 Java,不加载 so:
LaohuPlatform(Facade) → c.a / c.b / c.c
→ loginForGame → ActivityLogin / 三方(QQ/微信/微博/TapTap) → pay
完美 oneSdk(oneAppId=1289)域名加密,只有 dfga_domain.json 明文(clientlog.wanmei.com)。UE 侧只有标准桥 com.epicgames.unreal.NativeCalls(13 个 native,无业务),业务桥是 OneGlobalJNI。虎鱼包扫 kycgm / loadLibrary / RegisterNatives 全是 0,说明国密 libkycgm.so 的 GmCipher 调用方在 UE 层。
这一步用的工具:jadx 反编译 dex(scripts/find_login_classes.py 定位),unzip 出 dex。
九、资源内容
retoc 解包后 31,880 个文件:
- 类型:
.uasset27300 /.ubulk4500 /.umap71 /.ushaderbytecode9。 - 目录(
HT/Content/):Characters 13843、Blueprints 5595、FX 3087、UI 2237、sequence_pack 2195、DataTable 867、WwiseAudio 276、Text 137。
DataTable(数值)、Text(文案)、Blueprints(逻辑)最有用。UE5 资产要配 .usmap 才能读属性名。
这一步用的工具:unzip 取 OBB 里的 .utoc/.ucas,retoc -a <key> unpack 解包。
十、工具使用与补环境
三家工具各管一段。环境和 JNI 都得自己补,现成的跑不起来。
环境。 Python 要 capstone / pyelftools / unicorn / pycryptodome / loguru(见 requirements.txt)。Qiling 得源码装——pip 上那个 qiling 是空壳,用 pip install D:\qiling\qiling-master;rootfs 取 arm64_android,路径不能含中文(异环 会触发 latin-1 报错)。unidbg 用 JDK + Maven。IDA 是 Pro 9.4 的 idat.exe。Frida 配 frida-tools + Node。
IDA(静态破混淆)。 反编译 libtprt.so 的平坦化函数,出可读 C:
idat.exe -A -S"scripts/ida_libtprt.py" libtprt.so
脚本模式跑,不开 GUI。判据:F5 后状态机(mov wState,#imm + cmp/b.ge)还原成可读 C。产物 reports/ida_libtprt.txt。
Qiling(纯原生拟真)。 想把 libtprt.so 的 JNI_OnLoad 跑起来,先补三步:
- GOT 不自动重定位。 Qiling 加载后 PLT 读 GOT 拿到的是文件里的原值(未加基址),一
br就跳进未映射页崩掉。得解析.rela.plt(libmxcore.so有 512 条)给每个导入打桩、回填 GOT、hook 桩做分发(scripts/qiling_harness.py)。 - auto-map 未映射页,否则 JNI_OnLoad 跑一半就崩。
- 补完才能 hook 它对内联的
tp_syscall_imp(0x146be0),看它读了哪些文件。
跑通后能看到它读了 /proc/<pid>/cmdline(查可疑进程/改名)之类。边界:Qiling 是纯原生,JNIEnv 全被 stub,只到"外壳"。
unidbg(真 JNIEnv)。 纯原生跑不出 JNI 语义,得靠 unidbg 的 DalvikVM。但它自带的 JNI 实现不全,要在 MyJni.java 里补 4 处:
getClassLoader返回BaseDexClassLoader对象(不是ClassLoader);getObjectField收到的字段名是完整签名,要解析成裸名再匹配;- 补
DexFile.mFileName; - 补
String.getBytes("GB2312")。
补完,rL 和 ClassLoader→DEX 链才走得通(VM base = 0x12000000);也正是在 unidbg 里看到 libtprt.so 在 0x2603c 自退。
字符串枚举(黑盒转白盒)。 IDA 反编译里找到取串分发器 sub_111740(id) = off_163750[id % 100]();再用 unidbg m.callFunction(base + 0x111740, id) 枚举 id 0..2000,dump 出 101 条明文(检测点 + 命令通道)。
Frida(三层保稳)。
frida -U -f com.hottagames.yh.laohu --no-pause -l frida_stable.js -l frida_svc_patch.js
自检看 /proc/self/maps 有没有 frida、TracerPid 是不是 0。
脚本清单。 ida_libtprt.py(IDA 反编译)、tprt_deflat.py(找平坦化分发器)、tprt_strxref.py / tprt_syscall_xref.py(取串 / svc 引用)、qiling_harness.py / qiling_tprt_sim.py(Qiling 补环境)、unidbg/TprtProbe.java / SecsdkProbe.java / MyJni.java(unidbg 探针)、secsdk_*.py / search_sepdata*.py(易盾 VMP)、themis_svc_list.py / frida_themis_svc.js(themis svc)、frida_stable.js / frida_svc_patch.js(Frida)。
分工与边界。 IDA 破混淆 → Qiling 看纯原生行为(读文件 / 命令)→ unidbg 走真 JNI(协议 / VM)。三家都只到"外壳和监督行为"层,平坦化核心没打开。
十一、这些组件能干嘛(含推测)
libtprt.so(ACE):读/proc/{cmdline,status,maps,fd}、查调试器 / 模拟器 / ADB / 注入痕迹、通过ioctl命令通道收发、必要时让进程自退。检测全走自带tp_syscall_imp,绕 libc。推测:那条ioctl通道能远程下发策略(魔数0x87920101看着像固定协议头)。libsecsdk.so(易盾):把业务方法编译成自研 VM 字节码、运行时解释(DEX-VMP)。推测:它保护的正是 OAID 设备标识获取链(106 个rL调用全是miitmdid)。libthemis.so:读文件(2 处内联openat)、prctl反调试。除此之外很干净。libturingmfa.so(图灵盾):按导入表推断是套完整环境扫描——开文件、popen跑 shell、正则扫、读传感器指纹、socket 上报。具体扫什么没验。libUnreal.so:装载并解资源(Oodle + AES)、跑游戏逻辑。libmxcore.so:IM 消息收发(自带 OpenSSL + curl)。libkycgm.so:国密SM2+SM4-CBC。- 服务端(
h.trace.qq.com/clientlog.wanmei.com):收客户端上报做风控判定。这块完全没碰,纯属常识推测。
带"推测 / 推断"的都不是实证,只是从字符串表、导入表、调用点推出来的能力范围。
附:精确溯源(文件 · 偏移 · 函数)
| 结论 | 文件 | 偏移 / 符号 | 函数 |
|---|---|---|---|
| ACE 取串混淆 | libtprt.so |
— | sub_111740(id) → off_163750[id % 100] |
| ACE raw svc 引擎 | libtprt.so |
0x146be0 | tp_syscall_imp |
| ACE 命令分发 | libtprt.so |
0x136d7c | ioctl;构建 0x136f24 / 解析 0x137018 |
| ACE native 方法 | libtprt.so |
init 0x25ae0 / ioctl 0x136d7c / gp6 0x26058 / gp7 0x25ea4 | initialize/ioctl/gp6ioctl/gp7ioctl |
| ACE 自退 | libtprt.so |
0x2603c | 内联 svc #0(exit_group) |
| 易盾 rL 桥 | libsecsdk.so |
0xaddc | rL;worker 0xfcfc |
| 易盾解释器 | libsecsdk.so |
0x30374 / 主循环 0x307b8 | InterpretInternal |
| 易盾 opcode 表 | libsecsdk.so |
0x48b30(256×int32) | handler 0x30c00–0x34458 |
| 易盾 SepData 解析器 | libsecsdk.so |
0xc1c8(out,buf,len) | 读器 0x12968、写字节码 0xc2fc |
| 易盾 VM 上下文 | libsecsdk.so |
单例 0x64d30 / vtable 0x60c78 / lookup 0xc80c | vtable[4]=lookup(id) |
| 易盾串解密 | libsecsdk.so |
0xa868(init_array[0]) | ((K-c)^K) |
| themis 反调试 | libthemis.so |
0x71f0c / 0xc8a60 | prctl(PR_SET_DUMPABLE=21) |
| themis 内联 openat | libthemis.so |
0x33d38 / 0x356a8 | 内联 svc #0(x8=56) |
| Unreal AES(OpenSSL) | libUnreal.so |
0x267EC98 / 0x267EF30 | AES_encrypt / AES_decrypt |
| Unreal AES(类) | libUnreal.so |
0xABFA760 / 0xABFA9A8 / 0xABFA8E0 | SetKey(16B) / Encrypt / Decrypt(ECB) |
| Unreal Oodle | libUnreal.so |
0xB587284 / 0xB588538 | OodleLZ_Decompress / Compress 包装 |
| Unreal IoStore 密钥 | libUnreal.so |
常量 0xcdbc40 | key-provider 0x9a93d08(注册 0x26a26e8 / 触发 0x9aae9e0) |
| Unreal IoStore 解密 | libUnreal.so |
包装 0x3AC7CA8 / 取key 0x3AC7E14 / AES 0x264DA0C | — |
| mxcore SSL_write 包装 | libmxcore.so |
— | sub_46AA74 → 写 sub_489DBC |
| 国密 GmCipher | libkycgm.so |
0x4a80 / 0x4ca8 / 0x4f6c | sm2Encrypt / sm4CbcEncrypt / sm4CbcDecrypt |
| IM JNI 面 | libmxcore_javasupport.so |
JNI_OnLoad 0x11e58 | 140 个 Java_..._pwim_* |
顺带交代两个我自己踩过的坑。一是 libturingmfa.so 我最初把它标成了"JNI 名混淆",其实那是 libthemis.so 的特征,后来改了。二是早期找 libtprt.so 解密器时,被 disasm_range.py 里写死的 SO=libsecsdk.so 带偏过,报告 77 才更正——那之后我不再用那个脚本的旧结论。
没搞定的还有两处:libsecsdk.so 的 SepData 只在内存里,得真机动态抓;服务端风控(h.trace.qq.com / clientlog.wanmei.com)完全是另一摊,我没碰。
工程文件(GitHub)
脚本、.so、unidbg 探针、依赖清单都在这儿:
so/ 反作弊相关库(libtprt / libsecsdk / libthemis / libturingmfa / libDfga_Catch / libclient / libmxcore)
scripts/ 对抗脚本(.py / .js)
unidbg/ unidbg 探针(.java)
requirements.txt / README.md
本仓库不含 AES 密钥(已在脚本里抹掉)。