Control-Flow Hijacking
That completes the spatial memory errors: buffer overflows, format strings, and integer overflow. We now turn from the bugs themselves to what an attacker does with them: seizing the instruction pointer to run code of their choosing. Two families follow: code injection, then code reuse (return-to-libc and ROP).
Control-flow hijacking I: code injection
A control-flow hijacking exploit uses a memory bug to seize the instruction pointer. Aleph One wrote the technique up in 1996 {{ cite.aleph_one }} and it has not gone away since. The first flavour is code injection: a memory exploit that hijacks control to jump into the attacker's own data payload. We overflow the buffer, place machine code (“shellcode”) in it, and point the return address back at that code.
Two practical problems and their fixes appear in the walkthrough: you may not know the buffer's exact address, and your guess must land exactly on the first shellcode byte. The NOP sled, a long run of 0x90 (no-op) bytes before the shellcode, solves both: land anywhere in the sled and execution “slides” down into the payload.
Control-flow hijacking II: code reuse
Injecting your own code has a catch: the payload must be written into memory and also be executable. A neater idea sidesteps that: instead of injecting new code, reuse code that is already in the program and already runnable. Code reuse is a memory exploit that hijacks control to jump to an attacker-chosen address of code that already exists. The classic instance is return-to-libc: overwrite the return address with the address of an existing function like execv, and fake the arguments it expects on the stack.
Think of it as adding new edges to the program's control-flow graph, call/return edges that the programmer never wrote. No new code is introduced at all; the exploit is assembled entirely from code already in the program. Pushed further, attackers chain many tiny existing snippets (“gadgets”) ending in ret; that's return-oriented programming (ROP) {{ cite.shacham_rop }}, an optional deeper topic. Finding and chaining the gadgets can itself be automated {{ cite.pshape }}.
Return-oriented programming (ROP)
ROP is not a separate category of attack; it is the most general form of code-reuse control-flow hijacking from the previous section. Return-to-libc reused one whole function; return-oriented programming generalises the idea to its limit: chain together dozens of tiny existing snippets (gadgets), each just a couple of instructions ending in ret (opcode 0xc3). String enough of them and you can compute anything, all from code that is already in the program.
Key observation: x86 instructions overlap
x86 instructions are variable-length, and the CPU will start decoding at any byte you jump to. So the same bytes decode into completely different instructions depending on where you enter, and a huge program contains an enormous supply of unintended sequences that happen to end in c3 (ret).
Here is the raw byte stream in memory. Two different entry points carve out two overlapping windows, and each window decodes into a completely different instruction sequence:
f7 c7 07 00 00 00 test $0x00000007, %edi
0f 95 45 c3 setnzb -61(%ebp) ; c3 = the −61 disp
c7 07 00 00 00 0f movl $0x0f000000, (%edi)
95 xchg %ebp, %eax
45 inc %ebp
c3 ret ; same byte, now a gadget!
A ROP gadget is any such sequence ending in a control transfer. Because every gadget ends in ret, the attacker just fills the stack with a list of gadget addresses: each ret pops the next one and execution flows down the chain. Step through the running example from Carlini & Wagner's paper:
{{ ropCaption }}
- Pre-identify useful gadgets in the program's executable code.
- Corrupt the stack so it holds the chain of gadget addresses (plus any immediates).
- Hijack control once, to jump to gadget A. A ends in
ret, which reads the next address B off the stack. - The
retjumps to B, whoseretjumps to the next; the chain runs itself, gadget after gadget.
Vtable hijacking
A distinct flavour of code reuse, separate from return-to-libc. Every C++ object with virtual methods holds a pointer to its vtable, a table of function pointers {{ cite.sekar_oop }}. Getting a pointer to the wrong type is its own bug class, and tools exist to catch it {{ cite.caver }}. Corrupt that pointer (often via a heap or type-confusion bug) so it points at attacker-controlled data, and the next virtual call transfers control to an address of the attacker's choosing, hijacking control flow without ever touching a return address.