一、前言
今天我们继续来看破解apk的相关知识,在前一篇:Eclipse动态调试smali源码破解apk 我们今天主要来看如何使用IDA来调试Android中的native源码,因为现在一些app,为了安全或者效率问题,会把一些重要的功能放到native层,那么这样一来,我们前篇说到的Eclipse调试smali源码就显得很无力了,因为核心的都在native层,Android中一般native层使用的是so库文件,所以我们这篇就来介绍如何调试so文件的内容,从而让我们破解成功率达到更高的一层。
二、知识准备
我们在介绍如何调试so文件的时候,先来看一下准备知识:
第一、IDA工具的使用
早在之前的使用IDA工具静态分析so文件,通过分析arm指令,来获取破解信息,比如打印的log信息,来破解apk的,在那时候我们就已经介绍了如何使用IDA工具:
这里有多个窗口,也有多个视图,用到最多的就是:
1、Function Window对应的so函数区域:这里我们可以使用ctrl+f进行函数的搜索
2、IDA View对应的so中代码指令视图:这里我们可以查看具体函数对应的arm指令代码
3、Hex View对应的so的十六进制数据视图:我们可以查看arm指令对应的数据等
当然在IDA中我们还需要知道一些常用的快捷键:
1、强大的F5快捷键可以将arm指令转化成可读的C语言,帮助分析
首先选中需要翻译成C语言的函数,然后按下F5:
看到了,立马感觉清爽多了,这些代码看起来应该会好点了。
下面我们还需要做一步,就是还原JNI函数方法名,一般JNI函数方法名首先是一个指针加上一个数字,比如v3+676。然后将这个地址作为一个方法指针进行方法调用,并且第一个参数就是指针自己,比如(v3+676)(v3…)。这实际上就是我们在JNI里经常用到的JNIEnv方法。因为Ida并不会自动的对这些方法进行识别,所以当我们对so文件进行调试的时候经常会见到却搞不清楚这个函数究竟在干什么,因为这个函数实在是太抽象了。解决方法非常简单,只需要对JNIEnv指针做一个类型转换即可。比如说上面提到a1和v4指针:
我们可以选中a1变量,然后按一下y键:
然后将类型声明为:JNIEnv*。
确定之后再来看:
修改之后,是不是瞬间清晰了很多?另外有人( 貌似是看雪论坛上的)还总结了所有JNIEnv方法对应的数字,地址以及方法声明:
2、Shirt+F12快捷键,速度打开so中所有的字符串内容窗口
有时候,字符串是一个非常重要的信息,特别是对于破解的时候,可能就是密码,或者是密码库信息。
3、Ctrl+S快捷键,有两个用途,在正常打开so文件的IDA View视图的时候,可以查看so对应的Segement信息
可以快速得到,一个段的开始位置和结束位置,不过这个位置是相对位置,不是so映射到内存之后的位置,关于so中的段信息,不了解的同学可以参看这篇文章:Android中so文件格式详解 这篇文章介绍的很很清楚了,这里就不在作介绍了。
当在调试页面的时候,ctrl+s可以快速定位到我们想要调试的so文件映射到内存的地址:
因为一般一个程序,肯定会包含多个so文件的,比如系统的so就有好多的,一般都是在/system/lib下面,当然也有我们自己的so,这里我们看到这里的开始位置和结束位置就是这个so文件映射到内存中:
这里我们可以使用cat命令查看一个进程的内存映射信息:cat /proc/[pid]/maps
我们看到映射信息中有多so文件,其实这个不是多个so文件,而是so文件中对应的不同Segement信息被映射到内存中的,一般是代码段,数据段等,因为我们需要调试代码,所以我们只关心代码段,代码段有一个特点就是具有执行权限x,所以我们只需要找到权限中有x的那段数据即可。
4、G快捷键:在IDA调试页面的时候,我们可以使用S键快速跳转到指定的内存位置
这里的跳转地址,是可以算出来的,比如我现在想跳转到A函数,然后下断点,那么我们可以使用上面说到的ctrl+s查找到so文件的内存开始的基地址,然后再用IDA View中查看A函数对应的相对地址,相加就是绝对地址,然后跳转到即可,比如这里的:
Java_cn_wjdiankong_encryptdemo_MainActivity_isEquals 函数的IDA View中的相对地址(也就是so文件的地址):E9C
上面看到so文件映射到内存的基地址:74FE4000
那么跳转地址就是:74FE4000+E9C=74FE4E9C
注意:
一般这里的基地址只要程序没有退出,在运行中,那么他的值就不会变,因为程序的数据已经加载到内存中了,基地址不会变的,除非程序退出,又重新运行把数据加载内存中了,同时相对地址是永远不会变的,只有在修改so文件的时候,文件的大小改变了,可能相对地址会改变,其他情况下不会改变,相对地址就是数据在整个so文件中的位置。
这里我们可以看到函数映射到内存中的绝对地址了。
注意:
有时候我们发现跳转到指定位置之后,看到的全是DCB数据,这时候我们选择函数地址,点击P键就可以看到arm指令源码了:
5、调试快捷键:F8单步调试,F7单步进入调试
上面找到函数地址之后,我们可以下断点了,下断点很简单,点击签名的绿色圈点,变成红色条目即可,然后我们可以点击F9快捷键,或者是点击运行按钮,即可运行程序:
其中还有暂停和结束按钮。我们运行之后,然后在点击so的native函数,触发断点逻辑:
这时候,我们看到进入调试界面,点击F8可以单步调试,看到有一个PC指示器,其实在arm中PC是一个特殊的寄存器,用来存储当前指令的地址,这个下面会介绍到。
好了到这里,我们就大致说了一下关于IDA在调试so文件的时候,需要用到的快捷键:
1>、Shift+F12快速查看so文件中包含的字符串信息
2>、F5快捷键可以将arm指令转化成可读的C代码,这里同时可以使用Y键,修改JNIEnv的函数方法名
3>、Ctrl+S有两个用途,在IDA View页面中可以查看so文件的所有段信息,在调试页面可以查看程序所有so文件映射到内存的基地址
4>、G键可以在调试界面,快速跳转到指定的绝对地址,进行下断点调试,这里如果跳转到目的地址之后,发现是DCB数据的话,可以在使用P键,进行转化即可,关于DCB数据,下面会介绍的。
5>、F7键可以单步进入调试,F8键可以单步调试
第二、常用的ARM指令集知识
我们在上面看到IDA打开so之后,看到的是纯种的汇编指令代码,所以这就要求我们必须会看懂汇编代码,就类似于我们在调试Java层代码的时候一样,必须会smali语法,庆幸的是,这两种语法都不是很复杂,所以我们知道一些大体的语法和指令就可以了,下面我们来看看arm指令中的寻址方式,寄存器,常用指令,看完这三个知识点,我们就会对arm指令有一个大体的了解,对于看arm指令代码也是有一个大体的认知了。
1、arm指令中的寻址方式
1>. 立即数寻址
也叫立即寻址,是一种特殊的寻址方式,操作数本身包含在指令中,只要取出指令也就取到了操作数。这个操作数叫做立即数,对应的寻址方式叫做立即寻址。例如:
MOV R0,#64 ;R0 ← 64
2>. 寄存器寻址
寄存器寻址就是利用寄存器中的数值作为操作数,也称为寄存器直接寻址。例如:
ADD R0,R1, R2 ;R0 ← R1 + R2
3>. 寄存器间接寻址
寄存器间接寻址就是把寄存器中的值作为地址,再通过这个地址去取得操作数,操作数本身存放在存储器中。例如:
LDR R0,[R1] ;R0 ←[R1]
4>. 寄存器偏移寻址
这是ARM指令集特有的寻址方式,它是在寄存器寻址得到操作数后再进行移位操作,得到最终的操作数。例如:
MOV R0,R2,LSL #3 ;R0 ← R2 * 8 ,R2的值左移3位,结果赋给R0。
5>. 寄存器基址变址寻址
寄存器基址变址寻址又称为基址变址寻址,它是在寄存器间接寻址的基础上扩展来的。它将寄存器(该寄存器一般称作基址寄存器)中的值与指令中给出的地址偏移量相加,从而得到一个地址,通过这个地址取得操作数。例如:
LDR R0,[R1,#4] ;R0 ←[R1 + 4],将R1的内容加上4形成操作数的地址,取得的操作数存入寄存器R0中。
6>. 多寄存器寻址
这种寻址方式可以一次完成多个寄存器值的传送。例如:
LDMIA R0,{R1,R2,R3,R4} ;R1←[R0],R2←[R0+4],R3←[R0+8],R4←[R0+12]
7>. 堆栈寻址
堆栈是一种数据结构,按