剥离加固外壳:一次 Android 分身应用容器壳的逆向实战复盘(Frida 视角)
一、为什么研究这个方向
VirtualApp 类容器壳是 Android 多开、分身、沙盒类应用的底层基座。它本质上是一个"应用内的应用运行时":被启动的 cloned app 不安装到系统,而是运行在一个虚拟的 LoadedApk / ActivityThread 上下文中。市面上大量"分身大师""应用双开"产品都基于 DroidPlugin 或其变种二次开发。
研究它的价值不在"破解某个具体 App",而在于吃透三件事:
- 容器如何劫持系统服务(AMS、PMS、WMS),让被克隆应用误以为自己活在真实系统里;
- 加固壳与容器壳如何叠加,360 加固负责保护 dex,容器壳负责运行逻辑;
- Java → Native → VMP 三层防护下,如何用 Frida 在每一层插桩拿数据。
合规边界:本文所有分析均在自有设备 + 已授权样本范围内进行,用于安全防御研究、壳能力理解与防御加固评估,不涉及对第三方商业应用的未授权破解或盗版分发。
二、目标画像:双层壳结构
这次目标是两个关联样本:
| 维度 | M 应用(分身壳) | F 应用(官方宿主) |
|---|---|---|
| 角色 | 面向用户的"多开"壳 | 提供核心能力的宿主 |
| 容器框架 | DroidPlugin 变种 | 同上 |
| 加固方案 | 360 Jiagu(dex 层) | 360 Jiagu |
| 防护层次 | Java / Native / VMP | Java / Native / VMP |
双层结构的难点在于:即便你脱掉了 360 加固拿到明文 dex,里面的关键字符串、核心校验逻辑往往又被二次加密 + Native 化,所以单点脱壳不够,需要分层推进。
三、RE 路线图:7 个里程碑拆解
我把整个逆向过程拆成 7 个里程碑,目前已完成 5 个,剩余 2 个集中在 Native/VMP 层:
| 里程碑 | 内容 | 状态 |
|---|---|---|
| M1 | 环境搭建(Windows + Frida + Redmi Note 12 Turbo 真机) | ✅ 完成 |
| M2 | 样本粗分类(壳识别、加固判定、入口定位) | ✅ 完成 |
| M3 | 360 加固 dex 脱壳 + 类加载重建 | ✅ 完成 |
| M4 | DroidPlugin 容器启动流程梳理 | ✅ 完成 |
| M5 | 系统服务 Hook 点(AMS/PMS)定位 | ✅ 完成 |
| M6 | Native / VMP 层 so 脱壳与还原 | 🔲 进行中(IDA) |
| M7 | getString2 加密字符串明文批量还原 |
🔲 进行中(Frida) |
下面重点讲 M7 的实战和 M6 的思路——这也是当前卡点。
四、Java 层实战:用 Frida 还原 getString2 明文
很多加固壳会把敏感字符串(URL、密钥、校验值)在编译期加密,运行时通过 getString2(str) 这类函数动态解密。静态看 dex 只能看到密文,运行时 hook 才是正解。
4.1 先定位解密函数
不一定叫 getString2,先用 enumerateClassLoaders + 反射枚举可疑类的所有方法,找到返回值类型为 java.lang.String、参数也是 String 的解密入口:
Java.perform(function () {
// 在目标类的所有已声明方法中筛出解密函数
var target = Java.use("com.example.shell.StringFog");
var methods = target.class.getDeclaredMethods();
methods.forEach(function (m) {
var sig = m.toString();
if (sig.indexOf("getString2") !== -1) {
console.log("[*] 命中解密函数: " + sig);
}
});
});
4.2 Hook 实现,明文即出
定位后直接 implementation 包裹原函数,进参出参一起打印,批量跑出字符串映射表:
Java.perform(function () {
var F = Java.use("com.example.shell.StringFog");
F.getString2.overload('java.lang.String').implementation = function (enc) {
var dec = this.getString2(enc); // 调用原始解密逻辑
console.log("[getString2] 密文=" + enc + " 明文=" + dec);
return dec; // 原样返回,不影响运行
};
});
实战技巧:不要只 hook 一个重载。加固壳常对
int/byte[]也做了重载版本,建议先用overload枚举全部签名再逐一 hook。拿到明文后导成密文 -> 明文键值表,后续静态分析就能直接查表替换,效率翻倍。
这一步做完,M7 的"字符串盲区"基本扫清,配合 M3 脱出的 dex,整个 Java 层逻辑就基本可读了。
五、Native / VMP 层脱壳思路(M6)
Java 层扫清后,真正的硬骨头在 libshella-x.x.x.so 这类 Native 壳里,核心校验、反调试、VMP 解释器都在这里。我的分析路线:
- 动态 dump 优先:用 Frida 在
dlopen之后、JNI_OnLoad完成前,对内存中已解密 / 已修复的 so 段做Memory.dump,很多时候壳的"内存解密"比静态分析省事得多。 - IDA Pro 静态攻坚:把 dump 出的 so 丢进 IDA,先修复
init_array、重定位,再从JNI_OnLoad反推注册函数(RegisterNatives的第三个参数即方法数组)。 - VMP 还原:VMP 段通常是自定义字节码 + 解释器循环。做法是先用 Frida
Interceptor在解释器dispatch处插桩,按 pc 打印 opcode 与操作数,跑出 trace 后再手写还原脚本把伪指令映射回原始语义。 - 反调试对抗:
pthread_create起的反调试线程、inotify监控、ptrace自检,统一用 Frida 在对应 libc 函数入口返回假值或跳过,保证调试器稳定 attach。
经验:Native 层不要一上来就硬刚 IDA。先靠 Frida 动态把"已经解出来的东西"dump 出来,能省掉 70% 的逆向工作量。静态分析留给真正只存在于 so 里的逻辑。
六、成果与下一步
完成 5/7 里程碑后,这套壳的运行机制已经基本透明,我也从中提炼出两条可落地的方向:
- 构建商用 VirtualApp clone:把容器启动、服务 Hook、插件加载框架沉淀为一套可复用的自研底座,规避直接套用开源 DroidPlugin 的已知缺陷;
- 为 Frida 封装 GUI 工具:把上面这类
getString2批量还原、so 内存 dump、反调试一键绕过做成可视化界面,降低重复劳动。
剩余 M6 / M7 收尾后,会进入"自研底座 + GUI 工具"的工程化阶段。
七、写在最后
VirtualApp 类容器壳的逆向,本质是和系统运行时赛跑:壳在运行时解密,你就在运行时插桩;壳把逻辑搬进 Native/VMP,你就分层推进到 Native/VMP。Frida 的价值不是"一键脱壳",而是给你一个任意时刻暂停、观察、记录的显微镜。
同样重要的,是守住边界。逆向能力应该用于防御加固评估、自家产品安全自查与授权研究,而不是越界破解。把显微镜用在正确的地方,它才是有价值的工具。
- 点赞
- 收藏
- 关注作者
评论(0)