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

class Shifter(Builder)
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.
length
int
required
Number of bytes to shift the stack (must be word-aligned).
preserve_regs
set | None
default:"None"
Set of register names that should not be modified.
next_pc_idx
int
default:"-1"
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).
size
int
required
Total size in bytes for the ret-sled (must be word-aligned).
preserve_regs
set | None
default:"None"
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.
chain
RopChain
required
Chain to verify.
length
int
required
Expected shift amount.
preserve_regs
set
required
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.
chain
RopChain
required
Chain to verify.
size
int
required
Expected size in bytes.
preserve_regs
set
required
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:
  1. Gadgets grouped by stack_change and pc_offset
  2. Within groups, prefer fewer register changes
  3. Then prefer smaller stack changes
  4. Then simpler transit types
  5. 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:
  1. Try a different shift amount
  2. Use fast_mode=False for more gadgets
  3. 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.

Performance Considerations

  • 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

  1. Use for exact amounts: When you need precise stack shifts
  2. Prefer simpler shifts: Fewer words = fewer clobbered registers
  3. Check alignment: Always use word-aligned values
  4. Verify shifts: Use chain.pp() to inspect
  5. Consider alternatives: Sometimes pivot() is better

See Also

  • Pivot - Stack pivoting to arbitrary locations
  • FuncCaller - Uses shift for stack arguments
  • ChainBuilder - Main chain building interface

Build docs developers (and LLMs) love