Skip to main content

Utility Functions

angrop provides a comprehensive set of utility functions in rop_utils.py for working with symbolic execution, gadget analysis, and ROP chain construction.

Address and Assembly

addr_to_asmstring

def addr_to_asmstring(project, addr) -> str
Converts an address to a human-readable assembly string.
project
angr.Project
required
The project containing the binary
addr
int
required
The address to disassemble
Returns: String of semicolon-separated instructions (e.g., "pop rax; pop rbx; ret") Example:
asm = addr_to_asmstring(project, 0x401000)
print(asm)  # "mov eax, ebx; add eax, 0x10; ret"

AST Dependency Analysis

get_ast_dependency

def get_ast_dependency(ast) -> set
Identifies which registers affect a symbolic AST expression.
ast
claripy.ast.BV
required
The AST to analyze. Must be created from a symbolic state where registers are named "sreg_REG-"
Returns: Set of register names that affect the AST value Algorithm:
  • Extracts all variables starting with "sreg_"
  • Returns the register name portion (e.g., "sreg_rax-123""rax")
  • Returns empty set if any non-register variables are found
Example:
# After symbolic execution
rax_val = final_state.registers.load('rax')
deps = get_ast_dependency(rax_val)
print(deps)  # {'rbx', 'rcx'}  (rax depends on rbx and rcx)

get_ast_controllers

def get_ast_controllers(state, ast, reg_deps) -> set
Identifies which registers can fully control (unconstrain) an AST expression.
state
angr.SimState
required
The symbolic state
ast
claripy.ast.BV
required
The AST to analyze
reg_deps
set
required
Set of register dependencies (from get_ast_dependency)
Returns: Set of register names that can make the AST take arbitrary values Algorithm:
  1. For each dependent register, set all other registers to a test value
  2. Check if the resulting AST is unconstrained using fast_unconstrained_check
  3. Return registers that allow arbitrary values
Example:
# rax = rbx + rcx + 0x1000
controllers = get_ast_controllers(state, rax_val, {'rbx', 'rcx'})
print(controllers)  # {'rbx', 'rcx'}

# rax = (rbx & 0xFF) + rcx
controllers = get_ast_controllers(state, rax_val, {'rbx', 'rcx'})
print(controllers)  # {'rcx'}  (rbx is constrained by AND)

get_ast_const_offset

def get_ast_const_offset(state, ast, reg_deps) -> int
Extracts the constant offset from a memory access expression.
state
angr.SimState
required
The symbolic state
ast
claripy.ast.BV
required
The memory address AST
reg_deps
set
required
Register dependencies
Returns: The constant offset value Example:
# mov [rax + 0x10], rbx
addr_ast = mem_write_action.addr.ast
offset = get_ast_const_offset(state, addr_ast, {'rax'})
print(offset)  # 0x10

Constraint Checking

unconstrained_check

def unconstrained_check(state, ast) -> bool
Checks if an AST is completely unconstrained (can take any value). Returns: True if the AST has no constraints in the solver

fast_unconstrained_check

def fast_unconstrained_check(state, ast) -> bool
Quickly checks if an AST is probably unconstrained using heuristics. Heuristics:
  1. Allowed operations: Extract, BVS, add, sub, xor, Reverse, BVV, ZeroExt, SignExt
  2. Disallowed patterns:
    • Bitwise AND with non-all-ones constant
    • Bitwise OR with non-zero constant
    • Shifts by non-zero amount
    • Operations like x + x (constrained)
  3. Byte-level check: Each byte must be unconstrained
  4. Fallback: Uses loose_constrained_check if heuristics pass
Example:
# rax is completely controllable
if fast_unconstrained_check(state, state.regs.rax):
    print("Can set rax to any value")

# (rax & 0xFF) is constrained
if not fast_unconstrained_check(state, state.regs.rax & 0xFF):
    print("Cannot fully control lower byte")

loose_constrained_check

def loose_constrained_check(state, ast, extra_constraints=None) -> bool
Checks if an AST can take at least 3 out of 5 test values. Test values:
  1. 0x0
  2. 0xFFFFFFFF... (all ones)
  3. 0xAAAAAAAA... (alternating)
  4. 0x55555555... (alternating)
  5. 0x9ABC... (mixed pattern)
Returns: True if at most 2 test values are unsatisfiable

Register Utilities

get_reg_name

def get_reg_name(arch, reg_offset) -> str
Finds the register name for a given offset in the register file.
arch
angrop.arch.Arch
required
Architecture instance
reg_offset
int
required
Byte offset in the register file
Returns: Register name Raises: RegNotFoundException if no register found at offset Example:
from angrop.arch import get_arch

arch = get_arch(project)
reg_name = get_reg_name(arch, 0)  # "rax" on x64

State Creation

make_initial_state

def make_initial_state(project, stack_gsize) -> angr.SimState
Creates an optimized initial state for ROP analysis. Features:
  • Custom memory plugin (SpecialMem) for faster uninitialized memory
  • Symbolic stack of size stack_gsize * arch.bytes
  • Optimized angr options for gadget analysis
  • 1-second solver timeout
Options enabled:
  • CONSERVATIVE_READ_STRATEGY
  • AVOID_MULTIVALUED_WRITES
  • NO_SYMBOLIC_JUMP_RESOLUTION
  • TRACK_ACTION_HISTORY
  • TRACK_REGISTER_ACTIONS
  • TRACK_MEMORY_ACTIONS
Options disabled:
  • AVOID_MULTIVALUED_READS
  • SUPPORT_FLOATING_POINT
  • All resilience and simplification options

make_symbolic_state

def make_symbolic_state(project, reg_set, stack_gsize, extra_reg_set=None, symbolize_got=False) -> angr.SimState
Creates a symbolic state with specified registers symbolized.
project
angr.Project
required
The angr project
reg_set
set
required
Registers to symbolize (named "sreg_REG-")
stack_gsize
int
required
Symbolic stack size in pointer-sized elements
extra_reg_set
set | None
default:"None"
Additional registers to symbolize (named "esreg_REG-")
symbolize_got
bool
default:"False"
Symbolize the GOT table (for non-FULL RELRO binaries)
Example:
state = make_symbolic_state(
    project, 
    reg_set={'rax', 'rbx', 'rcx'},
    stack_gsize=80
)
# state.regs.rax is now symbolic (BVS("sreg_rax-...", 64))

Execution Control

step_one_inst

def step_one_inst(project, state, stop_at_syscall=False) -> angr.SimState
Steps a state forward by exactly one instruction. Handles:
  • Kernel execution (steps through if not stop_at_syscall)
  • Hooked addresses (steps through)

step_to_unconstrained_successor

def step_to_unconstrained_successor(project, state, max_steps=2, allow_simprocedures=False, stop_at_syscall=False) -> angr.SimState
Steps until reaching an unconstrained successor or syscall.
max_steps
int
default:"2"
Maximum steps to take
Returns: State at unconstrained successor or syscall Raises: RopException if cannot reach unconstrained state

step_to_syscall

def step_to_syscall(state) -> angr.SimState
Steps state forward until just before a syscall instruction. Returns: State at syscall instruction Raises: RuntimeError if unable to reach syscall

Address Checking

is_kernel_addr

def is_kernel_addr(project, addr) -> bool
Checks if an address is in kernel space. Returns: True if the address belongs to the kernel object (cle##kernel)

is_in_kernel

def is_in_kernel(project, state) -> bool
Checks if a state’s instruction pointer is in kernel space.

Timeout Decorator

timeout

@timeout(seconds_before_timeout)
def your_function(...):
    # Your code here
Decorator that raises RopTimeoutException if function exceeds time limit. Features:
  • Uses SIGALRM signal
  • Handles nested timeouts (respects shortest timeout)
  • Delays timeout during __del__ methods to avoid exceptions during cleanup
Example:
from angrop.rop_utils import timeout
from angrop.errors import RopTimeoutException

@timeout(3)
def analyze_gadget(addr):
    # This will timeout after 3 seconds
    result = expensive_analysis(addr)
    return result

try:
    gadget = analyze_gadget(0x401000)
except RopTimeoutException:
    print("Analysis timed out")

Value Conversion

cast_rop_value

def cast_rop_value(val, project) -> RopValue
Converts a value to a RopValue instance and performs rebase analysis.

bits_extended

def bits_extended(ast) -> int | None
Returns the number of bits added by ZeroExt or SignExt operations. Example:
# ast = ZeroExt(32, BVS('x', 32))
bits = bits_extended(ast)  # 32

Error Classes

angrop defines custom exceptions in errors.py for fine-grained error handling.

RegNotFoundException

class RegNotFoundException(Exception)
Raised when a register cannot be found at a specified offset. Example:
from angrop.rop_utils import get_reg_name
from angrop.errors import RegNotFoundException

try:
    reg = get_reg_name(arch, 9999)
except RegNotFoundException as e:
    print(f"Register not found: {e}")

RopException

class RopException(Exception)
Base exception for general ROP analysis errors. Common scenarios:
  • Gadget does not reach unconstrained state
  • Cannot get to single successor
  • SP change is symbolic or uncontrolled
  • Memory access with no dependencies
Example:
from angrop.errors import RopException

try:
    gadget = analyze_complex_gadget(addr)
except RopException as e:
    print(f"Gadget analysis failed: {e}")

RopTimeoutException

class RopTimeoutException(RopException)
Raised when gadget analysis exceeds the timeout limit. Usage with timeout decorator:
from angrop.rop_utils import timeout
from angrop.errors import RopTimeoutException

@timeout(3)
def slow_analysis(addr):
    # Analysis code
    pass

try:
    slow_analysis(0x401000)
except RopTimeoutException:
    print("Analysis exceeded 3 second timeout")

Common Patterns

Custom Gadget Analysis

from angrop.rop_utils import (
    make_symbolic_state,
    step_to_unconstrained_successor,
    get_ast_dependency,
    get_ast_controllers
)
from angrop.errors import RopException

def analyze_custom_gadget(project, addr, arch):
    # Create symbolic state
    state = make_symbolic_state(
        project,
        reg_set=arch.reg_list,
        stack_gsize=80
    )
    state.ip = addr
    
    try:
        # Step to unconstrained
        final_state = step_to_unconstrained_successor(
            project, state, max_steps=2
        )
        
        # Analyze register effects
        rax_val = final_state.registers.load('rax')
        deps = get_ast_dependency(rax_val)
        controllers = get_ast_controllers(state, rax_val, deps)
        
        return {
            'addr': addr,
            'rax_deps': deps,
            'rax_controllers': controllers
        }
    except RopException as e:
        return None

Memory Access Validation

from angrop.rop_utils import (
    get_ast_dependency,
    get_ast_controllers,
    get_ast_const_offset
)

def analyze_memory_write(state, action):
    addr_ast = action.addr.ast
    data_ast = action.data.ast
    
    # Analyze address
    addr_deps = get_ast_dependency(addr_ast)
    addr_controllers = get_ast_controllers(state, addr_ast, addr_deps)
    addr_offset = get_ast_const_offset(state, addr_ast, addr_deps)
    
    # Analyze data
    data_deps = get_ast_dependency(data_ast)
    data_controllers = get_ast_controllers(state, data_ast, data_deps)
    
    return {
        'addr_controlled_by': addr_controllers,
        'addr_offset': addr_offset,
        'data_controlled_by': data_controllers
    }

Build docs developers (and LLMs) love