0x00前言
最近在学习安卓安全,看到52破解上面有分析2014年阿里安全挑战赛的第二个crackme的文章。勾起了我的回忆,那是我第一次参加安全比赛,在安卓安全也没有做多深入的学习。第一题比较简单,直接在logcat里面就可以看到输出的信息,只要将数字和文字的关系对应关系搞明白就可以解出来。第二题我就遇到困难了,虽然临时学会了怎么用ida调试so,但只要ida附加到进程上去,程序就退出了,屡试不爽。心里也有往反调试那边想,但是功力不足没有能把反调试干掉,最后止步于这一题。现在又看到基于这道题的调试技巧文章,所以就拿起来练练手,练习练习动态调试。
0x01静态分析
应用程序打开的主界面是
把程序拖到jeb中,可以看到主程序的代码比较简单,就是取得输入的信息,然后调用native的securityCheck方法做比较,根据返回的结果展示不同的内容。没有提供有用的信息。
解压程序,将libcrackme.so拖到ida中,找到Java_com_yaotong_crackme_MainActivity_securityCheck函数直接f5,代码没有做混淆,条理也非常清晰,对比v6和v5的值。
其中v5是用户的输入,看起来比较别扭,参考蒸米《安卓动态调试七种武器之孔雀翎 – Ida Pro》的内容:一个指针加上一个数字,比如v3+676。然后将这个地址作为一个方法指针进行方法调用,并且第一个参数就是指针自己,比如(v3+676)(v3…)。这实际上就是我们在JNI里经常用到的JNIEnv方法。因为Ida并不会自动的对这些方法进行识别,所以当我们对so文件进行调试的时候经常会见到却搞不清楚这个函数究竟在干什么,因为这个函数实在是太抽象了。解决方法非常简单,只需要对JNIEnv指针做一个类型转换即可。比如说上面提到v3指针,我们选中后按一下”y”键,然后将类型声明为”JNIEnv*”。
可以看到off_628C的内容为
看到字符串为wojiushidaan,将该字符串输入,并没有通过校验,可见程序在运行的时候对比较的字符串做了修改,这就要求我们动态去调试这个程序了。
0x02动态调试
到ida安装目录.IDA 6.8dbgsrv下面将android_server拷贝到安卓设备中
adb push android_server /data/local/tmp/
修改文件的权限
adb shell chmod 755 /data/local/tmp/android_server
以9000作为调试端口启动,修改默认端口是因为有些反调试会读取/proc/net/tcp下的信息,默认端口容易躺枪
adb shell /data/local/tmp/android_server -p9000
另开一个窗口,在这个窗口里面做端口转发
adb forward tcp:9000 tcp:9000
修改Debugger-Run-RemoteArmLinux/Android debugger中的配置去调试,点击ok
就可以看到所有运行的程序,基本我们启动的需要调试的应用的pid号都比较大,可以按pid排序方便找到调试的程序。
载入的过程较长,其中库的载入有弹框确认,直接略过,在modules框中找到函数,在函数头下断点
F9运行,程序直接退出,ida输出如下
几次尝试都是这个结果,所以肯定有反调试。
0x03反调试
我们用以下命令以调试模式启动一个应用
adb shell am start -D -n com.yaotong.crackme/.MainActivity
其中主activity可以在logcat里面看到,或者反编译manifest文件也可以看到
程序启动停在等待调试器的状态
修改Debugger-debugger options的配置,让程序在载入lib的时候断下来,这样我们才可以调试JNI_Onload函数,还有某些可能会把反调试放在.init_array,该函数的加载比JNI_Onload还要早。
然后再附加进程,然后f9运行程序,可以看到程序还是停在等待调试器的状态
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
让程序恢复运行,这时程序会在linker停下来,在JNI_Oload函数上下断点,然后f9运行程序,f8逐条运行,运行到这条函数的时候就会退出,f7跟进去调试
是一个创建线程的库函数,如果在反汇编代码中可以看到具体的地址指令是 BLX R7,R7的值就是pthread_create函数的地址,如果是做比赛可以直接nop BLX R7,然后去试试有没有过掉反调试。
点击dword_9BC882B4函数也可以看到函数已经被定位到了pthread_create
而在静态中只有符号信息,没有做相应的映射
所以可以知道unk_9BC836A4就是该线程创建完成之后执行的函数,在该函数下断点,f9执行,断在了该函数
F5之后比原来的汇编代码还别扭
可以看到有一个明显的循环体,其中主要的函数调用是BL unk_9BC8330C,可以直接nop这个函数调用,然后去试试反调试是不是已经过掉了,不过现在不是做比赛,我想继续跟进去看看他是怎么做的。
不过里面的代码确实是难看懂,反正是调试,不如就动态跟着一步一步调一下,发现里面读取了/proc/pid/status的tracepid字段,然后做了字符串的比较,最后执行到了
可以看到R2寄存器指向了libc.so下的kill函数,这就是反调试的最后一步,当发现被调试时杀死进程,来实现自毁的目的,在流程图里面也可以看到这个一个单独的分支,之后就没有代码了。
所以我们把BGE loc_9BC59600这个函数nop掉,让这个分支的代码不执行。libcrackme.so加载的基地址为0x9BC58000,修改原文件的地址为0x9BC595D80-x9BC58000=0x15D8
然后重打包运行,ida附加上去没有退出,说明我们的反反调试成功了,在securityCheck函数上下断点,然后f8单步调试
此时R2指向了保存的密码
输入aiyou,bucuoo,答案正确
0x04其他思路
思路一:
在我们输入密码进行比较的时候,logcat有输出信息
所以我们可以使用这条打印信息来把密码打印出来
这是原来的代码布局
选择的patch方法是直接把这个log函数往下移,因为在0x12A4地址处正好有我们需要的打印的数据地址赋值给了R2寄存器,因此将代码段从0x1284到0x129C的地方都用NOP改写,在0x12AC的地方调用log函数,同时为了不影响R1的值,把0x12A0处的R1改成R3: 9BC7F1A8,同时由之前的动态调试我们也确认了此时R2指向的是密码,所以具体的patch方案如下:
①0x1284-0x129c:NOP (0000A0E1)
②0x12A8:MOV R0,#4 (0400A0E3)
③0x12AC:BL __android_log_print (88FFFFEB)这里的88是由一开始在0x1284的92FFFFEB得出的,两个地址相差40个字节,ARM指令4字节对齐,即最低两位是00,所以地址右移两位,应除以4,对应就为:0x92-10=0x88。
④0x12A40-0x12A84:NOP (0000A0E1)
patch之后的代码布局
重新打包运行,随便输入密码,打印出来的log如下
思路二:
静态时密码的偏移量是0x4450
所以我们attach上进程后不运行,直接在libcrackme.so的基地址加上0x4450得到的地址为
0x9BC30000+0x4450=0x9BC34450
G直接跳转到那个位置,然后就看到了答案,请允许我做一个悲伤的表情,为什么当初没有想到。
0x05小结
除了读取/proc/pid/status下面的tracepid方法外,现在反调试还采用了读取/proc/net/tcp下面的tcp信息,对常见的调试器的调试端口进行检测。还有函数的运行时间,读取进入函数和出来函数的系统时间,然后做差和预定值作比较,来判定是否处于调试状态。
现在的反调试还是在大粒度上面做的,采用nop线程开启和nop整个函数调用都可以过掉整个反调试,是不是可以采用线程之间互相配合的方式,主线程依赖于子线程的运行,如果子线程不运行,那么主线程也退出,细化一点就是把反调试函数和正常功能函数混合到一起,比如采用生产者消费者模型来建立主从线程的关系,这样过掉反函数的难度就更大一些了。
参考:
[Android 原创] 【练习】IDA调试Android native(Crackme)