前言
android的so文件调试,网上写了不少,但是按着网上来,却不一定是正确的,于是自己便总结了一份,给需要的人,同时也方便自己查阅。SO动态调试技巧
- SO加载流程
- 调试前奏: 在调试前需要那些工具
- 调试流程: 调试的相关细节
引言
随着攻防对抗的升级,或者是java没有相应的API函数,需要借助更底层的函数来支撑,也就有了JNI技术,让JAVA可以调用其他语言封装的库函数,在ANDROID上,也有对应的NDK技术,因此在调试之前,我们最好先了解JNI的相关技术,这样寻找问题的时候定位会更加快.比如常见的断点处。
SO加载流程
so文件在加载的时候,粗略的流程,通常会经过 1
.init->->.init array->->JNI_Onload->->java_com_XXX;
这几个步骤,根据断点的位置,可以初步划分为如下几种:
- 应用级:java_com_XXX;
- 外壳级:JNI_Onload,.init,.init_array;
- 系统级:fopen,fget,dvmdexfileopen;
.init和.init array常做为壳的入口,对于so反调试的情况,可以在这两个函数上下断点,而对于应用具体的函数调用就会在相应的
特殊情况下文会细述。函数下断点,而对于加壳函数不管怎么操作,最后都会去读取dex文件或者其他文件,那就需要系统函数,那在系统函数上下断点就显得有必要。
调试准备
调试的时候我们需要的工具有IDA,APKtool,adb,常见的工具可以在这里https://github.com/nanshihui/Android-reverse-tool获得
调试流程
在调试的时候,确保apk的AndroidManifest.xml文件中android:debuggable=”true”字段为true。如果不是的话,可以先解包修改,然后再打包签名。
添加android_server
需要在一个root手机上或者是模拟器上,添加一个android_server文件,这个文件来源于ida的dbgsrv文件夹。1
2
3
4
5adb push d:\android_server(IDA的dbgsrv目录下) /data/local/tmp/android_server(这个目录其实可以随便放,有的反调试会检测这)
adb shell
su(一定要有root权限)
cd /data/local/tmp
chmod 777 android_server(执行权限要给)
启动端口转发
1 | ./android_server |
重新打开一个窗口1
2adb forward tcp:23946 tcp:23946
adb shell am start -D -n 包名/类名;#包名和类名,可以在AndroidManifest.xml里找到
打开IDA,加载需要的so文件,并设置好断点,点击debugger,选择attch to process选择对应的进程。在debug option勾选suspend on process entry point,suspend on thread start/exit,suspend on library load/unload.
启动JDWP端口转发
1 | adb forword tcp:8700 jdwp:进程号 #可通过ps|grep 进程名 查找 |
IDA调试
通过以上步骤,在IDA界面按F9执行,程序将会在相应的断点停下,然后可以按f8进行单步调试。
如果之前没有断点,可以计算基地址和so代码的相对地址,通过跳转并添加断点(按F2),基地址可以通过
基地址为:ctrl+s显示对应的so模块地址,相对地址,可以通过静态加载so取得。
特殊情况
模拟机无法使用android_server
在使用模拟器的时候,由于是x86架构,无法启动android_server,这时候可以使用gbdserver一样可以调试
so文件反调试
如果下断点,无法越过so文件的反调试,可以通过自己写代码加载so对应的函数绕过。调用JNI_Onload代码如下所示。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
#include <jni.h>
int main()
{
JavaVM* vm;
JNIEnv* env;
jint res;
JavaVMInitArgs vm_args;
JavaVMOption options[1];
options[0].optionString = "-Djava.class.path=.";
vm_args.version=0x00010002;
vm_args.options=options;
vm_args.nOptions =1;
vm_args.ignoreUnrecognized=JNI_TRUE;
printf("[+] dlopen libdvm.so\n");
void *handle = dlopen("/system/lib/libdvm.so", RTLD_LAZY);//RTLD_LAZY RTLD_NOW
if(!handle){
printf("[-] dlopen libdvm.so failed!!\n");
return 0;
}
//这里我先创建一个java虚拟机。因为JNI_ONload函数参数第一个参数为JavaVM。
typedef int (*JNI_CreateJavaVM_Type)(JavaVM**, JNIEnv**, void*);
JNI_CreateJavaVM_Type JNI_CreateJavaVM_Func = (JNI_CreateJavaVM_Type)dlsym(handle, "JNI_CreateJavaVM");
if(!JNI_CreateJavaVM_Func){
printf("[-] dlsym failed\n");
return 0;
}
res=JNI_CreateJavaVM_Func(&vm,&env,&vm_args)
void* si=dlopen("/data/local/tmp/libbaiduprotect.so",RTLD_LAZY);
if(si == NULL){
printf("[-] dlopen err!\n");
return 0;
}
typedef jint (*FUN)(JavaVM* vm,void* res);
FUN func_onload=(FUN)dlsym(si,"JNI_OnLoad");
if(func_onload==NULL)//我将断点下在了这里可以正好获取到JNI_Onload的函数地址。
return 0;
func_onload(vm,NULL);
return 0;
}
通过这样的方法可以绕过。当然反调试的情况不止这一种,将会在下篇中继续添加。