Frida与DexDump实战:安卓应用动态脱壳原理与银行APP逆向分析

1. 项目概述:为什么选择Frida与DexDump进行脱壳?

在移动安全研究领域,对加固后的安卓应用进行脱壳分析,是逆向工程师的“基本功”,也是理解应用内部逻辑、发现潜在安全问题的关键一步。市面上加固方案众多,像360加固、腾讯乐固、梆梆加固等,它们通过加密、混淆、虚拟机保护等技术,将应用的原始Dex文件隐藏起来,给静态分析带来了巨大障碍。这时候,动态脱壳技术就成了破局的关键。

我选择Frida配合DexDump脚本这套组合拳,原因很直接:它足够灵活、强大且“接地气”。Frida作为一个动态插桩框架,允许我们在目标应用运行时注入JavaScript代码,从而能够Hook到应用加载、解密Dex的关键函数。而DexDump脚本,则是一个专门为Frida设计的“内存猎人”,它的任务就是在Dex文件被解密并加载到内存的瞬间,将其从内存中完整地“捞”出来,保存为标准的.dex文件。这套方法不依赖于特定的加固版本,只要应用最终要在ART/Dalvik虚拟机里运行,就绕不开内存加载这一步,理论上就存在被Dump的可能。相比于一些需要逆向脱壳机或依赖特定系统版本的“黑盒”工具,Frida+DexDump给了我们完全的控制权和透明度,你能清楚地知道每一步在做什么,遇到了什么问题,以及如何去解决。这对于学习加固与脱壳原理,以及后续的深度分析,是无可替代的。

2. 环境搭建与前期准备

工欲善其事,必先利其器。在开始实战之前,一个稳定、完备的环境是成功的一半。这里我以分析一个典型的银行类APP为例,带你走一遍完整的配置流程。银行APP通常采用高强度的商业加固,是检验我们技术的好对象。

2.1 核心工具链部署

我们的工具链主要包含三部分:运行在电脑上的Frida客户端、运行在手机上的Frida服务端,以及我们的核心武器——DexDump脚本。

1. 电脑端Frida环境安装: 在你的分析电脑(Windows/macOS/Linux)上,打开命令行,通过pip安装Frida-tools即可。这是最推荐的方式,能确保版本匹配。

pip install frida-tools

安装完成后,使用 frida --version 验证。同时,建议安装 frida 的Python绑定,方便后续编写自定义脚本:

pip install frida

2. 手机端Frida-server部署: 这是最容易出错的环节。首先,用 adb shell getprop ro.product.cpu.abi 命令查看你手机或模拟器的CPU架构(通常是arm64-v8a)。然后去Frida的GitHub Releases页面,下载对应版本的 frida-server-xx.x.x-android-xxx.xz 文件。 下载后解压,得到 frida-server 可执行文件。通过adb将其推送到手机的 /data/local/tmp/ 目录并赋予执行权限:

adb push frida-server /data/local/tmp/
adb shell
cd /data/local/tmp
chmod 755 frida-server

关键一步:在手机端以root权限启动server。很多教程说用 su -c ,但在部分系统上可能不生效。最稳妥的方式是先进入adb shell,获取root权限,再直接运行:

adb shell
su
cd /data/local/tmp
./frida-server &

注意后面的 & 符号,让服务在后台运行。验证是否成功,可以在电脑端执行 frida-ps -U ,如果能看到手机上的进程列表,说明连接成功。

3. DexDump脚本获取与理解: DexDump脚本有很多变种,原理大同小异。我推荐使用一个功能比较完善的版本,例如来自r0ysue或hanbinglengyue等安全研究员优化过的脚本。你可以从GitHub上搜索“DexDump”找到它们。核心原理是Hook dalvik.system.DexFile libart.so 中加载Dex的相关函数(如 OpenMemory ),在函数执行时,将其内存地址和大小参数记录下来,并将这块内存数据写入文件。

注意: 不同Android版本、不同加固方案,Hook的点可能不同。对于高版本Android(尤其是Android 8.0以上)和新型加固,可能需要Hook art::DexFile::OpenMemory dex2oat 相关的底层函数。准备多个不同策略的脚本是明智之举。

2.2 目标APP与测试设备选择

目标APP: 选择一款你感兴趣的、经过加固的银行APP。可以从各大应用市场下载。 请务必在法律允许和授权范围内进行测试,建议使用自己开发或明确获得授权的应用进行练习。

测试设备: 强烈推荐使用Root过的真实安卓手机或功能完整的模拟器(如夜神、雷电,并开启Root权限)。分析银行类APP,其反调试、环境检测机制通常非常严格。

  • 真机Root: 提供最真实的环境,但某些银行APP会检测Root状态,导致闪退。可能需要使用Magisk Hide等模块进行隐藏。
  • 模拟器: 环境可控,快照功能便于反复测试。但许多加固和银行APP会检测模拟器特征。需要修改模拟器的设备指纹(如build.prop)来绕过检测。

在开始前,先将目标APP安装到测试设备上,但 不要急于启动

3. 脱壳实战:一步步捕获内存中的Dex

环境就绪,目标锁定,现在让我们进入核心的脱壳操作环节。这个过程就像一场耐心的狩猎,我们需要在合适的时机,布下陷阱。

3.1 启动Frida并附着目标进程

首先,我们需要让目标APP运行起来,并用Frida附着它。这里有两种常用方式:

方式一:启动时注入(Spawn) 这种方式在APP启动的早期就进行注入,有机会捕获到最早期的类加载器初始化和Dex加载过程,对于某些在启动时就完成所有解密加载的加固方案非常有效。

frida -U -f com.example.bankapp --no-pause -l dexdump.js
  • -U : 连接到USB设备。
  • -f com.example.bankapp : 指定包名,以Spawn方式启动应用。
  • --no-pause : 启动后立即继续运行,不中断。
  • -l dexdump.js : 加载我们的DexDump脚本。

执行后,你会看到Frida连接成功,并且APP开始启动。此时脚本已经开始工作,如果Hook点正确,控制台会开始输出捕获到的Dex信息。

方式二:附着已运行进程(Attach) 如果APP已经启动,或者你想在应用进入某个特定界面后再开始脱壳,可以使用Attach模式。

# 首先,打开APP,进入主界面
frida-ps -U | grep bank # 找到目标进程的PID
frida -U -p <PID> -l dexdump.js

或者直接用包名附着:

frida -U com.example.bankapp -l dexdump.js

实操心得: 对于银行APP,我通常先尝试Spawn方式。如果APP有强烈的反调试导致崩溃,我会先尝试一些反反调试的Frida脚本(例如禁用ptrace检测、隐藏Frida特征等),或者改用Attach方式,在APP完全启动并完成部分环境检测后再进行附着。

3.2 DexDump脚本核心逻辑与Hook点解析

一个典型的DexDump脚本,其核心在于以下几个Hook点。理解它们,你就能在脚本不工作时自己进行调试。

1. Hook java.lang.ClassLoader 这是最上层的Hook点。目标是 dalvik.system.DexFile libcore.io.DexFile loadDex openDex 方法。当Java层通过 PathClassLoader DexClassLoader 加载Dex时,会调用到这里。

Java.perform(function() {
    var dexFile = Java.use(“dalvik.system.DexFile”);
    dexFile.loadDex.implementation = function(srcPath, outputPath, flags) {
        console.log(“[Java] loadDex called: “ + srcPath);
        var result = this.loadDex(srcPath, outputPath, flags);
        // 可以在这里尝试获取Dex数据,但此时数据可能还未完全解密或不在理想的内存区域
        return result;
    };
});

这个点能告诉我们Dex从哪里加载,但对于加固APP, srcPath 可能是一个临时解密文件,且此时Dex数据可能已被处理。

2. Hook libart.so 中的 OpenMemory (主流方法) 这是更底层、更有效的Hook点。ART虚拟机通过 OpenMemory 函数将内存中的Dex数据映射为 DexFile 对象。我们需要在Native层进行Hook。

var module_libart = Process.findModuleByName(“libart.so”);
var symbols = module_libart.enumerateSymbols();
var openMemoryAddr = null;
for (var i = 0; i < symbols.length; i++) {
    var symbol = symbols[i];
    if (symbol.name.indexOf(“OpenMemory”) !== -1 && symbol.name.indexOf(“DexFile”) !== -1) {
        openMemoryAddr = symbol.address;
        console.log(“Found OpenMemory at: “ + openMemoryAddr);
        break;
    }
}
if (openMemoryAddr) {
    Interceptor.attach(openMemoryAddr, {
        onEnter: function(args) {
            // args[0] 可能是 dex起始地址, args[1] 可能是大小
            this.dexBase = args[0];
            this.dexSize = args[1];
            console.log(`[Native] OpenMemory called. Base: ${this.dexBase}, Size: ${this.dexSize}`);
        },
        onLeave: function(retval) {
            // 有时在onLeave时数据才准备就绪
            if (this.dexBase && this.dexSize > 0) {
                dumpDex(this.dexBase, this.dexSize);
            }
        }
    });
}

dumpDex 函数负责将指定内存区域的数据读出来,并写入文件。它会检查Dex文件的魔数( dex\\n dey\\n )来确认数据的有效性。

3. 应对多ClassLoader与多次加载 一个复杂的APP,尤其是用了插件化或动态加载的,可能会有多个 ClassLoader 实例,Dex也会被多次加载。我们的脚本需要记录已经Dump过的Dex基址,避免重复输出。同时,要注意Dex可能在内存中被优化为OAT或VDEX格式,脚本需要能识别并做相应处理(比如,尝试从OAT中提取Dex)。

3.3 执行脱壳与文件验证

当脚本成功附着并运行后,你需要在APP界面上进行尽可能多的操作:点击登录、切换Tab、浏览功能页面等。目的是触发APP加载所有功能模块对应的Dex文件。

脚本会在控制台输出类似以下信息:

[Native] OpenMemory called. Base: 0x7a3f5c2000, Size: 456780
[Dump] Dex detected! Saved to /data/data/com.example.bankapp/1.dex
[Native] OpenMemory called. Base: 0x7a3f5d0000, Size: 102400
[Dump] Dex detected! Saved to /data/data/com.example.bankapp/2.dex
...

同时,Dex文件会被保存到手机存储的指定目录(通常是APP的数据目录 /data/data/<package_name>/ 下)。

文件验证:

  1. 使用 adb pull 将Dump下来的 .dex 文件拉到电脑上。
  2. 使用 file 命令检查文件类型: file 1.dex 应该显示 Dalvik dex file version 035 或类似信息。
  3. 使用反编译工具进行验证: 使用 jadx-gui GDA bytecode-viewer 直接打开拉取的Dex文件。如果能成功打开,看到大量的Java类(尤其是 com.example.bankapp 包下的类),并且代码不是完全的混乱或加密状态,说明脱壳基本成功。如果打开失败或只有零星几个类,可能只Dump到了壳程序本身的Dex,需要调整Hook点或策略。

4. 深度避坑与对抗指南

实战从来不会一帆风顺,尤其是面对防护严密的银行APP。下面是我在无数次“翻车”中总结出的常见问题与对抗技巧。

4.1 常见问题速查与解决方案

问题现象 可能原因 排查步骤与解决方案
Frida连接被拒绝或超时 1. frida-server 未运行或未以root运行。
2. 手机USB调试未开启。
3. 电脑ADB版本与手机不兼容。
4. 端口冲突。
1. adb shell 进入, ps | grep frida 检查进程,用 su 权限重新启动。
2. 确认开发者选项和USB调试已开。
3. 更新 platform-tools
4. 重启 adb server : adb kill-server && adb start-server
注入后APP立即闪退 1. APP检测到Frida。
2. Hook点错误导致崩溃。
3. 脚本存在语法错误或死循环。
1. 反反调试: 使用 -f spawn模式配合 --no-pause 有时能绕过。使用专门隐藏Frida的脚本(如修改 frida-gum 特征)。
2. 精准Hook: 确认Hook的函数签名完全正确,特别是Android版本差异。使用 Module.enumerateSymbols() 仔细查找。
3. 脚本调试: 先注入一个最简单的 console.log 脚本测试,再逐步增加功能。
控制台有Hook日志,但无Dex输出或输出大小异常 1. Hook时机不对,Dex未解密。
2. 获取的内存地址/大小参数不正确。
3. Dex数据在内存中被分段或混淆。
1. 尝试不同Hook点: loadDex OpenMemory ,再到 dex2oat 的编译过程。
2. 打印并验证参数: onEnter onLeave 都打印地址和大小,对比变化。可能需要在 onLeave 时Dump。
3. 内存扫描: 如果参数不对,可以尝试在函数调用前后,对相关内存区域进行扫描,搜索Dex魔数( 64 65 78 0a )。
Dump出的Dex文件无法反编译 1. Dump的数据不完整或错误。
2. Dex文件被抽取式加固(函数体为空)。
3. 反编译工具不支持该Dex版本。
1. 用010 Editor等二进制工具打开,检查文件头是否完整。
2. 对抗抽取: 需要在函数被执行(即代码被动态解密填充到内存)的瞬间进行Dump。这需要Hook art::ArtMethod::Invoke 等执行引擎函数,技术难度更高。
3. 尝试更新 jadx 到最新版。
只能Dump出壳的Dex,没有业务代码 加固将业务Dex隐藏得很深,可能通过自定义ClassLoader或直接在Native层加载。 1. 追踪ClassLoader: Hook所有 ClassLoader 的子类构造函数和 loadClass 方法,分析其父子关系。
2. Native层监控: 监控 dlopen dlsym 等函数,看是否有动态加载的so库负责解密和加载Dex。
3. 内存暴力搜索: 在APP运行到主界面后,对整个进程内存空间进行扫描,搜索Dex魔数。

4.2 高级对抗技巧

1. 对抗Frida检测: 银行APP常用的检测手段包括:检测 frida-server 的默认端口(27042)、检测 /proc/self/maps /proc/self/task/pid/fd 中的frida特征字符串、检测进程名等。

  • 端口检测: 启动 frida-server 时使用非默认端口: ./frida-server -l 0.0.0.0:8080 ,连接时指定 -H 手机IP:8080
  • 特征隐藏: 使用修改版的 frida-gum 库,或者使用 Frida Interceptor 在内存中实时抹去相关字符串特征。
  • 双进程保护: 有些壳会fork子进程,在子进程中做敏感操作。需要附着到正确的子进程上,或者使用 frida Child gating 功能。

2. 对抗反调试:

  • PTRACE_TRACEME检测: 这是经典手段。可以在 libc ptrace 函数上做Hook,让其直接返回0(表示成功)。
  • TracerPid检测: APP会读取 /proc/self/status 中的 TracerPid 字段。可以通过Hook文件读取相关函数(如 fopen , read ),在内存中返回修改后的内容。
  • 时间差检测: 在关键代码前后计算时间差,如果过长则认为被调试。可以通过Hook gettimeofday clock_gettime 等函数来“加速”时间。

3. 应对新型虚拟机保护(VMP): 部分高级加固会将关键Java方法翻译成自定义的指令集,在私有虚拟机中执行。对于这种保护:

  • 内存Dump可能无效: 因为原始字节码可能根本不存在于内存中。
  • 思路转变: 重点从“获取代码”转向“理解逻辑”和“获取数据”。可以Hook虚拟机解释器的调度函数,记录输入输出;或者直接Hook这些受保护方法前后的Java API(如网络请求、数据库操作、UI更新),进行高层逻辑分析。

5. 脱壳后的分析与修复

成功Dump出Dex文件只是第一步,如何让这些“生肉”变成可读、可分析的代码,还需要后续处理。

5.1 使用反编译工具分析Dex

将Dex文件拖入 jadx-gui 是最快捷的方式。jadx会尝试将Dalvik字节码重构为Java代码。对于银行APP,重点关注:

  • 登录认证逻辑: 搜索 login auth password encrypt 等关键词。
  • 网络通信模块: 查找 OkHttpClient Retrofit HttpURLConnection 相关的类,分析其加密、签名参数构造过程。
  • 加解密算法: 查找 Cipher MessageDigest SecretKeySpec 等类的使用,可能涉及AES、RSA、MD5、SHA等。
  • 核心业务接口: 寻找API接口URL的定义处。

注意: 由于混淆的存在,类名、方法名、字段名可能都是无意义的 a b c 。需要结合代码上下文逻辑、字符串常量(jadx能很好还原)以及动态调试来推测其真实含义。

5.2 处理抽取加固与Dex修复

如果你发现反编译出的方法体是空的,或者大量方法指向同一个“壳”方法,这很可能遇到了 抽取式加固 。Dex文件的结构是完整的,但关键方法的代码体( code_item )被移除了,只在运行时动态填充。

  • 动态Dump时机: 需要在方法第一次被解释执行或JIT编译时进行Dump。这需要更精细的Hook,例如Hook art::interpreter::ArtMethod::Invoke art::jit::JitCompiler::CompileMethod 。有现成的工具如 Youpk (针对ART)就是基于这个原理。
  • Dex修复: 将动态Dump出来的方法体,按照Dex格式,写回到原始的Dex文件对应的 code_item 偏移处。这是一个非常精细的二进制操作,需要深入理解Dex文件格式。有一些开源项目(如 dexfixer )提供了参考,但通常需要根据具体加固方案进行调整。

5.3 整合多Dex与资源文件

一个APP可能包含多个Dex文件(classes.dex, classes2.dex, …)以及资源文件( resources.arsc assets )。完整的分析需要将它们整合。

  • 多Dex合并: jadx-gui支持直接导入整个APK文件或包含多个Dex的目录。你也可以使用 d2j-dex2jar 工具将所有Dex合并成一个jar,但可能会丢失一些信息。
  • 资源分析: 使用 APKTool 解包原始APK,获取资源文件。将脱壳出的Dex与解包出的资源放在一起,用jadx打开,可以建立代码到资源ID(如 R.layout.xxx )的引用关系,使分析更直观。

6. 从实战到原理:理解加固与脱壳的博弈

经过以上步骤,你应该已经能够成功对多数加固APP进行脱壳。但作为一个研究者,我们不能只停留在“能用”的层面,更要理解背后的“为什么”。

加固技术的演进脉络:

  1. 第一代 - 整体加密: 将整个APK或Dex文件加密,运行时在内存中解密。对抗方式就是找到解密函数Hook,或在内存明文时Dump。DexDump主要对付这种。
  2. 第二代 - 抽取加密: 将关键方法的代码体抽走加密,留下空壳。运行时按需解密填充。对抗方式需要Hook执行引擎,在填充后Dump。
  3. 第三代 - 虚拟机保护(VMP): 将Java或Native代码转换为自定义指令集,在自研虚拟机中执行。对抗难度极大,静态分析几乎失效,需要深入分析虚拟机解释器。
  4. 第四代 - 混淆与混淆+: 在代码中插入大量无意义指令、控制流扁平化、不透明谓词等,增加人工分析的难度。对抗主要依靠动态调试和耐心。

脱壳技术的核心思想: 无论加固技术如何变化,其最终目的都是让代码在 某个时刻、某个位置 ,以CPU能够理解的指令(或ART/Dalvik能够理解的字节码)形态出现。脱壳技术的本质,就是 定位这个“时刻”和“位置” 。Frida提供的动态插桩能力,让我们有能力在这个“时刻”切入,从“位置”上把数据拷贝出来。这就是为什么Frida+DexDump的方法具有如此强的生命力和普适性。

未来的挑战: 随着Android版本更新(如Project Mainline模块化)、硬件安全(如TEE可信执行环境)的普及,以及加固方案与系统更深度的结合(如定制ROM),纯用户态的脱壳会越来越难。这要求安全研究者需要具备更底层的系统知识(如Binder通信、内核模块、TrustZone等)。但无论如何,理解内存、理解进程、理解代码执行的生命周期,这些基本原理永远不会过时。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值