Skip to main content

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.

The FuncCaller class builds ROP chains that call functions with arguments, automatically handling the target platform’s calling convention.

Overview

Accessed through the ROP instance as rop.func_call(), FuncCaller automatically:
  • Detects the calling convention (System V, Windows x64, ARM AAPCS, etc.)
  • Sets register arguments correctly
  • Handles stack arguments when needed
  • Manages return address to maintain control flow
  • Supports both returning and non-returning calls

Class Definition

class FuncCaller(Builder)
Located in angrop/chain_builder/func_caller.py

Public Method

func_call

func_call(address, args, preserve_regs=None, needs_return=True) -> RopChain
Builds a ROP chain that calls a function with arguments.
address
int | str
required
Address or symbol name of the function to call. Can be:
  • Integer address: 0x400123
  • Symbol name: "execve"
  • PLT entry: "printf"
args
list | tuple
required
List of arguments to pass to the function. Arguments are mapped to registers/stack based on calling convention.
preserve_regs
set | None
default:"None"
Set of register names that should not be modified.
needs_return
bool
default:"True"
Whether the ROP chain should continue after the function returns. If False, generates a shorter chain but loses control flow.
Returns: A RopChain that invokes the function. Raises: RopException if function cannot be called.

ROP Instance Method

rop.func_call(address, args, preserve_regs=None, needs_return=True)

Implementation Details

Calling Convention Detection

FuncCaller automatically detects the calling convention: From source code (func_caller.py:28-32):
self._cc = angr.default_cc(
    self.project.arch.name,
    platform=self.project.simos.name if self.project.simos is not None else None
)(self.project.arch)
Supported conventions:
  • x86_64 Linux/BSD: System V AMD64 ABI (rdi, rsi, rdx, rcx, r8, r9, stack)
  • x86_64 Windows: Microsoft x64 (rcx, rdx, r8, r9, stack)
  • x86 (32-bit): cdecl (all stack), fastcall, stdcall
  • ARM: AAPCS (r0-r3, stack)
  • ARM64: AAPCS64 (x0-x7, stack)
  • MIPS: O32, N32, N64

Argument Handling

Register Arguments

From source code (func_caller.py:115-131):
# Distinguish register and stack arguments
register_arguments = args
stack_arguments = []
if len(args) > len(cc.ARG_REGS):
    register_arguments = args[:len(cc.ARG_REGS)]
    stack_arguments = args[len(cc.ARG_REGS):]

# Set register arguments
registers = {}
for arg, reg in zip(register_arguments, cc.ARG_REGS):
    registers[reg] = arg

chain = self.chain_builder.set_regs(**registers, preserve_regs=preserve_regs)

Stack Arguments

From source code (func_caller.py:156-166):
if stack_arguments:
    shift_bytes = (len(stack_arguments)+1)*arch_bytes
    cleaner = self.chain_builder.shift(shift_bytes, next_pc_idx=-1, 
                                       preserve_regs=preserve_regs)
    chain.add_gadget(cleaner._gadgets[0])
    for arg in stack_arguments:
        chain.add_value(arg)
    next_pc = claripy.BVS("next_pc", self.project.arch.bits)
    chain.add_value(next_pc)

Return Address Handling

Different calling conventions handle returns differently:

Stack-based Return (x86, x86_64, ARM)

call function  ; Return address pushed to stack
ret            ; Pop return address and jump

Register-based Return (ARM64, MIPS)

bl function    ; Return address in link register (lr/ra)
ret            ; Jump to link register
From source code (func_caller.py:168-178):
if isinstance(cc.RETURN_ADDR, SimRegArg) and cc.RETURN_ADDR.reg_name != 'ip_at_syscall':
    # Set return register before call
    reg_name = cc.RETURN_ADDR.reg_name
    shifter = self.chain_builder._shifter.shift(self.project.arch.bytes)
    next_ip = rop_utils.cast_rop_value(shifter._gadgets[0].addr, self.project)
    pre_chain = self.chain_builder.set_regs(**{reg_name: next_ip})
    chain = pre_chain + chain

Symbol Resolution

From source code (func_caller.py:189-199):
if isinstance(address, str):
    symbol = address
    symobj = self.project.loader.main_object.get_symbol(symbol)
    if hasattr(self.project.loader.main_object, 'plt') and address in plt:
        address = plt[symbol]  # Use PLT entry
    elif symobj is not None:
        address = symobj.rebased_addr  # Use actual symbol
    else:
        raise RopException("Symbol does not exist")

Usage Examples

Basic Function Call

import angr
import angrop

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

# Call printf with one argument
chain = rop.func_call("printf", [0x400123])  # Address of format string

Multiple Arguments

# Call function with 3 arguments
# Convention: rdi, rsi, rdx on x86_64
chain = rop.func_call("read", [0, 0x61b100, 0x100])

Using Symbol Names

# Call by symbol name
chain = rop.func_call("execve", [0x61b100, 0, 0])

# Call PLT entry
chain = rop.func_call("printf", [format_str_addr])

Calling with Return Value

# Call function and use return value (in rax)
chain = rop.func_call("malloc", [0x100])
chain += rop.move_regs(rdi='rax')  # Move return to rdi
chain += rop.func_call("free", [], preserve_regs={'rdi'})

Non-Returning Calls

# Shorter chain, doesn't return
chain = rop.func_call("exit", [0], needs_return=False)

# Or execve - never returns anyway
chain = rop.func_call("execve", [0x61b100, 0, 0], needs_return=False)

Preserving Registers

# Don't modify rax during setup
chain = rop.func_call("some_func", [0x123, 0x456], 
                     preserve_regs={'rax'})

File Operations Example

import os

# Write "/etc/passwd" to memory
chain = rop.write_to_mem(0x61b100, b"/etc/passwd\x00")

# Open file
chain += rop.func_call("open", [0x61b100, os.O_RDONLY])

# Read into buffer (fd in rax)
chain += rop.move_regs(rdi='rax')
chain += rop.func_call("read", [], preserve_regs={'rdi'})
chain += rop.set_regs(rsi=0x61b200, rdx=0x100, preserve_regs={'rdi'})

# Write to stdout
chain += rop.func_call("write", [1, 0x61b200, 0x100])

Stack Arguments (7+ args on x86_64)

# Function with 10 arguments
# First 6 in registers (rdi, rsi, rdx, rcx, r8, r9)
# Last 4 on stack
args = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
chain = rop.func_call("big_function", args)

Kernel Function Calls

From angrop’s kernel test suite:
proj = angr.Project("./vmlinux_sym")
rop = proj.analyses.ROP(kernel_mode=True)
rop.find_gadgets()

init_cred = 0xffffffff8368b220
init_nsproxy = 0xffffffff8368ad00

# Call kernel function
chain = rop.func_call("find_task_by_vpid", [1])

# Use return value
chain += rop.move_regs(rdi='rax')
chain += rop.set_regs(rsi=init_nsproxy, preserve_regs={'rdi'})
chain += rop.func_call("switch_task_namespaces", [], 
                       preserve_regs={'rdi', 'rsi'})

Advanced Features

GOT/PLT Resolution

FuncCaller searches for function pointers: From source code (func_caller.py:48-72):
def _find_function_pointer_in_got_plt(self, func_addr):
    # Search symbols
    for sym in self.project.loader.main_object.symbols:
        if sym.rebased_addr == func_addr:
            func_name = sym.name
    
    # Search PLT
    for sym, val in self.project.loader.main_object.plt.items():
        if val == func_addr:
            func_name = sym
    
    # Return GOT entry if found
    if func_name:
        func_got = self.project.loader.main_object.imports.get(func_name)
        if func_got:
            return func_got.rebased_addr

Indirect Calls (jmp_mem)

When direct calls aren’t possible, FuncCaller uses indirect jumps:
; Instead of: call rax
; Use: jmp qword ptr [address_of_function_pointer]
From source code (func_caller.py:223-258):
ptr_to_func = self._find_function_pointer(address)
if ptr_to_func is not None:
    for g in self._func_jmp_gadgets:
        if g.transit_type != 'jmp_mem':
            continue
        # Build registers and use jmp_mem_target
        return self._func_call(g, self._cc, [], 
                              extra_regs=registers,
                              jmp_mem_target=ptr_to_func, **kwargs)

Calling Convention Details

x86_64 System V

# Arguments: rdi, rsi, rdx, rcx, r8, r9, stack
# Return: rax
# Caller-saved: rax, rcx, rdx, rsi, rdi, r8-r11
# Callee-saved: rbx, rsp, rbp, r12-r15

chain = rop.func_call("func", [arg1, arg2, arg3])
# rdi = arg1, rsi = arg2, rdx = arg3

x86 cdecl

# Arguments: all on stack (right-to-left)
# Return: eax
# Caller cleans stack

chain = rop.func_call("func", [arg1, arg2])
# Stack: [ret_addr][arg1][arg2]

ARM AAPCS

# Arguments: r0, r1, r2, r3, stack
# Return: r0
# Link register: lr

chain = rop.func_call("func", [arg1, arg2])
# r0 = arg1, r1 = arg2

Error Handling

”fail to invoke function: ”

Raised when function cannot be called. Solutions:
  1. Ensure find_gadgets(optimize=True) was called
  2. Check if symbol exists: proj.loader.find_symbol(name)
  3. Try needs_return=False for simpler chain
  4. Verify function address is correct

”Symbol does not exist in the binary”

Raised when symbol name is invalid. Solution: Use correct symbol name or address.

”fail to invoke function and return”

Raised when return mechanism cannot be built. Solution: Use needs_return=False or ensure ROP has necessary gadgets.

Performance Considerations

  • Simple calls (self-contained) are faster
  • Complex calls may need multiple gadgets
  • Stack arguments add overhead
  • Return handling adds complexity

Architecture Support

  • x86/x86_64: Full support
  • ARM/ARM64: Full support
  • MIPS: Full support
  • PowerPC: Basic support

See Also

Build docs developers (and LLMs) love