SECCON Beginners CTF 2020 Writeup
初めに
久しぶりの更新です。SECCON Beginners CTF 2020 にKUDoSで参加しました。 自分はpwnを担当したので解いた問題のwriteupを書きます。
2020年に入ってからwriteup系はgithubに練習がてら英語で書いてたりするので(こっちもあんまり更新できてない)、このブログは久しぶりです。
pythonコードは全部2系です。いい加減3系に移行します。。。
Beginners's Heap
概要
バイナリは配られませんが、接続するとfree_hookのアドレスやwin関数のアドレスを渡してくれます。
コマンド形式でchunkを操作できてヒントを見れるコマンドもあります。
しかも現在の状態に合わせて適切なヒントをくれるので、とても良心的。
ヒント見てもわからない方はglibcのmalloc/freeのchunkの管理手法についてや、tcacheについて調べると良いと思います。
方針
最終的にfree_hookの中身をwin関数のアドレスに書き換えてfreeを呼び出します。
注意すべき点はB=NULLでないとコマンド2は使えないので連続してchunkを確保することはできないようです。
以下、手順
- コマンド2でchunkを確保
- コマンド3でfreeしてchunkをtcacheに繋ぐ
- コマンド1でtcacheに繋いだchunkのsizeを0x21から0x31に、fdをlibc_free_hookに書き換える
・sizeを変える理由については、先述した連続してchunkを確保することができないため、確保した後freeするのだが、その時にまた0x20サイズのtcacheに繋いではlibc_free_hookをchunkとして確保できないため、freeしてこのchunkは0x30のtcacheに繋ぐ - コマンド2でtcacheから先ほどfreeしたchunkを確保すると同時にtcacheがlibc_free_hookを指すようになる
- コマンド3でB=NULLにしてコマンド2を実行できるようにする
- コマンド2でtcacheからchunkを獲得(libc_free_hook)、中身をwin関数のアドレスにする
- コマンド3でfreeを呼び出してwin関数実行
Exploit
Elementary Stack
概要
ファイルコマンド結果
* Arch : x86-64
* Library : Dynamically linked
* Symbol :Not stripped
checksec結果
* RELRO : Partial RELRO
* Canary : Disable
* NX : Enable
* PIE : Disable
cコード、バイナリ、libcが渡されます。
バイナリの挙動としてはmain関数はループしていて,配列xのindexを指定してその中身を任意の値に書き換えることができます。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #define X_NUMBER 8 __attribute__((constructor)) void setup(void) { setbuf(stdout, NULL); alarm(30); } __attribute__((noreturn)) void fatal(const char *msg) { printf("[FATAL] %s\n", msg); exit(0); } long readlong(const char *msg, char *buf, int size) { printf("%s", msg); if (read(0, buf, size) <= 0) fatal("I/O error"); buf[size - 1] = 0; return atol(buf); } int main(void) { int i; long v; char *buffer; unsigned long x[X_NUMBER]; if ((buffer = malloc(0x20)) == NULL) fatal("Memory error"); while(1) { i = (int)readlong("index: ", buffer, 0x20); v = readlong("value: ", buffer, 0x20); printf("x[%d] = %ld\n", i, v); x[i] = v; } return 0; }
方針
indexの範囲をチェックしていないことによる脆弱性があります。
またバイナリを見るとスタック上にはlocal変数が次のように配置しています。
rbp-0x54 i rbp-0x50 buffer rbp-0x48 v rbp-0x40 x[]
このためindexを-2にすればループに入る前にmallocで確保したbufferのアドレスを書き換えることができるので、スタック上の任意の値を書き換えることができたものが、スタック以外のアドレスでも任意の値に書き換えることができそうです。 これを利用して以下の手順で攻撃を仕掛けます。
- index = "-2", value = str(atol@gotのアドレス-0x8)で次のループよりgot領域を書き換えるようにする
・atol@got-0x8はmalloc@gotなので書き潰してしまって問題ない - index = ("%25$p,xx" + plt_printfのアドレス)にすることでatol関数を呼ぶとprintf関数が呼ばれるようにする
・この時atol関数の引数はbufferなのでprintf("%25$p,xx"+....)が実行される
・"%25$p"はmain関数のreturn先であるlibc_start_mainの中のアドレスなのでこれよりlibcのベースアドレスを計算する - value = ("A"*8 + one_gadget)でatol@gotをonegadgetに書き換えてshell起動
Exploit
ChildHeap
概要
ファイルコマンド結果
* Arch : x86-64
* Library : Dynamically linked
* Symbol : Not stripped
checksec結果
* RELRO : Full RELRO
* Canary : Enable
* NX : Enable
* PIE : Disable
バイナリとlibcが配られます。またlibcのバージョンは2.29です。 セキュリティ機構はフルでついてますね。
このバイナリもコマンド形式でchunkを操作します。
ただし操作できるchunkは1つのみでスタック状に格納されている(移行ptrと呼ぶ)
操作コマンドは
1. Alloc: ptr=nullの場合にsizeを指定してのptr=malloc(size<=0x180)とサイズ分のread(0,ptr,size)を行う
2. Delete: printf("%s", ptr)を行い、確認のメッセージに対して'y'を入力するとfree(ptr)を行う
3. Wipe: ptr=nullにする
です。chunkの管理が1つしか行えないのが難しかったです。
方針
tcacheですがlibc2.29以降はdouble freeのチェックがあり、tcacheの管理構造体にもnext以外にtcache_perthread_struct構造体のポインタ型であるkeyというメンバが追加されています。(このメンバを忘れていてつまずいたりした)
なので普通に操作してては同じサイズのtcacheには1つしかchunkをつなげません。
また、ここでコマンド1の操作にはoff-by-oneの脆弱性があるため、隣り合うchunkのsizeメンバの下位1byteを\x00にすることができます。
自分の解法はかなり周りくどい気がしているので、もう少し無駄のない解法があるかもしれませんが、自分なりにこの問題のポイントは以下の点だと思っています。
- 同じサイズのtcacheにchunkを複数繋ぐ (heapアドレスリーク)
- unsorted binにchunkを繋ぐ
- overlapped chunkを作る(libcリーク)
tcacheにchunkを複数繋ぐ
これは以下の手順で行います。
- chunkA(sizeは任意だがここでは0x20)とする,chunkB(size=0x110)を隣り合うように確保し、どちらもfreeしてtcacheに繋ぐ
- 再びchunkAを確保しoff-by-oneで隣り合うchunkBのsizeを0x100にする
- chunkBを確保し(この時要求サイズは0x110)、freeするとsize=0x100に変えられているため0x100のtcacheに繋がれる
これをchunkA, B(size=0x110), C(size=0x120)...と増やしていけば要求するときはサイズが0x100出ないのにfreeすると0x100のtcacheに繋がるようなchunkを複数作ることができます。
また複数つなげばDeleteしてWipeする前にもう一度Deleteをするとchunkの中身をprintfしてくれるのでheapアドレスのリークもできます。
unsorted binにchunkを繋ぐ&overlapped chunkを作る
tcacheには同じサイズのchunkは7つまでしか繋げず、それ以降はunsorted binに繋がれます。
上の手順でサイズ0x100のtcacheに7つ繋げて、8つ目をfreeすればunsorted binに繋がるから簡単では?と思うのですが、このときfreeするchunkのprev_inuseビットは0x00で書き潰されて0になっているため、単純にfreeするとback consolidateが発生し落ちてしまいます。
なのでback consolidateが発生してもバイナリが落ちないように、偽のchunkを用意し、また偽のchunkとのconsolidateの際の、unlink(偽chunk)の処理でエラーが出ないよう偽のchunkのfd,bkを適切に設定する必要があります。
ここの説明は図などがないと難しいのですが、気になる方は下に載せるpythonコードを見ていただければと思います。
このときheapアドレスが必要になるのですが、これは上の手順でわかっています。
freeするchunkの前に隣接するchunkを大きめに取っておいて、この中身に偽のchunkを作ればoverlapped chunkが作れて、unsorted binにもchunkを繋げられます。
overlapped chunkが作れればlibc leakもできて(雑)、そこからfree_hookをone_gadgetにすればシェルが取れます。
Exploit
終わりに
自分はpwnしか見てないのでpwnについてしかわからないのですが、誘導付きの優しい問題から、それが解けてしまった人が退屈しないような難易度の問題まであってとても楽しめました。
作問者の方も有名な方ばかりで良問揃いでとてもいいBeginners向けのCTFだったと思います。
自分は2年前の第1回からbeginnersCTFに参加していて、193位->9位->3位と徐々に成長を感じられているのでとても嬉しいです。 これからも精進したいと思います。