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 or symbol name of the function to call. Can be:
- Integer address:
0x400123
- Symbol name:
"execve"
- PLT entry:
"printf"
List of arguments to pass to the function. Arguments are mapped to registers/stack based on calling convention.
Set of register names that should not be modified.
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:
- Ensure
find_gadgets(optimize=True) was called
- Check if symbol exists:
proj.loader.find_symbol(name)
- Try
needs_return=False for simpler chain
- 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.
- 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