Beyond NX and DEP: Why Modern Applications Still Fall to ROP and What Security Teams Must Do Now
Return-Oriented Programming Explained: How Attackers Reuse Your Code — and How Defenders Fight Back

By Khalil Shreateh — Bug Bounty Hunter (Meta/Facebook) & Offensive Security Researcher

During a recent penetration test for a financial services client, I found myself staring at a hardened Linux server. NX was enabled. ASLR was fully randomized. Stack canaries were present. By all textbook metrics, the system was "secure." Forty-five minutes later, I had a root shell. The weapon? Return-Oriented Programming (ROP).

ROP is the great equalizer in modern exploitation. It doesn't inject a single byte of new code—it weaponizes the application's own legitimate instructions against itself. This guide isn't a theoretical lecture. It is the exact methodology I use to chain gadgets, defeat ASLR, and escape sandboxes during bug bounty hunts and red team engagements. Understanding how attackers think is the prerequisite for building controls that actually stop us.

 

What ROP Actually Looks Like in My Terminal

Before we dive into the theory, let's set the scene. When I target a binary, my first command is almost always:

$ ROPgadget --binary ./vuln_app --ropchain

Within seconds, I have a list of every useful instruction sequence ending in RET. On a standard x86_64 binary linked against glibc, I am looking for specific "golden" gadgets: pop rdi; ret (to set the first argument for a function call), pop rsi; ret (second argument), and pop rax; ret (for syscall numbers). If I find these three, I can call execve("/bin/sh", NULL, NULL) in about 15 lines of Python using pwntools.

What makes ROP devastating is its elegance. It doesn't need to inject shellcode. It simply hijacks the program's own control flow. Let's break down exactly how this works, how modern defenses try to stop it, and—most importantly—how I bypass those defenses in the field.


Section 1: The Anatomy of a ROP Chain

The Historical Context: Why We Needed ROP

In the early 2000s, exploiting a buffer overflow was straightforward: overwrite the return address, point it to your shellcode on the stack, and enjoy your shell. Then came the NX bit (No-Execute) / DEP (Data Execution Prevention). Memory pages were marked as either writable or executable—never both. Suddenly, executing shellcode from the stack was impossible.

ROP was the answer. Instead of injecting new code, attackers realized they could chain together snippets of existing code—"gadgets"—already present in the binary or its libraries. Every gadget ends with a RET instruction. By controlling the stack, the attacker dictates which gadget executes next, effectively building a custom program out of the binary's own organs.

Gadget Hunting: The Attacker's Shopping List

A gadget is simply a sequence of instructions ending in RET. Because x86_64 instructions are variable-length, jumping into the middle of an existing instruction often yields completely new, unintended gadgets. Tools like ROPgadget and rp++ automate this search.

Here is a typical gadget I look for on Linux x86_64:

0x00000000004012ab : pop rdi ; ret

Why? Because in the x86_64 System V ABI, the first argument to a function is passed in RDI. If I control RDI, I can pass any pointer (like a string) to a function. Combine this with a pop rsi; ret gadget and a pop rdx; ret, and I can call system() with arbitrary arguments.

My go-to gadget hunting strategy: On a recent engagement, the binary was compiled without PIE (Position-Independent Executable). This was a gift. The base address was fixed, meaning I didn't need an info leak. I ran ROPgadget against libc, found a pop rdi; ret at a static offset, and chained it with a system() call. The exploit took 10 minutes to write. The client paid $10,000 for the report. Always check for non-PIE binaries first—they are easy money.


Section 2: Defeating NX/DEP — Why the "Green Lock" Fails

NX/DEP asks one question: "Is this memory page executable?" ROP answers: "I don't care. I am executing code that is already marked executable." Since every gadget resides in the binary's .text section or in loaded libraries (which are executable by default), the CPU hardware never throws an exception. The processor executes the attacker's intended logic while believing it is running normal program code.

This is why NX/DEP alone is insufficient. It stops script-kiddies using Metasploit's default payloads, but it does nothing to stop a researcher who knows how to chain pop and mov instructions.

ASLR: The Annoying Speed Bump

ASLR randomizes the base addresses of the binary and libraries. If I don't know where libc is loaded, I don't know where my pop rdi gadget is. However, ASLR has a fatal flaw: information leaks. If the application outputs a memory address (e.g., a verbose error message, a format string vulnerability, or a use-after-free pointer), I can calculate the exact location of every gadget.

In 2026, the standard attack chain is:

  1. Leak: Exploit a memory disclosure bug to get the base address of libc or the binary.
  2. Calculate: Add the static offset from ROPgadget to the leaked base.
  3. Execute: Trigger the buffer overflow and chain the gadgets using the calculated addresses.

Bypassing ASLR without a leak: On 32-bit systems, the entropy is low enough that I can brute-force the address. I simply spray the stack with a sled of gadget addresses. One of them will eventually hit. On 64-bit systems, this is impossible—but I rarely encounter a 64-bit application without some form of info leak. In my experience, 80% of "secure" applications leak a pointer in their first HTTP response header or error log. Always check the JSON responses for stack traces.


Section 3: Modern Hardware Defenses — CET, Shadow Stacks, and CFI

In recent years, hardware vendors have fought back. Intel's CET (Control-flow Enforcement Technology) introduces a Shadow Stack—a second, hardware-protected stack that stores return addresses. When a RET executes, the CPU compares the return address on the regular stack against the one on the shadow stack. If they mismatch, the CPU faults immediately.

This is catastrophic for ROP. If the attacker can't overwrite the return address without corrupting the shadow stack, the attack chain dies at the first RET.

How I Deal with CET in the Field

When I encounter a CET-enabled target, I immediately switch tactics:

  • JOP (Jump-Oriented Programming): Instead of corrupting RET, I hijack indirect jumps (jmp [rax]). CET's shadow stack doesn't protect indirect jumps—only forward-edge CFI does.
  • Signal ROP: On Linux, signal handlers can change the stack context. There are known techniques to redirect control flow via signal frames without touching the shadow stack.
  • Return to libc (ret2libc): If I can control function arguments but not the return chain, I can redirect directly to a function like system() without a multi-gadget chain.

Testing CET limits: During a recent red team exercise, I faced a Windows 11 system with Hardware-enforced Stack Protection enabled. My classic ROP chain crashed instantly. I switched to a call qword ptr [rax] gadget where I controlled rax. Since CET doesn't verify the target of CALL instructions (only RET), I was able to call VirtualProtect and mark my shellcode executable. Defenders often overlook these edge-case jumps. CET is not a silver bullet—it forces attackers to be slightly more creative, but it does not eliminate the threat.


Section 4: Why ROP is Still the King of Exploitation in 2026

You might think that with CET, CFI, and Rust on the rise, ROP is dying. It is not.

  • Legacy Systems: I still regularly hack SCADA devices, embedded routers, and medical equipment running kernels from 2010. They have no ASLR, no PIE, and no CET. They are wide open.
  • Browser Exploitation: Browsers are enormous C++ codebases. Despite sandboxing, renderer exploits almost always use ROP to escape the sandbox. Chrome's V8 engine has had dozens of ROP-enabled escapes patched in the last two years alone.
  • 0-Day Development: Memory corruption vulnerabilities are still the most valuable 0-days. ROP is the default payload for these exploits because it works reliably across Windows, Linux, and macOS.

My busiest bug bounty category: Without a doubt, it's memory corruption in IoT firmware. I recently reviewed a popular smart camera. The firmware was compiled with -fno-stack-protector and no PIE. I extracted the binary, ran ROPgadget, found a system() call, and overwrote the return address with the exact offset to spawn a telnet shell. I didn't even need to leak an address because there was no ASLR. That vulnerability paid for my entire month's rent. Legacy hardware is the gift that keeps on giving.


Section 5: Hardening Your Defenses — A Pentester's Checklist

I am going to tell you exactly what makes my job difficult. If you do these things, you will stop 99% of the ROP attacks I attempt.

For Developers: Stop Writing Vulnerable Code

  • Adopt Memory-Safe Languages: Rust, Go, and Swift eliminate buffer overflows at compile time. If you are writing new network-facing services, use Rust. I cannot exploit a memory bug that doesn't exist.
  • Enable All Compiler Protections: On GCC/Clang, I check for -fstack-protector-strong, -fPIE, and -Wl,-z,relro,-z,now. If I see these missing, I mark the binary as "easily exploitable."
  • Fuzz Everything: Integrate AFL++ or libFuzzer into your CI/CD pipeline. The bugs I find are usually bugs that fuzzing would have caught if it had been run for 24 hours.

For Administrators: Harden the Runtime Environment

  • Enable CET: If you are on Intel Tiger Lake or newer, enable CET in the BIOS and in the OS. It raises the bar significantly.
  • Remove unnecessary tools: In many engagements, I rely on gcc, python, or wget being installed on the target server. Remove compilers and scripting runtimes from production servers.
  • Use seccomp-bpf: Restrict the syscalls a process can make. Even if I build a ROP chain, if I can't call execve or socket, I can't do anything useful.

My secret defense weapon: I tell all my clients to use seccomp-bpf. During a test, I spent 6 hours building a perfect ROP chain for a web service. I ran it, and my execve syscall was killed by seccomp. The process just crashed. My exploit was dead. Defenders, please deploy strict syscall filtering. It is an absolute nightmare for attackers.


Section 6: OWASP & NIST in Practice (How I Audit These)

I see organizations spend thousands of dollars on compliance documentation without implementing the actual controls. Here is how I test the OWASP and NIST recommendations in real audits:

  • OWASP ASVS Memory Management: I check if the binary uses strcpy or sprintf (unsafe). If I see these imported in the GOT, I know the codebase is ancient and likely riddled with bugs.
  • NIST SP 800-218: I review the build logs. If I don't see the -fstack-protector flag, the organization fails my audit immediately. I don't care about their policies—I care about their compiler output.

Compliance is not security. I have walked into "ISO 27001 certified" organizations and found binaries with no PIE. The auditors looked at the policy, not the code. My advice: treat NIST and OWASP as minimum baselines, not the finish line.


Quick-Reference ROP Defense Checklist

  • Compile with -fstack-protector-strong (stack canaries).
  • Compile with -fPIE and link with -pie (enable ASLR).
  • Enable -Wl,-z,relro,-z,now (full RELRO).
  • Use -fcf-protection=full (CET support) on Linux.
  • On Windows, enable /CETCOMPAT and /guard:cf.
  • Deploy seccomp-bpf or AppArmor to restrict syscalls.
  • Conduct regular fuzzing with AddressSanitizer enabled.
  • Remove info leaks (suppress verbose error logs in production).
  • Use Rust or Go for new security-critical components.
  • Verify ASLR entropy is active (kernel.randomize_va_space=2).

Final Thoughts: The Game of Cat and Mouse

Return-Oriented Programming is not a historical curiosity—it is the workhorse of modern exploitation. Every time the industry rolls out a new defense (NX, ASLR, CFI, CET), attackers adapt. We find new gadgets, we pivot to JOP, we hijack signal frames. This is the nature of the cybersecurity arms race.

For defenders, the lesson is clear: do not rely on a single control. Layer your defenses. Assume that ASLR will be bypassed. Assume that CET will be circumvented. Build your security posture on the assumption that an attacker will achieve code execution, and restrict what they can do with it (least privilege, micro-segmentation, strong egress filtering).

I have spent years on the offensive side of this equation, and I can tell you the hardest targets to breach are not the ones with the fanciest firewalls. They are the ones that have eliminated memory corruption bugs entirely by using Rust, or the ones that have deployed strict seccomp policies that strangle my exploit chains before they can establish persistence.

Be that target. Make my life difficult. Start by enabling CET and fuzzing your codebase today.

If you have questions about securing a specific binary or mitigating a specific ROP path, feel free to reach out through my website. I moonlight as a consultant specifically to help organizations fix these exact issues.

Written by Khalil Shreateh
Bug Bounty Hunter (Meta/Facebook) & Offensive Security Researcher
Official Website: khalil-shreateh.com

Social Media Share
About Contact Terms of Use Privacy Policy
© Khalil Shreateh — Cybersecurity Researcher & White-Hat Hacker — Palestine 🇵🇸
All content is for educational purposes only. Unauthorized use of any information on this site is strictly prohibited.