玩转 android 虚拟机
作者:FloatingGuy 转载请注明出处:https://floatingguy.github.io/
Title: Calling JNI Functions with Java Object Arguments from the Command Line
Core Skill: 在 native 层创建 Android 虚拟机,并调用 jni 方法。
Caleb Fenton’s Blog中包含很多关于 android 开发的技术,特别是虚拟机相关的技术。介绍的非常详细
这里介绍 Caleb Fenton’s Blog中关于 Android 虚拟机的系列文章,对文章中的技术做汇总和实验。
目前作者给出了 如下2篇文章:
- Creating a Java VM from Android Native Code
介绍如何通过编写 Native 可执行文件,创建Android VM;本质就是调用 libdvm.so 中未开放的代码。 - Calling JNI Functions with Java Object Arguments from the Command Line
重点介绍第二篇 文章, 因为第一篇只是一个技术基础。第二篇包含了一些案例 更有价值一些。
从 navice 层创建 Android VM
在介绍第二篇之前先将第一篇公布的完整代码公布出来。
在命令行调用 JNI 函数(参数可以包括 Java 对象)
当破解或者分析恶意代码时,可能关键的值(字符串)是在 Native层计算出来的,这时候我们有几种办法获取native 函数的返回值。
- hook native函数 [简单、不稳定、不方便]
- 静态分析 native 函数算法,重写方法 [复杂]
- 动态调试 [更不方便]
- 插桩 [不方便、校验完整性的防护]
- 创建一个可执行文件,加载目标 so 调用目标函数。通过命令行传递参数给目标函数。 [简单、有局限(无法创建 JNIEnv参数)]
- ….
下面我们会介绍 本文的技术来解决这个调用 jni 函数的问题。
首先下载我们的实验app, 使用下面的方法编译 apk.
APKs 输出目录:app/build/outputs/apk/
。
实验的设备:Nexus5
系统版本:android 6.0.1
Harness 服务端工具
Harness的灵感来自 shim项目,shim 的功能是加载 library 并调用其 JNI_OnLoad 函数。这样就简化了调试工作,现在只需要让调试器去启动 shim 并通过参数传递要目标 library,然后使用调试器下断点并 绕过 JNI_OnLoad。
首先我们需要使用上一节介绍的技术,在 native 层创建 java vm,并将 JavaVM 实例传递给 JNI_OnLoad 函数。
最终在代码中开启一个 socket, 通过这个 socket 读取参数作为目标函数的参数。使用 python 脚本可以很容易与它进行交互。
python 脚本就是 在PC 上执行的 客户端了。
逆向 Dex
|
|
注意:这里调用 native 函数的指令是invoke-virtual
, 这个指令一般用来调用实例方法,虚方法等。指令的第一个参数就是 class 实例。
注意:native 方法 和 invoke-virtual
没有直接联系。存在 static native 的方法,这时候使用invoke-static
,如下:
下一步 我们要查看目标 jni 函数的签名,两种方法:
- 通过反编译 so
- 但是方法1有可能 so 做了加固,查看不到函数签名;所以要使用 javah 生成头文件123$ d2j-dex2jar.sh app-universal-debug.apkdex2jar app-universal-debug.apk -> ./app-universal-debug-dex2jar.jar$ javah -cp app-universal-debug-dex2jar.jar:$ANDROID_SDK/platforms/android-19/android.jar org.cf.nativeharness.Cryptor
jni 函数前2个参数是固定的,从第3个参数开始是应用的参数。
这个 jobject 参数应该是 org.cf.nativeharness.Cryptor
实例,实际的函数签名:JNIEXPORT jstring JNICALL Java_org_cf_nativeharness_Cryptor_decryptString (JNIEnv *, jobject, jstring);
定义目标函数类型:typedef jstring(*decryptString_t)(JNIEnv *, jobject, jstring);
定义一个 server socket
|
准备:
1. 将目标 apk(或者 Dex、Jar)放在`/data/local/tmp/target-app.apk`,创建 VM 时的参数(`-Djava.class.path=/data/local/tmp/target-app.apk`)
2. 将需要的 native 库放在 `/data/local/tmp` (`-Djava.library.path=/data/local/tmp`)
3. 使用客户端 python 脚本时,需要 adb forward tcp:5001 tcp:5001
harness.c 文件主要负责:
- 加载目标native 动态库
- 调用 native 中的 JNI_OnLoad 函数
- 创建 VM 虚拟机,加载目标 Dex 和 libs
- [可选] 如果不是
static native
方法,需要创建实例对象 jobject, 如果是 static 方法可以给 jobject 传递 NULL. - 开启 TCP socket server (5001端口)
server.c 文件功能:
- 绑定 TCP 服务到5001 端口, 负责从客户端接受 参数传递个解密函数。
使用:
harness
decrypt_string.py
介绍完整个使用流程,第一感觉就是复杂,每次添加目标函数都要重新编译 harness,还不如 直接 frida-hook。确实,这种方法的好处是 适合解密量较大的情况。
如果要是换个 apk 破解,需要修改流程:
- 找到要调用的 jni 函数,参看签名信息(是否需要创建 jobject)
- [可选]在 harness.c 中 创建 class 实例
- server.c 中修改 调用目标函数的代码。
注释不要删。。
测试阶段:
- 编译 apk 。。。
ChangeLog
Time | Change |
---|---|
2017-4-19 | 创建 |
测试 art 模式 |