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 Shifter class builds ROP chains that shift the stack pointer by specific byte amounts, enabling precise stack manipulation and ret-sled construction.
Overview
Accessed through the ROP instance as rop.shift() and rop.retsled(), Shifter automatically:
- Finds pop chains that shift SP by exact amounts
- Constructs ret-sleds of arbitrary length
- Manages stack value placement
- Supports custom next PC positioning
- Verifies stack shifts
Class Definition
Located in angrop/chain_builder/shifter.py
Public Methods
shift
shift(length, preserve_regs=None, next_pc_idx=-1) -> RopChain
Builds a chain that shifts the stack pointer by a specific number of bytes.
Number of bytes to shift the stack (must be word-aligned).
Set of register names that should not be modified.
Index (in words) where the next PC should be placed. Supports negative indexing.
-1: Next PC at end (default)
0: Next PC at start
- Positive: Absolute position
Returns: A RopChain that shifts the stack.
Raises: RopException if shift cannot be performed.
retsled
retsled(size, preserve_regs=None) -> RopChain
Builds a ret-sled of specified size (chain of ret gadgets).
Total size in bytes for the ret-sled (must be word-aligned).
Set of register names that should not be modified.
Returns: A RopChain consisting only of ret gadgets.
Raises: RopException if ret-sled cannot be built.
verify_shift
verify_shift(chain, length, preserve_regs) -> bool
Verifies that a chain correctly shifts the stack.
Registers that should not be modified.
Returns: True if verification passes, False otherwise.
verify_retsled
verify_retsled(chain, size, preserve_regs) -> bool
Verifies that a ret-sled has the correct size.
Registers that should not be modified.
Returns: True if verification passes, False otherwise.
ROP Instance Methods
rop.shift(length, preserve_regs=None, next_pc_idx=-1)
rop.retsled(size, preserve_regs=None)
Implementation Details
Shift Gadget Dictionary
Shifter maintains a dictionary mapping stack changes to gadgets:
From source code (shifter.py:140-159):
def filter_gadgets(self, gadgets):
# Only self-contained gadgets with no memory access
gadgets = [
x for x in gadgets
if x.num_sym_mem_access == 0 and x.self_contained
]
gadgets = self._filter_gadgets(gadgets)
# Group by stack_change
d = defaultdict(list)
for g in gadgets:
d[g.stack_change].append(g)
# Sort each group by number of changed registers
for x in d:
d[x] = sorted(d[x], key=lambda g: len(g.changed_regs))
return d
Example dictionary:
{
8: [pop rax; ret, pop rbx; ret, ...],
16: [pop rax; pop rbx; ret, pop rdi; pop rsi; ret, ...],
24: [pop rax; pop rbx; pop rcx; ret, ...],
...
}
Shift Implementation
From source code (shifter.py:62-110):
def shift(self, length, preserve_regs=None, next_pc_idx=-1):
preserve_regs = set(preserve_regs) if preserve_regs else set()
arch_bytes = self.project.arch.bytes
# Validate alignment
if length % arch_bytes != 0:
raise RopException("Cannot shift misaligned sp change")
# Find gadget with exact stack change
if length not in self.shift_gadgets or \
all(preserve_regs.intersection(x.changed_regs)
for x in self.shift_gadgets[length]):
raise RopException("Cannot find suitable shift gadget")
# Calculate word count and normalize index
g_cnt = length // arch_bytes
next_pc_idx = (next_pc_idx % g_cnt + g_cnt) % g_cnt
# Find compatible gadget
for g in self.shift_gadgets[length]:
if preserve_regs.intersection(g.changed_regs):
continue
if g.transit_type != 'pop_pc':
continue
if g.pc_offset != next_pc_idx * arch_bytes:
continue
# Build chain with symbolic stack values
chain = RopBlock(self.project, self)
state = chain._blank_state
chain.add_gadget(g)
for idx in range(g_cnt):
if idx != next_pc_idx:
# Symbolic stack value
tmp = claripy.BVS(f"symbolic_stack_{idx}", arch_bits)
chain.add_value(tmp)
else:
# Next PC value
next_pc_val = rop_utils.cast_rop_value(
claripy.BVS("next_pc", arch_bits),
self.project
)
chain.add_value(next_pc_val)
if self.verify_shift(chain, length, preserve_regs):
return chain
Retsled Implementation
From source code (shifter.py:112-130):
def retsled(self, size, preserve_regs=None):
preserve_regs = set(preserve_regs) if preserve_regs else set()
arch_bytes = self.project.arch.bytes
if size % arch_bytes != 0:
raise RopException("retsled size must be word aligned")
if not self.shift_gadgets[arch_bytes]:
raise RopException("Cannot find ret-equivalent gadget")
# Use minimal ret gadget
for g in self.shift_gadgets[arch_bytes]:
chain = RopChain(self.project, self.chain_builder)
for _ in range(size // arch_bytes):
chain.add_gadget(g)
if self.verify_retsled(chain, size, preserve_regs):
return chain
Usage Examples
Basic Stack Shift
import angr
import angrop
proj = angr.Project("/bin/bash")
rop = proj.analyses.ROP()
rop.find_gadgets()
# Shift stack by 32 bytes
chain = rop.shift(32)
chain.pp()
Output:
0x0000000000410b23: pop rax; pop rbx; pop rcx; pop rdx; ret
<symbolic>
<symbolic>
<symbolic>
<symbolic>
Shift with Custom Next PC Position
# Next PC at the beginning (index 0)
chain = rop.shift(32, next_pc_idx=0)
# Next PC at position 2 (third word)
chain = rop.shift(32, next_pc_idx=2)
# Next PC at the end (default)
chain = rop.shift(32, next_pc_idx=-1)
Stack Arguments for Function Calls
Used internally by FuncCaller for stack arguments:
From source code context:
# Function with stack arguments
if stack_arguments:
shift_bytes = (len(stack_arguments) + 1) * arch_bytes
cleaner = rop.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)
chain.add_value(next_pc)
Ret-Sled
# Create 64-byte ret-sled
chain = rop.retsled(64)
chain.pp()
Output:
0x0000000000400123: ret
0x0000000000400123: ret
0x0000000000400123: ret
0x0000000000400123: ret
0x0000000000400123: ret
0x0000000000400123: ret
0x0000000000400123: ret
0x0000000000400123: ret
NOP Sled Equivalent
# Ret-sled as timing/alignment mechanism
alignment_sled = rop.retsled(24)
main_chain = rop.set_regs(rax=0x1337)
full_chain = alignment_sled + main_chain
Preserving Registers During Shift
# Don't clobber rax during shift
chain = rop.shift(32, preserve_regs={'rax'})
Stack Cleanup After Function
# Call function with 5 stack args
# Then clean up stack
chain = rop.func_call("big_func", [1, 2, 3, 4, 5, 6, 7])
chain += rop.shift(40) # Clean up 5 args (5 * 8 bytes)
Shift Gadget Patterns
Common Patterns by Stack Change
8 bytes (1 word)
ret ; Minimal shift
pop rax; ret ; Shift with pop
16 bytes (2 words)
pop rax; pop rbx; ret
pop rdi; pop rsi; ret
24 bytes (3 words)
pop rax; pop rbx; pop rcx; ret
pop rdi; pop rsi; pop rdx; ret
32 bytes (4 words)
pop rax; pop rbx; pop rcx; pop rdx; ret
pop rdi; pop rsi; pop rdx; pop rcx; ret
PC Offset Importance
The pc_offset determines where ret pops from:
; pop rax; pop rbx; ret
; pc_offset = 16 (2 * 8 bytes)
; Stack: [rax][rbx][ret_addr]
; 0 8 16 <- PC here
; For next_pc_idx=0:
; Stack: [ret_addr][rax][rbx]
; 0 <- PC here
Verification
From source code (shifter.py:26-42):
def verify_shift(self, chain, length, preserve_regs):
arch_bytes = self.project.arch.bytes
init_sp = chain._blank_state.regs.sp.concrete_value
state = chain.exec()
# Check SP shifted correctly (+arch_bytes for ret)
if state.regs.sp.concrete_value != init_sp + length + arch_bytes:
return False
# Check preserved registers
for act in state.history.actions:
if act.type != 'reg' or act.action != 'write':
continue
offset = act.offset
offset -= act.offset % arch_bytes
reg_name = arch.translate_register_name(offset)
if reg_name in preserve_regs:
return False
return True
Effect and Comparison Tuples
From source code (shifter.py:132-138):
def _effect_tuple(self, g):
v1 = g.stack_change
v2 = g.pc_offset
return (v1, v2)
def _comparison_tuple(self, g):
return (len(g.changed_regs), g.stack_change,
rop_utils.transit_num(g), g.isn_count)
This ensures:
- Gadgets grouped by stack_change and pc_offset
- Within groups, prefer fewer register changes
- Then prefer smaller stack changes
- Then simpler transit types
- Finally fewer instructions
Error Handling
”Currently, we do not support shifting misaligned sp change”
Raised when length is not word-aligned.
Solution: Use multiples of word size:
# x64: multiples of 8
chain = rop.shift(8) # OK
chain = rop.shift(16) # OK
chain = rop.shift(12) # ERROR
# x86: multiples of 4
chain = rop.shift(4) # OK
chain = rop.shift(7) # ERROR
“Encounter a shifting request that requires chaining multiple shifting gadgets”
Raised when exact shift amount isn’t available.
Solutions:
- Try a different shift amount
- Use
fast_mode=False for more gadgets
- Manually chain multiple shifts:
chain = rop.shift(32)
chain += rop.shift(16) # Total: 48 bytes
“the size of a retsled must be word aligned”
Raised when retsled size is not aligned.
Solution: Use word-aligned sizes.
”fail to find a ret-equivalent gadget”
Raised when no simple ret gadget exists.
Solution: Very rare; binary likely has unusual structure.
- Gadget dictionary is pre-built during bootstrap
- Shifts are fast (single gadget)
- Retsleds may be long but simple
- Verification adds small overhead
Architecture Support
- x86/x86_64: Full support, many pop patterns
- ARM/ARM64: Full support
- MIPS: Full support
- PowerPC: Basic support
Advanced Techniques
Precise Stack Control
# Need exactly 48 bytes of stack space
chain = rop.shift(48)
# Or combine shifts
chain = rop.shift(32)
chain += rop.shift(16)
Custom Stack Layouts
# Place next PC at specific position
# For custom stack frame layouts
chain = rop.shift(40, next_pc_idx=3)
# Stack: [val][val][val][next_pc][val]
Ret-Sled for Alignment
# Ensure 16-byte alignment
if (current_offset % 16) != 0:
padding = 16 - (current_offset % 16)
chain += rop.retsled(padding)
Best Practices
- Use for exact amounts: When you need precise stack shifts
- Prefer simpler shifts: Fewer words = fewer clobbered registers
- Check alignment: Always use word-aligned values
- Verify shifts: Use
chain.pp() to inspect
- Consider alternatives: Sometimes
pivot() is better
See Also