Flare-On Challenge 2023 Writeup (#1, #2)

Flare-On Challengeとは

FireEye社が毎年開催しているReverse Engineeringの技術を問われるCTFです。
実際のマルウェアから発想を得た問題を出題する傾向があり、PEファイルの解析が多いようです。
また、通常のCTFとは違い、簡単な問題から順番に出題されるようになっており、問題を解かなければ次の問題が出題されない形式となっています。

X

Windowsアプリケーションの実行ファイル一式が与えられます。

image

X.exeX.dllの表層解析を行うとX.dllが.NETのruntime上で動作するアプリケーションだということがわかります。

image

image

X.dllをiLSpyで中間言語から復元します。

image

monogame1名前空間以下にゲームロジックのようなものが実装されていることがわかります。

image

このうち、Game1クラス内に何かしらの値が42になった際にフラグが表示される処理がありました。

image

フラグ文字列が取得できたので問題自体は解けたのですが、一応exeを起動してみると以下のようになりました。

image

7セグを42に設定して錠マークをクリックするとフラグが表示されます。

image

ItsOnFire

ItsOnFire.apkというアンドロイドアプリケーションが渡されます。

jadxでソースコードを復元してみると、アプリケーションIDcom.secure.itsonfireにインベーダーゲーム?が実装されているようです。

image

しばらくソースコードを確認しているとResources/res/rawに画像ファイルが配置されていることがわかりました。

image

ただし、これらの画像ファイルはpngとしてInvalidであり、ビューアなどでは開けませんでした。

ファイルのエントロピーを確認してみるとどちらも暗号化されている可能性があることがわかります。

image

よってソースコード内にこれらを復号する処理がある可能性があることを念頭に置いて調査を継続しました。

またしばらく調査を続けるとソースコード内にcom.secure.itsonfire.MessageWorker.onMessageReceivedを起点とする復号の処理が存在することがわかりました。

復号処理に至るまでの関数呼び出しは以下のようになっています。

  1. com.secure.itsonfire.MessageWorker.onMessageReceived
    (MessageWorkerはFirebaseMessagingServiceを継承している)
  2. c.c.a (onMessageReceivedで受信したデータによる処理の分岐)
  3. b.b.f (4のラッパー?)
  4. b.b.c (ファイル復号&保存)

また4の処理で以下の処理が呼ばれていることがわかりました。

順を追って調査していきます。

まずonMessageReceivedによってc.c.aが呼ばれます。

処理は以下のようになっており、onMessageReceivedから渡ってきたparamの値によってb.b.f(bVar.f)に渡すi3の値を分岐させています。
i3の値はR.raw.psR.raw.ivのどちらか、即ち暗号化されたファイルのどちらかになります。

@Nullable
    public final PendingIntent a(@NotNull Context context, @NotNull String param) {
        String string;
        int i2;
        b bVar;
        int i3;
        Intrinsics.checkNotNullParameter(context, "context");
        Intrinsics.checkNotNullParameter(param, "param");
        Intent intent = new Intent();
        if (!Intrinsics.areEqual(param, context.getString(R.string.m1))) {
            if (Intrinsics.areEqual(param, context.getString(R.string.t1))) {
                bVar = b.f360a;
                i3 = R.raw.ps;
            } else if (Intrinsics.areEqual(param, context.getString(R.string.w1))) {
                bVar = b.f360a;
                i3 = R.raw.iv;
            } else if (Intrinsics.areEqual(param, context.getString(R.string.t2))) {
                intent.setAction(context.getString(R.string.av));
                i2 = R.string.t3;
            } else if (!Intrinsics.areEqual(param, context.getString(R.string.f1))) {
                if (Intrinsics.areEqual(param, context.getString(R.string.s1)) || Intrinsics.areEqual(param, context.getString(R.string.s2))) {
                    intent.setAction(context.getString(R.string.av));
                    string = context.getString(R.string.s3);
                    intent.setData(Uri.parse(string));
                }
                return PendingIntent.getActivity(context, 100, intent, 201326592);
            } else {
                intent.setAction(context.getString(R.string.av));
                i2 = R.string.f3;
            }
            return PendingIntent.getActivity(context, 100, bVar.f(context, i3), 201326592);
        }
        intent.setAction(context.getString(R.string.ad));
        i2 = R.string.m2;
        string = context.getString(i2);
        intent.setData(Uri.parse(string));
        return PendingIntent.getActivity(context, 100, intent, 201326592);
    }

b.b.fではb.b.cが呼ばれます。

最後のほうにFilesKt.writeBytesがあることからファイルの書き込みをしているっぽいことがわかります。

そこに至るまでの処理はe(ファイル読み込み)b.b.dnew SecretKeySpec(鍵生成)b.b.bとなっています。

    private final File c(int i2, Context context) {
        Resources resources = context.getResources();
        Intrinsics.checkNotNullExpressionValue(resources, "context.resources");
        byte[] e2 = e(resources, i2);
        String d2 = d(context);
        Charset charset = Charsets.UTF_8;
        byte[] bytes = d2.getBytes(charset);
        Intrinsics.checkNotNullExpressionValue(bytes, "this as java.lang.String).getBytes(charset)");
        SecretKeySpec secretKeySpec = new SecretKeySpec(bytes, context.getString(R.string.ag));
        String string = context.getString(R.string.alg);
        Intrinsics.checkNotNullExpressionValue(string, "context.getString(R.string.alg)");
        String string2 = context.getString(R.string.iv);
        Intrinsics.checkNotNullExpressionValue(string2, "context.getString(\n     …             R.string.iv)");
        byte[] bytes2 = string2.getBytes(charset);
        Intrinsics.checkNotNullExpressionValue(bytes2, "this as java.lang.String).getBytes(charset)");
        byte[] b2 = b(string, e2, secretKeySpec, new IvParameterSpec(bytes2));
        File file = new File(context.getCacheDir(), context.getString(R.string.playerdata));
        FilesKt.writeBytes(file, b2);
        return file;
    }

b.b.eは単純にファイルを読み込む処理でしたので解析はスキップします。

次にb.b.dの処理を確認します。

    private final String d(Context context) {
        String string = context.getString(R.string.c2);
        Intrinsics.checkNotNullExpressionValue(string, "context.getString(R.string.c2)");
        String string2 = context.getString(R.string.w1);
        Intrinsics.checkNotNullExpressionValue(string2, "context.getString(R.string.w1)");
        StringBuilder sb = new StringBuilder();
        sb.append(string.subSequence(4, 10));
        sb.append(string2.subSequence(2, 5));
        String sb2 = sb.toString();
        Intrinsics.checkNotNullExpressionValue(sb2, "StringBuilder().apply(builderAction).toString()");
        byte[] bytes = sb2.getBytes(Charsets.UTF_8);
        Intrinsics.checkNotNullExpressionValue(bytes, "this as java.lang.String).getBytes(charset)");
        long a2 = a(bytes);
        StringBuilder sb3 = new StringBuilder();
        sb3.append(a2);
        sb3.append(a2);
        String sb4 = sb3.toString();
        Intrinsics.checkNotNullExpressionValue(sb4, "StringBuilder().apply(builderAction).toString()");
        return StringsKt.slice(sb4, new IntRange(0, 15));
    }

ここではR.string.c2R.string.w1を元に文字列が生成されていることがわかります。

この二つはres/values/strings.xmlにて以下のように定義されています。

<string name="c2">https://flare-on.com/evilc2server/report_token/report_token.php?token=</string>
<string name="w1">wednesday</string>

この文字列を元にb.b.dの処理をPythonで再現すると以下のようになります。

import binascii
c2 = b"https://flare-on.com/evilc2server/report_token/report_token.php?token="
w1 = b"wednesday"

key = (str(binascii.crc32(c2[4:10]+w1[2:5])).encode("utf-8")*2)[0:16]

また、b.b.aが呼ばれている箇所がありますが、b.b.aは単純なCRC32計算のみでした。

b.b.dで生成した文字列はb.b.c内でnew secretKeySpecの第一引数として渡されます。

第二引数のR.string.agはres/values/strings.xml`にて以下のように定義されています。

<string name="ag">AES</string>

その後、R.string.algR.string.ivが参照されます。

これらはres/values/strings.xml`にて以下のように定義されています。

<string name="alg">AES/CBC/PKCS5Padding</string>
<string name="iv">abcdefghijklmnop</string>

そして、b.b.bに対して以下が渡されます。

byte[] b2 = b(string, e2, secretKeySpec, new IvParameterSpec(bytes2));

b.b.bはパラメータを使用して復号を行う処理です。

(cipher.initの第一引数2DECRYPT_MODEを表します。)

    private final byte[] b(String str, byte[] bArr, SecretKeySpec secretKeySpec, IvParameterSpec ivParameterSpec) {
        Cipher cipher = Cipher.getInstance(str);
        cipher.init(2, secretKeySpec, ivParameterSpec);
        byte[] doFinal = cipher.doFinal(bArr);
        Intrinsics.checkNotNullExpressionValue(doFinal, "cipher.doFinal(input)");
        return doFinal;
    }

ここまでで復号に必要な情報が揃っているのでスクリプトを書きます。

from Crypto.Cipher import AES
import binascii

c2 = b"https://flare-on.com/evilc2server/report_token/report_token.php?token="
w1 = b"wednesday"
iv = b"abcdefghijklmnop"

key = (str(binascii.crc32(c2[4:10]+w1[2:5])).encode("utf-8")*2)[0:16]

aes = AES.new(key, AES.MODE_CBC, iv)

iv_png = open("C:\\Users\\rikoteki\\Desktop\\Repository\\flare-on\\ItsOnFire\\app\\src\\main\\res\\raw\\iv.png", "rb").read()
ps_png = open("C:\\Users\\rikoteki\\Desktop\\Repository\\flare-on\\ItsOnFire\\app\\src\\main\\res\\raw\\ps.png", "rb").read()

open("./dec_iv.png", "wb").write(aes.decrypt(iv_png))
open("./dec_ps.png", "wb").write(aes.decrypt(ps_png))

このスクリプトを実行すると画像が復号され、iv.pngにフラグが描画されていました。

image