Malleus CTF Pwnを読んだ

前々からCTF(特にpwn)に興味があったので、Malleus CTF Pwnを読みました。

sanya.sweetduet.info

ページ数もそこまで多くないしさらっと読めるかなと思って買ったのですが、CTFやpwnに関する事前知識ゼロだったので、結局時間をかけてじっくり読むことになりました。その分得るものがたくさんあって、読み応えがあってよかったかなと。あとやっぱりスタックやらmallocやらその辺の仕組みを知っていくのは楽しい。

メモした内容を忘れないように残しておきます。

(最近商業版?も出たらしい)
解題pwnable セキュリティコンテストに挑戦しよう! (技術の泉シリーズ(NextPublishing)) | 草野 一彦 | 工学 | Kindleストア | Amazon

第1章 準備

ツールの説明とか。
使っているPCにそもそもDockerを入れてなかったので入れた。
https://docs.docker.com/engine/install/ubuntu/

第2章 login1, 第3章 login2

スタックバッファオーバーフローについて。このへんはすんなり読めた。

第4章 login3

このあたりから理解に時間がかかるようになってきた。
One-gadget RCEの存在を初めて知った。本来どういう用途で使われているか知らないので、狙われるだけでは?と思ったけど何のための機能なんだろう。

関数のアドレスを表示してランダム化されていること (ASLR) を確認するところで、以下のコードで関数のアドレスを確認していた。

printf("printf: %p\n", printf);

ふとアセンブリはどうなっているのだろうと気になって確認したところ、以下のようになっていた。これだと printf の GOT のスロットのアドレス (0x403fe8) が出力されるのでは?と思ったが、よくよく見ると mov が使われているので、ちゃんとGOTのスロットのアドレスに格納されている値、つまり printf のアドレスが出力される。

mov rax,QWORD PTR [rip+0x2eb7] # 403fe8 <printf@GLIBC_2.2.5>

ちなみに以下だと lea が使われる。コンパイラよくできてる。

int a;
printf("%p\n", &a);

あとはそもそもPythonの文字列とかバイトオブジェクトとかがわかってなかったので、以下を読んだりした。

第5章 rot13

Format String Attack
最初攻撃スクリプトが何をしているか全然わからなかったけど、丁寧に追っていくことでなんとか理解。
printfで書き込み (%n) もできるというのは初耳。

第6章 birdcage

ここからヒープ。

第7章 strstr

mallocががっつりでてきた。ちょっとついていけなくなってしまったのでmalloc動画をまずは見ることにした。

これもわかりやすい。ここまで詳細な文書をまとめることに脱帽。
malloc(3)のメモリ管理構造

あとはソースも適当に見ながら読み進めた。

libc-2.27.so内の main_arena のアドレスを逆アセンブル結果を見て特定するところがよくわからなかった。アセンブリ読めればわかるということかな。

第8章 strstrstr

本と同じようにPwngdbのparseheapとかheapinfoとかが使いたくていろいろ奮闘した。
まずは手元の環境 (Ubuntu 20.04.1 LTS) で配布されたlibc-2.27.soを使うようにpatchelfで実行ファイルにパッチを当ててgdbを動かしてみた。シェルは取れたけど、libc6-dbgがないよと怒られてparseheapはできなかった。

gdb-peda$ parseheap
Can not get libc version
Cannot get main_arena's symbol address. Make sure you install libc debug file (libc6-dbg & libc6-dbg:i386 for debian package).
can't find heap info
gdb-peda$ heapinfo
Can not get libc version
Cannot get main_arena's symbol address. Make sure you install libc debug file (libc6-dbg & libc6-dbg:i386 for debian package).
Can't find heap info

libc6-dbgの存在を知らなかったけど、調べてみるとlibcのデバッグ情報をlibcとは別出しで持っているパッケージとのこと。これがないとparseheapとかはできないらしい。
https://github.com/scwuaptx/Pwngdb#heapinfo

ということで、libc-2.27.soのlibc6-dbgも入れられるように、Ubuntu 18.04で試してみることにした。apt install でlibc6, libc-dbgを入れていざ試してみると、今度はシェルが取れない。。たぶんすでにlibc6にパッチがあたっているからだと思われる。

$ apt list -a libc6
Listing... Done
libc6/bionic-updates,now 2.27-3ubuntu1.4 amd64 [installed]
libc6/bionic-security 2.27-3ubuntu1.2 amd64
libc6/bionic 2.27-3ubuntu1 amd64

今度は一番古いバージョンのlibc6にダウングレードしてみたところ、配布されているlibc-2.27.soと全く同じものがインストールされた。

$ sudo apt install libc6=2.27-3ubuntu1 libc-dev-bin=2.27-3ubuntu1 libc6-dev=2.27-3ubuntu1 libc6-dbg=2.27-3ubuntu1
$ cmp /path/to/libc-2.27.so /lib/x86_64-linux-gnu/libc-2.27.so
$ 

配布されたソースコード-g 付きでコンパイル、止めたいところでbreakしてparseheapすることで、ようやく本と同じ結果が得られた。

第9章 freefree

House of Orange
ここもparseheap、heapinfoでヒープの状態を確認しながら攻撃スクリプトの挙動を追っていった。

第10章 freefree++

File stream oriented programming

第11章 writefree

House of Corrosion
fastbinにチャンクを格納する際のサイズチェックに引っかからないようにするために、各サイズで一旦確保し解放することでヒープ中の次のチャンクのsizeにあたる位置にサイズを書き込んでおく、というところがよくわからず小一時間悩んだ。なぜ一旦確保し解放することで次のチャンクのsizeに書き込めるのか。

元記事だとこのあたり。

This is achieved by allocating then freeing a chunk so that the top chunk size is written to that location.

けど↑にも書いてあるとおり、次のチャンク = top ということに気づいて納得。一旦確保することでtopの位置が変わり、それに伴いtopのサイズも変わるから、正常なサイズが書き込まれるということ。