前言
从OWASP CrackMe学Android逆向(一)感觉用题目来学习会比之前学得内容多很多, 然后这些题目有多种解法, 我也学到很多姿势, 下面就来分析UnCrackable-Level2
。这个题目涉及.so动态调试的知识。
UnCrackable-Level2
同样的, 先将UnCrackable-Level2.apk
放到JEB中分析, 一进到MainActivity, 就发现了一个神奇的东西, 这个应用加载了libfoo.so
文件。
能感觉出来这个应用和之前level1不同的是, check输入的关键代码应该是在libfoo.so
中, 所以不能使用Xposed去处理这个CrackMe了, 但是root和debug的检测还是Java层调用System.exit(0)
退出。所以上次的cracker.js, 重载系统exit函数部分保留。
将UnCrackable-Level2.apk
解压之后在UnCrackable-Level2\lib\arm64-v8a
目录下找到ibfoo.so
, 把它拖到IDA里面分析一下。查看Export表导出的函数, 发现存在含有关键词CodeCheck的函数。
按下F5查看其反编译的代码, 发现存在可疑的数据xmmword_EA0
。
双击xmmword_EA0
, 发现了一个传入v7的是一个大数, 使用Hex-View查看其Ascii码, 发现了一句不完整的话Thank for all t
CheckCode中还有v8参数, 虽然现在暂时不知道它有什么用, 还是先将它最后处理的过程写出来, 详情可以看注释, 最后我们知道v8的数据转换为ascii码之后为"eish", 暂时不知道这个的用处, 所以先放着。根据下面的result获取, 我们也就知道了, 我们最终想要的字符串是存放在v6参数中的。
分析libfoo.so
中的其他的函数, 发现应用限制了attach, 还是通过轮询的方法监视应用是否被调试。也就是说, 如果想使用IDA动态调试这个应用的话, 还需要我们绕过这种反调试的机制。
事先说明,上面静态分析的过程都是对lib\arm64-v8a
文件夹下的libfoo.so
文件进行分析的。在分析代码的过程中,存在一些函数调用的过程让我们看起来不是很舒服。实际上这些调用的函数都是JNI函数, 我们可以通过导入jni.h文件(File->Load File->Parse C header file...)——jni.h文件一般安装了Android Studio都有, 我的存放在Android Studio\jre\include
目录下, 选中v4指针,选中后按一下”y”键,然后将类型声明为JNIEnv*
即可。
修改之后效果如下:
那什么时候我们可以使用上面的方法还原函数名呢?在文章安卓动态调试七种武器之孔雀翎 – Ida Pro中有提及, 如果出现一个指针加上一个数字,比如v3+676。然后将这个地址作为一个方法指针进行方法调用,并且第一个参数就是指针自己,比如(v3+676)(v3…)。这实际上就是我们在JNI里经常用到的JNIEnv方法。
静态分析就分析到这里了。现在, 我们需要开始动手了。
静态分析代码
因为我手机arm版本为arm64-v8a
(使用命令 adb shell getprop ro.product.cpu.abi
可查看arm版本), 所以前面的分析都是基于lib\armeabi-v7a\libfoo.so
。当我将lib\armeabi-v7a\libfoo.so
拖到32位的IDA中分析代码的时候找到含有CodeCheck
的函数, 发现密钥以明文字符串的形式出现了。
这时候我们输入"Thanks for all the fish", 发现我们成功找到密钥。
即便如此简单, 我们还是需要学习通过动态调试的方法获取密钥。以防遇到加了密或者其他更难分析的情况,我们无从下手。
IDA动态调试代码
绕过反调试机制
通过上面的分析(不包含静态分析arm64-v7a
目录的libfoo.so文件的内容), 静态分析到现在也没找到完整的23个字母长度的密钥, 我们要通过IDA调试的方法获得密钥,。尝试动态调试.so文件的时候会发现应用会闪退, 所以我们需要来过反调试机制才能让应用正常运行并且获得密钥。那我们怎么做才能成功绕过反调试机制呢?
根据分析,我们要绕过的反调试机制分别处于Java层和Native层。
我们下面先修改应用的smali源码, 不然我们设置完AndroidManifest.xml文件debuggable="true"
之后, 还要面对下面的弹窗。
还记得我们最开始分析的Java层的代码不, 现在我们来回忆一下。发现应用在root环境或可调试的环境下运行之后, 会调用MainActivity类中的a函数。双击该函数, 分析代码逻辑。
发现这个函数会在我们做了点击OK的操作之后调用系统的exit函数退出。
反编译apk之后, 定位到刚刚的a函数。发现函数最后返回为return-void, 那我们直接把中间的代码删除就ok。接下来就是常规的反编译+重签名的操作, 这里就不赘述了。安装修改之后的应用, Java层的反调试就绕过了。
下面讲一下稍有难度的绕过Native层的代码, 用IDA反编译libfoo.so。从IDA左侧的Function窗口双击Java_sg_vantagepoint_uncrackable2_MainActivity_init
, 发现会调用sub_918
方法。双击sub_918
函数分析代码。
通过代码我们可以知道, sub_918
是起反调试作用的关键函数。接下来我们分析一下这个函数是如何实现反调试的,该函数会先fork自身, 产生一个子进程, 父进程在运行的过程中使用了轮询机制, 通过ptrace函数监视自身是否处于被调试的状态。子进程和fock失败的父进程, 会调用waitpid一般情况下, ptrace这种类型的函数是不会被混淆的, 所以我们直接将调用ptrace函数的部分nop掉就好了。
使用IDA View-A打开sub_918
函数, 可以看到有两个BL .ptrace
。选中其中的一个.ptrace
, 然后打开Hex View-1页面。
按下快捷键F2
, 输入1F 20 03 D5
(NOP), 将原本的9C FF FF 97
替换掉, patch结束之后按F2
完成修改(两个BL .ptrace
都用这种方法修改)。发现网上有部分文章使用指令00 00 00 00
和00 00 A0 E1
(mov r0,r0)修改, 我按照那种方式修改之后, 发现代码结构被破坏掉了, 应用不能正常运行, 最后直接使用NOP
指令才能让应用正常运行, 并绕过反调试的检测。
最后按下图选中下图Apply patches to input file....
, 就可以保存我们修改之后的结果。我们只需要patch我们设备使用到的libfoo.so
文件, 比如我设备的arm版本是arm64-v8a
。
这里只针对这个应用的反调试作了介绍, 如果想了解更多反调试和绕过反调试的内容, 可以看看文章【SO壳】17种安卓native反调试收集。
IDA动态调试Native代码
在正式开始之前, 我们需要做点准备工作。 首先打开目录<你IDA安装的目录下>\IDA\dbgsrv
, 将该目录下的android_server
push进手机里面并添加权限。
$ adb push android_server64 #要按照你自己的使用的设备的内核进行选择, 查看arm版本就知道
$ adb forward tcp:23946 tcp:23946 # 设置本地端口转发
$ adb shell
$ su
$ cd /data/local/tmp
$ chmod +x android_server64
$ ./android_server
经过测试,发现应用不能进行调试。我们先解决AndroidManifest.xml上的反调试,有4种方法:
- 反编译APK, 修改AndroidManifest.xml, 使
debuggable="true"
- 从手机内核中提取出boot.img并修改, 使
ro.debuggable=1
, 或者可以使用mprop, 但是有版本限制。
- 在装有Xposed的手机上安装BDOpener, 重启激活该模块即可。
-
设置全局变量, 使ro.debuggable=1
(每次开机重启后失效)
使用上面的方法让应用处于可调试的状态, 用DDMS打开, 效果如下:
先输入命令adb shell dumpsys activity top | findstr ACTIVITY
, 通过输出可知应用的包名及当前的Activity, 输入命令adb shell am start -D -n owasp.mstg.uncrackable2/sg.vantagepoint.uncrackable2.MainActivity
可让Uncrackable2这个应用处于被调试状态。这时候查看DDMS, 我们可以看到应用的状态改变了。
接下来要用IDA对.so
文件进行动态调试, 打开64位的IDA,打开debug选项Debugger->attach->Remoute Android debugger
。
这时候会出现一个弹窗, 点击Debug option
, 勾选上框出来的3项。点击OK之后, 在hostname里面填写127.0.0.1即可。
之后会出现手机加载的进程, 我们只需要选中我们想调试的uncrackable2
, 就可以进入IDA的调试界面。
之后使用jdb使应用继续运行, 因为上面的DDMS几次重新打开, 所以端口映射部分会不一致。我们在使用jdb调试的时候, 最好还是先打开DDMS, 查看应用真正映射的端口, 否则会出现无法附加到目标 VM
的错误, 按照下面的显示结果, 我们需要输入命令jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8605
(8700端口被DDMS进程占用, 改为8605端口, 不开DDMS就不用改)。
双开IDA计算进程动态运行时我们想要定位的函数的地址, 在动态调试的IDA按下F9
快捷键让程序正常运行, 这时候我们就能在IDA右侧Module
板块搜索到libfoo.so
动态加载的地址0x0000007B11291000
, 之后我们从分析静态libfoo.so
的IDA中获取函数CodeCheck
函数的偏移量为0xDAC
, 所以最后我们能够计算出我们要跳转到0x0000007B11291DAC
。
按下快捷键G
, 跳转到我们计算的CodeCheck函数开始的地址(0x0000007B11291DAC
), 发现代码不能解析出来, 选中部分代码, 用快捷键C
解析代码(一次不行就先按C
再按下Force
)。然后在地址0x0000007B11291DAC
下断点, 在手机应用界面上输入随便输入长度为23的字符串之后点击Verify
, 就可以运行到如下位置。
在动态调试汇编代码的过程中, 会发现我们很难根据变量名获取其中的值, 只能在调试的过程中看到寄存器中存储的内容, 将静态调试的IDA中的汇编代码和动态调试过程中的IDA的代码进行比对, 我们可以找出一些函数, 比如这里的strncmp
的函数的位置, 也可以通过静态分析的IDA找到strncmp
函数的位置(0x0000007B11291000+0xE5C=0x0000007B11291E5C
), 选中unk_7B11281820
, 使用快捷键N
可将地址名重命名为.strncmp
。
IDA右侧有General Registers
窗口, 但是这个窗口只会出现地址, 而我们需要获取寄存器中的值, View->Open subviews->Hex dump
可以查看内存中的数值。
F8
单步调试到BL .strncmp
处, 这时候我们就能看到传递的参数寄存器X1和X0存放的地址。鼠标移至X0存放的地址, 右键选中Select All
, 复制地址。
转到Hex View-3
的窗口, 按下快捷键G
, 跳转到刚刚我们复制的地址处。可以看到我们刚刚在应用中输入的数值, 想要获取X1的值可以用一样的方法, 最终我们得到要输入的密码为Thanks for all the fish
。
IDA动态调试的过程就到这里结束了, 下面还有一种不patch apk的方法可以实现绕过反调试和各种检测的方法, 使用到了Radare2, 感觉重要的是学习思路而不是工具如何使用这句话, 还是很有道理的。
结语
实际上最近有很多事情要做, 配置Radare2及其插件上, 我花了很多时间, 现在还有一个小插件没安装好。为了推着自己完成这篇系列文章, 就把UnCrackable2完成的部分先发出来。希望我能尽快完成环境的配置还有之后的文章吧.....