首页
社区
课程
招聘
[原创]一种基于 ART 内存特征的 LSPosed/Xposed/分身环境 完美检测方案
发表于: 5小时前 250

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

5小时前
250

在 Android 反作弊(Anti-Cheat)的战场上,检测 Xposed 类框架(LSPosed, EdXposed 等)一直是最核心的对抗环节。

传统的检测手段通常依赖于:

文件检测:扫描 /data/app 下的异常 APK 或 /proc/self/maps 中的异常 SO。

符号检测:尝试 dlopen 或 dlsym 寻找框架特有的导出函数。

堆栈回溯在 Java /JNI层制造异常,检测堆栈中是否有 LSPHooker 或 XposedBridge

然而,随着 Shamiko 等隐藏模块的出现,以及 Magisk/KernelSU 带来的内核级隐藏能力,上述手段正变得越来越无力。攻击者可以 Hook openreaddlopen 甚至系统调用,给反作弊 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 虚拟机的内部状态。

去符号化:完全移除对 xdldlsym 或私有系统库的依赖,规避了 Android 7.0+ 命名空间隔离带来的兼容性崩坏,也减少了因系统库版本差异导致的符号查找失败。

尽管有上述防护,但 “全量上线”仍需三思。由于我们采用了暴力枚举(从 Runtime 指针偏移 0 扫到 0x500)的方式,以下风险客观存在:

OEM 厂商魔改:部分深度定制的 ROM(如某些游戏手机或车机系统)可能大幅修改了 Runtime 或 ClassLinker 的内存布局,导致特征扫描误判,虽然不会崩,但可能导致检测失效(返回 -1)。

并发竞争(Race Condition):在遍历链表时,尽管有 mincore 保护,但理论上存在极低概率的“Time-of-Check to Time-of-Use”风险,即在检测可读和实际读取的微小时间窗内,内存页被系统回收(虽然在主线程或加锁环境下极少发生)。

性能抖动:在 App 启动瞬间进行内存扫描,虽然耗时通常在毫秒级,但在某些低端机型上可能会产生极其微弱的 CPU 峰值。


[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

最后于 5小时前 被世界美景编辑 ,原因: 格式乱了 删除重复内容
收藏
免费 34
支持
分享
最新回复 (9)
雪    币: 3360
活跃值: (8228)
能力值: ( LV7,RANK:102 )
在线值:
发帖
回帖
粉丝
2
111
5小时前
0
雪    币: 204
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
111
5小时前
0
雪    币: 1750
活跃值: (2565)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
学习
5小时前
0
雪    币: 396
活跃值: (2973)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
5
感谢分享
2小时前
0
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
666
2小时前
0
雪    币: 12
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
7
看看
2小时前
0
雪    币: 2159
活跃值: (2860)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
打工人何苦为难打工人
1小时前
0
雪    币: 27
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
9
感谢
1小时前
0
雪    币: 205
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
10
学习
51分钟前
0
游客
登录 | 注册 方可回帖
返回