IDA 的调试(上)

感觉好久没发文章了,来一篇。

快捷键

  • F5 汇编转伪c
  • Shift + F12 快速查看so中的字符串
  • Ctrl + F12 可以预览代码流程块
  • Ctrl + f 搜索
  • y 改名 等同右键 Rename lvar
  • Ctrl + s 在IDA View页面中查看so的所有段信息,在调试页面查看程序所有so文件映射到内存的基地址
  • G 快速跳转指定内存位置
  • x 查看交叉函数(调用处)
  • \ 右键 hide casts 美化代码的快捷键
  • 空格 切换视图
  • P DCB数据转 arm

还原JNI函数名方法

。解决方法非常简单,只需要对JNIEnv指针做一个类型转换即可。比如说上面提到a1和v4指针,比如我们把 a1 替换为 JNIEnv*:

其实经常看的应该知道 这里668 就是 NewStringUTF

这里是常见 JNIEnv 方法对应数字

对应数字 函数 详细
662 NewStringUTF
672 GetStringUTFLength jsize(*)(JNIEnv*, jstring)
662 NewStringUTFChars const char* (*)(JNIEnv*, jstring, jboolean*)
662 ReleaseStringUTFChars void (*)(JNIEnv*, jstring, const char*)
662 GetArrayLength jsize (*) (JNIEnv*, jarray)
662 NewObjectArray jobjectArray (*)(JnIEnv*, jsize, jclass, jobject)

常识

  1. 有的时候IDA识别不出来一个函数的参数,我们需要先跳转进去,再退回来!
  2. 00 代表结尾 比如下图真正内容是前面红框中

ARM基础

其实这类型的文章,网上太多了,我这里就只是简单的说几个常见 的 这文章不错

ARM中的寄存器

名称 含义
R0-R3 用于函数参数及返回值的传递
R4-R6, R8, R10-R11 没有特殊规定,就是普通的通用寄存器
R7 栈帧指针(Frame Pointer)
R9 操作系统保留
R12 IP(intra-procedure scratch )
R13 SP(stack pointer),是栈顶指针
R14 LR(link register),存放函数的返回地址
R15 PC(program counter),指向当前指令地址

常用指令

名称 含义
ADD 加指令
SUB 减指令
STR 把寄存器内容存到栈上去
LDR 把栈上内容载入一寄存器中
.W 是一个可选的指令宽度说明符。它不会影响为此指令的行为,它只是确保生成 32 位指令。Infocenter.arm.com的详细信息
BL 执行函数调用,并把使lr指向调用者(caller)的下一条指令,即函数的返回地址
BLX 同上,但是在ARM和thumb指令集间切换
CMP 指令进行比较两个操作数的大小

寻址

立即数寻址

也叫立即寻址,是一种特殊的寻址方式,操作数本身包含在指令中,只要取出指令也就取到了操作数。这个操作数叫做立即数,对应的寻址方式叫做立即寻址。例如:

1
MOV R0,#64   ;R0  ← 64

寄存器寻址

寄存器寻址就是利用寄存器中的数值作为操作数,也称为寄存器直接寻址。

1
ADD R0,R1, R2   ;R0  ← R1 + R2

寄存器间接寻址

寄存器间接寻址就是把寄存器中的值作为地址,再通过这个地址去取得操作数,操作数本身存放在存储器中。

1
LDR R0,[R1] ;R0 ←[R1]

寄存器偏移寻址

这是ARM指令集特有的寻址方式,它是在寄存器寻址得到操作数后再进行移位操作,得到最终的操作数。

1
MOV R0,R2,LSL  #3   ;R0 ← R2 * 8 ,R2的值左移3位,结果赋给R0。

寄存器基址变址寻址

寄存器基址变址寻址又称为基址变址寻址,它是在寄存器间接寻址的基础上扩展来的。它将寄存器(该寄存器一般称作基址寄存器)中的值与指令中给出的地址偏移量相加,从而得到一个地址,通过这个地址取得操作数。

多寄存器寻址

这种寻址方式可以一次完成多个寄存器值的传送。

1
LDMIA  R0,{R1,R2,R3,R4} ;R1←[R0],R2←[R0+4],R3←[R0+8],R4←[R0+12]

堆栈寻址

堆栈是一种数据结构,按先进后出(First In Last Out,FILO)的方式工作,使用堆栈指针(Stack Pointer, SP)指示当前的操作位置,堆栈指针总是指向栈顶。

1
2
STMFD  SP!,{R1-R7, LR} ;将R1-R7, LR压入堆栈。满递减堆栈。
LDMED SP!,{R1-R7, LR} ;将堆栈中的数据取回到R1-R7, LR寄存器。空递减堆栈。

示例

C 代码如下

1
2
3
4
5
6
#include <stdio.h>
int func(int a, int b, int c, int d, int e, int f)
{
int g = a + b + c + d + e + f;
return g;
}

ARM对应如下

1
2
3
4
5
6
7
add r0, r1  将参数a和参数b相加再把结果赋值给r0
ldr.w r12, [sp] 把最的一个参数f从栈上装载到r12寄存器
add r0, r2 把参数c累加到r0
ldr.w r9, [sp, #4] 把参数e从栈上装载到r9寄存器
add r0, r3 累加d累加到r0
add r0, r12 累加参数f到r0
add r0, r9 累加参数e到r0

常见c

  • calloc 申请内存
  • memset 从数组中取指定长度,(如果参数是0 就不用管)
  • *a1 表示读取a1指针指向的内存内容
  • sub_5ED4 函数名起头为 sub 的是因为符号表被隐藏ida无法识别, 后面跟的16进制数值是该函数的内存地址

动态调试

前置需求

  • 使用 arm 架构来调试,以你为IDA7一下不支持其他
  • 真机的情况下,请开启调试模式先

上传安卓服务端

拷贝ida下的 文件到手机目录中 data/local/tmp/

给这个文件执行权限

1
2
3
4
5
6
7
8
9
10
11
12
13
// 【真机开启调试模式】

// 发送到手机
adb push F:\Android\IDA_Pro_v7.0_Portable\dbgsrv\android_server /data/local/tmp

// 在su下给执行权限
chmod 777 android_server

// 执行
./android_server

// 端口转发
adb forward tcp:23946 tcp:23946

打开待调试app

附加进程(最好是打开2个ida 一个静态对比、一个动态)

我这里是 IDA7.0

  1. 打开一个空白 IDA 项目
  2. Debugger -> Attach -> 选择安卓
  3. 上面的端口,就是你转发的端口号

反调试策略

  1. 自行附加,让调试无法附加
    在so中加上这行代码即可:ptrace(PTRACE_TRACEME, 0, 0, 0);
  2. 签名校验, 本地校验和服务器校验双飞!
  3. 用系统api判断应用调试状态属性,属于基础判断
  4. 检查android_server调式端口信息和进程信息, 反IDA的有效方式
  5. 检查自身status中的TracerPid字段,防止被其他进程附加调试

如果要做到更安全点,记得把反调试方案放到native层中,时机最早,一般在JNI_OnUnload函数里面,为了更安全点,native中的函数可以自己手动注册,函数名自己混淆一下也是可以的。现在一些加固平台为了更有效的防护,启动的多进程之间的防护监听,多进程一起参与反调试方案,这种方式对于破解难度就会增大,但是也不是绝对安全的。

处理有反调试的方法

这只是通常请下的处理方法,有很多情况需要自己处理

  1. 查看apk是否为可调式状态,可以使用aapt命令查看他的AndroidManifest.xml文件中的android:debuggeable属性是否为true,如果不是debug状态,那么就需要手动的添加这个属性,然后回编译,在签名打包从新安装
  2. 使用adb shell am start -D -n com.yaotong.crackme/.MainActivity 命令启动程序,出于wait Debug状态
  3. 打开IDA,进行进程附加,进入到调试页面
  4. 使用 jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700 命令attach之前的debug状态,让程序正常运行
  5. 设置Debug Option选项,设置Suspend on library start/exit/Suspend on library load/unload/Suspend on process entry point选项
  6. 点击运行按钮或者F9键,程序运行停止在linker模块中,这时候表示so文件加载进来了,我们通过Ctrl+S和G键跳转到JNI_OnLoad函数出,进行下断点
  7. 然后继续运行,进入JNI_OnLoad断点处,使用F8进行单步调试,F7进行单步跳入调试,找到反调试代码处
  8. 然后使用二进制软件修改反调试代码为nop指令,即00值
  9. 修改之后,在替换原来的so文件,进行回编译,从新签名打包安装即可
  10. 按照上面的无反调试的so代码步骤即可

总结

现在很多应用防止别的进程调试或者注入,通常会用自我检测装置,原理就是循环检测/proc/[mypid]/status文件,查看他的TracerPid字段是否为0,如果不为0,表示被其他进程trace了,那么这时候就直接退出程序。因为现在的IDA调试时需要进程的注入,进程注入现在都是使用Linux中的ptrace机制,那么这里的TracePid就可以记录trace的pid,我们可以发现我们的程序被那个进程注入了,或者是被他在调试。进而采取一些措施。

IDA 调试原理

首先他得在被调试端安放一个程序,用于IDA端和调试设备通信,这个程序就是android_server,因为要附加进程,所以这个程序必须要用root身份运行,这个程序起来之后,就会开启一个端口23946,我们在使用adb forward进行端口转发到远程调试端,这时候IDA就可以和调试端的android_server进行通信了。后面获取设备的进程列表,附加进程,传递调试信息,都可以使用这个通信机制完成即可。IDA可以获取被调试的进程的内存数据,一般是在 /proc/[pid]maps 文件中,所以我们在使用Ctrl+S可以查看所有的so文件的基地址,可以遍历maps文件即可做到。