剥离加固外壳:一次 Android 分身应用容器壳的逆向实战复盘(Frida 视角)

举报
yd_280141640 发表于 2026/08/22 22:02:23 2026/08/22
【摘要】 本文复盘一次针对 VirtualApp 类"分身 / 多开"容器壳的逆向分析过程。目标是一组分身大师类应用(以下简称 **M 应用**)与其官方宿主(以下简称 **F 应用**),底层采用 DroidPlugin 容器 + 360 加固双层结构。文章完整拆解我划分的 7 个 RE 里程碑,重点演示 Java 层 Frida 还原加密字符串(`getString2`)的实战手法,并给出 Native

一、为什么研究这个方向

VirtualApp 类容器壳是 Android 多开、分身、沙盒类应用的底层基座。它本质上是一个"应用内的应用运行时":被启动的 cloned app 不安装到系统,而是运行在一个虚拟的 LoadedApk / ActivityThread 上下文中。市面上大量"分身大师""应用双开"产品都基于 DroidPlugin 或其变种二次开发。

研究它的价值不在"破解某个具体 App",而在于吃透三件事:

  1. 容器如何劫持系统服务(AMS、PMS、WMS),让被克隆应用误以为自己活在真实系统里;
  2. 加固壳与容器壳如何叠加,360 加固负责保护 dex,容器壳负责运行逻辑;
  3. 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 解释器都在这里。我的分析路线:

  1. 动态 dump 优先:用 Frida 在 dlopen 之后、JNI_OnLoad 完成前,对内存中已解密 / 已修复的 so 段做 Memory.dump,很多时候壳的"内存解密"比静态分析省事得多。
  2. IDA Pro 静态攻坚:把 dump 出的 so 丢进 IDA,先修复 init_array、重定位,再从 JNI_OnLoad 反推注册函数(RegisterNatives 的第三个参数即方法数组)。
  3. VMP 还原:VMP 段通常是自定义字节码 + 解释器循环。做法是先用 Frida Interceptor 在解释器 dispatch 处插桩,按 pc 打印 opcode 与操作数,跑出 trace 后再手写还原脚本把伪指令映射回原始语义。
  4. 反调试对抗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 的价值不是"一键脱壳",而是给你一个任意时刻暂停、观察、记录的显微镜。

同样重要的,是守住边界。逆向能力应该用于防御加固评估、自家产品安全自查与授权研究,而不是越界破解。把显微镜用在正确的地方,它才是有价值的工具。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。