All Articles

ångstrom CTF 2023 Writeup

4 月に開催されていた ångstrom CTF 2023 にチーム 0nePadding で参加しました。

得点は 920 点で、最終順位は 151 位 / 1429 チームでした。

image-20230429174138393

今回も 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;
}

というわけで、anextremelycomplicatedkeythatisdefinitelyuselessss2& =$!-( <*+*( ?!&$$6,. )\' $19 , #9=!1 <*=6 <6;66#を XOR してあげると、Flag を取得するために必要なキーがSHEEPSHEEPSHEEPSHEEPSHEEP(OAPXJDIFJWTUTLE_NSLYEHEEBであると特定できました。

image-20230429175845337

これを問題サーバに送ることで 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 個の関数が定義されていることがわかります。

image-20230429182044810

各関数はどれも同じフォーマットになっており、データ部に確保されている check という8バイト * 1293の領域の先頭から 8 バイトずつに値を足す処理を行っています。

image-20230429182346601

そして、入力値の検証を行う処理を読むと、neededの値とcheckの値が一致するか否かを確認しているようでした。

image-20230429183003795

このneededcheckと同じく8バイト * 1293の領域ですが、こちらには値がハードコードされています。

また、入力値については最大で 1293 個の数値を受け付け、各桁の数値の回数だけ関数が呼び出されます。

(入力値が 123… の場合、func0 が 1 回、func1 が 2 回、func2 が 3 回実行される。)

そのため、最終的にneededcheckの値が完全に一致するように 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 ファイルを送信していると思われるデータを確認できます。

image-20230422205325945

pcap から WireShark で出力したファイルを 7-zip で解凍すると中のファイルを確認できます。

解凍したファイルから Flag が取得できました。

まとめ

moon はもうちょいで解けそうだと思ったものの頭が固くて Solver を思いつきませんでした。

3年くらい言ってる気がしますが精進が必要ですね。。