Skip to main content
Be Bitwise

The C you need before learning exploit development

Pointer fluency, struct memory layout, the call stack, what malloc returns, function pointers, and undefined behaviour — the slice of C that exploit dev assumes you've internalised.

#c#exploit-development#memory-model

There are two ways to know C. One is the textbook way — you can write a linked list, you understand malloc/free, you've used strtok once. The other is the memory-model way: you can predict, byte by byte, what's in memory after a series of statements. Exploit development assumes the second.

This post is the gap between those two. You probably know C in the first sense. Here's what you need to add.

Pointer fluency, the model not the syntax

Most C tutorials teach pointers as syntax — the asterisk, the ampersand, the arrow operator. Exploit dev cares about the underlying model. Concretely: when you write int *p = malloc(16);, you should be able to picture p as a 64-bit register or stack slot containing an address, with that address pointing into a 16-byte region somewhere on the heap. When you write p[2], you should automatically compute "address held in p, plus 8 bytes (because int is 4 and the index is 2, no wait, plus 2*sizeof(int) = 8), then read 4 bytes there."

The exercise that builds this fastest: write a function that walks an array using only pointer arithmetic, no subscript syntax. Then do the same for a 2D array. Then for a struct member. After ten of those, the syntax stops mattering and the address arithmetic stays.

The single most useful question to be able to answer instantly is: given T *p, what does p + 1 compute? Answer: the address (uintptr_t)p + sizeof(T). Pointers carry their pointee type around precisely so the compiler can scale arithmetic for you. When that scaling surprises you, you've found a bug.

Structs in memory

A struct is a contiguous block of memory with named offsets and possibly some padding. Be able to predict the layout of:

c
struct s {
char a;
int b;
char c;
void *d;
};

Without compiling: 1 byte for a, 3 bytes of padding, 4 bytes for b, 1 byte for c, 7 bytes of padding, 8 bytes for d. Total 24 bytes. The struct's overall alignment is the largest alignment of any member — 8 in this case.

This matters because exploits frequently overwrite struct fields, and you need to know which byte ends up where. Struct layout questions also crop up the moment you start reading kernel data structures.

offsetof(struct s, b) from <stddef.h> answers the question programmatically. Use it to check yourself when learning, then learn to predict without it.

How function calls map to the stack

You should be able to draw, on paper, the stack frame for a function call. For System V x86-64:

c
high address
┌─────────────────────────┐
│ caller's stack frame │
├─────────────────────────┤
│ stack-passed arguments │ (only args 7+; first 6 are in registers)
├─────────────────────────┤
return address │ (pushed by call)
├─────────────────────────┤
│ saved rbp │ (pushed by callee prologue)
├─────────────────────────┤
│ local variables │
...
├─────────────────────────┤
│ red zone (128 bytes)(sometimes used for spills)
└─────────────────────────┘
low address

You should know which direction the stack grows (downward on x86-64), where return addresses live (just above the saved frame pointer), and how a buffer overflow in a local variable can reach them. The whole genre of stack-smashing attacks is about exactly this layout. Get it wrong and the exploits don't make sense.

Dynamic allocation and what malloc actually returns

malloc(16) does not return "16 bytes of memory". It returns a pointer into a heap allocator's data structures. The allocator stores metadata immediately before (or interleaved with) the user region — chunk size, free flags, pointers to neighbouring chunks. Different allocators (glibc's ptmalloc2, jemalloc, mimalloc) have different layouts.

For exploit development you don't need to memorise glibc's heap layout on day one, but you need to know that this metadata exists and that overwriting it is the basis for a whole class of attacks (the "house of" techniques). The right mental model: malloc returns a pointer into the middle of a structure you don't own, with secret bookkeeping bytes adjacent to your usable region. Free a pointer twice and you corrupt those bookkeeping bytes.

Function pointers and indirect calls

A function pointer is just an address — the address of the first instruction of the target function. void (*fn)() = something; fn(); compiles to a load and an indirect call. C++ vtables, glibc's __free_hook, and the GOT in dynamically linked binaries are all collections of function pointers an attacker would love to overwrite.

You should be comfortable declaring function pointer types (typedef void (*handler_t)(int); is the readable form). You should be able to take the address of a function and store it. You should know that calling through a corrupted function pointer is one of the cleanest ways to redirect control flow.

Undefined behaviour as a concept

This is the one most people skip. UB is not "the program crashes" or "the program does something weird". It's a contract between you and the compiler: if your program executes UB, the compiler is permitted to assume it doesn't, and to optimise accordingly. That assumption can delete your bounds checks, fold away if (p == NULL) branches when p was already dereferenced, or reorder operations in ways that change observable behaviour.

For exploit dev you need to recognise the common UB classes: out-of-bounds reads/writes, signed integer overflow, dereferencing freed memory, type-punning through incompatible pointers, racing on shared data without synchronisation. You don't need to memorise the standard. You need to know that "but the code works on my machine" is meaningless — UB programs work until the compiler version, optimisation level, or surrounding code changes.

The exercises

Skip the books. The fastest path is exercises:

  1. Write a memcpy from scratch. Then write a version that handles overlapping regions correctly. Then explain why memcpy is documented as undefined behaviour on overlapping regions while memmove isn't.
  2. Implement a singly linked list using only malloc and pointer arithmetic. Insert, delete, traverse. Run it under valgrind.
  3. Take the struct above and write a function that prints each field's offset using offsetof. Add a member, predict the new offsets, check.
  4. Write a function void run(void (*f)(int), int x) that calls f(x). Pass it different functions. Then make f a function pointer stored in a struct field.
  5. Compile a program with a deliberate use-after-free at -O0 and again at -O2. Watch the second one behave differently. Sit with that for a moment.

Five evenings. After that you have the C model that exploit development assumes.

Where this fits in

The C code pathway is the structured version of this — it walks the same ground with progress checks at each step, and it dovetails with the assembly and exploitation tracks once the memory model clicks.