0x00
为了避免我们的so文件被动态分析,我们通常在so中加入一些反调试代码,常见的Android native反调试方法有以下几种。
1、直接调用ptrace(PTRACE_TRACEME, 0, 0, 0),参考Android Native反调试。
2、根据上面说的/proc/$pid/status中TracerPid行显示调试程序的pid的原理, 可以写一个方法检查下这个值, 如果!=0就退出程序。参考Android Native反调试,用JNI实现APK的反调试。
3、检查代码执行的间隔时间,参考Android应用方法隐藏及反调试技术浅析的0×03反调试初探。
4、检测手机上的一些硬件信息,判断是否在调试器中,参考Android应用方法隐藏及反调试技术浅析的0×03反调试初探。
0x01
那么我们如何过掉这些反调试呢?
我们以阿里比赛第二题为例,参考安卓动态调试七种武器之孔雀翎 – Ida Pro。
我们讲解两种方式:
1、Ida Patch so
2、Ida动态修改内存数据和寄存器数值
我们首先讲解Ida Patch so,有几处都可以patch。我们从易到难依次讲解。
第一处:
我们在JNI_ONLOAD下断点,如下图:
依次单步执行到BLX R7
我们发现当执行完这步后,我们的ida就退出了,说明反调试代码是从这个入口进入执行的。那么我们只要把这个入口给NOP掉,就可以绕过反调试了。
Patch so就是修改so中的二进制代码,然后再重新签名生成新的apk。Patch so,需要修改的本地so中的代码,而不是内存中的,所以我们需要通过上图内存中指令地址减去so在内存中的基地址来获取这条指令在本地so文件中的偏移。那么so在内存中的基地址怎么获取呢?按Crtl+s。
我们看到libcrackme.so的基地址是AB732000,用BLX R7的地址AB733C58减去AB732000,等于1C58。
然后我们双开ida,在另一个ida中打开libcrackme.so,按G,然后输入1C58,果然我们调到了BLX R7的位置,如下图:
下面就要把这行代码NOP,可以修改为00 00 00 00,也可以修改为00 00 A0 E1。
修改后点击右键,applay change。然后重新签名生成apk,再次运行apk,ida调试时就没有反调试的干扰了。
第二处:
我们按F7进入BLX R7的内部执行,如下图:
是创建了一个线程去执行反调试,这里有个小技巧,如果我们想回到刚才的函数BLX R7,怎么办呢?选择寄存器LR,然后点击右键选择Jump即可,同理选择PC是跳到当前的位置。
这个线程执行的函数体是sub_AB7336A4,如下图:
sub_AB7336A4函数体如下:
这个方法循环执行sub_AB73330C。我们进入sub_AB73330C,怀疑这里就是真正检查是否处于调试状态的地方,但是代码经过了严重的混淆,所以找不到反调试的代码。
那怎么办呢?在0x00中我们谈到了常见的反调试代码,最常见的是第二种方式,第二种方式检查的过程中会调用fopen,所以我们在libc的fopen方法下断点,来是哪个函数调用的fopen,基本上就可以断定这个函数是反调试代码。
首先我们需要找到fopen的位置,按Alt+T,然后输入fopen关键字,如下图:
找到fopen后,代码是这样的:
此时按P,就可以变成代码形式。如下图,在fopen处下断点。
点击F9,继续运行,我们看到程序停在fopen处,此时LR就是刚刚我们谈到的sub_AB73330C,如下图:
所以我们可以确定sub_AB73330C就是进行反调试的代码。我们可以看到这个函数是被sub_AB7336A4调用的。
点击右侧的CODE XREF:sub_AB7336A4就能进入到调用sub_AB73330C的地方。在sub_AB7336A4函数上按F5,就能看到对应的C语言代码。如下:
可见程序是在sub_AB73330C循环检测是否被反调试的。
此时我们可以用和第一处一样的方式,找到本地so中对应的方法,然后Patch so,Nop掉对应的方法,然后重新签名,重新运行。
第三处:
其实第三处和第二处原理是一样的,只不过这里不使用Nop了,sub_AB73330C开始和结束的汇编代码如下:
开始时:
结束时:
所以我们可以把AB733310的代码修改为AB73363C处的代码,不执行任何操作,直接返回。
0x02
Ida动态修改内存数据和寄存器数值
我们看到反调试方法第二点,代码如下:
void be_attached_check(){ try { const int bufsize = 1024; char filename[bufsize]; char line[bufsize]; int pid = getpid(); sprintf(filename, "/proc/%d/status", pid); FILE* fd = fopen(filename, "r"); if (fd != nullptr) { while (fgets(line, bufsize, fd)) { if (strncmp(line, "TracerPid", 9) == 0) { int statue = atoi(&line[10]); LOGD("%s", line); if (statue != 0) { LOGD("be attached !! kill %d", pid); fclose(fd); int ret = kill(pid, SIGKILL); } break; } } fclose(fd); } else { LOGD("open %s fail...", filename); } } catch (...) { }}我们发现该程序会用fopen ()打开/proc/[pid]/status这个文件,随后会用fgets()和strcmp()来比较,于是我们在strcmp()处下个断点,然后让hex view的数据与R0同步。每次点击继续,我们都会看到strstr传入的参数。当传入的参数变为TracerPid:XXXX的时候我们停一下。因为在正常情况下,TracerPid的值应该是0。但是当被调试的时候就会变成调试器的pid。
我们在strcmp下断点:
程序会在此处断下,当我们发现R0地址中的内容为TracerPid:XXXX时,我们停一下,如下图:
R0的里面存的地址是AB731B25,里面的内容为,如下图:
我们可以通过修改内存值的方式来过掉这一次反调试。
把TracerPid改为0,如下图:
然后点击Apply changes。这样就可以过掉这次反调试。我们在前面也看到了,程序是在一个循环中进行反调试检查,所以这样的方试只是过了其中一次反调试。
不推荐使用这种方式,最好使用Patch So的方式。