从OWASP CrackMe学Android逆向(二)
本帖最后由 Se8s0n 于 2019-11-5 11:15 编辑## 前言
从(https://www.52pojie.cn/thread-1048786-1-1.html)感觉用题目来学习会比之前学得内容多很多, 然后这些题目有多种解法, 我也学到很多姿势, 下面就来分析`UnCrackable-Level2`。这个题目涉及.so动态调试的知识。
## UnCrackable-Level2
同样的, 先将`UnCrackable-Level2.apk`放到JEB中分析, 一进到MainActivity, 就发现了一个神奇的东西, 这个应用加载了`libfoo.so`文件。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920916/OWASP/OWASP-2/1.png)
能感觉出来这个应用和之前level1不同的是, check输入的关键代码应该是在`libfoo.so`中, 所以不能使用Xposed去处理这个CrackMe了, 但是root和debug的检测还是Java层调用`System.exit(0)`退出。所以上次的cracker.js, 重载系统exit函数部分保留。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920917/OWASP/OWASP-2/2.png)
将`UnCrackable-Level2.apk`解压之后在`UnCrackable-Level2\lib\arm64-v8a`目录下找到`ibfoo.so`, 把它拖到IDA里面分析一下。查看Export表导出的函数, 发现存在含有关键词CodeCheck的函数。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920917/OWASP/OWASP-2/3.png)
按下F5查看其反编译的代码, 发现存在可疑的数据`xmmword_EA0`。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920917/OWASP/OWASP-2/4.png)
双击`xmmword_EA0`, 发现了一个传入v7的是一个大数, 使用Hex-View查看其Ascii码, 发现了一句不完整的话`Thank for all t`
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920918/OWASP/OWASP-2/5.png)
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920918/OWASP/OWASP-2/6.png)
CheckCode中还有v8参数, 虽然现在暂时不知道它有什么用, 还是先将它最后处理的过程写出来, 详情可以看注释, 最后我们知道v8的数据转换为ascii码之后为"eish", 暂时不知道这个的用处, 所以先放着。根据下面的result获取, 我们也就知道了, 我们最终想要的字符串是存放在v6参数中的。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920918/OWASP/OWASP-2/7.png)
分析`libfoo.so`中的其他的函数, 发现应用限制了attach, 还是通过轮询的方法监视应用是否被调试。也就是说, 如果想使用IDA动态调试这个应用的话, 还需要我们绕过这种反调试的机制。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920918/OWASP/OWASP-2/8.png)
事先说明,上面静态分析的过程都是对`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*`即可。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572922697/OWASP/OWASP-2/re_8.png)
修改之后效果如下:
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920919/OWASP/OWASP-2/9.png)
那什么时候我们可以使用上面的方法还原函数名呢?在文章[安卓动态调试七种武器之孔雀翎 – Ida Pro](https://wooyun.js.org/drops/%E5%AE%89%E5%8D%93%E5%8A%A8%E6%80%81%E8%B0%83%E8%AF%95%E4%B8%83%E7%A7%8D%E6%AD%A6%E5%99%A8%E4%B9%8B%E5%AD%94%E9%9B%80%E7%BF%8E%20%E2%80%93%20Ida%20Pro.html)中有提及, 如果出现一个指针加上一个数字,比如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`的函数, 发现密钥以明文字符串的形式出现了。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920919/OWASP/OWASP-2/10.png)
这时候我们输入"Thanks for all the fish", 发现我们成功找到密钥。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920920/OWASP/OWASP-2/11.png)
即便如此简单, 我们还是需要学习通过动态调试的方法获取密钥。以防遇到加了密或者其他更难分析的情况,我们无从下手。
### IDA动态调试代码
#### 绕过反调试机制
通过上面的分析(不包含静态分析`arm64-v7a`目录的libfoo.so文件的内容), 静态分析到现在也没找到完整的23个字母长度的密钥, 我们要通过IDA调试的方法获得密钥,。尝试动态调试.so文件的时候会发现应用会闪退, 所以我们需要来过反调试机制才能让应用正常运行并且获得密钥。那我们怎么做才能成功绕过反调试机制呢?
根据分析,我们要绕过的反调试机制分别处于Java层和Native层。
我们下面先修改应用的smali源码, 不然我们设置完AndroidManifest.xml文件`debuggable="true"`之后, 还要面对下面的弹窗。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920920/OWASP/OWASP-2/12.png)
还记得我们最开始分析的Java层的代码不, 现在我们来回忆一下。发现应用在root环境或可调试的环境下运行之后, 会调用MainActivity类中的a函数。双击该函数, 分析代码逻辑。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920920/OWASP/OWASP-2/13.png)
发现这个函数会在我们做了点击OK的操作之后调用系统的exit函数退出。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920920/OWASP/OWASP-2/14.png)
反编译apk之后, 定位到刚刚的a函数。发现函数最后返回为return-void, 那我们直接把中间的代码删除就ok。接下来就是常规的反编译+重签名的操作, 这里就不赘述了。安装修改之后的应用, Java层的反调试就绕过了。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920920/OWASP/OWASP-2/15.png)
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920921/OWASP/OWASP-2/16.png)
下面讲一下稍有难度的绕过Native层的代码, 用IDA反编译libfoo.so。从IDA左侧的Function窗口双击`Java_sg_vantagepoint_uncrackable2_MainActivity_init`, 发现会调用`sub_918`方法。双击`sub_918`函数分析代码。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920921/OWASP/OWASP-2/17.png)
通过代码我们可以知道, `sub_918`是起反调试作用的关键函数。接下来我们分析一下这个函数是如何实现反调试的,该函数会先fork自身, 产生一个子进程, 父进程在运行的过程中使用了轮询机制, 通过(https://refspecs.linuxbase.org/LSB_5.0.0/LSB-Core-generic/LSB-Core-generic/baselib-ptrace-1.html)监视自身是否处于被调试的状态。子进程和fock失败的父进程, 会调用waitpid一般情况下, ptrace这种类型的函数是不会被混淆的, 所以我们直接将调用ptrace函数的部分nop掉就好了。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920921/OWASP/OWASP-2/18.png)
使用IDA View-A打开`sub_918`函数, 可以看到有两个`BL .ptrace`。选中其中的一个`.ptrace`, 然后打开Hex View-1页面。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920921/OWASP/OWASP-2/19.png)
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920921/OWASP/OWASP-2/20.png)
按下快捷键`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`指令才能让应用正常运行, 并绕过反调试的检测。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920922/OWASP/OWASP-2/21.png)
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920922/OWASP/OWASP-2/22.png)
最后按下图选中下图`Apply patches to input file....`, 就可以保存我们修改之后的结果。我们只需要patch我们设备使用到的`libfoo.so`文件, 比如我设备的arm版本是`arm64-v8a`。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920922/OWASP/OWASP-2/23.png)
这里只针对这个应用的反调试作了介绍, 如果想了解更多反调试和绕过反调试的内容, 可以看看文章[【SO壳】17种安卓native反调试收集](https://bbs.pediy.com/thread-223460.htm)。
#### 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的手机上安装(https://security.tencent.com/index.php/opensource/detail/17), 重启激活该模块即可。
* 设置全局变量, 使`ro.debuggable=1`(每次开机重启后失效)
使用上面的方法让应用处于可调试的状态, 用DDMS打开, 效果如下:
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920922/OWASP/OWASP-2/24.png)
先输入命令`adb shell dumpsys activity top | findstr ACTIVITY`, 通过输出可知应用的包名及当前的Activity, 输入命令`adb shell am start -D -n owasp.mstg.uncrackable2/sg.vantagepoint.uncrackable2.MainActivity`可让Uncrackable2这个应用处于被调试状态。这时候查看DDMS, 我们可以看到应用的状态改变了。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920922/OWASP/OWASP-2/25.png)
接下来要用IDA对`.so`文件进行动态调试, 打开64位的IDA,打开debug选项`Debugger->attach->Remoute Android debugger`。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920922/OWASP/OWASP-2/26.png)
这时候会出现一个弹窗, 点击`Debug option`, 勾选上框出来的3项。点击OK之后, 在hostname里面填写127.0.0.1即可。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920922/OWASP/OWASP-2/27.png)
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920922/OWASP/OWASP-2/28.png)
之后会出现手机加载的进程, 我们只需要选中我们想调试的`uncrackable2`, 就可以进入IDA的调试界面。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920922/OWASP/OWASP-2/29.png)
之后使用jdb使应用继续运行, 因为上面的DDMS几次重新打开, 所以端口映射部分会不一致。我们在使用jdb调试的时候, 最好还是先打开DDMS, 查看应用真正映射的端口, 否则会出现`无法附加到目标 VM`的错误, 按照下面的显示结果, 我们需要输入命令`jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8605`(8700端口被DDMS进程占用, 改为8605端口, 不开DDMS就不用改)。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920922/OWASP/OWASP-2/30.png)
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920922/OWASP/OWASP-2/31.png)
双开IDA计算进程动态运行时我们想要定位的函数的地址, 在动态调试的IDA按下`F9`快捷键让程序正常运行, 这时候我们就能在IDA右侧`Module`板块搜索到`libfoo.so`动态加载的地址`0x0000007B11291000`, 之后我们从分析静态`libfoo.so`的IDA中获取函数`CodeCheck`函数的偏移量为`0xDAC`, 所以最后我们能够计算出我们要跳转到`0x0000007B11291DAC`。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920922/OWASP/OWASP-2/32.png)
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920922/OWASP/OWASP-2/33.png)
按下快捷键`G`, 跳转到我们计算的CodeCheck函数开始的地址(`0x0000007B11291DAC`), 发现代码不能解析出来, 选中部分代码, 用快捷键`C`解析代码(一次不行就先按`C`再按下`Force`)。然后在地址`0x0000007B11291DAC`下断点, 在手机应用界面上输入随便输入长度为23的字符串之后点击`Verify`, 就可以运行到如下位置。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920922/OWASP/OWASP-2/34.png)
在动态调试汇编代码的过程中, 会发现我们很难根据变量名获取其中的值, 只能在调试的过程中看到寄存器中存储的内容, 将静态调试的IDA中的汇编代码和动态调试过程中的IDA的代码进行比对, 我们可以找出一些函数, 比如这里的`strncmp`的函数的位置, 也可以通过静态分析的IDA找到`strncmp`函数的位置(`0x0000007B11291000+0xE5C=0x0000007B11291E5C`), 选中`unk_7B11281820`, 使用快捷键`N`可将地址名重命名为`.strncmp`。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920922/OWASP/OWASP-2/35.png)
IDA右侧有`General Registers`窗口, 但是这个窗口只会出现地址, 而我们需要获取寄存器中的值, `View->Open subviews->Hex dump`可以查看内存中的数值。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920922/OWASP/OWASP-2/36.png)
`F8`单步调试到`BL .strncmp`处, 这时候我们就能看到传递的参数寄存器X1和X0存放的地址。鼠标移至X0存放的地址, 右键选中`Select All`, 复制地址。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920922/OWASP/OWASP-2/37.png)
转到`Hex View-3`的窗口, 按下快捷键`G`, 跳转到刚刚我们复制的地址处。可以看到我们刚刚在应用中输入的数值, 想要获取X1的值可以用一样的方法, 最终我们得到要输入的密码为`Thanks for all the fish`。
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920922/OWASP/OWASP-2/38.png)
![](https://res.cloudinary.com/dq4c0zqqy/image/upload/v1572920922/OWASP/OWASP-2/39.png)
IDA动态调试的过程就到这里结束了, 下面还有一种不patch apk的方法可以实现绕过反调试和各种检测的方法, 使用到了Radare2, 感觉重要的是学习思路而不是工具如何使用这句话, 还是很有道理的。
## 结语
实际上最近有很多事情要做, 配置Radare2及其插件上, 我花了很多时间, 现在还有一个小插件没安装好。为了推着自己完成这篇系列文章, 就把UnCrackable2完成的部分先发出来。希望我能尽快完成环境的配置还有之后的文章吧..... 有个问题一直解决不了,我把root和debugger检测的那一块删掉,再用androidkiller回编会报错。之后试了下拿到apk之后什么都不做,直接用androidkiller回编也会报错。报错信息:
当前 Apktool 使用版本:Android Killer Default APKTOOL
正在编译 APK,请稍等...
>I: Using Apktool 2.3.1
>I: Smaling smali folder into classes.dex...
>I: Building resources...
>S: WARNING: Could not write to (C:\Users\11046\AppData\Local\apktool\framework), using C:\Users\11046\AppData\Local\Temp\ instead...
>S: Please be aware this is a volatile directory and frameworks could go missing, please utilize --frame-path if the default storage directory is unavailable
>W: fakeLogOpen(/dev/log_security, O_WRONLY) failed
>W: fakeLogOpen(/dev/log_security, O_WRONLY) failed
>W: fakeLogOpen(/dev/log_security, O_WRONLY) failed
>W: E:\CTF\Tools\Re\AndroidKiller\projects\UnCrackable-Level2\Project\AndroidManifest.xml:1: error: No resource identifier found for attribute 'compileSdkVersion' in package 'android'
>W:
>W: E:\CTF\Tools\Re\AndroidKiller\projects\UnCrackable-Level2\Project\AndroidManifest.xml:1: error: No resource identifier found for attribute 'compileSdkVersionCodename' in package 'android'
>W:
>Exception in thread "main" brut.androlib.AndrolibException: brut.androlib.AndrolibException: brut.common.BrutException: could not exec (exit code = 1):
> at brut.androlib.Androlib.buildResourcesFull(Androlib.java:492)
> at brut.androlib.Androlib.buildResources(Androlib.java:426)
> at brut.androlib.Androlib.build(Androlib.java:305)
> at brut.androlib.Androlib.build(Androlib.java:270)
> at brut.apktool.Main.cmdBuild(Main.java:227)
> at brut.apktool.Main.main(Main.java:75)
>Caused by: brut.androlib.AndrolibException: brut.common.BrutException: could not exec (exit code = 1):
> at brut.androlib.res.AndrolibResources.aaptPackage(AndrolibResources.java:456)
> at brut.androlib.Androlib.buildResourcesFull(Androlib.java:478)
> ... 5 more
>Caused by: brut.common.BrutException: could not exec (exit code = 1):
> at brut.util.OS.exec(OS.java:95)
> at brut.androlib.res.AndrolibResources.aaptPackage(AndrolibResources.java:450)
> ... 6 more
APK 编译失败,无法继续下一步签名! 悦来客栈的老板 发表于 2019-11-8 15:06
楼主有没有试过直接 NOP掉 sub_918这个函数啊?
已尝试过, 代码最后还需要返回result的值, 所以直接nop掉会失败 很详细,支持写一个系列:lol 风景这边独好 谢谢楼主分享 很到的作品,感谢楼主 谢谢楼主的分享 写的真的棒!爱了爱了 欢迎多发技术帖,加精鼓励下 楼主写的很棒,支持,希望出个系列 继续学习,谢谢楼主