The Pivot class builds ROP chains that perform stack pivoting - moving the stack pointer to a different memory location to enable exploitation when stack space is limited.
Overview
Accessed through the ROP instance as rop.pivot(), Pivot automatically:
- Finds gadgets that modify the stack pointer
- Supports pivoting to addresses or register values
- Handles different pivot mechanisms (mov, xchg, add, etc.)
- Manages pre-pivot stack setup
- Verifies pivot operations
Class Definition
Located in angrop/chain_builder/pivot.py
Public Methods
pivot
Builds a chain that pivots the stack pointer.
Target for the pivot. Can be:
- Address (RopValue with concrete address)
- Register (RopValue with register name)
Returns: A RopChain that pivots the stack.
Raises: RopException if pivot cannot be performed.
pivot_addr
pivot_addr(addr) -> RopChain
Pivots stack to a specific address.
Target address for the new stack location.
Returns: A RopChain that pivots to the address.
pivot_reg
pivot_reg(reg_val) -> RopChain
Pivots stack to an address stored in a register.
RopValue representing a register containing the target address.
Returns: A RopChain that pivots to the register value.
ROP Instance Method
Automatically detects whether target is address or register:
# Pivot to address
chain = rop.pivot(rop_utils.cast_rop_value(0x61b100, proj))
# Pivot to register
reg_val = rop_utils.cast_rop_value('rax', proj)
chain = rop.pivot(reg_val)
Implementation Details
Pivot Gadget Types
Pivot recognizes several gadget patterns:
- Direct move:
mov rsp, rbp; ret
- Exchange:
xchg rsp, rax; ret
- Add to SP:
add rsp, 0x100; ret
- Pop to SP:
pop rsp; ret
- Leave:
leave; ret (equivalent to mov rsp, rbp; pop rbp; ret)
From source code (pivot.py:10-25):
def cmp(g1, g2):
# Prefer gadgets with fewer SP controllers
if len(g1.sp_reg_controllers) < len(g2.sp_reg_controllers):
return -1
# Then prefer smaller stack changes
if g1.stack_change + g1.stack_change_after_pivot < \
g2.stack_change + g2.stack_change_after_pivot:
return -1
# Finally prefer fewer instructions
if g1.isn_count < g2.isn_count:
return -1
return 0
SP Controllers
Gadgets are analyzed to identify which registers control the final SP value:
From source code (pivot.py:111-113):
def _effect_tuple(self, g):
v1 = tuple(sorted(g.sp_controllers))
return (v1, g.stack_change, g.stack_change_after_pivot)
Example:
mov rsp, rbp; ret
; sp_controllers = {rbp}
xchg rsp, rax; ret
; sp_controllers = {rax}
add rsp, 0x18; ret
; sp_controllers = {} # Offset-based, not register-controlled
Pivot to Address
From source code (pivot.py:43-75):
def pivot_addr(self, addr):
for gadget in self._pivot_gadgets:
# Make symbolic state
init_state = self.make_sim_state(
gadget.addr,
gadget.stack_change_before_pivot//arch_bytes+1
)
# Step gadget
final_state = rop_utils.step_to_unconstrained_successor(
self.project, init_state
)
# Constrain final SP to target
final_state.solver.add(final_state.regs.sp == addr.data)
# Extract required register values
registers = {}
for x in gadget.sp_reg_controllers:
registers[x] = final_state.solver.eval(
init_state.registers.load(x)
)
# Build chain
chain = self.chain_builder.set_regs(**registers)
chain.add_gadget(gadget)
# Verify
state = chain.exec(stop_at_pivot=True)
if state.solver.eval(state.regs.sp == addr.data):
return chain
Pivot to Register
From source code (pivot.py:77-109):
def pivot_reg(self, reg_val):
reg = reg_val.reg_name
for gadget in self._pivot_gadgets:
if reg not in gadget.sp_reg_controllers:
continue
# Build and verify chain
chain = self.chain_builder.set_regs()
chain.add_gadget(gadget)
# Verify SP is from correct register
state = chain.exec(stop_at_pivot=True)
variables = set(state.regs.sp.variables)
if len(variables) == 1 and \
variables.pop().startswith(f'sreg_{reg}'):
return chain
Usage Examples
Basic Stack Pivot to Address
import angr
import angrop
from angrop import rop_utils
proj = angr.Project("/bin/bash")
rop = proj.analyses.ROP()
rop.find_gadgets()
# Pivot to new stack location
new_stack = 0x61b100
addr = rop_utils.cast_rop_value(new_stack, proj)
chain = rop.pivot(addr)
Pivot to Register
# Set register to target address
chain = rop.set_regs(rbp=0x61b100)
# Pivot SP to value in rbp
reg_val = rop_utils.cast_rop_value('rbp', proj)
chain += rop.pivot(reg_val)
# Or combined:
chain = rop.set_regs(rbp=0x61b100)
chain += rop.pivot(rop_utils.cast_rop_value('rbp', proj))
Complete Pivot Example
proj = angr.Project("./vuln")
rop = proj.analyses.ROP()
rop.find_gadgets()
# Scenario: Limited stack space in initial buffer
# but control of heap buffer at 0x61b000
# Build ROP chain in heap buffer
heap_addr = 0x61b000
main_chain = rop.write_to_mem(0x61b100, b"/bin/sh\x00")
main_chain += rop.execve(path_addr=0x61b100)
# Initial exploit: pivot to heap
initial_chain = rop.pivot(rop_utils.cast_rop_value(heap_addr, proj))
# Deploy both chains
# initial_chain goes in limited buffer
# main_chain goes at heap_addr
Pivot for Buffer Extension
# Small buffer overflow (8 bytes)
# Pivot to larger controllable region
small_buffer_exploit = rop.set_regs(rax=large_buffer_addr)
small_buffer_exploit += rop.pivot(
rop_utils.cast_rop_value('rax', proj)
)
# large_buffer_addr contains full ROP chain
Chained Pivots
# Pivot through multiple locations
# Stage 1: Pivot to intermediate buffer
chain = rop.pivot(rop_utils.cast_rop_value(0x61b000, proj))
# At 0x61b000:
stage2 = rop.write_to_mem(0x61c000, b"data")
stage2 += rop.pivot(rop_utils.cast_rop_value(0x61c000, proj))
# At 0x61c000:
stage3 = rop.execve()
Pivot with Leave Gadget
# Classic buffer overflow with saved rbp control
# leave == mov rsp, rbp; pop rbp
# Set rbp to target - 8
chain = rop.set_regs(rbp=target_addr - 8)
# Find leave gadget
for gadget in rop.pivot_gadgets:
if 'leave' in str(gadget):
chain.add_gadget(gadget)
break
Gadget Requirements
For pivoting to work:
- SP modification: Gadget must change stack pointer
- Controllable: SP target must be controllable via registers or offsets
- No conditional branches: Ensures predictable execution
- Not jmp_reg: Direct jumps don’t help with pivoting
- No symbolic access: Memory accesses must be concrete
From source code (pivot.py:118-123):
def filter_gadgets(self, gadgets):
gadgets = [x for x in gadgets if not x.has_conditional_branch and
x.transit_type != 'jmp_reg' and
not x.has_symbolic_access()]
gadgets = self._filter_gadgets(gadgets)
return sorted(gadgets, key=functools.cmp_to_key(cmp))
Common Pivot Patterns
x86_64 Patterns
; Direct move
mov rsp, rbp; ret
mov rsp, rax; ret
; Exchange
xchg rsp, rax; ret
xchg rsp, rbp; ret
; Leave (very common)
leave; ret ; mov rsp, rbp; pop rbp; ret
; Pop
pop rsp; ret
; Add offset
add rsp, 0x100; ret
x86 (32-bit) Patterns
mov esp, ebp; ret
xchg esp, eax; ret
leave; ret
pop esp; ret
ARM Patterns
mov sp, r4; ret
pop {sp, pc}
add sp, sp, #0x10; pop {pc}
Stack Change Tracking
Pivot gadgets track two stack changes:
- stack_change_before_pivot: Stack movement before SP is modified
- stack_change_after_pivot: Stack movement after pivot
From source code (pivot.py:113):
return (v1, g.stack_change, g.stack_change_after_pivot)
Example:
pop rbx; mov rsp, rax; ret
; stack_change_before_pivot = 8 (pop rbx)
; stack_change_after_pivot = 8 (ret from new stack)
Verification
Pivot chains are verified using symbolic execution:
state = chain.exec(stop_at_pivot=True)
if state.solver.eval(state.regs.sp == addr.data):
return chain # Pivot successful
The stop_at_pivot=True flag stops execution when SP changes significantly.
Error Handling
”Fail to pivot the stack to !”
Raised when no pivot chain can be built.
Solutions:
- Check if pivot gadgets exist:
rop.pivot_gadgets
- Try pivoting to a register instead
- Use
find_gadgets(fast_mode=False) for more gadgets
- Verify target address is valid
”Fail to pivot the stack to !”
Raised when register-based pivot fails.
Solutions:
- Check if gadgets control SP via that register
- Try a different register
- Use address-based pivot instead
- Pivot gadgets are pre-filtered and sorted during bootstrap
- Symbolic execution adds overhead for verification
- Simpler pivots (direct moves) are faster
- Complex pivots may require multiple gadgets
Architecture Support
- x86/x86_64: Full support, many pivot patterns
- ARM/ARM64: Full support
- MIPS: Basic support
- PowerPC: Basic support
Advanced Techniques
Partial Overwrites
# If you can only overwrite low bytes of saved rbp
# Calculate target that requires minimal change
target_low = desired_addr & 0xffff
current_high = (saved_rbp & 0xffffffffffff0000)
target = current_high | target_low
chain = rop.set_regs(rbp=target)
chain += rop.pivot(rop_utils.cast_rop_value('rbp', proj))
Info Leak + Pivot
# Leak stack address
leaked_stack = leak_function()
# Calculate pivot target relative to leak
target = leaked_stack + 0x1000
# Pivot to calculated address
chain = rop.pivot(rop_utils.cast_rop_value(target, proj))
Best Practices
- Verify target alignment: Stack should be aligned to word boundary
- Plan stack layout: Know what will be at target address
- Test pivot gadgets: Use
chain.pp() to inspect
- Consider stack growth: Stack grows down on most architectures
- Account for saved values: Leave gadget pops rbp first
See Also