Skip to main content

Supported Architectures

From the README, angrop supports multiple processor architectures:
  • x86 (32-bit Intel/AMD)
  • x64 (AMD64, 64-bit Intel/AMD)
  • MIPS (32-bit and 64-bit)
  • ARM (32-bit, including Thumb mode)
  • AArch64 (ARM 64-bit)
  • RISC-V (64-bit)

Architecture-Agnostic Design

From the README:
“Its design is architecture-agnostic so it supports multiple architectures.”
angrop achieves architecture independence through abstraction layers:

The ROPArch Class

From arch.py:5-39, the base architecture class:
class ROPArch:
    def __init__(self, project, kernel_mode=False):
        self.project = project
        self.kernel_mode = kernel_mode
        
        # Architecture-specific configuration
        self.alignment = project.arch.instruction_alignment
        self.reg_list = self._get_reg_list()
        self.max_block_size = None
        self.fast_mode_max_block_size = None
        
        # Key architecture registers
        a = project.arch
        self.stack_pointer = a.register_names[a.sp_offset]
        self.base_pointer = a.register_names[a.bp_offset]
        
        # Architecture-specific instruction patterns
        self.syscall_insts = None
        self.ret_insts = None
        
        # Syscall numbers
        self.execve_num = None
        self.sigreturn_num = None
This provides a uniform interface across architectures while allowing specialization.

Register Management

From arch.py:24-39:
def _get_reg_list(self):
    """Get general-purpose registers + bp"""
    arch = self.project.arch
    sp_reg = arch.register_names[arch.sp_offset]
    ip_reg = arch.register_names[arch.ip_offset]
    bp_reg = arch.register_names[arch.bp_offset]
    
    # Get default symbolic registers
    default_regs = arch.default_symbolic_registers
    
    # Exclude sp and ip, include bp
    reg_list = [r for r in default_regs 
                if r not in (sp_reg, ip_reg, bp_reg)]
    reg_list.append(bp_reg)
    return reg_list
This automatically adapts to different register naming conventions:
  • x86: eax, ebx, ecx, edx, esi, edi, ebp
  • x64: rax, rbx, rcx, rdx, rsi, rdi, r8-r15, rbp
  • ARM: r0-r12, lr
  • AArch64: x0-x30
  • MIPS: $v0-$v1, $a0-$a3, $t0-$t9, $s0-$s8

x86 (32-bit)

From arch.py:44-86:
class X86(ROPArch):
    def __init__(self, project, kernel_mode=False):
        super().__init__(project, kernel_mode=kernel_mode)
        self.max_block_size = 20
        self.fast_mode_max_block_size = 12
        
        # System call instruction
        self.syscall_insts = {b"\xcd\x80"}  # int 0x80
        
        # Return instructions  
        self.ret_insts = {b"\xc2", b"\xc3", b"\xca", b"\xcb"}
        
        # Segment registers
        self.segment_regs = {"cs", "ds", "es", "fs", "gs", "ss"}
        
        # Syscall numbers
        self.execve_num = 0xb        # sys_execve = 11
        self.sigreturn_num = 0x77    # sys_sigreturn = 119
Key characteristics:
  • Uses int 0x80 for system calls
  • Multiple return instruction variants (ret, ret imm16, retf, etc.)
  • Segment registers not used in ROP (filtered out)
  • Smaller maximum gadget size (20 instructions)
Filtered instructions (arch.py:69-77):
def block_make_sense(self, block):
    capstr = str(block.capstone).lower()
    # Exclude problematic instructions:
    if any(x in capstr for x in ('cli', 'rex', 'repz ret', 
                                  'retf', 'hlt', 'wait', 
                                  'loop', 'lock')):
        return False
    if not kernel_mode and ("fs:" in capstr or "gs:" in capstr):
        return False  # Segment register operations
    return True

x64 (AMD64)

From arch.py:87-96:
class AMD64(X86):
    def __init__(self, project, kernel_mode=False):
        super().__init__(project, kernel_mode=kernel_mode)
        
        # Different syscall instruction
        self.syscall_insts = {b"\x0f\x05"}  # syscall
        
        # Different segment register names
        self.segment_regs = {"cs_seg", "ds_seg", "es_seg", 
                            "fs_seg", "gs_seg", "ss_seg"}
        
        # Different syscall numbers (64-bit ABI)
        self.execve_num = 0x3b      # sys_execve = 59
        self.sigreturn_num = 0xf    # sys_rt_sigreturn = 15
Key differences from x86:
  • Uses syscall instruction instead of int 0x80
  • Different syscall numbers (Linux x64 ABI)
  • 64-bit registers (rax vs eax)
  • Different calling convention (registers vs stack)

ARM (32-bit)

From arch.py:100-128:
class ARM(ROPArch):
    def __init__(self, project, kernel_mode=False):
        super().__init__(project, kernel_mode=kernel_mode)
        
        # Thumb mode support
        self.is_thumb = False
        
        # Instruction alignment
        self.alignment = self.project.arch.bytes  # 4 bytes
        self.max_block_size = self.alignment * 8   # 32 bytes
        self.fast_mode_max_block_size = self.alignment * 6  # 24 bytes
        
        # Syscall number
        self.execve_num = 0xb
    
    def set_thumb(self):
        """Switch to Thumb mode (16-bit instructions)"""
        self.is_thumb = True
        self.alignment = 2           # 2-byte alignment
        self.max_block_size = 16
        self.fast_mode_max_block_size = 12
    
    def set_arm(self):
        """Switch to ARM mode (32-bit instructions)"""
        self.is_thumb = False
        self.alignment = 4
        self.max_block_size = 32  
        self.fast_mode_max_block_size = 24
ARM-specific features:
  • Return via pop {pc} or bx lr
  • Conditional execution on every instruction
  • Thumb mode for 16-bit instruction encoding
Conditional instruction filtering (arch.py:122-128):
arm_conditional_postfix = ['eq', 'ne', 'cs', 'hs', 'cc', 'lo', 
                           'mi', 'pl', 'vs', 'vc', 'hi', 'ls', 
                           'ge', 'lt', 'gt', 'le', 'al']

def block_make_sense(self, block):
    for insn in block.capstone.insns:
        # Disable conditional jumps for now
        if insn.insn.mnemonic[-2:] in arm_conditional_postfix:
            return False
    return True

Thumb Mode

From rop.py:36-37:
def __init__(self, ..., is_thumb=False, ...):
    """  
    :param is_thumb: execute ROP chain in thumb mode. Only makes 
                     difference on ARM architecture. angrop does 
                     not switch mode within a rop chain
    """
Thumb mode characteristics:
  • 16-bit instruction encoding (vs 32-bit ARM)
  • 2-byte instruction alignment
  • Smaller code size
  • Most ARM instructions available in Thumb
  • Cannot mix ARM and Thumb in same ROP chain
Setting Thumb mode:
import angr
import angrop

p = angr.Project("/path/to/arm_binary")
rop = p.analyses.ROP(is_thumb=True)  # Enable Thumb mode
rop.find_gadgets()
When is_thumb=True, gadgets are searched at 2-byte aligned addresses with Thumb decoding.

AArch64 (ARM 64-bit)

From arch.py:130-143:
class AARCH64(ROPArch):
    def __init__(self, project, kernel_mode=False):
        super().__init__(project, kernel_mode=kernel_mode)
        
        # Fixed-size 4-byte instructions
        self.ret_insts = {b'\xc0\x03_\xd6'}  # ret instruction
        self.max_block_size = self.alignment * 10
        self.fast_mode_max_block_size = self.alignment * 6
        
        # Syscall number
        self.execve_num = 0xdd
    
    def block_make_sense(self, block):
        for x in block.capstone.insns:
            # Filter out PAC (Pointer Authentication)
            if x.mnemonic == 'autiasp':
                return False
        return True
AArch64 features:
  • 64-bit registers: x0-x30 (vs ARM’s r0-r12)
  • Fixed 4-byte instruction size (no Thumb)
  • Return via ret instruction
  • PAC (Pointer Authentication Codes) filtered - incompatible with ROP

MIPS

From arch.py:145-152:
class MIPS(ROPArch):
    def __init__(self, project, kernel_mode=False):
        super().__init__(project, kernel_mode=kernel_mode)
        
        # MIPS always 4-byte aligned
        self.alignment = self.project.arch.bytes
        self.max_block_size = self.alignment * 8
        self.fast_mode_max_block_size = self.alignment * 6
        
        # Syscall instruction and number
        self.execve_num = 0xfab
        self.syscall_insts = {b"\x0c\x00\x00\x00"}  # syscall
MIPS characteristics:
  • Delay slots after branches (branch executes, then next instruction, then jump)
  • Different syscall number encoding
  • Both big-endian and little-endian variants supported

RISC-V (64-bit)

From arch.py:154-160:
class RISCV64(ROPArch):
    def __init__(self, project, kernel_mode=False):
        super().__init__(project, kernel_mode=kernel_mode)
        
        # Return instruction
        self.ret_insts = {b"\x82\x80"}  # ret (jalr x0, x1, 0)
        self.max_block_size = self.alignment * 10
        self.fast_mode_max_block_size = self.alignment * 6
        
        # Syscall number  
        self.execve_num = 0xdd
RISC-V features:
  • Clean RISC architecture
  • Compressed instruction set (16-bit and 32-bit)
  • Return via ret (pseudo-instruction for jalr x0, x1, 0)

Architecture Detection

From arch.py:162-178:
def get_arch(project, kernel_mode=False):
    """Automatically detect and return correct architecture class"""
    name = project.arch.name
    mode = kernel_mode
    
    if name == 'X86':
        return X86(project, kernel_mode=mode)
    elif name == 'AMD64':
        return AMD64(project, kernel_mode=mode)
    elif name.startswith('ARM'):
        return ARM(project, kernel_mode=mode)
    elif name == 'AARCH64':
        return AARCH64(project, kernel_mode=mode)
    elif name == 'RISCV64':
        return RISCV64(project, kernel_mode=mode)
    elif name.startswith('MIPS'):
        return MIPS(project, kernel_mode=mode)
    else:
        raise ValueError(f"Unknown arch: {name}")
This is called automatically when creating an ROP analysis:
p = angr.Project("/bin/some_binary")
rop = p.analyses.ROP()  # Architecture auto-detected from binary

Kernel Mode

All architectures support kernel mode for finding gadgets in kernel code:
rop = p.analyses.ROP(kernel_mode=True)
Kernel mode differences:
  • Uses kernel syscall numbers
  • Allows segment register operations (x86/x64)
  • Searches in .text section of kernel images
  • Different constraints on memory accesses
From arch.py:
if self.arch.kernel_mode:
    segs = [x for x in project.loader.main_object.sections 
            if x.name in ('.data', '.bss')]
else:
    segs = [s for s in project.loader.main_object.segments 
            if s.is_writable]

Adding New Architectures

From the README:
“It should be relatively easy to support other architectures that are supported by angr. If you’d like to use angrop on other architectures, please create an issue and we will look into it :)”
To add a new architecture:
  1. Create a new class inheriting from ROPArch:
class NewArch(ROPArch):
    def __init__(self, project, kernel_mode=False):
        super().__init__(project, kernel_mode=kernel_mode)
        self.syscall_insts = {b"\x..."}  # Syscall bytes
        self.ret_insts = {b"\x..."}      # Return bytes  
        self.execve_num = 0x...          # Execve syscall number
        # Set size limits
        self.max_block_size = ...
        self.fast_mode_max_block_size = ...
  1. Implement block_make_sense() to filter invalid instruction sequences
  2. Add to get_arch() function:
elif name == 'NEWARCH':
    return NewArch(project, kernel_mode=mode)
The rest of angrop (gadget finding, chain building, symbolic execution) works without modification due to the architecture-agnostic design.

Cross-Architecture Exploitation

The same angrop API works across all architectures:
# Works on x86, x64, ARM, MIPS, etc.
chain = rop.set_regs(register=0x1234)
chain += rop.write_to_mem(addr, data)
chain += rop.execve()
angrop handles:
  • Register name differences
  • Calling convention differences
  • Instruction encoding differences
  • Endianness differences
  • Alignment requirements
This makes it easy to develop exploits for different architectures without learning architecture-specific ROP techniques.

Build docs developers (and LLMs) love