阿里2014移动安全挑战赛第二题调试笔记
2016-12-16 12:57
246 查看
0x00前言
最近在学习安卓安全,看到52破解上面有分析2014年阿里安全挑战赛的第二个crackme的文章。勾起了我的回忆,那是我第一次参加安全比赛,在安卓安全也没有做多深入的学习。第一题比较简单,直接在logcat里面就可以看到输出的信息,只要将数字和文字的关系对应关系搞明白就可以解出来。第二题我就遇到困难了,虽然临时学会了怎么用ida调试so,但只要ida附加到进程上去,程序就退出了,屡试不爽。心里也有往反调试那边想,但是功力不足没有能把反调试干掉,最后止步于这一题。现在又看到基于这道题的调试技巧文章,所以就拿起来练练手,练习练习动态调试。
0x01静态分析
应用程序打开的主界面是
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124004370-254019362.png)
把程序拖到jeb中,可以看到主程序的代码比较简单,就是取得输入的信息,然后调用native的securityCheck方法做比较,根据返回的结果展示不同的内容。没有提供有用的信息。
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124020558-1160007157.png)
解压程序,将libcrackme.so拖到ida中,找到Java_com_yaotong_crackme_MainActivity_securityCheck函数直接f5,代码没有做混淆,条理也非常清晰,对比v6和v5的值。
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124038792-756646862.png)
其中v5是用户的输入,看起来比较别扭,参考蒸米《安卓动态调试七种武器之孔雀翎 – Ida Pro》的内容:一个指针加上一个数字,比如v3+676。然后将这个地址作为一个方法指针进行方法调用,并且第一个参数就是指针自己,比如(v3+676)(v3…)。这实际上就是我们在JNI里经常用到的JNIEnv方法。因为Ida并不会自动的对这些方法进行识别,所以当我们对so文件进行调试的时候经常会见到却搞不清楚这个函数究竟在干什么,因为这个函数实在是太抽象了。解决方法非常简单,只需要对JNIEnv指针做一个类型转换即可。比如说上面提到v3指针,我们选中后按一下”y”键,然后将类型声明为”JNIEnv*”。
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124056901-1323278320.png)
可以看到off_628C的内容为
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124111354-1670689308.png)
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124118620-1691583579.png)
看到字符串为wojiushidaan,将该字符串输入,并没有通过校验,可见程序在运行的时候对比较的字符串做了修改,这就要求我们动态去调试这个程序了。
0x02动态调试
到ida安装目录.\IDA 6.8\dbgsrv\下面将android_server拷贝到安卓设备中
修改文件的权限
以9000作为调试端口启动,修改默认端口是因为有些反调试会读取/proc/net/tcp下的信息,默认端口容易躺枪
另开一个窗口,在这个窗口里面做端口转发
修改Debugger-Run-RemoteArmLinux/Android debugger中的配置去调试,点击ok
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124456214-518863256.png)
就可以看到所有运行的程序,基本我们启动的需要调试的应用的pid号都比较大,可以按pid排序方便找到调试的程序。
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124511698-1596393057.png)
载入的过程较长,其中库的载入有弹框确认,直接略过,在modules框中找到函数,在函数头下断点
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124525683-2035347429.png)
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124538386-1792754089.png)
F9运行,程序直接退出,ida输出如下
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124552933-2011447418.png)
几次尝试都是这个结果,所以肯定有反调试。
0x03反调试
我们用以下命令以调试模式启动一个应用
其中主activity可以在logcat里面看到,或者反编译manifest文件也可以看到
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124635229-368901405.png)
程序启动停在等待调试器的状态
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124648120-1976635546.png)
修改Debugger-debugger options的配置,让程序在载入lib的时候断下来,这样我们才可以调试JNI_Onload函数,还有某些可能会把反调试放在.init_array,该函数的加载比JNI_Onload还要早。
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124702073-78612211.png)
然后再附加进程,然后f9运行程序,可以看到程序还是停在等待调试器的状态
让程序恢复运行,这时程序会在linker停下来,在JNI_Oload函数上下断点,然后f9运行程序,f8逐条运行,运行到这条函数的时候就会退出,f7跟进去调试
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124740839-1030870927.png)
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124746073-1574506021.png)
是一个创建线程的库函数,如果在反汇编代码中可以看到具体的地址指令是 BLX R7,R7的值就是pthread_create函数的地址,如果是做比赛可以直接nop BLX R7,然后去试试有没有过掉反调试。
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124759104-1892534135.png)
点击dword_9BC882B4函数也可以看到函数已经被定位到了pthread_create
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124813417-2001693299.png)
而在静态中只有符号信息,没有做相应的映射
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124852120-2037958297.png)
所以可以知道unk_9BC836A4就是该线程创建完成之后执行的函数,在该函数下断点,f9执行,断在了该函数
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124905433-1588953917.png)
F5之后比原来的汇编代码还别扭
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124922839-1038623220.png)
可以看到有一个明显的循环体,其中主要的函数调用是BL unk_9BC8330C,可以直接nop这个函数调用,然后去试试反调试是不是已经过掉了,不过现在不是做比赛,我想继续跟进去看看他是怎么做的。
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124935839-1959751335.png)
不过里面的代码确实是难看懂,反正是调试,不如就动态跟着一步一步调一下,发现里面读取了/proc/pid/status的tracepid字段,然后做了字符串的比较,最后执行到了
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125004511-1569511406.png)
可以看到R2寄存器指向了libc.so下的kill函数,这就是反调试的最后一步,当发现被调试时杀死进程,来实现自毁的目的,在流程图里面也可以看到这个一个单独的分支,之后就没有代码了。
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125018854-1904665589.png)
所以我们把BGE loc_9BC59600这个函数nop掉,让这个分支的代码不执行。libcrackme.so加载的基地址为0x9BC58000,修改原文件的地址为0x9BC595D80-x9BC58000=0x15D8
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125037308-1859506416.png)
然后重打包运行,ida附加上去没有退出,说明我们的反反调试成功了,在securityCheck函数上下断点,然后f8单步调试
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125052323-138152369.png)
此时R2指向了保存的密码
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125106651-239773176.png)
输入aiyou,bucuoo,答案正确
0x04其他思路
思路一:
在我们输入密码进行比较的时候,logcat有输出信息
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125139479-2142466224.png)
所以我们可以使用这条打印信息来把密码打印出来
这是原来的代码布局
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125153573-803739627.png)
选择的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之后的代码布局
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125229995-1448292470.png)
重新打包运行,随便输入密码,打印出来的log如下
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125244933-279630097.png)
思路二:
静态时密码的偏移量是0x4450
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125257620-77102044.png)
所以我们attach上进程后不运行,直接在libcrackme.so的基地址加上0x4450得到的地址为
0x9BC30000+0x4450=0x9BC34450
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125310229-586870865.png)
G直接跳转到那个位置,然后就看到了答案,请允许我做一个悲伤的表情,为什么当初没有想到。
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125323933-1770926720.png)
0x05小结
除了读取/proc/pid/status下面的tracepid方法外,现在反调试还采用了读取/proc/net/tcp下面的tcp信息,对常见的调试器的调试端口进行检测。还有函数的运行时间,读取进入函数和出来函数的系统时间,然后做差和预定值作比较,来判定是否处于调试状态。
现在的反调试还是在大粒度上面做的,采用nop线程开启和nop整个函数调用都可以过掉整个反调试,是不是可以采用线程之间互相配合的方式,主线程依赖于子线程的运行,如果子线程不运行,那么主线程也退出,细化一点就是把反调试函数和正常功能函数混合到一起,比如采用生产者消费者模型来建立主从线程的关系,这样过掉反函数的难度就更大一些了。
参考:
ARM中跳转指令BL/BLX偏移值计算规则
[Android 原创] 【练习】IDA调试Android native(Crackme)
Android逆向之旅—动态方式破解apk进阶篇(IDA调试so源码)
安卓APP动态调试-IDA实用攻略
安卓动态调试七种武器之长生剑 - Smali Instrumentation
最近在学习安卓安全,看到52破解上面有分析2014年阿里安全挑战赛的第二个crackme的文章。勾起了我的回忆,那是我第一次参加安全比赛,在安卓安全也没有做多深入的学习。第一题比较简单,直接在logcat里面就可以看到输出的信息,只要将数字和文字的关系对应关系搞明白就可以解出来。第二题我就遇到困难了,虽然临时学会了怎么用ida调试so,但只要ida附加到进程上去,程序就退出了,屡试不爽。心里也有往反调试那边想,但是功力不足没有能把反调试干掉,最后止步于这一题。现在又看到基于这道题的调试技巧文章,所以就拿起来练练手,练习练习动态调试。
0x01静态分析
应用程序打开的主界面是
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124004370-254019362.png)
把程序拖到jeb中,可以看到主程序的代码比较简单,就是取得输入的信息,然后调用native的securityCheck方法做比较,根据返回的结果展示不同的内容。没有提供有用的信息。
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124020558-1160007157.png)
解压程序,将libcrackme.so拖到ida中,找到Java_com_yaotong_crackme_MainActivity_securityCheck函数直接f5,代码没有做混淆,条理也非常清晰,对比v6和v5的值。
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124038792-756646862.png)
其中v5是用户的输入,看起来比较别扭,参考蒸米《安卓动态调试七种武器之孔雀翎 – Ida Pro》的内容:一个指针加上一个数字,比如v3+676。然后将这个地址作为一个方法指针进行方法调用,并且第一个参数就是指针自己,比如(v3+676)(v3…)。这实际上就是我们在JNI里经常用到的JNIEnv方法。因为Ida并不会自动的对这些方法进行识别,所以当我们对so文件进行调试的时候经常会见到却搞不清楚这个函数究竟在干什么,因为这个函数实在是太抽象了。解决方法非常简单,只需要对JNIEnv指针做一个类型转换即可。比如说上面提到v3指针,我们选中后按一下”y”键,然后将类型声明为”JNIEnv*”。
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124056901-1323278320.png)
可以看到off_628C的内容为
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124111354-1670689308.png)
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124118620-1691583579.png)
看到字符串为wojiushidaan,将该字符串输入,并没有通过校验,可见程序在运行的时候对比较的字符串做了修改,这就要求我们动态去调试这个程序了。
0x02动态调试
到ida安装目录.\IDA 6.8\dbgsrv\下面将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
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124456214-518863256.png)
就可以看到所有运行的程序,基本我们启动的需要调试的应用的pid号都比较大,可以按pid排序方便找到调试的程序。
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124511698-1596393057.png)
载入的过程较长,其中库的载入有弹框确认,直接略过,在modules框中找到函数,在函数头下断点
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124525683-2035347429.png)
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124538386-1792754089.png)
F9运行,程序直接退出,ida输出如下
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124552933-2011447418.png)
几次尝试都是这个结果,所以肯定有反调试。
0x03反调试
我们用以下命令以调试模式启动一个应用
adb shell am start -D -n com.yaotong.crackme/.MainActivity
其中主activity可以在logcat里面看到,或者反编译manifest文件也可以看到
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124635229-368901405.png)
程序启动停在等待调试器的状态
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124648120-1976635546.png)
修改Debugger-debugger options的配置,让程序在载入lib的时候断下来,这样我们才可以调试JNI_Onload函数,还有某些可能会把反调试放在.init_array,该函数的加载比JNI_Onload还要早。
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124702073-78612211.png)
然后再附加进程,然后f9运行程序,可以看到程序还是停在等待调试器的状态
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
让程序恢复运行,这时程序会在linker停下来,在JNI_Oload函数上下断点,然后f9运行程序,f8逐条运行,运行到这条函数的时候就会退出,f7跟进去调试
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124740839-1030870927.png)
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124746073-1574506021.png)
是一个创建线程的库函数,如果在反汇编代码中可以看到具体的地址指令是 BLX R7,R7的值就是pthread_create函数的地址,如果是做比赛可以直接nop BLX R7,然后去试试有没有过掉反调试。
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124759104-1892534135.png)
点击dword_9BC882B4函数也可以看到函数已经被定位到了pthread_create
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124813417-2001693299.png)
而在静态中只有符号信息,没有做相应的映射
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124852120-2037958297.png)
所以可以知道unk_9BC836A4就是该线程创建完成之后执行的函数,在该函数下断点,f9执行,断在了该函数
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124905433-1588953917.png)
F5之后比原来的汇编代码还别扭
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124922839-1038623220.png)
可以看到有一个明显的循环体,其中主要的函数调用是BL unk_9BC8330C,可以直接nop这个函数调用,然后去试试反调试是不是已经过掉了,不过现在不是做比赛,我想继续跟进去看看他是怎么做的。
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216124935839-1959751335.png)
不过里面的代码确实是难看懂,反正是调试,不如就动态跟着一步一步调一下,发现里面读取了/proc/pid/status的tracepid字段,然后做了字符串的比较,最后执行到了
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125004511-1569511406.png)
可以看到R2寄存器指向了libc.so下的kill函数,这就是反调试的最后一步,当发现被调试时杀死进程,来实现自毁的目的,在流程图里面也可以看到这个一个单独的分支,之后就没有代码了。
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125018854-1904665589.png)
所以我们把BGE loc_9BC59600这个函数nop掉,让这个分支的代码不执行。libcrackme.so加载的基地址为0x9BC58000,修改原文件的地址为0x9BC595D80-x9BC58000=0x15D8
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125037308-1859506416.png)
然后重打包运行,ida附加上去没有退出,说明我们的反反调试成功了,在securityCheck函数上下断点,然后f8单步调试
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125052323-138152369.png)
此时R2指向了保存的密码
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125106651-239773176.png)
输入aiyou,bucuoo,答案正确
0x04其他思路
思路一:
在我们输入密码进行比较的时候,logcat有输出信息
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125139479-2142466224.png)
所以我们可以使用这条打印信息来把密码打印出来
这是原来的代码布局
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125153573-803739627.png)
选择的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之后的代码布局
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125229995-1448292470.png)
重新打包运行,随便输入密码,打印出来的log如下
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125244933-279630097.png)
思路二:
静态时密码的偏移量是0x4450
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125257620-77102044.png)
所以我们attach上进程后不运行,直接在libcrackme.so的基地址加上0x4450得到的地址为
0x9BC30000+0x4450=0x9BC34450
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125310229-586870865.png)
G直接跳转到那个位置,然后就看到了答案,请允许我做一个悲伤的表情,为什么当初没有想到。
![](https://images2015.cnblogs.com/blog/646980/201612/646980-20161216125323933-1770926720.png)
0x05小结
除了读取/proc/pid/status下面的tracepid方法外,现在反调试还采用了读取/proc/net/tcp下面的tcp信息,对常见的调试器的调试端口进行检测。还有函数的运行时间,读取进入函数和出来函数的系统时间,然后做差和预定值作比较,来判定是否处于调试状态。
现在的反调试还是在大粒度上面做的,采用nop线程开启和nop整个函数调用都可以过掉整个反调试,是不是可以采用线程之间互相配合的方式,主线程依赖于子线程的运行,如果子线程不运行,那么主线程也退出,细化一点就是把反调试函数和正常功能函数混合到一起,比如采用生产者消费者模型来建立主从线程的关系,这样过掉反函数的难度就更大一些了。
参考:
ARM中跳转指令BL/BLX偏移值计算规则
[Android 原创] 【练习】IDA调试Android native(Crackme)
Android逆向之旅—动态方式破解apk进阶篇(IDA调试so源码)
安卓APP动态调试-IDA实用攻略
安卓动态调试七种武器之长生剑 - Smali Instrumentation
相关文章推荐
- MSC-2015移动安全挑战赛 第二题
- 2015阿里&看雪移动安全挑战赛-第二题
- 阿里ctf-2014 android 第三题――so动态调试及破解加固
- MSC-2015移动安全挑战赛 第一题
- [笔记]2016阿里中间件性能挑战赛(三)
- 【阿里聚安全技术公开课】移动APP漏洞风险与解决方案
- 【阿里聚安全技术公开课】移动APP漏洞风险与解决方案
- 笔记:Microsoft.net和windows应用程序调试(第二部分:强大的调试技术)
- 阿里聚安全移动安全专家分享:APP渠道推广作弊攻防那些事儿
- [笔记]2016阿里中间件性能挑战赛(一)
- 你不知道的声纹识别,尽在阿里聚安全攻防挑战赛!
- Android逆向学习笔记---逆向腾讯2016游戏安全挑战赛Tencent2016A.apk
- 阿里移动安全发布《2015物联网安全年报》,威胁攻击日益凸显
- 【移动安全】so动态调试对抗反编译及反调试
- [笔记]2016阿里中间件性能挑战赛(二)
- 2015移动安全挑战赛 第一题
- 2015移动安全挑战赛MSC(第二届)第一题解题思路
- 2015ali android挑战赛第二题之反调试技术解析