Adnroid so文件动态调试技巧

前言

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
5
adb 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
2
adb 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
2
adb forword tcp:8700 jdwp:进程号 #可通过ps|grep 进程名  查找
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700

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;
}

  通过这样的方法可以绕过。当然反调试的情况不止这一种,将会在下篇中继续添加。

文章目录
  1. 1. 前言
    1. 1.1. SO动态调试技巧
  2. 2. 引言
  3. 3. SO加载流程
  4. 4. 调试准备
  5. 5. 调试流程
    1. 5.1. 添加android_server
    2. 5.2. 启动端口转发
    3. 5.3. 启动JDWP端口转发
    4. 5.4. IDA调试
  6. 6. 特殊情况
    1. 6.1. 模拟机无法使用android_server
    2. 6.2. so文件反调试
|
Fork me on GitHub