Timer(阿里CTF) WriteUp

Author Avatar
ciaoly 2021年08月09日
  • 在其它设备中阅读本文章

题目来源: Timer(阿里CTF) - Bugku CTF

解压题目附件, 是一个APK. 安装到开发机上, 发现有一个倒计时功能. 倒计时结束(大约两天半左右)后即可获取到flag

alictf{Y0vAr3TimerMa3te7}


解法2:

静态分析

使用jadx加载apk进行反编译. 由于该apk并未混淆, 可以非常好的还原其源代码.

jadx得到的关键代码如下所示:

 handler.postDelayed(new Runnable() {
            /* class net.bluelotus.tomorrow.easyandroid.MainActivity.AnonymousClass1 */

            public void run() {
                MainActivity.this.t = System.currentTimeMillis();
                MainActivity.this.now = (int) (MainActivity.this.t / 1000);
                MainActivity.this.t = 1500 - (MainActivity.this.t % 1000);
                tv2.setText("AliCTF");
                if (MainActivity.this.beg - MainActivity.this.now <= 0) {
                    tv1.setText("The flag is:");
                    tv2.setText("alictf{" + MainActivity.this.stringFromJNI2(MainActivity.this.k) + "}"); // 关键代码在这里, 需要解出k
                }
                if (MainActivity.is2(MainActivity.this.beg - MainActivity.this.now)) {
                    MainActivity.this.k += 100;
                } else {
                    MainActivity mainActivity = MainActivity.this;
                    mainActivity.k--;
                }
                tv1.setText("Time Remaining(s):" + (MainActivity.this.beg - MainActivity.this.now));
                handler.postDelayed(this, MainActivity.this.t);
            }
        }, 0);

可以看到, 在第11行处调用了JNI, 该调用利用传入的k值计算出flag的内容并返回. 因此, 求解本题的flag就需要两个关键操作:

  1. 求解k
  2. k值传入函数stringFromJNI2, 解出flag.

求解flag

分两步走, 首先:

求解变量k

变量k的运算过程其实很简单, 就是一个循环过程而已. 使用循环进行重写即可.

//let date = new Date();
//let beg = (Number.parseInt(date.getTime() / 1000)) + 200000;// 因为只是计算时间差值,
let beg = 200000; // 因此并不需要实际的时间值
let t = 0;
let k = 0;

function is2(n) {
    if (n <= 3) {
        return n > 1;
    }
    if (n % 2 == 0 || n % 3 == 0) {
        return false;
    }
    for (let i = 5; i * i <= n; i += 6) {
        if (n % i == 0 || n % (i + 2) == 0) {
            return false;
        }
    }
    return true;
}

//i = date.getTime();
i = 0; // i是毫秒级

while (1) {
    t = i;
    now = Number.parseInt(t / 1000); // now是"秒"级
    t = 1500 - (t % 1000); // t是"毫秒"级
    if (beg - now <= 0) { // beg和now都是"秒"级
        console.log("k is ", k);
        break;
    }
    if (is2(beg - now)) {
        k += 100;
    } else {
        k--;
    }
    i+=t;
}

最终求解出的k值为1616384

传入函数stringFromJNI2, 求解flag

吐槽

起初我想的是使用Ghidra逆向so库, 将函数stringFromJNI2逆向出来执行. 但是奈何对arm的汇编不熟, 捉急了老半天没能把算法逆向出来. 遂决定放弃, 使用动态调试的方式拿flag.

首先尝试使用jadx导出gradle工程, 使用Android Studio导入工程, 连接adb对该应用debug, 从而实现传入kstringFromJNI2, 得到flag. 但是未果, Android Studio编译时出现各种各样奇奇怪怪的Gradle兼容性问题.

正文:

在安卓手机中使用MT管理器打开该app, 直接修改dalvik虚拟机字节码. 之后签名重装, 成功拿到flag.

在MT管理器中打开apk文件, 打开classes.dex, 点击MainActivity$1类.

image-20210809191027321

定位到run方法, 找到取k值的地方, 将k改为定值1616384;

k k改为定值
image-20210809191010538 image-20210809192328014
图中v3寄存器即为变量k

定位到变量begnow大小判断的逻辑, 将差v0直接修改为定值0.

begnow大小判断的逻辑代码 v0修改为定值0
image-20210809191538755 image-20210809192312692

最终改动的字节码如下所示:

    get v1, v1, Lnet/bluelotus/tomorrow/easyandroid/MainActivity;->now:I
    const v0, 0x0
    if-gtz v0, :cond_64

    .line 53
    iget-object v0, p0, Lnet/bluelotus/tomorrow/easyandroid/MainActivity$1;->val$tv1:Landroid/widget/TextView;
    const-string v1, "The flag is:"
    invoke-virtual {v0, v1}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V

    .line 54
    iget-object v0, p0, Lnet/bluelotus/tomorrow/easyandroid/MainActivity$1;->val$tv2:Landroid/widget/TextView;
    new-instance v1, Ljava/lang/StringBuilder;
    invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V
    const-string v2, "alictf{"
    invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v1
    iget-object v2, p0, Lnet/bluelotus/tomorrow/easyandroid/MainActivity$1;->this$0:Lnet/bluelotus/tomorrow/easyandroid/MainActivity;
    iget-object v3, p0, Lnet/bluelotus/tomorrow/easyandroid/MainActivity$1;->this$0:Lnet/bluelotus/tomorrow/easyandroid/MainActivity;
    const v3, 0x18aa00
    invoke-virtual {v2, v3}, Lnet/bluelotus/tomorrow/easyandroid/MainActivity;->stringFromJNI2(I)Ljava/lang/String;

最后再把drwable里面那阴间配色的背景图给换掉, 不然很影响看flag.

最终用MT管理器回签名, 安装新apk, 就可以看到flag了.


关于JNI的逆向可以看一看安卓逆向之自动化 JNI 静态分析 (seebug.org), Ghidra安装插件jni_helper时, Ghidra内置变量$GHIDRA_HOME的目录是位于Ghidra的安装目录下的Ghidra文件夹. 另外, 在Script Manager里右击鼠标可以选择Script Directories菜单项, 在这里可以添加脚本目录.