这道题涉及了动态调试so、反调试、基本的ARM汇编等等。以下是逆向得到注册码的过程。
app装入模拟器后可以看出,界面依然是逆向获得注册码的类型。
那就找找看校验验证码的地方在哪里吧。将app用解压软件打开,拿出其中的classes.dex文件,拖出静态分析工具JEB中直接分析,得到的JAVA代码如下:
由上面的代码可以看出,app启动后就加载了动态库crackme, 从从这库中导出了本地函数securityCheck,然后流程才到app的入口onCreate函数中。入口函数的代码也不复杂,只是设定了“输入密码”这个按钮的点击监听函数,然后在监听函数中校验验证码,根据校验的结果显示不同的界面。
现在的问题是校验验证码的代码不在dex文件,而在某个库中。再次打开apk文件查看lib目录,发现只有一个动态库:libcrackme.so。同样,将其拖出,然后拖拽进IDA分析(这里使用的IDA是32位7.0版本)。
首先在左边Function name窗口找到securityCheck在库中对应的函数名Java_com_yaotong_crack
me_MainActivity_securityCheck(名字这么长是由so中导出导出函数的命名规则所致)后双击它,然后在右边显示ARM汇编的窗口中就可以用IDA的F5反汇编功把ARM汇编转成C代码,结果如下:
根据so库中有关java导出函数的参数的约定,第一个参数的类型必然是结构体指针JNIEnv*,第二个参数的类型一定是jobject。而导出函数具有3个参数,那么第三个必然是Java代码中本地函数唯一的参数String。不过JNI中没有String类的定义,而是jstring。这也是JNI的规则。
使用IDA对相关类型重新命名后,可读性就好了许多:
3到8行是相关定义,10和11两个赋值在这里对结果没有影响;12行到21行是两个对全局变量的判断语句,不影响后面的功能;22行是打印函数,仍然没有影响到后面检验验证码的过程。真正的代码从23行才开始,前面就暂且当是花指令处理了。
23行开始就使用GetStringUTFChars函数把输入的验证码转成了UTF格式,然后让v6指向一个偏移off_628C,双击跳转后发现是字符串”wojiushidaan”。把这个字符串输入模拟器中的app,显示验证失败:
看来事情并没有那么简单。
然后进入了一个死循环,从代码来看是类似C语言strcmp的功能了,被比较的两个字符串对象分别是off_628C指向的字符串以及UTF格式的验证码了,比较结果被直接返回,成功返回1,失败返回0。
所以现在的关键就是把偏移off_628C处的字符串从UTF格式转回它原来的格式;或者也可以动态调试让app自己说出来。
动态调试就和模拟器无关了。由于市面上的模拟器多是用x86来模拟arm,总会有这样那样的问题,用真机调试是最为稳妥的。
此次用来调试的安卓机器是Android6.0的Nexus5,是已经root过的。将手机连接到笔记本上,打开手机的调试选项,Nexus5就能成功连接上了。
然后就是安装apk,需要用adb工具提供的选项来安装,如果成功的话如下图所示:
由于apk启动后的内存并不在本机的内存中,而是在手机上,所以不能想exe一样直接用OllyDbg调试exe一样调试,需要借助几个工具:32位的IDA以及android_server(在IDA安装目录下的dbgsrv目录)。
在本机上打开IDA,android_server则需要推送到真机上运行:
仅是这样IDA还无法连接到它,android_server只是监听在手机的23946端口上而已,还不了解是那种协议,只能看看了。
android_server拖进IDA,左边函数列表窗口选中main(可执行程序默认入口)并跳转过去,为了节省时间点击函数名用F5功能把代码解析C代码。可以发现在248行出现了一个可疑的j_accept函数:
双击它跳转后:
果然是accept函数,毫无疑问是tcp协议的服务器。为了让IDA能调试,必须要用adb把本机的23946端口转发到手机的23946端口上:
若要测试端口是否被成功监听了,可以使用命令查看:
下一步,让app加载到内存中,以被调试的方式启动。Win32下的调试是让调试器以调试的方式打开,Android下也一样,不过使用的是adb工具:
此时被调试的手机就会黑屏,然后弹出一个标题为“Waitting For Debugger”。接下来只需要让调试器附加到这个app上去。启动IDA,然后在菜单栏找到Debugger,打开选中Attach项,再点击它的子菜单中的Remote ARMLinux\Android Debugger,就会弹出一个对话框:
对话框的设置按上图的设定即可,点击OK,就有新的对话框用来选择被调试的进程:
按照app的包名不难找到目标进程。双击它,IDA会卡住几秒,然后换成了类似OD的调试界面:
现在是断点怎么停在securityCheck函数的问题。这个函数由库libcrackme.so导出,用快捷键CTRL+S查看,libcrackme.so其实还没有加载进来。
其实不难理解,手机还在等待调试器,而被调试app的内存由调试器加载。
这里就要用到jdb工具了:
出现这个结果,原因是apk其实还缺少一个让apk可以被调试的选项。这个tag在编译debug版本apk的时候是自动加上的,Release版本则没有。
既然如此,只能重来。反编译工具很多,有命令行版的apktool,也有界面版的AndroidKiller。这里使用后者,因为可以少做一个签名的动作。
将APK拖拽进入AndroidKiller,点击左边标签栏的AliCrackme_2,双击点开AndoridManifest.xml文件,在application节点中添加 android:debuggable="true",CTRL+S保存,再打开界面顶端的Android选项卡,点击工具栏中向下的绿色箭头。如无意外,下方的日志中就会提示编译完成。点击日志中蓝色高亮的文字就可以在资源管理器中跳转到编译签名过的apk了。
准备完毕,再来一次:
1. 安装新的apk,替换旧的
2. 设置端口转发
3. 管理员权限启动android_server
4. adb shell am start –D –n com.yaotong.crackme/.MainActivity 目标apk等待调试器
5. IDA中附加目标app
6. jdb工具连接远程调试器转发的调试端口
这次没有出错,但命令发出去也没有反馈。
再次用快捷键CTRL+S查看,libcrackme.so还是没有被加载进来,看看手机,仍然停留在“等待调试器”的界面。
现在的调试器只有IDA,没有下断点,如果点击工具栏上绿色的三角形直接运行的话,运行流程有可能会直接转到登录界面,这样动态库肯定加载到手机上了,似乎可行。
点击后手机上的确到了登录界面,IDA却没有停在动态库加载的起始处。还需要一个设定,点击IDA的菜单栏的Debugger菜单中的Debugger options项,会弹出一个对话框,里面要勾选“Suspend on library load/unload”,这样就可以点OK结束了。
现在上面总结的123456要再来一次,这次点击绿色三角形,就会有下面的反应:
手机上的“等待调试器”对话框消失了,不过依然黑屏。
现在需要谨慎,因为仍然不能确定动态库是否加载。CTRL+S再次查看,如果没有就点击一次运行再查看,如无意外三次以内可以看到下图所示的对话框:
无论如何,调试的目标是libcrackme.so的导出函数securityCheck,而它在内存中的地址就等于动态库的基址加上函数的偏移,根据上图可知基址是B3BAC000,而根据第一次IDA对libcrackme.so的分析可知偏移是11A8,那么加起来就是B3BAD1A8。选中IDA的代码区,使用快捷键G跳转到B3BAD1A8,代码区就来到了下图所在的地方:
当然断点要打上,点击绿色三角形运行,IDA的附加又结束了。
只能猜测有检测调试器的逻辑了,只是代码在哪里而已。程序还没起来就结束,肯定是so加载之后,而界面出来之前检测到的。这种代码的位置一般是JNI_OnLoad、初始化函数列表、so特有的“构造函数”。下面逐个查看。
首先要123456再来一次。是JNI_OnLoad,和securityCheck一样,可以得到偏移是1B9C,加上这次的基址B3BAC000,就得到B3BADB9C,然后快捷键G跳转后:
断点下好,再次点击绿色三角形运行,程序终于断下来了:
原来红色的底色变成了淡紫色。
根据IDA的提示,单步是F8,步入是F7,界面呢左边是汇编,右边有栈、寄存器、模块、线程,下方则是16进制内存,已经很像OD了。
接下来只要找到检测调试器的关键代码,patch掉就能正常调试了。
经过多次调试,终于发现两行红底的代码之间的两个跳转BLX和BL就是反调试的关键代码。只要patch掉它们就可以搞掉反调试功能。
好在ARM汇编一行固定是4个字节,两个就是8个字节,16个字节一行的话就正好半行。而根据基址B3BAC000可知patch的偏移是1C58开始8个字节。
回到电脑上,将已经加上debug选项的apk再次用AndroidKiller反编译,找到这个文件:
选中它右键菜单项可以跳转到它所在的文件夹。这样就可以用WinHex打开,跳转到偏移1C58,然后把8个字节编辑为0后保存即可。
关闭WinHex,回到AndroidKiller,这时候可以重新编译了。由于没有什么特别的操作,应该会很顺利。卸载手机中的目标app,安装现在patch过的apk。现在动态调试so应该轻车熟路。
为了测试patch的效果,把断点下到securityCheck函数。点击运行几次后,手机的界面出来了,IDA中的附加也没有停止,现在看来patch是生效了。在输入框输入1到9,点击输入密码按钮,IDA中在securityCheck函数中停下。终于可以开始正常的调试了。
为了加快速度,回想刚开始的分析出来的C代码,securityCheck函数内部只有一个循环,然后里面有数组元素的选取,然后比较,像是个比较字符串的逻辑。
断点打在循环开始的第一个比较,这样每执行到这里一次,R3的值就是正确的注册码的值。
经过多番调试,每次都记录R3的值,就得到ASCII码表里对应的值。把记录好的16进制ASCII码输入WinHex的16进制编辑框,在右边就会显示出对应的ANSI字符串:
将这个字符串输入手机上的app,已经提示“Congratulations!!!You Win.”,说明这个验证码是正确的,只是在模拟器上会有不同的表现。显然还有一些模拟器检测的功能没有被挖掘出来。
不过FLAG已经得到,就不多此一举了。
经过这个crackme的练习,增进了对一系列Android反编译工具(IDA、AndroidKiller、DDMS、adb、JEB、WinHex)的熟悉,对调试so也已经有一定了解。