OIS-ROP: Executing Code That Doesn't Exist
No shellcode. No injected code. Just addresses pointing into Microsoft's own signed binaries.
Every byte of the "malware" is Microsoft's code. I just control which bytes execute and in what order.
What if the malicious code never existed?
Not hidden. Not obfuscated. Not encrypted. Simply never written.
The payload is a list of addresses. Every address points to instructions already present in legitimately signed Windows DLLs: kernel32.dll, ntdll.dll, user32.dll. The CPU executes Microsoft's code, in Microsoft's memory, with Microsoft's signatures. I'm just the orchestra conductor.
The Problem with Traditional Shellcode
EDR and security tools have gotten good at detecting malicious code. They look for:
- Private executable memory: regions marked RWX or transitioned from RW to RX
- Suspicious byte patterns: known shellcode sequences, syscall stubs
- Anomalous execution flow: code running from regions that shouldn't contain code
The common thread: they're looking for your code. Code you wrote. Code you injected. Code that exists.
What if there was nothing to find?
The OIS Foundation
OIS (Offset Instruction Set) starts from a simple observation: every byte value (0x00 through 0xFF) exists somewhere in the Windows system DLLs. These files are massive. kernel32.dll alone contains hundreds of thousands of bytes.
Instead of storing a payload, you store coordinates:
Traditional: [0x48, 0x31, 0xC0, 0xC3] <-- actual shellcode bytes
OIS: [k32:0x1055, k32:0x12D7, k32:0x8F2A, k32:0x3B91]
^-- offsets into kernel32.dll that contain those bytes
Finding a byte:
+------------------------------------------------------------------+
| kernel32.dll (Microsoft-signed, 800KB+) |
+------------------------------------------------------------------+
| ... |
| 0x1055: 0x48 <-- Need 0x48? It's at offset 0x1055 |
| ... |
| 0x12D7: 0x31 <-- Need 0x31? It's at offset 0x12D7 |
| ... |
| 0x8F2A: 0xC0 <-- Every byte exists somewhere |
| ... |
+------------------------------------------------------------------+
The DLL is the dictionary. Just cite page numbers.
To reconstruct, you read each offset from the signed DLL. The bytes were always there. You just indexed them.
This was the first stage: using signed binaries as a byte lookup table. The payload exists nowhere until reconstruction. But there's still a problem: eventually you have to put those bytes somewhere executable. And that's where EDR catches you. Private memory allocation with executable permissions.
OIS-ROP: Executing in Place
Return-Oriented Programming isn't new. It's been used in exploit development for years to bypass DEP. But OIS-ROP applies it differently: not to exploit a vulnerability, but as the primary execution model.
The key insight: I don't need to reconstruct shellcode at all. I can execute directly within the signed DLL's memory space.
A ROP chain is a sequence of addresses, each pointing to a "gadget," a small instruction sequence ending in RET. When RET executes, it pops the next address off the stack and jumps there. Chain enough gadgets together, you can perform arbitrary computation.
+-----------------------------------+ +-----------------------------------+
| Image Memory (kernel32.dll) | | Private Memory (Our Stack) |
| [Microsoft-signed, executable] | | [Data only, not executable] |
+-----------------------------------+ +-----------------------------------+
| | | |
| 0x7FF81234: pop rcx |<----+-- 0x7FF81234 <-- RSP starts here |
| ret ----------------+-----+--> |
| | | 0x41414141 (value for RCX) |
| 0x7FF85678: pop rdx |<----+-- 0x7FF85678 |
| ret ----------------+-----+--> |
| | | 0x00000001 (value for RDX) |
| 0x7FF8ABCD: jmp WinExec |<----+-- 0x7FF8ABCD |
| | | |
+-----------------------------------+ +-----------------------------------+
CPU bounces here We control this
(trusted code runs) (just addresses)
The "stack" I control contains only addresses and data. Never executable code. All execution happens within kernel32's image-backed memory. From the perspective of the EDR:
| Attribute | Traditional Shellcode | OIS-ROP |
|---|---|---|
| Code location | Private memory (suspicious) | Image memory (trusted) |
| Memory protection | RWX or RW→RX transition | No changes. Image was always RX |
| Code origin | Unknown/injected | Microsoft-signed binary |
| What I control | Code content | Stack data only |
Composing the Score
To execute WinExec("calc", 1) via OIS-ROP:
# Windows x64 calling convention:
# RCX = first argument (lpCmdLine)
# RDX = second argument (uCmdShow)
# Find gadgets in kernel32.dll:
pop_rcx = find_gadget(kernel32, [0x59, 0xC3]) # pop rcx; ret
pop_rdx = find_gadget(kernel32, [0x5A, 0xC3]) # pop rdx; ret
winexec = get_proc_address(kernel32, "WinExec")
# The chain (just addresses and values):
chain = [
pop_rcx, # Gadget: pop rcx; ret
calc_string_addr, # Value loaded into RCX
pop_rdx, # Gadget: pop rdx; ret
0x1, # Value loaded into RDX (SW_SHOWNORMAL)
winexec, # Address of WinExec
]
Every address in this chain points to code inside kernel32.dll. When I pivot RSP to the chain and trigger the first RET, the CPU starts bouncing through Microsoft's code, loading values into registers, until it hits WinExec with the arguments set up.
The "shellcode" never exists. Only addresses into signed code.
The Stack Pivot
The remaining challenge: how do you get RSP to point to the chain? Several options exist:
- SetThreadContext: create a suspended thread, modify its context to set RSP, resume
- Exception handlers: trigger an exception, handler pivots the stack
- Existing gadgets: find a
xchg rax, rsp; retormov rsp, [reg]; ret
Once RSP points to the chain, you're not executing injected code. You're executing a very specific sequence of legitimate instructions that happen to do what you want.
This is also the moment of highest risk for the operator. The ROP chain itself is invisible. Just data. But the act of pivoting the stack pointer to an unexpected location is a detectable event. It's the conductor raising the baton.
The Cost of Precision
OIS-ROP is not a blunt instrument. It's a scalpel, and scalpels require precision.
Every gadget address is version-specific. If Microsoft updates ntdll.dll and a gadget moves by four bytes, the entire chain breaks. The orchestra plays the wrong notes. The process crashes.
This means:
- Chains must be built per-target or dynamically resolved at runtime
- Gadget availability varies between Windows versions and patch levels
- Complex payloads require careful gadget selection and may not be possible on all systems
Brittleness is the tradeoff for stealth. You're not writing portable shellcode. You're composing a symphony for a specific orchestra on a specific night.
Implications for Defense
What does this mean for detection?
Signature-based detection fails completely. There are no malicious bytes to signature. The payload is addresses into trusted code.
Behavioral detection gets harder. The execution happens within image-backed memory. Every instruction executed is part of a Microsoft-signed binary. The behavior (calling WinExec) is normal. It's the orchestration that's malicious.
Memory forensics finds nothing recognizable. No injected code. No shellcode patterns. Just a list of addresses that looks like stack data.
The defensive response has to shift from "look for malicious code" to "look for malicious orchestration." What does that look like in practice?
- Hardware stack protection: Intel CET and AMD Shadow Stack detect when returns don't match calls
- Call stack telemetry: flag when sensitive APIs (WinExec, VirtualProtect) are reached via RET instead of CALL
- Gadget chain heuristics: detect rapid sequences of small instruction blocks ending in RET
- Stack pivot detection: alert on RSP pointing outside the thread's legitimate stack region
These are harder problems than pattern matching. They require understanding execution flow, not just scanning memory.
The Evolution of the Invisible
OIS-ROP sits at the end of a trajectory that's been developing for nearly two decades.
2007: Return-Oriented Programming. Hovav Shacham formalized ROP in his seminal paper "The Geometry of Innocent Flesh on the Bone." The insight: you don't need to inject code if you can chain together existing instruction sequences. But traditional ROP was a means to an end, a stage-one technique to disable DEP and execute conventional shellcode. The malicious payload still existed; ROP just opened the door.
2013: JIT-ROP. Snow et al. demonstrated that gadgets could be discovered dynamically at runtime, defeating fine-grained ASLR. The memory space became a dictionary to be harvested on-the-fly. But again, the goal was to enable execution of a separate payload.
2016: AtomBombing. Liberman showed that Windows atom tables could store data across process boundaries, with ROP triggering execution. The "where" of storage became creative. But the payload still had to be delivered somewhere.
2021: Rope. D'Elia et al. took ROP further, distributing entire payloads as ROP chains across multiple processes, coordinated via transacted NTFS files. Sophisticated orchestration. But they still compiled C code into ROP chains. The malware logic existed; it was just encoded differently.
OIS-ROP: The Endpoint. I ask a different question: what if there's no payload to encode? No shellcode to translate. No C code to compile. Just a list of coordinates into signed binaries that, when interpreted as a ROP chain, produces the desired behavior. The malicious semantics are never written down. They emerge from orchestration alone.
The shift: Previous techniques asked "how do I hide my payload?" OIS-ROP asks "what if there is no payload?"
The Continuum
OIS-ROP fits into a broader research program:
- Veriduct: destroy format markers so files can't be identified
- OIS: store payloads as coordinates into signed binaries
- OIS-ROP: execute using only addresses in signed code
- Fermion: run Windows PE binaries in the browser via JavaScript
Each layer asks the same question: what assumptions does security tooling make, and what happens when you violate them?
Format destruction violates the assumption that files have identifiable structure. OIS violates the assumption that malicious bytes must be present somewhere. OIS-ROP violates the assumption that malicious execution requires malicious code.
The Interpreter Defines Meaning
At the heart of all this work is a single principle: meaning is not inherent in data. The interpreter creates it.
A byte sequence isn't "malicious" until something executes it that way. A file isn't "a PE" until a loader treats it as one. And code doesn't have to exist to be executed. You just need to convince the CPU to execute the right sequence of existing instructions.
Security tools look for malicious content. OIS-ROP demonstrates that content is irrelevant. What matters is orchestration, and orchestration leaves very different traces than injection.
The code I execute belongs to Microsoft. I just wrote the sheet music.