4 月に開催されていた ångstrom CTF 2023 にチーム 0nePadding で参加しました。
得点は 920 点で、最終順位は 151 位 / 1429 チームでした。
今回も Rev 問をいくつか解いたので、簡単に Writeup としてまとめていきます。
もくじ
zaza(Rev)
Ghidra でデコンパイルすると、以下のようなシンプルなコードを取得できました。
void main(void)
{
int iVar1;
size_t sVar2;
long in_FS_OFFSET;
int local_60;
uint local_5c;
char local_58 [72];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
setbuf(stdout,(char *)0x0);
local_60 = 0;
local_5c = 0;
printf("I\'m going to sleep. Count me some sheep: ");
__isoc99_scanf(&DAT_00102092,&local_60);
if (local_60 != 0x1337) {
puts("That\'s not enough sheep!");
/* WARNING: Subroutine does not return */
exit(1);
}
printf("Nice, now reset it. Bet you can\'t: ");
__isoc99_scanf(&DAT_00102092,&local_5c);
if (local_5c * local_60 == 1) {
printf("%d %d",(ulong)local_5c,(ulong)(local_60 + local_5c));
puts("Not good enough for me.");
/* WARNING: Subroutine does not return */
exit(1);
}
puts("Okay, what\'s the magic word?");
getchar();
fgets(local_58,0x40,stdin);
sVar2 = strcspn(local_58,"\n");
local_58[sVar2] = '\0';
xor_(local_58);
iVar1 = strncmp(local_58,"2& =$!-( <*+*( ?!&$$6,. )\' $19 , #9=!1 <*=6 <6;66#",0x32);
if (iVar1 != 0) {
puts("Nope");
/* WARNING: Subroutine does not return */
exit(1);
}
win();
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
以下の箇所に着目しました。
ユーザ入力を 0x40 バイト分受け取り、xor_
関数に与え、結果が2& =$!-( <*+*( ?!&$$6,. )\' $19 , #9=!1 <*=6 <6;66#
に一致する場合には Flag を表示します。
puts("Okay, what\'s the magic word?");
getchar();
fgets(local_58,0x40,stdin);
sVar2 = strcspn(local_58,"\n");
local_58[sVar2] = '\0';
xor_(local_58);
iVar1 = strncmp(local_58,"2& =$!-( <*+*( ?!&$$6,. )\' $19 , #9=!1 <*=6 <6;66#",0x32);
if (iVar1 != 0) {
puts("Nope");
/* WARNING: Subroutine does not return */
exit(1);
}
win();
xor_
関数は以下のような実装になっていました。
入力値の文字列の長さがanextremelycomplicatedkeythatisdefinitelyuselessss
と一致するかを調べた後、入力値の各文字と XOR を行っています。
void xor_(long param_1)
{
size_t sVar1;
int local_24;
local_24 = 0;
while( true ) {
sVar1 = strlen("anextremelycomplicatedkeythatisdefinitelyuselessss");
if (sVar1 <= (ulong)(long)local_24) break;
*(byte *)(param_1 + local_24) =
*(byte *)(param_1 + local_24) ^
"anextremelycomplicatedkeythatisdefinitelyuselessss"[local_24];
local_24 = local_24 + 1;
}
return;
}
というわけで、anextremelycomplicatedkeythatisdefinitelyuselessss
と2& =$!-( <*+*( ?!&$$6,. )\' $19 , #9=!1 <*=6 <6;66#
を XOR してあげると、Flag を取得するために必要なキーがSHEEPSHEEPSHEEPSHEEPSHEEP(OAPXJDIFJWTUTLE_NSLYEHEEB
であると特定できました。
これを問題サーバに送ることで Flag を取得できました。
Bananas(Rev)
問題バイナリとして与えられたElixir.Bananas.beam
は、Elixir によって開発されたものであることがわかります。
デコンパイルには以下のツールを使用しました。
参考:GitHub - michalmuskala/decompile
ツールを使用するためには、端末に Elixir をインストールして mix コマンドを使用できるようにしておく必要があります。
デコンパイルには以下の通り複数のオプションを指定できますが、今回は expanded が一番直感的に読める形式で出力してくれたのでこれを使用しました。
mix decompile ElixirModule --to expanded
mix decompile ElixirModule --to erlang
mix decompile ElixirModule --to asm
mix decompile ElixirModule --to core
出力結果は以下です。
defmodule Bananas do
defp to_integer([num, string]) do
[:erlang.binary_to_integer(num), string]
end
defp to_integer(list) do
list
end
defp print_flag(false) do
IO.puts("Nope")
end
defp print_flag(true) do
IO.puts(File.read!("flag.txt"))
end
def main(args) do
print_flag(check(convert_input(IO.gets("How many bananas do I have?\n"))))
end
def main() do
super([])
end
defp convert_input(string) do
to_integer(String.split(String.trim(string)))
end
defp check([num, "bananas"]) do
:erlang.==(:erlang.-(:erlang.*(:erlang.+(num, 5), 9), 1), 971)
end
defp check(_asdf) do
false
end
end
このコード自体は一部を改変すればそのまま AtCoder などのオンラインサービスで実行できたので、動作確認は非常に簡単でした。
見てわかる通り、数値 banana
のフォーマットで入力を行った場合のみ、check
関数による検証に進みます。
そして、入力値が(数値 + 5) * 9 - 1 = 971
の式を満たす値である 103 が正解となります。
最終的に、103 banana
を問題サーバに送ることで Flag を取得できました。
moon(Rev)
問題文を読むと、バイナリへの正しい入力値が Flag になるようです。
To the moon! The correct sequence of inputs is the flag in ASCII.
バイナリを解析すると、func0 から func1292 までの 1293 個の関数が定義されていることがわかります。
各関数はどれも同じフォーマットになっており、データ部に確保されている check という8バイト * 1293
の領域の先頭から 8 バイトずつに値を足す処理を行っています。
そして、入力値の検証を行う処理を読むと、needed
の値とcheck
の値が一致するか否かを確認しているようでした。
このneeded
もcheck
と同じく8バイト * 1293
の領域ですが、こちらには値がハードコードされています。
また、入力値については最大で 1293 個の数値を受け付け、各桁の数値の回数だけ関数が呼び出されます。
(入力値が 123… の場合、func0 が 1 回、func1 が 2 回、func2 が 3 回実行される。)
そのため、最終的にneeded
とcheck
の値が完全に一致するように 1293 個の関数を実行するような入力値を特定する必要があります。
しかし、ここまで特定したものの、肝心の Solver のアルゴリズムが思いつかず、Flag 取得に至りませんでした。
冷静に考えてみると、各関数の実行回数を x,y,z… とした場合には 1293 個の変数を持つ多項式が 1293 個作成されるため、連立方程式ですべての値を特定できることがわかります。
どうしても頭が凝り固まると Solver のアルゴリズムが思いつかないので、もっと柔軟に考えたいですね。
最終的な Solver は以下を参考にしました。
参考:ångstromCTF 2023 - Reverse Engineering Writeups - FazeCT Blogs
まず、pwntools を使ってneeded
に埋め込まれている 1293 個の値を取得します。
from pwn import *
e = ELF('./moon',checksec=False)
needed = []
for i in range(1293):
data = e.read(e.sym['needed']+i*8, 8)
data = int.from_bytes(data,'little')
needed.append(data)
print(needed)
ここで取得した値を元に連立方程式を構成するためにも、各 func 関数で追加している値を取得します。
この値を取得するために pwntools の ELF モジュールを使用しますが、実行時間には結構な時間を要します。
from sage.all import *
mat = Matrix(check)
mat = mat.T
needed = vector(needed)
print(mat.solve_right(needed))
最終的に、取得したneeded
の値と取得した各関数の加算値を連立方程式で解決し、Flag が取得できます。
Physics HW(Misc)
問題バイナリとして提供された pcap ファイルを解析すると、破損した ZIP ファイルを送信していると思われるデータを確認できます。
pcap から WireShark で出力したファイルを 7-zip で解凍すると中のファイルを確認できます。
解凍したファイルから Flag が取得できました。
まとめ
moon はもうちょいで解けそうだと思ったものの頭が固くて Solver を思いつきませんでした。
3年くらい言ってる気がしますが精進が必要ですね。。