re学习笔记(101)i春秋 全国大学生信息安全竞赛 计时挑战

本文首发于 i春秋公众号 巧用Frida与Unidbg快速在CTF中解题

题目名称:计时挑战,你能当人肉计时器么?
题目下载地址:https://pan.baidu.com/s/11BcKF6LTWQTeqYmUuP_ukA 提取码(fjiw)
题目练习地址:https://www.ichunqiu.com/battalion?t=1&r=70900

巧用Frida与Unidbg快速在CTF中解题

Frida是一个方便快捷的hook框架,在安卓逆向中是必不可少的hook工具。而对代码量不大的CTF安卓逆向题目来讲,使用Frida更是如鱼得水,调试程序、打印输出等都极为方便,就算有相应的检测,由于CTF中代码量并不是很大的情况,也可以快捷的定位、并对其进行hook绕过。而Unidbg可以对so层中的函数进行模拟执行,对于CTF题目来讲,往往无需太多的“补环境”,Unidbg就能将指定的函数模拟执行起来并获得结果。在本文中笔者将以全国大学生信息安全竞赛中的“计时挑战”为例,对两种工具的实践使用进行讲解。

初步分析

计时挑战

你能当人肉计时器么?

将apk安装到测试机中,先查看功能

re学习笔记(101)i春秋 全国大学生信息安全竞赛 计时挑战_第1张图片

有两个按钮,当点击START按钮时 END按钮下的文本框会开始输出计时,当点击了END按钮后,会出现提示too soon。当再次点击START按钮时,会重新进行计时。

Java层分析

观察完题目的大概作用后,使用Jdax反编译工具将APK载入,代码量比较少,只有MainActivity一个类。

首先当点击START按钮时,会执行它的onClick方法,代码如下

((Button) findViewById(R.id.bt1)).setOnClickListener(new View.OnClickListener() { // from class: an.droid.j.MainActivity.2
    @Override // android.view.View.OnClickListener
    public void onClick(View view) {
        MainActivity.this.start_time = System.currentTimeMillis();	// 点击按钮后记录点击按钮的初始时间start_time
        MainActivity mainActivity = MainActivity.this;
        mainActivity.count_time = mainActivity.start_time;	// 将计数时间count_time赋值为初始时间start_time
        handler.postDelayed(runnable, 0L);	// 使用handler,开一个线程执行runnable的内容
    }
});

而执行的runnable代码如下:

final Runnable runnable = new Runnable() { // from class: an.droid.j.MainActivity.1
    @Override // java.lang.Runnable
    public void run() {
        MainActivity.this.now_time = System.currentTimeMillis();	// 获取当前时间
        long j = MainActivity.this.now_time - MainActivity.this.count_time;	// 计算时间的差值j
        MainActivity mainActivity = MainActivity.this;
        mainActivity.df_all = mainActivity.now_time - MainActivity.this.start_time;	// 计算时间的总差值df_all
        if (j > 100) {	// 当时间差值j大于100时
            MainActivity.this.count_time += 100;	// 将计数count_time添加100ms,确保差值j始终<100ms,达到每100毫秒就执行一次if内语句的效果
            MainActivity mainActivity2 = MainActivity.this;
            mainActivity2.zygote = mainActivity2.p(mainActivity2.zygote);	// 调用p方法,更新zygote
            textView.setText(String.format("Time:%.1f", Double.valueOf(Math.floor(MainActivity.this.df_all / 100) / 10.0d)));	// 将时间秒数更新到文本框上
        }
        handler.postDelayed(this, 30L);	// 每30秒执行一次run方法
    }
};

当点击END按钮时,执行的回调方法如下:

((Button) findViewById(R.id.bt2)).setOnClickListener(new View.OnClickListener() { // from class: an.droid.j.MainActivity.3
    @Override // android.view.View.OnClickListener
    public void onClick(View view) {
        handler.removeCallbacks(runnable);
        if (MainActivity.this.df_all / 100 == 99999) {	// 如果当点击按钮时候,时间总差值恰好为9999秒
            TextView textView2 = textView;
            StringBuilder sb = new StringBuilder();
            sb.append("flag{");
            MainActivity mainActivity = MainActivity.this;
            sb.append(mainActivity.j(mainActivity.zygote));		// 执行j方法,传入迭代后的zygote来得到flag,如果传入的zygote不正确,也无法得到正确的flag
            sb.append("}");
            textView2.setText(sb.toString());	// 将flag字符串设置到文本框上来展示
        } else if (MainActivity.this.df_all / 100 < 9999) {
            textView.setText("too soon");	// 时间太短,提示
        } else {
            textView.setText("too late");	// 时间太长,提示
        }
    }
});

到这里,我们便对该APK的执行流程分析完成了。当点击START按钮时,程序就开始计时,并每隔100毫秒对zygote进行一次p方法操作。当点击END按钮时,如果时间恰好过了9999900毫秒,则计算并输出正确的flag。

每隔100毫秒执行一次p方法,所以说我们来计算输出正确flag时,需要执行99999p方法来得到正确的zygote。最后通过j方法来计算获得最终的flag。

public class MainActivity extends AppCompatActivity {
    long df_all;

    /* renamed from: t */
    long start_time;

    /* renamed from: t1 */
    long count_time;

    /* renamed from: t2 */
    long now_time;
    int zygote = 1357024680;

    public native String j(int i);

    public native int p(int i);

    static {
        System.loadLibrary("j");
    }
}

而其中涉及到的p方法与j方法均为native方法,接下来便是需要深入so层分析了。

so层分析

取出位于APK压缩包中lib/armeabi/下的libj.sobak_libj.so文件,虽然在程序代码System.loadLibrary("j");中只加载了libj.so一个so文件,但这里却有两个。

libj.so

先使用IDA32载入libj.so,对so文件进行分析。第一步先查看有无初始化函数,发现没有;接着查看是否有JNI_OnLoad函数,发现也没有,那就是静态注册了。

我们很明显的发现在函数列表中存在三个静态注册的函数,如下图所示。

re学习笔记(101)i春秋 全国大学生信息安全竞赛 计时挑战_第2张图片

但在Java层只用了其中两个函数,先不管它,先分析p方法。双击进入p方法,缩略图就让人望而却步。

re学习笔记(101)i春秋 全国大学生信息安全竞赛 计时挑战_第3张图片

但如今我们有葫芦娃大佬编写的obpo-project/obpo-plugin: An ida plugin for recovering control flow flattening (github.com)插件。使用插件后F5反编译结果如下图所示。

re学习笔记(101)i春秋 全国大学生信息安全竞赛 计时挑战_第4张图片

我们当然可以对这部分代码还原为C代码来编写脚本计算,但在比赛过程中,为了争分夺秒且保证准确性来讲,我们可以使用Frida来进行Java层的主动调用或者Unidbg来进行模拟执行。

当然我们先分析完其余的函数,查看j方法

re学习笔记(101)i春秋 全国大学生信息安全竞赛 计时挑战_第5张图片

该函数恒定返回结果"FlagLostHelpMeGetItBack",也就是对于源程序的调用逻辑来讲,就算真的能把握好时间,也没有办法获得真正的flag,因为该函数只会返回一个虚假的固定字符串。

继续查看init方法,该方法并没有在Java层调用。

re学习笔记(101)i春秋 全国大学生信息安全竞赛 计时挑战_第6张图片

该方法是将传入的double数据,分别取高四位与低四位作为两个数据,然后计算一个含“libinit”的字符串并返回。

bak_libj.so

该so含有两个静态注册的函数

请添加图片描述

ej函数并没有做操作,自然我将目标放在了j函数上,该函数的逻辑如下图

re学习笔记(101)i春秋 全国大学生信息安全竞赛 计时挑战_第7张图片

同样是接受一个double类型的数据,然后让其高四位与低四位两个数参与运算,最终返回一个含"hav3f4n"的字符串。

对于关键的函数全部分析完毕后,我们便开始动手编写脚本了。

Frida主动调用获得zygote

出于学习与兴趣,在时间不紧迫的情况下,我们可以慢慢对算法进行还原。但在比赛途中,我们需要的是最短的时间解出题目抢夺Flag,是时候感受一下Frida的威力了。

在原程序中,每100毫秒才会计算一次g方法,这可等不及!我们可以选择使用Frida来主动调用g方法,让其快速的完成99999次的运算。

相关代码如下:

function fff() {
    Java.perform(function () {
        console.log("start");
        var mainActivity = null;
        // 内存中搜索an.droid.j.MainActivity的实例对象
        Java.choose("an.droid.j.MainActivity", {
            onMatch: function (instance) {
                mainActivity = instance;
                console.log(mainActivity.zygote.value);
            },
            onComplete: function () {
                console.log("search completed");
            },
        });

        var n = 0;
        var zy = 1357024680;	// 程序中zygote的初始值
        var start = new Date().getTime();
        for (n = 0; n < 99999 + 1; ++n) {
            console.log(n, "-->", zy);	// 在这里打印zygote的值
            zy = mainActivity.p(zy);
        }
        var stime = new Date().getTime() - start;
        console.log("stime: ",stime,"ms");
    });
}

setTimeout(fff, 2000);	// 延迟2秒,等待MainActivity实例化

编写完脚本后,脚本运行以及输出如下:

$ frida -H 192.168.0.102:8888 -f an.droid.j -l jstz.js --no-pause
     ____
    / _  |   Frida 12.8.0 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://www.frida.re/docs/home/
Spawned `an.droid.j`. Resuming main thread!                             
[Remote::an.droid.j]-> start
1357024680
search completed
0 --> 1357024680
1 --> 1357024967
2 --> 1357024968
3 --> 1357025255
4 --> 1357050156
5 --> 1357050443
6 --> 1357064799
7 --> 1357066339
8 --> 1357066340
9 --> 1357066627
...
...
99992 --> 1738885579
99993 --> 1738885580
99994 --> 1738885867
99995 --> 1738885868
99996 --> 1738886155
99997 --> 1738911056
99998 --> 1738911343
99999 --> 1738911344
stime:  37650 ms

可见只用了37秒,就得到了99999次运算后zygote的结果。

接下来便是对zygote进行j方法运算得到最终的flag了。

libj.so中的Java_an_droid_j_MainActivity_j函数是恒定输出的,我们只能把目标放在Java_an_droid_j_MainActivity_init函数、或bak_libj.so中的Java_an_droid_j_MainActivity_j函数。

由于bak_libj.so并没有被加载,我们使用Unidbg来对其进行模拟调用。

Unidbg模拟执行得到flag

简单搭建一个Unidbg架子,加载bak_libj.so后模拟执行其中的j方法。

package an.droid.j;

import com.github.unidbg.Module;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.DvmObject;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;

import java.io.File;


public class MainActivity {

    public static void main(String[] args) {

        MainActivity mainActivity = new MainActivity();

        int temp = 1738911344;
        System.out.println("flag{" + mainActivity.runJ(temp) + "}");

    }

    private final AndroidEmulator emulator;
    private final Memory memory;
    private final VM vm;
    private Module module;

    public MainActivity() {
        // 创建模拟器
        emulator = AndroidEmulatorBuilder
                .for32Bit().build();
        memory = emulator.getMemory();
        memory.setLibraryResolver(new AndroidResolver(23));
        // 根据APK文件创建VM
        vm = emulator.createDalvikVM(new File(MainActivity.class.getResource("jstz.apk").getFile()));
        vm.loadLibrary(new File(MainActivity.class.getResource("bak_libj.so").getFile()), false);
    }

    public String runJ(double i) {
        DvmObject obj = ProxyDvmObject.createObject(vm, this);
        // 执行函数Java_an_droid_j_MainActivity_j(D)Ljava/lang/String;
        DvmObject object = obj.callJniMethodObject(emulator, "j(D)Ljava/lang/String;", i);
        String result = (String) object.getValue();
        return result;
    }
}

得到结果

flag{ichav3f4nnbp}

但是很遗憾,不正确。

只剩下一个选择了,继续使用Unidbg来模拟执行libj.so中的Java_an_droid_j_MainActivity_init函数。

package an.droid.j;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.DvmObject;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;

import java.io.File;

public class MainActivity {

    public static void main(String[] args) {
        MainActivity mainActivity = new MainActivity();
        int temp  = 1738911344;
        System.out.println("flag{" + mainActivity.runInit(temp) + "}");
    }
    private final AndroidEmulator emulator;
    private final Memory memory;
    private final VM vm;

    public MainActivity() {
        emulator = AndroidEmulatorBuilder
                .for32Bit().build();
        memory = emulator.getMemory();
        memory.setLibraryResolver(new AndroidResolver(23));
        vm = emulator.createDalvikVM(new File(MainActivity.class.getResource("jstz.apk").getFile()));
        vm.setVerbose(false);
        vm.loadLibrary("j",false);
    }

    public  String runInit(int i){
        DvmObject obj = ProxyDvmObject.createObject(vm,this);
        DvmObject object = obj.callJniMethodObject(emulator, "init(D)Ljava/lang/String;", i);
        String result = (String) object.getValue();
        return result;
    }



    public int runP(int i){
        DvmObject obj = ProxyDvmObject.createObject(vm,this);
        int number = obj.callJniMethodInt(emulator, "p(I)I", i);
        return number;
    }
}

得到结果flag{nllibinitxv!},输入、正确,就是我们心心念念的Flag啦!

同样我们也可以使用Unidbg对整个流程进行模拟,相关代码如下

public class MainActivity {
    int zygote = 1357024680;

    public static void main(String[] args) {
        MainActivity mainActivity = new MainActivity();

        // 使用Unidbg模拟执行99999次并计算Flag
        long start = System.currentTimeMillis();
        for(int i =0;i<99999;i++){
            mainActivity.zygote = mainActivity.runP(mainActivity.zygote);
        }
        System.out.println("zygote = " +mainActivity.zygote);
        System.out.println("flag{" + mainActivity.runInit(mainActivity.zygote) + "}");
        long times = System.currentTimeMillis() - start;
        System.out.println("times = " + times);

    }
}

得到的输出如下:

flag{nllibinitxv!}
times = 61553

可见Unidbg的模拟执行速度还是慢了些的,用了整整61秒,不如Frida的主动调用速度快。

总结

通过对本例子的讲解,Frida和Unidbg对于某些情况来说确实是更好的一种选择,特别是在比赛途中时间紧迫的情况下。相信大家已经感受到了Frida与Unidbg的魅力了,还不赶紧学起来!

你可能感兴趣的:(ctf小白成长ing,#,reverse,Android逆向,CTF,逆向,安全,reverse,i春秋)