InterKosenCTF 2020 Writeup
初めに
InterKosenCTFにKUDoSで参加しました。最終順位は4位となかなかいい感じでした。
自分が解いたpwnの問題4問のwriteupを書きます。
よろしくです。
babysort
概要
配布ファイル
* main.c
* chall(問題バイナリ)
ファイルコマンド結果
* Arch : x86-64
* Library : Dynamically linked
* Symbol :Not stripped
checksec結果
* RELRO : Partial RELRO
* Canary : Disable
* NX : Enable
* PIE : Disable
libcのqsortを利用した問題。qsortは関数ポインタを第4引数にとっており、qsortが呼ばれると登録した関数が呼ばれてソートが行われるみたいです(自分も調べるまで使ったことなかったので、こういうのは出てきたら適宜調べればいいと思います)。
ユーザは最初に5つの数字をscanfで配列se.elmに入力して、その後0か1の入力で予めse.cmp[]に登録された関数をqsortの引数に渡して実行をするような流れです。
typedef int (*SORTFUNC)(const void*, const void*); typedef struct { long elm[5]; SORTFUNC cmp[2]; } SortExperiment; /* call me! */ void win(void) { char *args[] = {"/bin/sh", NULL}; execve(args[0], args, NULL); } int cmp_asc(const void *a, const void *b) { return *(long*)a - *(long*)b; } int cmp_dsc(const void *a, const void *b) { return *(long*)b - *(long*)a; } int main(void) { SortExperiment se = {.cmp = {cmp_asc, cmp_dsc}}; int i; /* input numbers */ puts("-*-*- Sort Experiment -*-*-"); for(i = 0; i < 5; i++) { printf("elm[%d] = ", i); if (scanf("%ld", &se.elm[i]) != 1) exit(1); } /* sort */ printf("[0] Ascending / [1] Descending: "); if (scanf("%d", &i) != 1) exit(1); qsort(se.elm, 5, sizeof(long), se.cmp[i]); /* output result */ puts("Result:"); for(i = 0; i < 5; i++) { printf("elm[%d] = %ld\n", i, se.elm[i]); } return 0; } __attribute__((constructor)) void setup(void) { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); alarm(300); }
方針
後半の0か1を入力して関数を呼び出す際の入力のチェックが行われていないところに脆弱性があります。
例えば-1を入力するとse.cmp[-1]がqsortの呼び出しに当たって呼び出されます。se.cmp[-1]はメモリ上だとse.elm[4]を指しているので、前半se.elm[]に値を入力する際にripを飛ばしたい値をセットすることで、任意のアドレスに飛ぶことができます。
バイナリにはshellが起動するwin関数が用意されているのでこちらに飛ばしましょう。
Exploit
authme
概要
配布ファイル
* main.c
* password(dummy)
* username(dummy)
* chall(問題バイナリ)
ファイルコマンド結果
* Arch : x86-64
* Library : Dynamically linked
* Symbol :Not stripped
checksec結果
* RELRO : Partial RELRO
* Canary : Disable
* NX : Enable
* PIE : Disable
username,passwordファイルを予めグローバルなchar配列に読み込み、
ユーザーはfgetsを2回繰り返し、usernameとpasswordを入力。
ファイルから読み込んだusername,passwordと一緒ならシェルが起動するという流れ。
方針
bufのサイズが0x20なのに対してfgetsが入力サイズを0x40を指定しているので、BOFの脆弱性があります。
ただし、普通に入力をし実行していると、mainからreturnが行われずexit()でmain関数が終了するので、BOFを起こしたとしても任意のアドレスに飛ぶことができません。
idaのグラフビューをみるとわかりやすいですが、BOFを起こしてかつmainからreturnするには、1回目のfgetsでBOFを起こして2回目のfgetsの返り値を0(null)にできれば、攻撃が成功しそうです。
fgetsの返り値ですが、manualを見ると成功した場合は文字列を読み込んだアドレスが返り、EOFを読み込んだかつ1文字も読み込めなかった場合にエラーとしてnullが返るようです。
なので、2回目のfgetsでEOFを送れば良さそうです。
ただEOFで入力を閉じてしまうと、シェルが起動できてもcatコマンド等が打てないので、今回はシェルの起動はなくusernameとpasswordをputsでリークすることを目標にしましょう。
BOFでstackに仕込める値も3qwordなのでgadget(pop rdi; ret),
username or passwordのアドレス,plt_putsで良さそうです。
が、、、EOFってどうやって送るんでしょうか?? 自分はこれに無限に時間を溶かしました。。。0x03や0x04を送ってみてもダメ。。。EOFの送信とかについて調べてもパッとした情報に辿り着けませんでした。
試行錯誤した結果、最終的な僕の解法はこれです(クソださ)
$(python -c 'print "A"*0x28 + "\x03\x0b\x40\x00\x00\x00\x00\x00\xe0\x20\x60\x00\x00\x00\x00\x00\xa0\x06\x40\x00\x00\x00\x00"'|tr -d "\n";cat ) |nc -q 3 pwn.kosenctf.com 9002
ターミナル上で上記のコマンドを打ったあと、手打ちでCtrl-Dを押してやります。そうするとputsによるリークができるので、この情報を使って正規にauthlizationを行い/bin/shを起動しましょう。
余談
上のコマンドにたどり着くまでの過程ですが
$echo "payload"|./chall
ローカルだとこれで入力を閉じれて、putsが実行されましたが、
$echo "payload"|nc host port
これはダメでした。ソケットだとまた別なんでしょうか。
次にやったこととして
$(echo "payload";cat)|nc host port
catで入力できるようにしつつ、手打ちでCtrl-Dを押してEOFを送ろうという目論見です。 これもダメでした。ただCtrl-Dは送信できていて、Ctrl-Dの後に改行等を送信しても何も反応がなく、時間が経つとalarmで強制終了になってしまいます。
そこで解法のnc -qオプションでtimeoutを指定する方法を試し、コマンド入力後Ctrl-Dを手打ちすると、、、
$(echo "payload";cat) |nc -q (timeout) host port
できました。もうパソコンよくわかりません。qオプションを指定しないとEOF後も入力を待ち続けちゃうのでしょうか。。。
Exploit
Fables_of_aeSOP
概要
配布ファイル
* chall(問題バイナリ)
* libc-2.23.so
* banner.txt
ファイルコマンド結果
* Arch : x86-64
* Library : Dynamically linked
* Symbol :Stripped
checksec結果
* RELRO : Full RELRO
* Canary : Enable
* NX : Enable
* PIE : Enable
問題名からもlibc2.23であることからも分かるとおりFSOPの問題です。
FSOPについては知らない場合はここら辺を参照するといいと思います。
PIEが有効ですが、最初にshellを起動するwin関数のアドレスを教えてくれます。
banner.txtをfopenして返り値をグローバル変数に代入します(以下streamと呼びます)。
またgetsでグローバル領域に用意されているsize=0x200のbuffer(以下bufと呼ぶ)にgetsで入力します。
その後fclose(stream)でファイルをクローズします。
流れはこんな感じで、この時のメモリレイアウトは以下のようになっています。(PIEがEnableなのでアドレスはオフセット)
0x202060: char buf[0x200] 0x202260: FILE* stream
方針
bufにgetsで入力しているので、banner.txtのFILE構造体をさすポインタを書き換えることができます。
libc2.23ではvtableのアドレスのチェックが行われないのでbuf内にfakeのFILE構造体とfakeのvtableをどちらも用意しましょう。
あとはfake_vtableの_IO_FINISHをwin関数のアドレスにしておきます。
Exploit
fakeのFILE構造体は値は結構適当にしている箇所があります。_flagsとか_lockとかのメンバは注意する必要があります。
confusing
概要
配布ファイル
* chall(問題バイナリ)
* libc-2.27.so
* main.c
* type.h
ファイルコマンド結果
* Arch : x86-64
* Library : Dynamically linked
* Symbol :Not Stripped
checksec結果
* RELRO : Partial RELRO
* Canary : Disable
* NX : Enable
* PIE : Disable
Undefined, String, Double, Integerを扱うunionをテーマにした問題です。
問題文からもwebkitのjsエンジンが元ネタのようですが、ここら辺の知識がなくてもコードを読めば十分に解けます。(自分もliveoverflowの動画で見たくらいで、あまり詳しくないです)
またこのバイナリでは入力はlibcのgetline関数で行っています。getline関数の内部ではmalloc,reallocが実行されるのでここら辺の挙動もよく観察しましょう。
上でテーマであると話したこれらの型(jsではプリミティブと呼ばれたりして、cの型と概念が少し異なるようですが、ここでは単に型と呼びます)はtype.hで定義されていて、全て64bit長のデータとして扱われます。Integerも表現できる幅は32bitですが64bitで扱われます。
説明が難しいのでtype.hにあるコメントとコードを見た方が早そうですね。
... ... * > The top 16-bits denote the type of the encoded JSValue: * > * > Pointer { 0000:PPPP:PPPP:PPPP * > / 0001:****:****:**** * > Double { ... * > \ FFFE:****:****:**** * > Integer { FFFF:0000:IIII:IIII ... ... #define VALUE_UNDEFINED ((void*)0x0a) #define MAGIC_STRING 0x0000 #define MAGIC_INTEGER 0xFFFF /* A magic type that can keep string, double and integer! */ typedef union __attribute__((packed)) { char *String; double Double; int Integer; struct __attribute__((packed)) { unsigned long data : 48; unsigned short magic: 16; } data; } Value;
全ての型は64bitの上位16bitで識別することができます。Stringなら0、Doubleなら1~0xfffe、Integerなら0xffffといった感じです。
またUndefinedは0x0aで表現されます。
次にバイナリの実行の流れを見ていきましょう。 このバイナリでは長さが10のunionの配列listの中身をセット、表示、消去の動作を3つのコマンドで選択できます。
Set
indexとセットしたい型をString, Double, Integerの中から数字で選択します。
指定したindexにすでにStringがセットされていた場合は、その値をfreeして新しい値をセットします。
指定したindexがStringでない場合は値がセットされている、いないにかかわらず新しい値で書き換えを行います。
新しくStringをセットする場合はgetline関数で入力を読み込み、そのまま確保したchunkのアドレスをセットします。Show listの10個の要素について、型とその値を表示します。Stringであった場合はポインタが指す先の文字列を表示します。
delete indexを指定して値をUndefinedにします。指定したindexがStringであった場合はその値をfreeしてからUndefinedにします。
大まかな流れはこんな感じです。
方針
概要でStringは上位16bitが0、Doubleは0x1~0xfffeを取りうると言ったのですが、この定義通りに実装されておらず、Doubleをセットするときに上位16bitが0だったとしてもその値がそのまま格納されてしまいます。つまりStringとDoubleの判別が付かなくなってしまうのです。各コマンドでlist[index]がStringであるかをチェックする関数Value_IsStringがあるのですが、上位16bitが0かつUndefinedであればTrue、つまりStringであると判別されます。なので上位16bitが0になるようなdoubleの値をセットすると、以降それはStringとして扱われてしまいます。これを利用して攻撃を行います。
大まかな流れは以下の通りです。
1. libc,heapアドレスリーク
2. 複数のchunkにまたがるoverlapped chunkの作成
3. tcache poisoning
4. free_hookのoverwrite
libc, heapアドレスリーク
これはPIEがDisableなので結構簡単にできます。
適当にStringを確保しlistにheapのアドレスをセットしておきます。(例index=0, String, "hogehuga")
次にその値を格納したindexを参照するような値をdoubleで表現しlistにセットしておきます。(例index=1, Double, "3.1125187e-317")
するとshowで一覧を表示したときに、先述の通りStringと扱われてheapのアドレスが文字列として表示されます。("3.1125187e-317"は内部では0x6020a0となりlist[0]のアドレスになります)
heapアドレスのリークと同様にlibcのリークもできます。これは先にStringでchunkを確保する必要はなく、got_putsのアドレスを浮動小数点表記したものをDoubleとしてセットして、showを実行すれば良いです。
複数のchunkにまたがるoverlapped chunkの作成 & tcache poisoning
Doubleの値がStringとして扱われてしまう場合があるということは、それらの値をdeleteで指定すれば任意のアドレスをfreeできます。
またheapアドレスの値はすでにわかっているのでoverlapped chunkも簡単に作れますね。
getline関数で呼ばれるreallocが確保するchunkの最小単位は0x80であるということ、fakechunkのnext_chunkのsizeを適切な値にセットしておかないとfree時にエラーが起きることに注意しましょう。
overlapped chunkが作れたら、tcacheにつながっているchunkのnextchunkをfree_hook等に書き換えることができます。
free_hookのoverwrite
このバイナリでは全ての入力をgetlineで行っているのでコマンドの入力"1","2","3"に対してもchunkを確保します。
またそれらのchunkを律儀にfreeしているので、free_hookをtcacheに繋げたあとの挙動には注意する必要があります。
僕の場合はtcacheにfree_hook-0x8のアドレスを繋いで、次のgetnlineの入力で"/bin/sh\x00"(8byte)+p64(addr_system)を入力することで、free_hookの書き換えとfreeの実行のユーザからするとアトミックな処理にも対応するようにしました。
Exploit
まとめ
CTFは楽しい。チームメイトが強い。
最近ガッツリ時間割けてないですが、もっと頑張ります。
でかいCTFだと相変わらず無力なので。。。