论坛首页 安全工具分享区 阅读主题

[原创]一种基于 ART 内存特征的 LSPosed/Xposed/分身环境 完美检测方案

471 浏览 15 回复
#1 楼主 2026-06-01 21:09:10
在 Android 反作弊(Anti-Cheat)的战场上,检测 Xposed 类框架(LSPosed, EdXposed 等)一直是最核心的对抗环节。传统的检测手段通常依赖于:文件检测:扫描 /data/app 下的异常 APK 或 /proc/self/maps 中的异常 SO。符号检测:尝试 dlopen 或 dlsym 寻找框架特有的导出函数。堆栈回溯:在 Java /JNI层制造异常,检测堆栈中是否有 LSPHooker 或 XposedBridge。然而,随着 Shamiko 等隐藏模块的出现,以及 Magisk/KernelSU 带来的内核级隐藏能力,上述手段正变得越来越无力。攻击者可以 Hook open、read、dlopen 甚至系统调用,给反作弊 SDK 返回一份“完美”的虚假数据。我们是否能跳出 API 调用的维度,直接从虚拟机内存的物理本质上抓出作弊框架?答案是肯定的。本文将分享一种 Tier 0 级别 的检测方案:基于 ART 内存布局特征的 ClassLoader 计数检测。
核心原理:ART 的软肋与死局在 Android 的 ART 虚拟机中,Runtime 结构体持有一个核心组件——ClassLinker。它的职责是管理所有的类加载器(ClassLoader)和类。在 ClassLinker 的 C++ 对象内部,维护了一个链表:class_loaders_。
这是一个 std::list,记录了当前进程中所有 存活 的 ClassLoader。LSPosed 要实现模块注入,必须创建自己的 PathClassLoader 或 DexClassLoader 来加载模块代码。这里存在一个无法解决的死局:为了生存:LSPosed 创建的 ClassLoader 必须被注册到 ClassLinker 的 class_loaders_ 链表中。如果它试图将自己从链表中移除(隐藏),ART 的垃圾回收机制(GC)会认为该 ClassLoader 不可达,进而将其回收。一旦回收,模块代码被卸载,Hook 瞬间失效,甚至导致 App 崩溃。为了隐藏:它必须从链表中消失。结论:LSPosed 不得不 赖在这个链表里。只要它在,我们就能抓到它。为了绕过所有的 Hook(包括 PLT Hook, Inline Hook, Syscall Hook),本方案不调用任何系统 API(如 GetClassLinker),而是直接进行C++ 内存指针运算。通过标准的 JNI 接口获取 JavaVM,进而拿到 Runtime 指针。这是极其稳定的,几乎所有 Android 版本通用。由于不同 Android 版本和厂商 ROM 的 Runtime 结构体布局不同,硬编码偏移量(Offset)是不可靠的。我们采用运行时特征扫描:特征 A:VTable 校验
ClassLinker 是一个 C++ 对象,其首地址一定是虚函数表(VTable)指针。该 VTable 地址必然位于 libart.so 的只读数据段(.rodata)内。特征 B:双向循环链表
class_loaders_ 是 std::list,其底层是双向循环链表。必然满足以下指针关系:特征 C:数量合理性
正常的 App 启动后,至少包含 BootClassLoader 和 PathClassLoader。因此,链表节点数必然 >= 2。结合上述特征,我们在 Runtime 内存范围内进行暴力搜索:一旦锁定链表位置,直接遍历并计数。纯净环境:通常只有 2-3 个 ClassLoader(Boot + App + WebView)。注入环境:LSPosed 会为框架自身、每个模块、以及沙箱环境创建额外的 ClassLoader。在实际测试中,LSPosed 环境下的 ClassLoader 数量通常高达 13-15 个,分身环境实测只会比正常环境+1。判定逻辑:Count > 10 即视为异常。
检测代码 (C++):
必须承认,内存盲扫(Memory Scanning) 即使在 PC 端反作弊中也属于激进(Aggressive)手段,在碎片化极度严重的 Android 生态中更是如此。虽然我在理论层面构建了多重防护,但面对魔改的 ROM 和千奇百怪的设备,我保持极度谨慎的态度,反正我的SDK目前不敢上线使用哈哈哈。为了将 Crash 风险降至最低,我在代码实现上极其克制:系统级护盾 (:这是最核心的安全机制。在对任何指针进行解引用(Dereference)之前,强制调用 mincore 系统检测该内存页是否映射在物理内存中。这从根本上阻断了 99% 因访问野指针或非法地址导致的 SIGSEGV 崩溃。零侵入(Read-Only):全程仅进行“读取”操作,绝不尝试写入或修改任何内存数据,确保不会破坏 ART 虚拟机的内部状态。去符号化:完全移除对 xdl、dlsym 或私有系统库的依赖,规避了 Android 7.0+ 命名空间隔离带来的兼容性崩坏,也减少了因系统库版本差异导致的符号查找失败。尽管有上述防护,但 “全量上线”仍需三思。由于我们采用了暴力枚举(从 Runtime 指针偏移 0 扫到 0x500)的方式,以下风险客观存在:OEM 厂商魔改:部分深度定制的 ROM(如某些游戏手机或车机系统)可能大幅修改了 Runtime 或 ClassLinker 的内存布局,导致特征扫描误判,虽然不会崩,但可能导致检测失效(返回 -1)。并发竞争(Race Condition):在遍历链表时,尽管有 mincore 保护,但理论上存在极低概率的“Time-of-Check to Time-of-Use”风险,即在检测可读和实际读取的微小时间窗内,内存页被系统回收(虽然在主线程或加锁环境下极少发生)。性能抖动:在 App 启动瞬间进行内存扫描,虽然耗时通常在毫秒级,但在某些低端机型上可能会产生极其微弱的 CPU 峰值。


回复或点赞可查看完整内容

---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-289567.htm
‹ 上一页 1 2 下一页 ›

请登录后参与讨论

立即登录 注册账号