DEV Community

Cover image for What Is Jump-Oriented Programming? How Can Attackers Hijack Control Flow Without RET?
Aditya Sharma
Aditya Sharma

Posted on

What Is Jump-Oriented Programming? How Can Attackers Hijack Control Flow Without RET?

Protecting return addresses is not the same as protecting control flow.

That statement might seem strange. Return addresses are the classic target of stack-based exploitation. Stack canaries detect their corruption. Shadow stacks maintain protected copies and validate every return. These are meaningful, well-motivated defenses.

But they protect a specific subset of control-flow transfers. A program has many ways to change where it executes next, and a return is only one of them.

Jump-Oriented Programming emerged as a response to exactly this gap. If return addresses become harder to abuse, what happens when an attacker builds malicious computation around a different primitive entirely?


Control Flow and Its Moving Parts

Execution in a program is not purely sequential. The instruction pointer advances through instructions, but branches, calls, and returns interrupt that sequence constantly.

Some of these transfers are direct: the destination is encoded in the instruction itself. A direct call to a known function, a direct jump to a labeled location, a direct branch over a conditional block. For a direct transfer, the destination encoded by the instruction is fixed rather than being read from attacker-controlled runtime state. Whether execution reaches that instruction can still depend on program state.

Indirect transfers are different. An indirect call reads a destination from a register or memory location and transfers execution there. An indirect jump does the same. The destination is not fixed in the instruction; it depends on whatever value is in that register or memory location at runtime.

This is what makes indirect transfers interesting from an exploitation perspective. If an attacker can influence the state that an indirect transfer reads, they can influence where execution goes. The instruction itself is correct and legitimate. The destination becomes attacker-controlled.

Returns are one example of an indirect backward transfer. They read a return address from the stack. But indirect jumps and indirect calls are forward-edge indirect transfers, and they are present throughout compiled code: virtual function dispatch, function pointers, computed gotos, jump tables for switch statements, callbacks stored in data structures.


Why ROP Mattered

Return-Oriented Programming established that an attacker does not need to inject new executable code to accomplish something useful. If the attacker can corrupt return addresses, they can chain together short sequences of existing instructions, each ending in a RET, by controlling what the stack contains. Each RET reads the next attacker-chosen address and transfers there. The chain assembles computation from fragments that already exist in legitimate executable memory.

This was significant because it bypassed NX and DEP entirely. Those protections mark data regions non-executable, blocking shellcode placed on the stack or heap. But ROP uses only existing executable code. There is nothing to mark non-executable.

Shadow stacks address ROP directly. By maintaining a protected copy of legitimate return addresses and comparing against them at each RET, a shadow stack can detect when the return target has been corrupted. If the value on the normal stack differs from the shadow-stack record, the return is blocked.

That protection is real. It specifically targets the primitive ROP depends on: the ability to substitute an attacker-chosen address wherever a RET expects to find a legitimate one.

Which raises an obvious question. Can code-reuse techniques be built without RET?


Jump-Oriented Programming

Jump-Oriented Programming is a code-reuse technique that chains existing instruction sequences through indirect jumps rather than returns. The attacker does not need to corrupt return addresses for each transition. Instead, each gadget ends in an indirect jump, and the destination of that jump can be influenced by attacker-controlled register or memory state.

Like ROP, JOP requires no new executable code. It reuses instructions already present in legitimate executable memory. The program's existing code, and the libraries it links against, contain indirect-jump instructions throughout their compiled output. Some of those can be useful gadgets.

The important conceptual difference:

ROP chains gadgets by controlling what appears on the stack. Each RET pops the next address.

JOP chains gadgets by controlling what register or memory value an indirect jump uses as its destination. Each JMP [register] goes wherever that register points.

A shadow stack that validates return addresses does not inherently validate the destinations of indirect jumps. Those are a different category of control-flow transfer, operating through a different mechanism. A defense scoped to backward-edge returns does not automatically cover forward-edge indirect jumps.


What Makes a Useful JOP Gadget

A useful gadget for JOP performs some computation and then reaches an indirect jump whose target the attacker can influence.

That second requirement is the constraint. Not every indirect jump is usable. If the jump reads from a register that the attacker cannot control at the point the gadget executes, it is not useful. The attacker needs some primitive that lets them set up machine state, specifically the register or memory location that the gadget's indirect jump will use, in advance of reaching that gadget.

This is substantially more complex than finding ROP gadgets. In ROP, the chaining mechanism is the stack. The stack is easy to reason about: each RET reads from the current stack pointer, and the attacker controls the stack layout. Every gadget inherits the same chaining mechanism.

In JOP, gadgets may read from different registers. Maintaining useful state across the chain requires careful attention to which gadgets modify which registers and whether the next gadget's indirect jump target can still be set correctly by the time execution reaches it.


The Dispatcher

This complexity is why JOP introduces a design concept that ROP does not need: a dispatcher gadget.

In a ROP chain, the chaining mechanism is automatic. RET reads from the stack. As long as the stack is controlled, every gadget chains to the next without additional structure.

In a JOP chain, the attacker needs a way to repeatedly determine which gadget executes next. A dispatcher is a gadget that uses attacker-controlled state to select and transfer execution to the next gadget in the chain. The dispatcher does not perform useful computation itself. Its job is to advance the chain.

Conceptually, a dispatcher might use a register pointing into a table of gadget addresses and advance through that table on each dispatch. The computation gadgets perform useful operations, then return control to the dispatcher through an indirect jump. The dispatcher then selects the next computation gadget.

Attacker-controlled dispatch state
           ↓
     Dispatcher gadget
           ↓
    Computation gadget A
    (performs operation, ends in indirect jump)
           ↓
     Dispatcher gadget
           ↓
    Computation gadget B
           ↓
     Dispatcher gadget
           ↓
    Computation gadget C
Enter fullscreen mode Exit fullscreen mode

This structure requires finding both useful computation gadgets and a suitable dispatcher. The dispatcher has to be usable: it has to read attacker-controlled state to select targets, and those targets have to be reachable through an indirect jump. Finding a dispatcher in a real binary is often harder than finding computation gadgets.


JOP vs ROP

The comparison is worth making precisely.

ROP depends on corrupting return addresses. The stack is the chaining medium. Each RET pops the next gadget address. Shadow stacks protect this: by maintaining a protected independent record of legitimate return addresses, they can catch the substitution.

JOP uses indirect jumps. Return addresses are not the chaining mechanism, so a defense specifically protecting return addresses does not inherently address the indirect jumps JOP relies on. This does not mean JOP bypasses all defenses, but it does mean it operates through a different class of control-flow transfer.

The defenses relevant to JOP are those that protect indirect branches: CFI policies constraining valid indirect-jump targets, and hardware mechanisms that enforce constraints on where indirect jumps can go.


Shadow Stacks and CFI Are Different Things

This distinction is worth stating clearly because the shadow-stack article and this one are closely connected.

Shadow stacks protect backward-edge control flow. A return should go to where the corresponding call came from. The shadow stack records that expected destination and validates it.

CFI is broader. It attempts to constrain control-flow transfers to destinations that are valid according to a policy or control-flow graph. Depending on implementation, this can cover indirect calls, indirect jumps, and returns. The question CFI asks is "is this destination allowed?" not "does this return match its corresponding call?"

A system with shadow stacks but no forward-edge CFI has strong protection for returns and limited protection for indirect jumps. A system with CFI but no shadow stacks might constrain indirect-jump targets while leaving return addresses less specifically protected.

JOP is primarily a problem for forward-edge protection. Shadow stacks do not directly constrain where indirect jumps can go. CFI does, to varying degrees depending on how precise the policy is.


How CFI Changes JOP

CFI constrains which destinations are valid for indirect control transfers. When applied to indirect jumps, it reduces the set of usable JOP gadgets.

A coarse-grained CFI policy might allow indirect jumps to land on any function entry point. That still leaves many potential gadget targets across a binary and its libraries. The set of usable gadgets shrinks compared to an unconstrained binary, but it may not shrink to zero.

A fine-grained CFI policy tries to restrict each indirect-jump site to a much smaller set of legitimate targets based on the program's intended structure. This can reduce the availability of JOP gadgets significantly, since many instruction sequences the attacker might want to reach are not in the permitted set for any indirect-jump site.

The protection depends on implementation. Fine-grained CFI requires accurate static analysis of the program's intended control-flow structure, has compatibility constraints with dynamic behavior, and carries some overhead. Real deployments involve tradeoffs.

The concept of control-flow bending describes a scenario where an attacker constructs an attack using only destinations that satisfy the CFI policy. With a coarse policy, enough usable-but-unintended targets may still exist. With a fine policy, the available set shrinks enough to make construction extremely difficult.


Intel CET and IBT

Intel CET provides two distinct control-flow protection mechanisms, and keeping them separate matters here.

The Shadow Stack component protects backward-edge control flow by tracking legitimate return addresses.

Indirect Branch Tracking (IBT) addresses a different problem. IBT requires that valid indirect branch targets be preceded by an ENDBR instruction. An indirect jump or call that lands on a location without ENDBR raises a control-protection fault.

IBT is relevant to JOP because JOP relies on indirect jumps. If every valid indirect-branch destination is required to carry an ENDBR marker, the attacker needs gadgets that begin with or include ENDBR, and any gadget not at an intended indirect-branch target cannot be reached without triggering a fault.

This reduces the pool of usable JOP gadgets. Instruction sequences in the middle of functions, or at locations that compilers never intended to be indirect-branch targets, become unreachable through protected indirect jumps.

IBT and the Shadow Stack are complementary. Shadow Stack covers backward edges. IBT constrains forward-edge indirect branches. A system deploying both addresses more of the control-flow surface than either does alone.


JOP in the Code-Reuse Landscape

Code-reuse techniques have evolved as defenses developed. Return-to-libc predates ROP: instead of chaining gadgets, redirect execution to an existing function like system. ROP generalized this, enabling arbitrary computation from gadget fragments. SROP exploited the signal-return mechanism for bulk state restoration. JOP extends the code-reuse idea to indirect jumps when return-address defenses make ROP harder.

None of these techniques is universally applicable. Each requires specific primitives: the ability to corrupt the relevant control-flow data, some way to influence the destination of the relevant indirect transfer, and enough usable fragments in the target binary to construct something useful.

JOP is generally more difficult to construct than ROP. Finding a usable dispatcher, maintaining consistent register state through a chain that does not have a uniform chaining mechanism, and doing all of this while satisfying any CFI or IBT constraints imposed by the target, makes JOP construction a harder engineering problem than building a ROP chain in an unprotected binary.


Why Control Flow Has to Be Treated as a System

The defense against JOP is not one thing. ASLR makes gadget addresses harder to predict. NX blocks executing injected code. Stack canaries detect some stack corruption. Shadow stacks protect return addresses specifically. CFI constrains indirect-transfer targets. IBT marks valid indirect-branch landing sites. Memory-safe languages eliminate the memory-corruption primitives that make control-flow hijacking possible in the first place.

Each mechanism addresses a different part of the problem. ASLR addresses knowledge of addresses. NX addresses code injection. Shadow stacks address return-address substitution. CFI and IBT address the destinations of indirect transfers. None of them, individually, closes the entire attack surface.

JOP illustrates this clearly. It does not defeat shadow stacks: it simply operates through a class of control-flow transfer that shadow stacks are not designed to protect. That is not a flaw in shadow stacks. It is a reminder that a defense designed around one assumption does not automatically handle every other assumption.


The Point

Protecting RET does not mean you have protected control flow.

This is the deeper architectural insight. A program's execution can be influenced through returns, but also through indirect jumps and indirect calls. Those transfers all read destinations from program state that can potentially be corrupted. Protecting one class of them, through whatever mechanism, leaves the others as potential attack surface.

Security mechanisms are scoped to specific assumptions about how execution can be manipulated. Shadow stacks assume the threat is return-address corruption. CFI assumes the threat is indirect-transfer destinations outside the intended control-flow graph. IBT assumes the threat is indirect branches to unintended locations.

When attackers encounter a defense targeting one primitive, they look for another. JOP is what that looks like when the primitive being avoided is RET. The response is not to find a single new defense that covers everything, but to build the layered combination of mechanisms that closes more of the control-flow surface together than any of them does alone.

Top comments (0)