本帖最后由 PJ头狼 于 2016-12-12 13:31 编辑
阿里Crackme分析(native)
这个Crackme分析时之前阿里安全挑战赛的第二题,在修改掉其so反调试的过程,刚好可以练习练习IDA的动态调试。这个破解方法会比较复杂,就当作练习用,后边有简单粗暴的方法
帖子比较长。。。多图。。。
实例apk:AliCrackme.apk apk运行界面如下: 1.用apktool进行反编译,确定程序的主活动 通过反编译得到AndroidManifest.xml,查看文件内容如下,可以确定该程序的主活动为com.yaotong.crackme.MainActivity。 2.结合dex2jar和jd-gui两个工具,分析主活动源码 分析源码可知,主活动中加载了crackme这个动态库,在按下按钮后便会调用动态库中的securityCheck方法对输入的字符串进行检验。 3.使用IDA分析动态库中的检验算法 定位到securityCheck方法的程序位置,按F5,经过修改、简化代码,得到方法相应的C代码程序: 这里v5对应输入的字符串,而v6对应的是进行比较的密码,进到v6的字符串存放位置查看得: 将上边的wojiushidaan输入,没有成功,可见在进行比较前,程序已经对这里的字符串进行了修改,程序的前半部分便是对字符串进行改写的操作,大致分析程序修改逻辑,无法快速摸清程序中字符修改的实现方法,尝试用动态调试进行分析。 4.尝试动态调试,分析检验方法 在securityCheck方法的反汇编代码起始处设下断点 动态加载该程序,按下F9运行,结果程序直接退出,动态调试失败 再重新加载,继续尝试动态调试,仍是直接退出,可确定该程序在加载crackme动态库前,有进行反调试检验。 5.反调试原理分析 IDA是使用android_server在root环境下注入到被调试的进程中,用到的技术是Linux中的ptrace,Android中如果一个进程被另外一个进程ptrace了之后,在它的status文件中有一个字段:TracerPid 可以标识是被哪个进程trace。 在没使用IDA对程序进行调试时,其status文件内容如下,TracerPid为0,没有被trace 当使用IDA来动态加载该程序时,查看其status文件内容,TracerPid标识着该进程正在被860这个进程trace 查看860进程信息,果真是android_server所在的进程 反调试原理便是在程序运行过程中,循环检测进程中的TracerPid标识来确定程序是否被调试。只要一运行程序,就退出了调试界面,说明这个循环检测程序执行的时机非常早,已知最早的两个时机是:一个是.init_array,一个是JNI_OnLoad .init_array是一个so最先加载的一个段信息,时机最早,现在一般so解密操作都是这里做的;JNI_OnLoad是so被System.loadLibrary调用的时候执行,时机要早于native方法执行,但是没有.init_array时机早 6.检测是否在JNI_OnLoad方法执行过程中启动反调试检测 由于JNI_OnLoad方法很早就被启动,所以不能用之前的动态调试方法,这里要采用debug方式来启动程序进行分析。 ①使用am以debug方式启动 adbshell am start -D -n com.yaotong.crackme/.MainActivity 这时程序便会处于waiting for debugger的状态: ②使用IDA加载程序 在attach该程序前,应先进行debugger options配置,Debugger-Debugger Options进入配置窗口,勾选下边Events中的3个选项: 然后加载程序到IDA中,这时使用Ctrl+S是查找不到crackme动态库的,说明使用这种debug方式启动程序,so文件是还没加载到内存的。 ③使用jdb来attach程序Java,使程序跑起来 jdb-connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
结果attach失败,这种情况是因为该程序被设置为不可调试,查看程序AndroidManifest.xml可知,没有设置android:debuggable,默认为false。修改xml文件,重新编译、打包、签名、安装程序。 ④重新进行调试 重新进行前边几个步骤,以debug方式来动态调试该程序。当jdb执行成功后,程序界面便会结束waiting: 同时IDA上成功加载程序后,便会进入到linker模块: ⑤定位到JNI_OnLoad方法位置 在另一静态IDA程序中确定JNI_OnLoad方法的偏移量,为1B9C: 确定crackme动态库的起始地址,为4A847000: 跳转到JNI_OnLoad方法的起始地址4A848B9C: ⑥设下断点,进行单步调试 单步步过,当执行下边BLX R7指令后,程序便会调试失败、退出 其中R7寄存器的地址值对应的是pthread_create函数,这个是Linux中新建一个线程的方法,可推测出在该线程中执行的便是,循环读取/proc/[pid]/status文件中的TrackerPid字段值,来确定程序是否被调试。 7.去掉相关指令,删除反调试检测 由上边可知,应将BLX R7指令给nop掉,来实现删除反调试的操作。 在IDA静态程序中确定指令的偏移位置,为1C58,对应的二进制为37 FF 2F E1: 使用010Editor进行二进制代码修改,将1C58中对应的37 FF 2F E1修改为00 00 00 00 保存后,可查看指令已被修改 8.动态调试securityCheck方法,分析密码检测过程 对上边修改后的程序进行重新编译、打包、安装,加载程序并在securityCheck方法设下断点,F9运行,输入任意密码点击按钮:
成功执行到securityCheck方法的起始地址
F8,开始单步步过调试
可以发现,程序在执行到上边CMP R3,R1指令处,便是进行密码的匹配检测,其中R1便是对应R0中存放的地址;R3则是对应R2中存放的地址。 跳转到R0存放的地址,可发现,便是存放着输入的密码: 那么R2则是程序中真正的密码了: 输入aiyou,bucuoo,破解成功: 简单粗暴的破解方法 上边通过nop掉反调试检测机制来实现动态跟踪程序,获取到最终比较的密码字符串。通过这样的实操、分析,可以更好地掌握Android Native的动态调试方法以及native层反调试机制实现的原理。 这里有一种更为简单快捷的方法,来获取到最终的密码。就是通过对so进行patch操作,使程序帮助我们打印出密码来。 注意到在程序最后进行输入字符串与正确密码进行比较前,调用了android_log_print函数: 允许程序,过滤打印信息得到: 这里就是调用android_log_print来打印<tag>为yaotong的信息,我们可以尝试直接patch这个libcrackme.so,修改打印的内容,直接利用这个函数输出此时真正的密码。 选择的patch方法是直接把这个log函数往下移,因为在0x12A4地址处正好有我们需要的打印的数据地址赋值给了R2寄存器,因此将代码段从0x1284到0x129C的地方都用NOP改写,在0x12AC的地方调用log函数,同时为了不影响R1的值,把0x12A0处的R1改成R3: ①0x1284-0x129c:NOP(00 00A0 E1) ②0x12A8:MOV R0,#4(0400 A0 E3) ③0x12AC:BL __android_log_print(88 FF FF EB)这里的88是由一开始在0x1284的92 FFFF EB得出的,两个地址相差40个字节,应除以4,对应就为:0x92-0x88=10。 ④0x12A40-0x12A84:将R1修改为R3(60 30 9F E5 07 20 93 E7) 这里R1对应<tag>不修改 使用010editor进行机器码修改: 修改后的so的反汇编代码如下: 重新打包、签名、安装程序,运行后打印的信息如下:
|