/

从OWASP CrackMe学Android逆向(二)

前言

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 0000 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_serverpush进手机里面并添加权限。

$ 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完成的部分先发出来。希望我能尽快完成环境的配置还有之后的文章吧…..