RSSAmplifier

Kayssel - Offensive Security Blog · Aug 15, 2026

Stack Overflows to ROP: Beating NX, Canaries, ASLR

0
Sign in to vote or save

Ruben Santos Garcia · Kayssel

10 min read

August 15, 2026

Stack Overflows to ROP: Beating NX, Canaries, ASLR

Table of contents

Contents

👋 Introduction

Hey everyone!

Last week we turned the browser into a side channel. This week we go all the way down to the stack, to the oldest bug class in the book and the mitigations that supposedly killed it.

A stack buffer overflow is memory corruption in its purest form. You write past a buffer, you overwrite the saved return address the CPU uses to know where to resume, and when the function returns the processor jumps wherever you told it. That single primitive, control of the instruction pointer through a stack write, has been the root of exploitation since 1996. Three mitigations were built to stop it. None of them did.

NX made the stack non-executable. Stack canaries put a guard value in front of the return address. ASLR randomized every address so you could not hardcode a target. Each one broke a specific technique, and each one forced a smarter replacement. Exploitation did not die. It evolved into reusing the program’s own code against it.

This week: smashing the stack the classic way, ret2libc to survive NX, ROP chains that turn the stack into a program, leaking the canary that guards the return address, and the info leak that unravels ASLR entirely.

Let’s get into it 👇

🧱 Smashing the Stack

Start with the primitive everything else builds on. When a function is called, the CPU pushes the return address, the bookmark it uses to resume after the function finishes, onto the stack. The function’s local buffers sit just below it. The stack grows downward, but array writes move upward, so overflowing a local buffer walks straight into that saved return address.

With no mitigations, you overwrite the return address with a pointer to shellcode you placed in the same buffer. On return, the CPU jumps into the stack and runs your bytes as code.

from pwn import *
# No NX, no canary, no ASLR: inject code, point the return address at it
payload  = shellcode
payload  = payload.ljust(offset, b"\x90")   # NOP pad up to the saved return addr
payload += p64(stack_address_of_shellcode)  # overwrite return address -> stack

This is the whole of Aleph One’s 1996 “Smashing the Stack”, still the founding text. Then NX arrived and marked the stack non-executable. The CPU still jumps to your address, but the instant it tries to execute stack bytes, it faults. Injecting code onto the stack stopped working overnight, and that single change forced everything that follows.

🔁 ret2libc: Living Off the Land

If you cannot run your own code, run the code that is already there. NX marks the stack non-executable, but libc, the C standard library linked into nearly every binary, is full of executable functions. One of them is system(). Point the return address at it, hand it "/bin/sh", and you get a shell without executing a single injected byte.

The argument setup is where architecture matters. On 32-bit x86, function arguments go on the stack, so you fake a stack frame. On x86-64, the first argument goes in the RDI register, which you cannot set by just placing a value on the stack. You need a small piece of existing code to load it, which is already the first step toward ROP.

# x86-64 ret2libc: a "pop rdi ; ret" gadget loads the argument register
payload  = b"A" * offset
payload += p64(pop_rdi) + p64(binsh_addr)   # RDI = pointer to "/bin/sh"
payload += p64(ret)                         # 16-byte stack alignment for movaps
payload += p64(system_addr)                 # system("/bin/sh")

Nergal’s “Advanced return-into-lib(c)” generalized this in 2001 into chaining multiple library calls. NX did not stop exploitation. It just moved the payload from your code into the program’s own code, and taught attackers to build attacks out of pieces that were always meant to be there.

⛓️ ROP: The Stack Becomes a Program

ret2libc reuses whole functions. Return-Oriented Programming reuses fragments, and it is Turing-complete. This is the technique that made non-executable memory almost irrelevant.

A gadget is a short sequence of existing instructions ending in ret, like pop rdi ; ret or mov [rax], rdx ; ret. Because it ends in ret, control bounces back to the stack when it finishes. So you fill the stack with a chain: a gadget address, the data it consumes, the next gadget, its data. Each ret pops the next address and jumps to it. The stack becomes a program and the gadgets are its instructions, all from bytes already marked executable.

# The stack drives execution: every ret advances to the next gadget
chain  = p64(pop_rdi) + p64(arg1)             # load first arg
chain += p64(pop_rsi_r15) + p64(arg2) + p64(0)  # load second arg
chain += p64(target_func)                     # call with registers set

You find gadgets with ROPgadget or Ropper, scanning the binary and libc for every sequence ending in ret. This is the same memory-corruption discipline behind the browser chains from Issue 59, where NX and modern mitigations forced the exact same evolution from injected code to code reuse. With enough gadgets, non-executable memory protects nothing. You never execute new code. You just rearrange the old.

🐤 Leaking the Canary

NX is handled. Now the canary. A stack canary is a secret value the compiler places between the buffer and the saved return address, then checks right before the function returns. A linear overflow that reaches the return address has to pass through the canary first, changing it, and the check aborts the program. Overwrite the return address, and you trip the alarm before you ever get to use it.

You do not disable the canary. You learn it, then write it back unchanged. A format-string bug or any memory disclosure leaks the value, and you place that exact value in its slot so the check passes.

# Stage 1: leak the canary via a format-string or overread bug
canary = u64(leak[:8])
# Stage 2: put it back exactly, then overflow past it into the return address
payload  = b"A" * canary_offset
payload += p64(canary)     # check sees the right value, no abort
payload += b"B" * 8        # saved RBP
payload += rop_chain       # now the return address is yours

On forking servers there is a second path: each child inherits the parent’s canary, so you brute-force it one byte at a time, a correct guess keeps the child alive and a wrong one crashes it. Either way the canary is not a wall. It is a value, and any leak turns a value into a formality.

🎲 Defeating ASLR: Leak, Then Win

The last mitigation is the strongest. ASLR randomizes where libc and the binary load, so you cannot hardcode the address of system or any gadget. But it randomizes the base, not the internals. Every function and gadget keeps the same offset from the start of libc. Leak one real address and you compute the base, and from the base you compute everything.

The canonical first stage uses the program’s own resolver. The GOT, or Global Offset Table, holds the resolved runtime addresses of libc functions. Call puts with a GOT entry as its argument, and it prints a live libc address straight to you.

# Stage 1: leak libc by printing a GOT entry, then return to main for stage 2
rop1  = p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
# receive the leak, then: libc_base = leaked_addr - libc.symbols['puts']
# Stage 2: full ret2libc with system and "/bin/sh" now at known addresses

The two-stage “leak, then exploit” pattern is the heart of modern stack exploitation, and ir0nstone’s notes walk it end to end. ASLR does not stop you. It adds one prerequisite: find an address before you use one. Once you have a single leak, the randomization that looked like the strongest defense collapses into arithmetic.

🔫 The one_gadget Finisher

Once you have a libc leak, you often do not even need a full ret2libc chain. libc hides a handful of “magic” addresses where a single jump gives you execve("/bin/sh", NULL, NULL), as long as a few register or stack conditions happen to hold at that moment.

The tool one_gadget finds them and prints the constraint each one requires.

one_gadget libc.so.6
# 0x50a37 execve("/bin/sh", rsp+0x40, environ)
#   constraint: [rsp+0x40] == NULL

Add the leaked libc base to a one_gadget offset, arrange for its constraint to hold, and one address becomes your entire second stage. For binaries too gadget-poor for a clean pop rdi, ret2csu reuses the compiler-generated __libc_csu_init to populate the argument registers instead. The pattern that generalizes: the more of libc you can see, the shorter your payload gets, until a working exploit is a single address.

🎯 Key Takeaways

The mental model to carry out of this issue: modern binary exploitation is not one payload, it is a pipeline, and every mitigation adds a stage rather than a wall. NX means you reuse code instead of injecting it. A canary means you leak it before you overflow. ASLR means you leak an address before you aim. When you look at a hardened binary, do not ask whether it is exploitable. Ask how many leaks you need to line up first.

The information leak is the master key. Almost every mitigation on a modern binary falls to a single memory disclosure, because a leak defeats the canary and ASLR at once. That reframes what you hunt for. The overflow gives you control of the instruction pointer, but the leak is what makes that control usable, so a format-string bug or an overread is often worth more than the overflow itself.

Code reuse is the permanent answer to non-executable memory. ret2libc reuses functions, ROP reuses instruction fragments, and neither ever executes new code, so NX is a speed bump, not a stop. The skill that transfers everywhere is reading a binary for gadgets and turning its own instructions into your program.

For the workflow: run checksec first to see which mitigations are on, because that dictates your pipeline. No canary and no PIE, a straight ROP chain works. Canary present, find your leak first. ASLR on, plan the two-stage puts-GOT leak. Reach for pwntools to script it, ROPgadget or Ropper to find the gadgets, and pwndbg to watch the stack as your chain executes. The mitigations tell you the order of operations.


Practice:


Thanks for reading, and happy hunting!

— Ruben

Other Issues

XS-Leaks: Turning the Browser Into a Side Channel

XS-Leaks: Turning the Browser Into a Side Channel

Previous Issue

Read the original on kayssel.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.