快捷键

  • 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 方法对应数字

对应数字函数详细
662NewStringUTF
672GetStringUTFLengthjsize(*)(JNIEnv*, jstring)
662NewStringUTFCharsconst char* (*)(JNIEnv*, jstring, jboolean*)
662ReleaseStringUTFCharsvoid (*)(JNIEnv*, jstring, const char*)
662GetArrayLengthjsize (*) (JNIEnv*, jarray)
662NewObjectArrayjobjectArray (*)(JnIEnv*, jsize, jclass, jobject)

segment 表

Ctrl+s 可以快速打开 segemt 表

这是正常情况下, 可以快速查看

  • .init_array 一些全局变量的初始化
  • .data.rel.ro 这个段里面放的是动态注册的函数

常识

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

手动还原函数名

c++filt _Z14stringFromJNI2P7_JNIEnvP7_jclass

ARM基础

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

ARM中的寄存器

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

常用指令

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

寻址

立即数寻址

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

MOV R0,#64   ;R0  ← 64

寄存器寻址

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

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

寄存器间接寻址

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

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

寄存器偏移寻址

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

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

寄存器基址变址寻址

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

多寄存器寻址

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

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)指示当前的操作位置,堆栈指针总是指向栈顶。

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

示例

C 代码如下

#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对应如下

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/

给这个文件执行权限

// 【真机开启调试模式】

// 发送到手机
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文件即可做到。

Last modification:October 21st, 2020 at 09:00 am
如果觉得我的文章对你有用,请随意赞赏