研究一下如何调试Android so中的init,init_array,JNI_OnLoad段。
Android逆向-Native调试init,init_array,JNI_OnLoad段
阅读Android源码
可以自己下载下来,也可以在线看
这里推荐这个网址:
我挑了个android11.0.0_r21
的来看。
官方还有: Android Code Search
loadLibrary执行原理(Android11)
首先阅读 android loadLibrary源码分析 - 简书 (jianshu.com) ,了解loadLibrary
的调用机制。不过这篇博客是Android4的,版本有点太低了。
所以我选择再自己去读一下Android11的源码。
loadLibrary
首先是System.java
:
1 |
|
loadLibrary0
然后是Runtime.java
:
1 | // This overload exists for @UnsupportedAppUsage |
1 | private synchronized void loadLibrary0(ClassLoader loader, Class<?> callerClass, String libname) { |
nativeLoad
反正后面调nativeLoad
:
1 | private static String nativeLoad(String filename, ClassLoader loader) { |
下面的代码都是C实现:
PS:吐槽一下,这里我为了找到nativeLoad的C实现找了好久。
Runtime_nativeLoad
Runtime.c - Android Code Search
定位libcore/ojluni/src/main/native/Runtime.c
1 | JNIEXPORT jstring JNICALL |
JVM_NativeLoad
然后在art/openjdkvm/OpenjdkJvm.cc
里面找到:
1 |
|
重点应该在vm->LoadNativeLibrary()
上面。
LoadNativeLibrary⭐
art/runtime/jni/java_vm_ext.cc
:
1 | bool JavaVMExt::LoadNativeLibrary(JNIEnv* env, |
感觉与4相比,区别大了不少啊。
调用JNI_OnLoad
LoadNativeLibrary
后面有这么一段:
1 | bool was_successful = false; |
大抵就是从so里面找到JNI_OnLoad
的符号,然后直接执行了。
分析.init
和.init_array
⭐(最后的结果要看到call_array
)
只不过在此之前,就已经通过OpenNativeLibrary
进行对so的加载操作了。
下面分析.init
和.init_array
的执行流程。
重点似乎在这里:
1 | void* handle = android::OpenNativeLibrary( |
OpenNativeLibrary
定位到art/libnativeloader/native_loader.cpp
:
1 | void* OpenNativeLibrary(JNIEnv* env, int32_t target_sdk_version, const char* path, |
重点:
1 | void* handle = android_dlopen_ext(path, RTLD_NOW, &dlextinfo); |
android_dlopen_ext⭐
这是我们在Frida里面即时打印刚刚加载的so时Hook的函数。但是实际上粒度还不是特别细。下面具体分析一下函数里面干了啥。
在bionic/libdl/libdl.cpp
中:
1 | __attribute__((__weak__)) |
__loader_android_dlopen_ext
linker/dlfcn.cpp - platform/bionic - Git at Google (googlesource.com)
1 | void* __loader_android_dlopen_ext(const char* filename, |
dlopen_ext
linker/dlfcn.cpp - platform/bionic - Git at Google (googlesource.com)
1 | static void* dlopen_ext(const char* filename, int flags, const android_dlextinfo* extinfo, const void* caller_addr) { |
do_dlopen
linker/linker.cpp - platform/bionic.git - Git at Google (googlesource.com)
1 | void* do_dlopen(const char* name, int flags, |
关注si
结构体。此结构体会调用下面的构造函数:
1 | si->call_constructors(); |
call_constructors
linker_soinfo.cpp - Android Code Search
1 |
|
注意到最后面,有调用.init
和.init_array
的代码段:
1 | // DT_INIT should be called before DT_INIT_ARRAY if both are present. |
现在要找到init_func_
和init_array_
的赋值点。
linker_ctor_function_t
首先是这两个变量的类型:
linker_soinfo.h - Android Code Search
1 | linker_ctor_function_t init_func_; |
prelink_image
linker.cpp - Android Code Search
在bool soinfo::prelink_image()
中有:
1 | case DT_INIT: |
call_array
下面重新去看call_function
和call_array
。
linker_soinfo.cpp - Android Code Search
1 | template <typename F> |
底层也是遍历调用call_function
。
call_function
linker_soinfo.cpp - Android Code Search
1 | static void call_function(const char* function_name __unused, |
底层调用了function
函数。这个玩意就是将传入的地址进行reinterpret_cast
变成函数指针后直接底层调用。
总结
在函数LoadNativeLibrary
中,会先调用OpenNativeLibrary
,把so给加载了;然后,在调用完OpenNativeLibrary
之后,再去执行JNI_Onload
。而在这调用OpenNativeLibrary
的过程中,函数里会调用android_dlopen_ext
,这个函数的内部会首先会把.init
和.init_array
段中的函数给遍历执行一遍,通过call_function
函数。
总体来说的执行逻辑应该是这样,用层状图标识:
- LoadNativeLibrary
- OpenNativeLibrary
- android_dlopen_ext
- 执行
.init
段 - 执行
.init_array
段
- 执行
- android_dlopen_ext
- 执行
JNI_Onload
- OpenNativeLibrary
所以就是说,如果我们直接暴力的去跳过android_dlopen_ext
的话,那么.init
段是没法Hook或调试到的。因此需要更细节的去深入这个函数内的地址。
查找二进制中的函数地址
根据前面的分析,我们知道要定位call_function
函数中的function(g_argc, g_argv, g_envp);
地址,和LoadNativeLibrary
函数中int version = (*jni_on_load)(this, nullptr);
地址,以便调试.init
,.init_array
和JNI_OnLoad
。
获得linker.so
连接手机,执行:
1 | adb pull /system/bin/linker |
得到32位与64位的linker。
定位call_function地址
我们先分析64位的。将linker64拖入IDA。
搜索字符串[ Calling c-tor %s @ %p for '%s' ]
,交叉引用。
给我了4个交叉引用结果,说明可能是被内联了。
找到后,再定位BL
或者BLX
这样汇编的地址,这里就是调用funciton的位置。
这是64位的结果:(相对偏移)
- 0x4A0F0:
call_array
里的 - 0x4A36C:
call_constructors
这是32位的:
- 0x2C7AC
- 0x2C9D2
获得libart.so
在Android shell里面执行grep -r "No JNI_OnLoad"
大概查了一下有啥so是与JNI_OnLoad
有关系的。发现应该是libart.so
。
1 | adb pull ./apex/com.android.art/lib64/libart.so (然后手动重命名一下) |
定位LoadNativeLibrary中的调用JNI_OnLoad的地址
查找字符串[Calling JNI_OnLoad in
定位到了之后往后翻,经过一个B
跳转,找到一个BLX
指令;再反编译看一下,长得大概是这样:
1 | v169 = v166(v164, 0); // 定位到的地方 |
这是64位的:
- 0x389178
BLR X24
这是32位的:
- 0x298C60
BLX R8
Frida Hook init/init-array⭐
思路:Hook android_dlopen_ext
函数,然后进行机器码上的patch
这是旧Android版本对dlopen
的一个Hook方法。但是现在的call_function
变成内联函数了,因此只能从外层函数下手。
不同手机里的linker64都不一样,需要具体分析。
1 | function dis(address, number) { |
复盘一下,在
call_constructors
里面有:
1
2
3 // DT_INIT should be called before DT_INIT_ARRAY if both are present.
call_function("DT_INIT", init_func_, get_realpath()); // 调用.init
call_array("DT_INIT_ARRAY", init_array_, init_array_count_, false, get_realpath()); // 调用.init_array现在考虑
call_function
内联的情况。call_array
里面实际上也是用for
循环调用call_function
。因此实际上我们现在需要去Hook两个位置点,一个是call_constructors
,另一个是call_array
。
anyway,反正最后差不多就这个样子:
1 | function hook_linker64() { |
IDA动态调试
测试程序
一个PUBG的外挂,
包名是:
1 | com.mycompany.application |
启动android_server
1 | ./android_server64 -p 1145 |
调试模式下打开APP
用XAppDebug
APP来实现任意APP的调试附加功能。在XAppDebug
中选中想要调试的程序,
1 | adb shell |
然后在开发者选项里面设置一下需要附加的APP即可。
或者
1 | adb shell am start -D -n com.mycompany.application/.MainActivity |
也行
设置IDA选项
1 | Debugger -> Debugger optionss |
选中
- Suspend on thread start/exit
- Suspend on library load/unload
IDA附加至so
IDA中选择Debugger-->Attach to process
,找到包名,并执行。
打开DDMS
打开DDMS后,应该能看到明确显示被调试的这个APP。
注意一定得打开才行。
JDB附加
1 | jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700 |
8700是DDMS用来获取手机APP信息的一个端口。所以DDMS首先得打开,否则JDB附加会失败。
执行了这个命令后,APP就会恢复执行。
在linker.so上下断点
上面的一堆操作执行后,就会停留在linker.so上。
然后开始根据刚刚的地址下断点。
在Modules
界面搜索linker
模块,找到了点进去,找到一大堆函数符号。
我们要的是这两个:
_dl__ZL10call_arrayIPFviPPcS1_EEvPKcPT_mbS5_
_dl__ZN6soinfo17call_constructorsEv
找到了之后就在之前分析的函数调用点处,下个断点即可。
方法二⭐
还有种方法,就是知道模块的基址,然后直接加上刚刚分析的偏移,来快速下断点。(需要在加载库的时候打开断点列表进行重新激活)
比如在Module
界面里面发觉Base基址是0x79A6AC1000
那么加上刚刚静态分析的偏移,就能直接定位了。
在libart.so上下断点
同样的操作,为JNI_OnLoad
下断点。
在Modules
界面里面找到
1 | /apex/com.android.art/lib64/libart.so |
总结一下我下断点的地方:(我揣摩应该库地址不会经常有变动)
- 0x7710009178
- 0x79A6B0B0F0
- 0x79A6B0B36C
多次调试
这几个断点会多次遇到,但不是所有加载的so都是我们想要的那个;因此需要多次F9运行,直到遇到我们需要的那个so库。
问题解决
DDMS在哪里
现在Android Studio IDE界面里面已经找不到了,但是在SDK里面还是能单独打开使用的。
1 | path_to_android_sdk/tools/monitor.bat |
DDMS报错
我一开始执行的时候报了错:
1 | Failed to load library: .... |
多方确认之后,了解到这个玩意只能在Java8及其之前的版本使用。
check了一下我的JAVA_PATH
,发现居然是jdk16,版本太高了。
因此修改成我Java8的路径,这样就成功了。
参考
IDA 动态调试Android8 SO .init .init_array JNI_Onload - 简书 (jianshu.com)
android loadLibrary源码分析 - 简书 (jianshu.com)
安利一个看 Android 源代码的网站 - 知乎 (zhihu.com)
linker/linker.cpp - platform/bionic.git - Git at Google (googlesource.com)
java_vm_ext.cc - Android Code Search
ARM 汇编代码1 | Hexo (cola307.github.io)
Android 动态调试so文件 - 知乎 (zhihu.com)
- 本文作者: Taardis
- 本文链接: https://taardisaa.github.io/2023/06/08/Android逆向-Native调试init,init-array,JNI-Onload段/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!