实验用到的四个代码都可以在我的Github上下载:https://github.com/OnlyLegends/Bugku–CTF-/tree/master/逆向类CTF
[ LoopAndLoop ]
The friendship between native and dex.
下载LoopAndLoop.apk在安卓模拟器中打开:
题目要求输入一串数字,随便输入字符串abc时会显示:Not a Valid Integer number。
随便输入数字123,显示错误:Not Right!
所以程序应该是让我们输入某串数字。
下面开始逆向LoopAndLoop(阿里CTF).apk:先解压缩,得到它的一些配置文件:
然后用dex2jar将classes.dex反编译成jar包:d2j-dex2jar.bat classes.dex
再用jd-gui将jar包打开,看到主要部分代码为:
通过对输入的字符串int i = Integer.parseInt(paramAnonymousView)转换成数字。
分析OnClick方法。
1、try-catch
NumberFormatException为数字格式异常,当试图将一个String转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常。故若在apk输入有数字外的字符,程序捕获该异常,输出"Not a Valid Integer number"信息。
2、if-else
对check(i,99)==1835996258进行判断,若满足条件通过MainActivity.this.stringFromJNI2(i)函数输出flag,否则输出"Not Right!"。
所以我们在这无法去修改if跳转,因为输出的flag是与取决于输入的数字,所以我们只能找到正确的数字,那么关键就是check函数了。
2)分析check方法
check方法中调用了native层的原生函数chec,另外还有check1,check2,check3方法也调用了chec函数。
Android架构分为5层:
1、Applications(核心应用程序):Java应用程序基本可以理解为各个App,由Java语言实现。
2、Java API Framework(开发框架包 ):Java框架层(系统服务)就是常说的Framework,我们编写的Android代码之所以能够正常识别和动作,都要依赖这一层的支持。这一层也是由Java语言实现。
3、Android运行环境或者是Native c/c++ 核心库):这部分常见一些本地服务和一些链接库等。这一层的一个特点就是通过C和C++语言实现。比如我们现在要执行一个复杂运算,如果通过java代码去实现,那么效率会非常低,此时可以选择通过C或C++代码去实现,然后和我们上层的Java代码通信(这部分在android中称为jni机制)。又比如我们的设备需要运行,那么必然要和底层的硬件驱动交互,也要通过Native层。
4、Hardware Abstraction Layer(硬件抽象层)
5、Linux Kernel(Linux内核)
所以在这里,这个app层的apk文件调用了Native层的库函数。
对本地方法的加载通过 System.loadLibrary 方法实现,System.loadLibrary 的参数是程序员任意选取的库名,这里是"lhm"。
之后我们在apk的配置文件lib/armeabi目录下找到了调用的库liblhm.so:
因为它是C/C++语言编写的,所以我们可以用IDA把它打开:
代码的关键部分:
上面chec(a1,a2,a3,a4)方法的前两个参数a1为JNI接口指针和a2为对对象和Java类的引用,是默认的,后两个参数a3和a4即为Java层参数i和99。
GetMethodID与CallIntMethod方法:
jmethodID GetMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig); 获取一个Java方法的ID,这个函数将返回非静态类或接口实例方法的方法 ID。这里是获得了check1、check2、check3的ID。
CallIntMethod:通过jmethodID调用的返回值为int的java方法。
但是这里的代码遇到了一个IDA的F5优化问题:由于Java_net_bluelotus_tomorrow_easyandroid_MainActivity_chec是一个JNI函数,我们在JNI开发的过程中知道第一个参数为JNIEnv*,第二个参数为jobject类型。JNI函数可能出现问题,导致我的IDA的F5优化后的代码为:
而实际上网上其他人的代码为:
这里不知是我的IDA的版本还是F5优化的问题,导致后面的v7,v8-1被IDA的F5反编译认为是没有意义的变量优化掉了。参考:
https://blog.csdn.net/feibabeibei_beibei/article/details/52733249
https://www.cnblogs.com/gm-201705/p/9863956.html
之后开始修改CallIntMethod的参数个数,先单击进入CallIntMethod函数查看:
发现它原本就是有四个参数的,而之前F5反编译出来的代码之后三个,再返回原chec函数查看:
我们只是查看了一下CallIntMethod函数再返回,它竟然就又反编译出了第四个参数v7。。。可能IDA的F5反编译优化在反编译JNI函数时真的可能出现问题。
加上第五个参数的方法:可以右击CallIntMethod函数选择Set item type或按快捷键Y即可编辑函数名及其参数:
点击OK之后第五个参数也出现了:
再查看CallIntMethod函数:
的确有了int a5这个参数。
那我们给它加个a6会怎样呢?
依旧显示出来了,但是代码变量名发生了一些变化,除此之外对函数没有什么影响了。后面再加a7也是没有逻辑算法上的影响了。所以CallIntMethod函数应该就是在5个参数的时候有意义。
另外:其实GetMethodID函数只有一个参数,但它开始反编译的结果确有四个参数:
再返回去看,有变成了一个参数:
看来,这里的IDA的F5反编译优化真的有点问题。
0x03
程序算法分析
言归正传,我们继续逆向,其实代码到了这,我们已经能看懂chec函数的主要算法了:
可以看到具体流程就是:根据CallIntMethod函数的第三个个参数*(&v10 + 2 * v8 % 3)的值来选择回调哪个Java函数:check(1、2、3)。
满足check(i,99)==1835996258即可得到flag,i为apk中输入的数值大小,check为native层的原生函数,功能是根据传入check的第二个参数k*2%3值选择回调Java层三个方法中的一个,check1,check2,check3都是对i值进行改变,分析知check最后返回的值也是i,满足最终的i等于1835996258即可,传入的99在check中每次会减1,check函数的终止条件即为减到2。
所以我们有了两种思路,1是进行算法求逆,将所有函数逆向,从最后的结果1835996258算出开始输入的数字串。2是进行暴力破解,从0逐个开始带入算法计算,直到某个数字算出的结果等于1835996258。
流程图如下:
0x04
解题脚本
方法1、进行算法求逆:
写脚本需要注意的细节是在对check2方法的求逆的时候,if的判断条件为(num-1)%2==0,不要忘记减去1,因为native层check函数传给Java层方法的第二个参数减去1了。
源代码:
LoopAndLoop.py
运行结果截图:
算出最初输入的数字串为:236492408。将它输入开始的Android应用:
Flag为:alictf{Jan6N100p3r}
方法2、进行暴力破解
到这里就已经很简单了,仅需对该程序进行爆破就好,先说说我刚开始踩的一个坑,一开始我为了移植方便,直接用Java写的爆破程序,代码如下:
LoopAndLoop.java
但是由于Java的速度不高,而且主要原因是这段代码的时间复杂度本身就非常大,Java不支持代码优化,所以半个小时都没跑完一轮。
之后我就用C语言重新写了一遍:
LoopAndLoop.c
代码优化很重要,不开启的话即使是C语言也快不到哪里去,不开启在Windows下跑,依旧跑不出来:
在Linux下运行并开启了代码优化,正是因为代码优化所以编译出来的程序时间复杂度就缩小了很多,我用gcc编译的,编译的时候开启了-O3(最高优化),结果如下:
用了54秒跑出了结果,就是236492408,输入结果即可得到flag。
为了更加快速的进行的爆破了,我还写了一个多线程版本的,兼容Windows和Linux,还要一件事,具体的线程数还要看看自己的机器,代码如下:
LoopAndLoop_thread.c
实验用到的四个代码都可以在我的Github上下载:https://github.com/OnlyLegends/Bugku–CTF-/tree/master/逆向类CTF