{{ course.notes }} · Lecture 3 · Control-Flow Hijacking
Instructors: {{ course.instructors }}

Control-Flow Hijacking

What an attacker does with a memory bug: seizes the instruction pointer to run code of their choosing: code injection, code reuse, and return-oriented programming.

Instructors  {{ course.instructors }} Builds on  Spatial & Temporal errors Time  ~50 min
Part 2 · From Vulnerabilities to Exploits

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

07 · From bug to exploit

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.

Code injection needs three things
REQ 1Write the attack payload somewhere in memory.
REQ 2Have that payload be executable.
REQ 3Divert control flow to the payload.
08 · From bug to exploit

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

09 · Code reuse, advanced
Optional · Advanced

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:

◄ window A · enter at byte 0 ────────────────────────►
f7
c7
07
00
00
00
0f
95
45
c3
◄ window B · enter at byte 1 (shifted +1) ──────►
Window A: decoded from byte 0, as intended
f7 c7 07 00 00 00   test   $0x00000007, %edi
0f 95 45 c3         setnzb -61(%ebp)   ; c3 = the −61 disp
Window B: decoded from byte 1, where the attacker jumps
c7 07 00 00 00 0f   movl   $0x0f000000, (%edi)
95                  xchg   %ebp, %eax
45                  inc    %ebp
c3                  ret    ; same byte, now a gadget!
The byte stream decoded as intended: test and setnzb instructions The same byte stream decoded one byte later: movl, xchg, inc, ret
The overlap, from the lecture slide: one byte stream, entered one byte apart, yields different instructions, and the shifted decoding ends in a usable ret.

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:

ROP chain (Carlini et al., USENIX Security 2014) · step {{ ropNum }} of 8
Goal: *(0x4a304120) = *(0x4a304120) + 0x00032400, no injected code
The Stack grows down ↓ · high addresses on top {{ stackTag }}
address   :  4 bytes stored on the stack
{{ c.esp }}
{{ c.addr }}:{{ c.val }}
{{ c.instr }}
low addresses ↓ · each ret pops ESP upward
CPU state
EIP{{ regEip.val }}
EAX{{ regEax.val }}
EBX{{ regEbx.val }}
ECX{{ regEcx.val }}
MEM{{ regMem.val }}
MEM = *(0x4a304120)
Memory at the gadget addresses
{{ m.addr }}{{ m.bytes }}{{ m.disasm }}
Data memory: the write target
{{ dataMem.addr }}{{ dataMem.val }}
A gadget address points at its first byte, not a ret. 0x080485d9 holds 01 d8 (add eax,ebx); the c3 ret sits two bytes later at 0x080485db.

{{ ropCaption }}

The attack procedure
  1. Pre-identify useful gadgets in the program's executable code.
  2. Corrupt the stack so it holds the chain of gadget addresses (plus any immediates).
  3. Hijack control once, to jump to gadget A. A ends in ret, which reads the next address B off the stack.
  4. The ret jumps to B, whose ret jumps to the next; the chain runs itself, gadget after gadget.
10 · Code reuse, advanced
Coming soon

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.

Object memory layout with a virtual method table (VMT) for a class under inheritance
Object layout and the virtual method table: the target of the corruption. Figure credit: R. Sekar, CS307.
End of section

Control-flow hijacking, in brief

  • A control-flow hijack turns a memory bug into control of the instruction pointer, running code the attacker chose.
  • Code injection writes shellcode and jumps to it; code reuse (return-to-libc, ROP) needs no injected code at all.
  • ROP chains tiny existing gadgets ending in ret, enough to compute anything, entirely from code already present.

{{ course.notes }} · Instructors: {{ course.instructors }}

← The vulnerabilities ⌂ Overview Continue: Non-Control-Flow Attacks →
05 · Sources

References

[{{ r.n }}] {{ r.cite }} ↗ {{ r.linkLabel }}

Links last checked July 2026.