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