All Articles

Cyber Security Rumble CTF 2023 Writeup

7/8 から開催されていた Cyber Security Rumble CTF 2023 に 0nePadding として参加し、35 位/ 622 チームでした。

今回は日本の参加者が少なかったようで、まさかの国内チームとしては 1 位の結果でした笑

いつも通り Writeup を書いていきます。

SHELLCODE-CEPTION(Rev)

I think I lost my flag in some kind of inception. Can you help me find my flag?

問題バイナリをデコンパイルすると、以下のような出力を得られました。

image-20230709011605412

プログラム内のデータを XOR してメモリに保存して、その領域に mprotect で PROT_EXEC などの属性を設定しています。

参考:mprotect(2) - Linux manual page

このことから、プログラム内でデコードしたシェルコードを実行する機能を持っていると推測できました。

そこで、以下のスクリプトで対象のシェルコードをファイルとして抽出します。

data = b'\x3c\x21\xe0\x8c\x21\xd1\x6b\x7b\x7a\x53\x58\x1c\x4b\x43\x21\xd3\x41\x46\x4f\x77\x19\x46\x4b\x1b\x21\xe0\x2c\xb9\x21\xe0\x3c\xb1\x21\xd1\x58\x5c\x19\x18\x46\x77\x4b\x1c\x21\xd3\x46\x77\x4a\x4d\x77\x1c\x46\x46\x21\xe0\x2c\x89\x21\xe0\x3c\x81\xae\x2c\x99\x47\x51\x19\x46\x0f\xae\x2c\x9d\x4f\x55\xaf\x2c\x9f\x69\xae\x2c\x95\x69\x69\x69\x69\x82\x75\xe2\x2c\x95\x21\xf1\x66\xdf\x2d\x6c\xb9\xea\x99\x28\xe0\xab\xe2\x2c\x95\x21\xf1\xe1\x3d\x6c\xb9\xea\x2c\x95\x68\xea\x14\x95\x4c\x17\xb7\xae\x2c\x91\x69\x69\x69\x69\x82\x67\xe2\x2c\x91\x21\xf1\xaf\x2d\x6c\xb9\x69\xea\x2c\x91\x68\xea\x14\x91\x4c\x17\x85\xf9\x34\xaa'
dist = b''
for d in data:
    dist += (d^0x69).to_bytes(1,'big')
with open("shellcode", "wb") as f:
    f.write(dist)

抽出したシェルコードをさらに Ghidra で解析すると、以下の結果が得られました。

image-20230709011545535

処理自体はシンプルでしたので、以下の Solver を作成して Flag を取得しました。

import struct

a = 0x2a2275313a131202
b = 0x72222f701e262f28
c = 0x75221e2f71703531
d = 0x2f2f751e24231e2f
e = 0x2f70382e
f = 0x3c26

# https://docs.python.org/ja/3.9/library/struct.html
data = struct.pack("<qqqqih", a, b, c, d,e,f)
for d in data:
    print(chr(d^0x41),end="")

# CSR{p4cking_1nc3pt10n_c4n_be_4nnoy1ng}

LIGHTBULB(Rev)

I have a very nice light bulb at home, and I found out I can switch with my phone using the app I wrote.

But the app only works on my phone, so good luck switching my light!

問題バイナリとして apk ファイルが与えられます。

エミュレータで実行してみると、パスワードを使用してログインをした後に、電灯スイッチの ON/OFF を切り替えることができるアプリであることがわかりました。

とりあえず apktool で展開したファイルを一通り眺めていくと、ログインパスワードに使用する情報である 583908295080 が平文で格納されていることがわかります。

そして、ログイン後の動作を追うために Smali2Java を使用して Smali ファイルを Java にデコンパイルした結果を確認しました。

すると、以下の処理が LightSwitchingActivity で行われていることがわかります。

String string = sharedPreferences.getString("secret_key", "");
Log.d("LIGHTSWITCH", "the sk was " + string);
Intrinsics.checkNotNull(string);
byte[] bytes = string.getBytes(Charsets.UTF_8);
Intrinsics.checkNotNullExpressionValue(bytes, "this as java.lang.String).getBytes(charset)");
APIKeyHash aPIKeyHash = new APIKeyHash(bytes);
StringBuilder sb = new StringBuilder("CSR{");
byte[] bytes2 = "APIKEY".getBytes(Charsets.UTF_8);
Intrinsics.checkNotNullExpressionValue(bytes2, "this as java.lang.String).getBytes(charset)");
this.apiKey = sb.append(LightSwitchingActivityKt.toHex(aPIKeyHash.hash(bytes2))).append('}').toString();
RequestQueue newRequestQueue = Volley.newRequestQueue((Context) this);
Intrinsics.checkNotNullExpressionValue(newRequestQueue, "newRequestQueue(this)");
this.requestQueue = newRequestQueue;
SwitchCompat findViewById = findViewById(2131231147);
Intrinsics.checkNotNullExpressionValue(findViewById, "findViewById(R.id.switch_light_bulb)");
findViewById.setOnCheckedChangeListener(new LightSwitchingActivity$.ExternalSyntheticLambda0(this));

ここではまず、ログインパスワードにもなっている secret_key の文字列をバイト列に変換した後に、APIKeyHash クラスを作成しています。

その後、APIKEY という文字列をバイト列に変換したものを引数として APIKeyHash クラスの hash メソッドを呼び出します。

そして、hash メソッドの戻り値になるバイト列をさらに LightSwitchingActivityKt.toHex() に与えた結果が Flag になりることがわかりました。

APIKeyHash クラスと LightSwitchingActivityKt の実装は、さきほどと同じく Smali2Java を使用することで Java にデコンパイルすることで確認しました。

// LightSwitchingActivityKt 
package club.redrocket.lightbulb;

import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
/* compiled from: LightSwitchingActivity.kt */
@Metadata(d1 = {"\u0000\u0012\n\u0000\n\u0002\u0010\u0019\n\u0000\n\u0002\u0010\u000e\n\u0002\u0010\u0012\n\u0000\u001a\n\u0010\u0002\u001a\u00020\u0003*\u00020\u0004\"\u000e\u0010\u0000\u001a\u00020\u0001X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\u0005"}, d2 = {"HEX_CHARS", "", "toHex", "", "", "app_release"}, k = 2, mv = {1, 8, 0}, xi = 48)
/* loaded from: /tmp/jadx-9359415826287627730.dex */
public final class LightSwitchingActivityKt {
    private static final char[] HEX_CHARS;

    static {
        char[] charArray = "0123456789ABCDEF".toCharArray();
        Intrinsics.checkNotNullExpressionValue(charArray, "this as java.lang.String).toCharArray()");
        HEX_CHARS = charArray;
    }

    public static final String toHex(byte[] bArr4) {
        Intrinsics.checkNotNullParameter(bArr4, "<this>");
        StringBuffer stringBuffer = new StringBuffer();
        for (byte b : bArr4) {
            char[] cArr = HEX_CHARS;
            stringBuffer.append(cArr[(b & 240) >>> 4]);
            stringBuffer.append(cArr[b & 15]);
        }
        String stringBuffer2 = stringBuffer.toString();
        Intrinsics.checkNotNullExpressionValue(stringBuffer2, "result.toString()");
        return stringBuffer2;
    }
}

// APIKeyHash 
package club.redrocket.lightbulb;

import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
/* compiled from: APIKeyHash.kt */
@Metadata(d1 = {"\u0000\u001a\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u0012\n\u0002\b\u0005\n\u0002\u0010\u0002\n\u0002\b\u0002\u0018\u00002\u00020\u0001B\u000f\b\u0000\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0002\u0010\u0004J\u000e\u0010\u0006\u001a\u00020\u00032\u0006\u0010\u0007\u001a\u00020\u0003J\b\u0010\b\u001a\u00020\tH\u0002J\u0006\u0010\n\u001a\u00020\tR\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000R\u000e\u0010\u0005\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\u000b"}, d2 = {"Lclub/redrocket/lightbulb/APIKeyHash;", "", "key", "", "([B)V", "s", "hash", "plaintext", "initializeS", "", "reset", "app_release"}, k = 1, mv = {1, 8, 0}, xi = 48)
/* loaded from: /tmp/jadx-2851593408582218842.dex */
public final class APIKeyHash {
    private final byte[] key;
    private final byte[] s;

    public APIKeyHash(byte[] bArr) {
        Intrinsics.checkNotNullParameter(bArr, "key");
        this.key = bArr;
        this.s = new byte[256];
        if ((bArr.length == 0) || bArr.length > 256) {
            throw new IllegalArgumentException("key length must be between 1 and 256");
        }
        initializeS();
    }

    private final void initializeS() {
        byte[] bArr = new byte[256];
        for (int i = 0; i < 256; i++) {
            this.s[i] = (byte) i;
            byte[] bArr2 = this.key;
            bArr[i] = bArr2[i % bArr2.length];
        }
        int i2 = 0;
        for (int i3 = 0; i3 < 256; i3++) {
            byte[] bArr3 = this.s;
            byte b = bArr3[i3];
            i2 = (i2 + b + bArr[i3]) & 255;
            byte b2 = bArr3[i2];
            bArr3[i2] = b;
            bArr3[i3] = b2;
        }
    }

    public final void reset() {
        initializeS();
    }

    public final byte[] hash(byte[] bArr) {
        Intrinsics.checkNotNullParameter(bArr, "plaintext");
        byte[] bArr2 = new byte[bArr.length];
        int length = bArr.length;
        int i = 0;
        int i2 = 0;
        for (int i3 = 0; i3 < length; i3++) {
            i = (i + 1) & 255;
            byte[] bArr3 = this.s;
            byte b = bArr3[i];
            i2 = (i2 + b) & 255;
            byte b2 = bArr3[i2];
            bArr3[i2] = b;
            bArr3[i] = b2;
            bArr2[i3] = (byte) (bArr3[(b2 + bArr3[i2]) & 255] ^ bArr[i3]);
        }
        return bArr2;
    }
}

上記のデコンパイル結果を元に以下の Solver を Java で作成し、アプリの動作を再現することで、静的解析で Flag を取得することに成功しました。

import java.util.Arrays;
import java.nio.charset.StandardCharsets;

public class Solver {    
    public static void main(String[] args) {
        byte[] key;
        byte[] s;
        byte[] bytes = "583908295080".getBytes(StandardCharsets.UTF_8);
        key = bytes;
        s = new byte[256];

        // initializeS
        byte[] bArr = new byte[256];
        for (int i = 0; i < 256; i++) {
            s[i] = (byte) i;
            byte[] bArr2 = key;
            bArr[i] = bArr2[i % bArr2.length];
        }

        int i4 = 0;
        for (int i3 = 0; i3 < 256; i3++) {
            byte[] bArr3 = s;
            byte b = bArr3[i3];
            i4 = (i4 + b + bArr[i3]) & 255;
            byte b2 = bArr3[i4];
            bArr3[i4] = b;
            bArr3[i3] = b2;
        }

        // hash
        byte[] bhash = "APIKEY".getBytes(StandardCharsets.UTF_8);
        byte[] bArr4 = new byte[bhash.length];
        int length = bhash.length;
        int i5 = 0;
        int i6 = 0;
        for (int i7 = 0; i7 < length; i7++) {
            i5 = (i5 + 1) & 255;
            byte[] bArr5 = s;
            byte b = bArr5[i5];
            i6 = (i6 + b) & 255;
            byte b2 = bArr5[i6];
            bArr5[i6] = b;
            bArr5[i5] = b2;
            bArr4[i7] = (byte) (bArr5[(b2 + bArr5[i6]) & 255] ^ bhash[i7]);
        }

        char[] charArray = "0123456789ABCDEF".toCharArray();
        for (int j = 0; j < bArr4.length; j++) {
            System.out.print(charArray[(bArr4[j] & 240) >>> 4]);
            System.out.print(charArray[(bArr4[j] & 15)]);
        }
    }
}
// DE404B983322

RANSOMWARE TRAFFIC ANALYSIS PART 1(Forensic)

DISCLAIMER: This might contain real ransomware. DO NOT EXECUTE ANY FILES IN AN UNSAFE ENVIRONMENT. Please use a virtual machine for this challenge.

ARC Industries dedicated an entire month of hard work and effort to create a tender document for a highly esteemed project that held the key to their financial stability. Unfortunately, on Thursday, May 25th 2023, the company fell victim to a malicious act of ransomware, suspected to be orchestrated by a rival competitor. As a result, the crucial final version of the tender document has been rendered inaccessible due to encryption. It is both unreadable and has a file extension appended to the filename. Urgently, ARC Industries seeks the assistance of a skilled expert capable of decrypting this invaluable document.

Armed with only the network traffic logs, it’s time for you to showcase your expertise, Defender!!

問題バイナリとしてランサムウェアに感染した端末の pcap ファイルが与えられます。

pcap を調査してみると、HTTP や SMB でいくつか怪しげなファイルのやり取りをしています。

image-20230709010301843

特に SMB の通信を見たところ、以下のようなランサムノートとともに暗号化された .micro 拡張子付きのファイルが送信されていました。

image-20230709010859694

これは、以下の記事のランサムウェアの特徴に一致します。

参考:Micro Ransomware - Decryption, removal, and lost files recovery (updated)

このランサムウェアは TESLACRYPT と呼ばれるマルウェアのようですが、どうやらすでに攻撃者が復号用のマスターキーを開示しているようです。

そのため、複数のセキュリティベンダが TESLACRYPT の復号ツールを公開していました。

今回は、Trendmicro のツールを利用します。

参考:Using the Trend Micro Ransomware File Decryptor Tool

上記からダウンロードしたツールを起動し、以下のように TESLACRYPT を指定して問題バイナリを復号します。

image-20230709011100079

復号が完了すると、以下のように Flag を取得することができました。

image-20230709011117849

RANSOMWARE TRAFFIC ANALYSIS PART 2(Forensic)

DISCLAIMER: This might contain real ransomware. DO NOT EXECUTE ANY FILES IN AN UNSAFE ENVIRONMENT. Please use a virtual machine for this challenge.

In the aftermath of the ransomware attack, ARC Industries faces a new challenge. The threat actor has hidden the vital private key required for decryption within their network. As the defender, your mission is to tirelessly locate this elusive key. Analyze network traffic logs, investigate suspicious activities, and unravel the threat actor’s techniques. Time is critical as the financial stability of ARC Industries hangs in the balance. Your expertise is essential to recover the key and restore the remaining files.

Take up the challenge and safeguard the future of the organization. Success relies on your determination and investigative skills. Armed with only the network traffic logs, it’s time for you to showcase your expertise, Defender!! Your colleague who captured the network traffic logs also started gathering the evidence and put this in a fileshare…

PART 1 は面白かったのですが、正直 PART 2 は意図がよくわかりませんでした。(そして結局 Solver も 1 チームのみという)

PART 1 と同じ pcap から鍵を見つけろという問題でしたが、正直とっかかりがなさすぎて全くわかりませんでした。

どうやら、SMB でやり取りしている eml ファイルに着目する必要があったようです。

image-20230709104545133

このメールには画像が 2 つ添付されているので、まずは画像をロードしました。

これをファイルとして保存してみると、フッターに使用されている jpg ファイルにステガノで Flag が埋め込まれていました。

以下のコマンドで Flag を取得できます。

steghide extract -sf image.jpg

解凍用のパスワードは、HTTP でやり取りしている password.txt に記載されていたものを使用しました。

まとめ

RISC-V の Reversing 問題が解けなかったので、もう少し勉強したら改めて Writeup を書きます。