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.
Overview
angrop’s chain builder automatically assembles ROP gadgets into functional exploit chains. Rather than manually selecting and linking gadgets, you specify high-level goals (“set rax to 0x1234”, “write data to memory”, “call execve”) and angrop finds the optimal gadget sequence.
Architecture
The chain builder consists of specialized modules, each responsible for different types of operations:
Core Modules
From chain_builder/__init__.py:42-53:
self._reg_setter = RegSetter(self) # Set registers to values
self._reg_mover = RegMover(self) # Move data between registers
self._mem_writer = MemWriter(self) # Write data to memory
self._mem_changer = MemChanger(self) # Modify memory in-place
self._func_caller = FuncCaller(self) # Call functions
self._sys_caller = SysCaller(self) # Invoke syscalls
self._sigreturn = SigreturnBuilder(self) # Build sigreturn frames
self._pivot = Pivot(self) # Pivot the stack
self._shifter = Shifter(self) # Adjust stack pointer
Each module:
- Bootstraps by analyzing available gadgets and building internal data structures
- Optimizes by finding better gadget combinations
- Generates chains on demand using graph search and constraint solving
The RegSetter Module
The most fundamental module. Sets registers to arbitrary values using graph search.
Graph Search Algorithm
From reg_setter.py:474-612, RegSetter builds a directed graph where:
- Nodes: States representing which registers have been set to target values
- Edges: Gadgets that transition between states
Example: To set rax=0x1234 and rbx=0x5678:
(False, False) → (True, False) → (True, True)
↓ ↓ ↓
Neither rax set Both set
Edges are gadgets like:
pop rax; ret - Sets rax
pop rbx; pop r12; ret - Sets rbx (and r12)
pop rax; pop rbx; ret - Sets both at once
Register Setting Strategy
From reg_setter.py:749-777:
def run(self, preserve_regs=None, warn=True, **registers):
# 1. Handle "hard" registers (can't pop, or contain badbytes)
hard_chain = self._handle_hard_regs(gadgets, registers, preserve_regs)
# 2. Build graph and search for paths
chains = self.find_candidate_chains_giga_graph_search(
modifiable_memory_range, registers, preserve_regs, warn)
# 3. Try each candidate chain
for gadgets in chains:
chain = self._build_reg_setting_chain(gadgets, registers)
if self.verify(chain, preserve_regs, registers):
return chain
Hard registers are handled specially:
- Registers containing badbytes: Use arithmetic gadgets to construct the value
- Unpoppable registers: Use register moves or memory reads
Concrete Value Crafting
When a register can’t be popped directly, angrop crafts the value using arithmetic:
From reg_setter.py:699-725:
def _find_add_chain(self, reg, val):
# Find chain like: pop rax; ret + add rax, 0x1000; ret
for g1 in concrete_setter_gadgets:
for g2 in delta_gadgets:
init_ast, final_ast = g2.concrete_reg_changes[reg]
# Check if g1's value + g2's delta == target value
Example: To set rax = 0x41424344 (contains null bytes):
pop rax; ret with value 0x41424300
add rax, 0x44; ret
- Result:
rax = 0x41424344 (no null bytes in payload)
The MemWriter Module
Writes arbitrary data to memory addresses.
Basic Memory Writing
From mem_writer.py:464-492:
def _write_to_mem(self, addr, string_data, preserve_regs, fill_byte):
# 1. Find gadgets with controllable memory writes
for gadget in self._gen_mem_write_gadgets(string_data, key):
mem_write = gadget.mem_writes[0]
# 2. Set up registers to control address and data
reg_vals = {}
for reg in mem_write.addr_dependencies:
reg_vals[reg] = addr_value
for reg in mem_write.data_dependencies:
reg_vals[reg] = data_value
# 3. Build chain: set_regs + memory_write_gadget
chain = self.set_regs(**reg_vals, preserve_regs=preserve_regs)
chain += RopBlock.from_gadget(gadget)
Gadget selection criteria (from mem_writer.py:358-375):
- Self-contained
- Exactly one memory write
- Address and data independently controllable
- No symbolic memory reads
Handling Badbytes
When data contains badbytes, angrop uses memory modification gadgets:
From mem_writer.py:631-661:
def _try_write_plan(self, plan, addr, data, preserve_regs, fill_byte):
# Try to transform data to avoid badbytes:
for init_blob, op, arg in self._find_chunk_transforms(chunk, badbytes):
# Write safe initial value
chain = self._write_to_mem(ptr, init_blob)
# Modify memory to get target value
chain += self.mem_xor(ptr, arg) # or mem_add, mem_or, mem_and
Example: Writing \x00\x41\x42\x43 with \x00 as badbyte:
- Write
\x01\x41\x42\x43 (no badbytes)
- Execute
xor dword [rax], 0x00000001
- Result:
\x00\x41\x42\x43
The MemChanger Module
Modifies memory in-place using arithmetic/logical operations.
Supported operations:
mem_add(addr, value) - [addr] += value
mem_xor(addr, value) - [addr] ^= value
mem_or(addr, value) - [addr] |= value
mem_and(addr, value) - [addr] &= value
These are used both for crafting values and for direct exploitation.
The FuncCaller Module
Invokes functions with specified arguments, handling calling conventions.
From the ChainBuilder API:
def func_call(self, address, args, preserve_regs=None, needs_return=True):
# Automatically handles calling convention:
# - x86: Arguments on stack
# - x64: Arguments in rdi, rsi, rdx, rcx, r8, r9, then stack
# - ARM: Arguments in r0-r3, then stack
The SysCaller Module
Builds syscall invocation chains.
From chain_builder/__init__.py:138-152:
def do_syscall(self, syscall_num, args, needs_return=True):
# Sets up syscall registers:
# - x86: eax=syscall_num, ebx, ecx, edx, esi, edi, ebp
# - x64: rax=syscall_num, rdi, rsi, rdx, r10, r8, r9
# Then executes syscall gadget
Special case: execve chains
def execve(self, path=b"/bin/sh\x00", path_addr=None):
# 1. Write path string to writable memory
# 2. Set syscall registers (rax=59, rdi=path_addr, rsi=0, rdx=0)
# 3. Execute syscall gadget
Chain Composition
Chains are built incrementally and can be combined:
RopChain Objects
From usage:
# Create individual chains
chain1 = rop.set_regs(rax=0x1234)
chain2 = rop.set_regs(rbx=0x5678)
chain3 = rop.write_to_mem(0x601000, b"Hello")
# Combine chains
full_chain = chain1 + chain2 + chain3
# Generate payload
payload = full_chain.payload_str()
full_chain.print_payload_code()
RopBlock Normalization
Complex gadgets are wrapped in RopBlocks that encapsulate:
- The gadget sequence
- Required stack values
- Symbolic constraints
- Register preservation requirements
From builder.py:879-956:
def normalize_gadget(self, gadget):
# Converts raw gadget into usable RopBlock:
# 1. Handle conditional branches
# 2. Normalize transit type (jmp_reg, jmp_mem)
# 3. Add stack shifters if needed
# 4. Constrain symbolic memory accesses
Optimization
The chain builder optimizes iteratively:
From chain_builder/__init__.py:232-244:
def optimize(self, processes=1):
again = True
cnt = 0
while again and cnt < 5:
# Try to improve reg_mover
again = self._reg_mover.optimize(processes=processes)
# Try to improve reg_setter
again |= self._reg_setter.optimize(processes=processes)
cnt += 1
Optimization strategies:
- Register moves: Find ways to set hard registers by moving from easy ones
- Gadget normalization: Make non-self-contained gadgets usable
- Chain shortening: Replace long chains with shorter equivalent ones
Constraint Solving
The chain builder uses symbolic execution and constraint solving throughout:
Building Reg Setting Chains
From builder.py:362-556:
def _build_reg_setting_chain(self, gadgets, register_dict):
# 1. Create symbolic state
test_symbolic_state = make_symbolic_state(self.project, ...)
# 2. Step through each gadget symbolically
for gadget in gadgets:
state = step_to_unconstrained_successor(state)
# 3. Constrain final register values
for reg, val in register_dict.items():
state.solver.add(state.regs[reg] == val)
# 4. Solve for required stack values
chain = RopChain(self.project, state=test_symbolic_state)
Rebalancing Constraints
When gadgets transform values, angrop “rebalances” constraints:
From builder.py:245-359:
def _rebalance_ast(self, lhs, rhs):
# Given: stack_value + 0x1000 == user_value
# Solve: stack_value == user_value - 0x1000
# Supports: add, sub, xor, and, or, shifts, extracts
This allows using gadgets like pop rax; add rax, 0x10; ret transparently.
Example: Building a Complete Chain
import angr
import angrop
p = angr.Project("/bin/vulnerable")
rop = p.analyses.ROP()
rop.find_gadgets()
# High-level chain construction
chain = rop.write_to_mem(0x601000, b"/bin/sh\x00")
chain += rop.func_call("system", [0x601000])
# angrop automatically:
# 1. Finds memory write gadgets
# 2. Sets up address and data registers
# 3. Writes the string
# 4. Sets up argument registers per calling convention
# 5. Calls system()
print(chain.payload_str().hex())
chain.print_payload_code()
The chain builder handles all the complexity:
- Gadget selection and ordering
- Register allocation and preservation
- Constraint solving for stack values
- Calling convention adherence
- Badbyte avoidance