Skip to main content
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