[SECCON Beginners CTF 2020] siblangs

Well, they look so similar... siblangs.apk

解説

apkファイルはただのzipファイルなのでとりあえずunzipします

ctf4b{で展開したフォルダを検索に掛けるとassets/index.android.bundleが見つかります
どうやらReact Native製のアプリのようです
さっそく周辺の処理を見てみましょう

function v() {
    var t;
    (0, l.default)(this, v);
    for (var o = arguments.length, n = new Array(o), c = 0; c < o; c++) n[c] = arguments[c];
    return (t = y.call.apply(y, [this].concat(n))).state = {
        flagVal: "ctf4b{",
        xored: [34, 63, 3, 77, 36, 20, 24, 8, 25, 71, 110, 81, 64, 87, 30, 33, 81, 15, 39, 90, 17, 27]
    }, t.handleFlagChange = function(o) {
        t.setState({
            flagVal: o
        })
    }, t.onPressValidateFirstHalf = function() {
        if ("ios" === h.Platform.OS) {
            for (var o = "AKeyFor" + h.Platform.OS + "10.3", l = t.state.flagVal, n = 0; n < t.state.xored.length; n++)
                if (t.state.xored[n] !== parseInt(l.charCodeAt(n) ^ o.charCodeAt(n % o.length), 10)) return void h.Alert.alert("Validation A Failed", "Try again...");
            h.Alert.alert("Validation A Succeeded", "Great! Have you checked the other one?")
        } else h.Alert.alert("Sorry!", "Run this app on iOS to validate! Or you can try the other one :)")
    }, t.onPressValidateLastHalf = function() {
        "android" === h.Platform.OS ? p.default.validate(t.state.flagVal, function(t) {
            t ? h.Alert.alert("Validation B Succeeded", "Great! Have you checked the other one?") : h.Alert.alert("Validation B Failed", "Learn once, write anywhere ... anywhere?")
        }) : h.Alert.alert("Sorry!", "Run this app on Android to validate! Or you can try the other one :)")
    }, t
}

とても怪しげなxordという数列と"AKeyFor" + h.Platform.OS + "10.3"を鍵にしてxorを掛けてることが分かります
エラー表示からiOS上で実行することを想定してるみたいなのでh.Platform.OSにはiosが入り鍵はAkeyForios10.3となります
では復号しましょう

#!/usr/bin/env python3
from Crypto.Cipher import AES

c1 = [34, 63, 3, 77, 36, 20, 24, 8, 25, 71, 110, 81, 64, 87, 30, 33, 81, 15, 39, 90, 17, 27]
o = "AKeyForios10.3"
flag = ""

for i in range(len(c1)):
    flag += chr(c1[i] ^ ord(o[i % len(o)]))

print(flag)

ctf4b{jav4_and_j4va5cr

正しそうですが後ろが欠けています
どうやら後半は別のところにあるようです

xor以降の処理を追ってみるとp.default.validateという関数があるのが分かります
しかし、assets/index.android.bundle内でvalidateを検索を掛けても関数が見つかりません

どうやらネイティブメソッドを呼んでいるようです
dex2jarを使いネイティブメソッドを解析してきます
es/o0i/challengeapp/nativemodule/ValidateFlagModule.classという見るからに怪しいファイルがあるのでcfrでデコンパイルしてみましょう

/*
 * Decompiled with CFR 0.149.
 * 
 * Could not load the following classes:
 *  com.facebook.react.bridge.Callback
 *  com.facebook.react.bridge.ReactApplicationContext
 *  com.facebook.react.bridge.ReactContextBaseJavaModule
 *  com.facebook.react.bridge.ReactMethod
 */
package es.o0i.challengeapp.nativemodule;

import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.security.Key;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class ValidateFlagModule
extends ReactContextBaseJavaModule {
    private static final int GCM_IV_LENGTH = 12;
    private final ReactApplicationContext reactContext;
    private final SecretKey secretKey;
    private final SecureRandom secureRandom = new SecureRandom();

    public ValidateFlagModule(ReactApplicationContext reactApplicationContext) {
        super(reactApplicationContext);
        this.secretKey = new SecretKeySpec("IncrediblySecure".getBytes(), 0, 16, "AES");
        this.reactContext = reactApplicationContext;
    }

    public String getName() {
        return "ValidateFlagModule";
    }

    @ReactMethod
    public void validate(String arrby, Callback callback) {
        byte[] arrby2;
        byte[] arrby3 = arrby2 = new byte[43];
        arrby2[0] = 95;
        arrby3[1] = -59;
        arrby3[2] = -20;
        arrby3[3] = -93;
        arrby3[4] = -70;
        arrby3[5] = 0;
        arrby3[6] = -32;
        arrby3[7] = -93;
        arrby3[8] = -23;
        arrby3[9] = 63;
        arrby3[10] = -9;
        arrby3[11] = 60;
        arrby3[12] = 86;
        arrby3[13] = 123;
        arrby3[14] = -61;
        arrby3[15] = -8;
        arrby3[16] = 17;
        arrby3[17] = -113;
        arrby3[18] = -106;
        arrby3[19] = 28;
        arrby3[20] = 99;
        arrby3[21] = -72;
        arrby3[22] = -3;
        arrby3[23] = 1;
        arrby3[24] = -41;
        arrby3[25] = -123;
        arrby3[26] = 17;
        arrby3[27] = 93;
        arrby3[28] = -36;
        arrby3[29] = 45;
        arrby3[30] = 18;
        arrby3[31] = 71;
        arrby3[32] = 61;
        arrby3[33] = 70;
        arrby3[34] = -117;
        arrby3[35] = -55;
        arrby3[36] = 107;
        arrby3[37] = -75;
        arrby3[38] = -89;
        arrby3[39] = 3;
        arrby3[40] = 94;
        arrby3[41] = -71;
        arrby3[42] = 30;
        byte[] arrby4 = Cipher.getInstance("AES/GCM/NoPadding");
        GCMParameterSpec gCMParameterSpec = new GCMParameterSpec(128, arrby2, 0, 12);
        arrby4.init(2, (Key)this.secretKey, gCMParameterSpec);
        arrby4 = arrby4.doFinal(arrby2, 12, arrby2.length - 12);
        arrby = arrby.getBytes();
        int n = 0;
        do {
            if (n >= arrby4.length) break;
            if (arrby[n + 22] != arrby4[n]) {
                callback.invoke(new Object[]{false});
                return;
            }
            ++n;
        } while (true);
        try {
            callback.invoke(new Object[]{true});
        }
        catch (Exception exception) {
            callback.invoke(new Object[]{false});
        }
    }
}

AESで暗号化された配列を復号して入力文字列と比較しているようです
復号された文字を抜くためにちょっとコードを弄ります

import java.security.Key;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

class ValidateFlagModule {
    private static final int GCM_IV_LENGTH = 12;
    private final SecureRandom secureRandom = new SecureRandom();

    public static void main(String[] args) {
        byte[] arrby2;
        byte[] arrby3 = arrby2 = new byte[43];
        arrby2[0] = 95;
        arrby3[1] = -59;
        arrby3[2] = -20;
        arrby3[3] = -93;
        arrby3[4] = -70;
        arrby3[5] = 0;
        arrby3[6] = -32;
        arrby3[7] = -93;
        arrby3[8] = -23;
        arrby3[9] = 63;
        arrby3[10] = -9;
        arrby3[11] = 60;
        arrby3[12] = 86;
        arrby3[13] = 123;
        arrby3[14] = -61;
        arrby3[15] = -8;
        arrby3[16] = 17;
        arrby3[17] = -113;
        arrby3[18] = -106;
        arrby3[19] = 28;
        arrby3[20] = 99;
        arrby3[21] = -72;
        arrby3[22] = -3;
        arrby3[23] = 1;
        arrby3[24] = -41;
        arrby3[25] = -123;
        arrby3[26] = 17;
        arrby3[27] = 93;
        arrby3[28] = -36;
        arrby3[29] = 45;
        arrby3[30] = 18;
        arrby3[31] = 71;
        arrby3[32] = 61;
        arrby3[33] = 70;
        arrby3[34] = -117;
        arrby3[35] = -55;
        arrby3[36] = 107;
        arrby3[37] = -75;
        arrby3[38] = -89;
        arrby3[39] = 3;
        arrby3[40] = 94;
        arrby3[41] = -71;
        arrby3[42] = 30;
        try{
            Cipher arrby4 = Cipher.getInstance("AES/GCM/NoPadding");
            GCMParameterSpec gCMParameterSpec = new GCMParameterSpec(128, arrby2, 0, GCM_IV_LENGTH);
            SecretKey secretKey = new SecretKeySpec("IncrediblySecure".getBytes(), 0, 16, "AES");
            arrby4.init(javax.crypto.Cipher.DECRYPT_MODE, (Key)secretKey, gCMParameterSpec);
            byte[] d = arrby4.doFinal(arrby2, 12, arrby2.length - 12);
            int n = 0;
            System.out.println(new String(d));
        } catch (Exception e) {}
    }
}

1pt_3verywhere}

2つ合わせるとflag

ctf4b{jav4_and_j4va5cr1pt_3verywhere}