← thoughts

On Control Flow

"There are no facts, only interpretations."

— Friedrich Nietzsche

The hardware has memory addresses, and nothing about an address says who put it there or why. What a programmer writes as a function call is, at the machine level, an address being loaded into a register and jumped to. Those addresses live in memory that can be written to.

Pointers

A function pointer's destination lives in memory alongside program data, resolved only when the call actually happens.

In this struct, a 64-byte buffer sits next to a function pointer.

struct Object {
    char buffer[64];
    void (*handler)(char *data);
};

void process(struct Object *obj, char *input) {
    strcpy(obj->buffer, input);
    obj->handler(obj->buffer);
}

Here, handler is a legitimate function pointer that gets called after the buffer is filled. Since strcpy doesn't check bounds, overflowing the buffer writes past its 64 bytes and into handler.

+---------------------+
|  buffer (64 bytes)    |  <- input
+---------------------+
|  handler (8 bytes)    |  <- overflow
+---------------------+
Sidenote: The buffer and function pointer are adjacent in memory.

When the program calls obj->handler(), it jumps to whatever address is stored there.

At runtime, C++ vtables, callbacks, and any other indirect call are all just addresses being dereferenced and jumped to. The processor executes whatever address ends up in the register, whether it came from a legitimate call or corrupted memory.

CFI checks that indirect calls target valid functions, and CET uses hardware shadow stacks to verify return addresses, adding hardware enforcement to what was previously just software convention.

Overflow

To the hardware, the programmer's intended call and the attacker's payload are both just an address. The only fact is whatever ends up in the register, and everything else was just interpretation.