Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions angrop/arch.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def __init__(self, project, kernel_mode=False):
self.syscall_insts = None
self.ret_insts = None
self.execve_num = None
self.sigreturn_num = None

def _get_reg_list(self):
"""
Expand Down Expand Up @@ -49,6 +50,7 @@ def __init__(self, project, kernel_mode=False):
self.ret_insts = {b"\xc2", b"\xc3", b"\xca", b"\xcb"}
self.segment_regs = {"cs", "ds", "es", "fs", "gs", "ss"}
self.execve_num = 0xb
self.sigreturn_num = 0x77

def _x86_block_make_sense(self, block):
capstr = str(block.capstone).lower()
Expand Down Expand Up @@ -88,6 +90,7 @@ def __init__(self, project, kernel_mode=False):
self.syscall_insts = {b"\x0f\x05"} # syscall
self.segment_regs = {"cs_seg", "ds_seg", "es_seg", "fs_seg", "gs_seg", "ss_seg"}
self.execve_num = 0x3b
self.sigreturn_num = 0xf

def block_make_sense(self, block):
return self._x86_block_make_sense(block)
Expand Down
24 changes: 23 additions & 1 deletion angrop/chain_builder/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .mem_changer import MemChanger
from .func_caller import FuncCaller
from .sys_caller import SysCaller
from .sigreturn import SigreturnBuilder
from .pivot import Pivot
from .shifter import Shifter
from .. import rop_utils
Expand Down Expand Up @@ -45,6 +46,7 @@ def __init__(self, project, rop_gadgets, pivot_gadgets, syscall_gadgets, arch, b
self._func_caller = FuncCaller(self)
self._pivot = Pivot(self)
self._sys_caller = SysCaller(self)
self._sigreturn = SigreturnBuilder(self)
if not SysCaller.supported_os(self.project.loader.main_object.os):
l.warning("%s is not a fully supported OS, SysCaller may not work on this OS",
self.project.loader.main_object.os)
Expand Down Expand Up @@ -146,7 +148,8 @@ def do_syscall(self, syscall_num, args, needs_return=True, **kwargs):
if not self._sys_caller:
l.exception("SysCaller does not support OS: %s", self.project.loader.main_object.os)
return None
return self._sys_caller.do_syscall(syscall_num, args, needs_return=needs_return, **kwargs)
return self._sys_caller.do_syscall(syscall_num, args,
needs_return=needs_return, **kwargs)

def execve(self, path=None, path_addr=None):
"""
Expand All @@ -159,6 +162,25 @@ def execve(self, path=None, path_addr=None):
return None
return self._sys_caller.execve(path=path, path_addr=path_addr)

def sigreturn_syscall(self, syscall_num, args, sp=None):
"""
build a sigreturn syscall chain with syscall gadget and ROP syscall registers => SigreturnFrame.
:param syscall_num: syscall number for sigreturn
:param args: syscall arguments for sigreturn [list]
:param sp: address to jump to after sigreturn
:return: RopChain object
"""
return self._sigreturn.sigreturn_syscall(syscall_num, args, sp=sp)

def sigreturn(self, **registers):
"""
build a rop chain that invokes sigreturn/rt_sigreturn and loads registers from a frame
:param syscall_num: override syscall number if needed
:param registers: register values to set in the sigreturn frame
:return: a RopChain that performs sigreturn
"""
return self._sigreturn.sigreturn(**registers)

def shift(self, length, preserve_regs=None, next_pc_idx=-1):
"""
build a rop chain to shift the stack to a specific value
Expand Down
10 changes: 4 additions & 6 deletions angrop/chain_builder/func_caller.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,19 +140,17 @@ def _func_call(self, func_gadget, cc, args, extra_regs=None, preserve_regs=None,
state = chain._blank_state
state.solver.add(claripy.And(*constraints))
state.solver.add(jmp_mem_target == func_gadget.pc_target)

# invoke the function
chain.add_gadget(func_gadget)
# we are done here if we don't need to return
if not needs_return:
return chain
# recover stack from previous gadget effect.
for delta in range(func_gadget.stack_change//arch_bytes):
if func_gadget.pc_offset is None or delta != func_gadget.pc_offset:
chain.add_value(self._get_fill_val())
else:
chain.add_value(claripy.BVS("next_pc", self.project.arch.bits))

# we are done here if we don't need to return
if not needs_return:
return chain

# now we need to cleanly finish the calling convention
# 1. handle stack arguments
# 2. handle function return address to maintain the control flow
Expand Down
156 changes: 156 additions & 0 deletions angrop/chain_builder/sigreturn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import logging
import angr

from ..errors import RopException
from ..sigreturn import SigreturnFrame
l = logging.getLogger(__name__)


class SigreturnBuilder:
"""
Build srop chains
"""
def __init__(self, chain_builder):
self.chain_builder = chain_builder
self.project = chain_builder.project
self.arch = chain_builder.arch

def _execute_sp_delta(self, chain):
state = chain.sim_exec_til_syscall()
if state is None:
raise RopException("Fail to execute sigreturn chain until syscall")
# but here @angrop/rop_chain.py#L213: we executed a stack pop here, so should consider that.
init_state = chain._blank_state.copy()
init_state.stack_pop()
init_sp = init_state.solver.eval(init_state.regs.sp)
sp_at_syscall = state.solver.eval(state.regs.sp) # pad sp change
delta = sp_at_syscall - init_sp
if delta % self.project.arch.bytes != 0:
raise RopException("Unexpected stack alignment for sigreturn")
offset_words = delta // self.project.arch.bytes
return offset_words

def sigreturn_syscall(self, syscall_num, args, sp=None):
"""
Build a sigreturn syscall chain with syscall gadget and ROP syscall registers => SigreturnFrame.
:param syscall_num: syscall number for sigreturn
:param args: syscall arguments for sigreturn [list]
:param sp: address to jump to after sigreturn (new rsp)
:return: RopChain object
"""
if self.project.simos.name != "Linux":
raise RopException(f"{self.project.simos.name} is not supported!")
if not self.chain_builder.syscall_gadgets:
raise RopException("target does not contain syscall gadget!")
if self.arch.sigreturn_num is None:
raise RopException("sigreturn is not supported on this architecture")

cc = angr.SYSCALL_CC[self.project.arch.name]["default"](self.project.arch)

if len(args) > len(cc.ARG_REGS):
raise RopException("sig frame arguments exceeded.")
registers = {}
for arg, reg in zip(args, cc.ARG_REGS):
registers[reg] = arg
sysnum_reg = self.project.arch.register_names[self.project.arch.syscall_num_offset]
registers[sysnum_reg] = syscall_num

target_regs = set(cc.ARG_REGS)
target_regs.add(sysnum_reg)

def _prologue_ok(g):
p = g.prologue
if p is None:
return True
if p.changed_regs.intersection(target_regs):
return False
if p.stack_change not in (0, None):
return False
if p.mem_reads or p.mem_writes or p.mem_changes:
return False
return True

candidates = [g for g in self.chain_builder.syscall_gadgets if _prologue_ok(g)]
# TODO: better prologue filter?
if not candidates:
raise RopException("Fail to find a suitable syscall gadget for sigreturn_syscall")

gadget = min(candidates, key=lambda g: (g.isn_count, g.stack_change, g.num_sym_mem_access))

ip_reg = self.project.arch.register_names[self.project.arch.ip_offset]
registers[ip_reg] = gadget.addr

# set stack pointer to for another rop after sigreturn.
if sp is not None:
sp_reg = self.project.arch.register_names[self.project.arch.sp_offset]
registers[sp_reg] = sp

chain = self.chain_builder.do_syscall(self.arch.sigreturn_num, [], needs_return=False)
if not chain or not chain._gadgets:
raise RopException("Fail to build sigreturn syscall chain")

return self.sigreturn(**registers)

def sigreturn_execve(self, path_addr=None):
"""
srop helper to build a execve chain
:param path_addr: address to store the path string
:return: RopChain object
"""
if self.project.simos.name != 'Linux':
raise RopException(f"{self.project.simos.name} is not supported!")
if not self.chain_builder.syscall_gadgets:
raise RopException("target does not contain syscall gadget!")
# TODO: badbytes, write to mem, etc.
if path_addr is None:
raise RopException("path_addr is required for sigreturn_execve")
execve_syscall = self.chain_builder.arch.execve_num
return self.sigreturn_syscall(execve_syscall, [path_addr, 0, 0])


def sigreturn(self, **registers):
"""
Build a sigreturn chain with syscall gadget and SigreturnFrame.
:param registers: registers to set in the SigreturnFrame
:return: RopChain object
"""
if self.project.simos.name != "Linux":
raise RopException(f"{self.project.simos.name} is not supported!")
if not self.chain_builder.syscall_gadgets:
raise RopException("target does not contain syscall gadget!")
if self.arch.sigreturn_num is None:
raise RopException("sigreturn is not supported on this architecture")

frame = SigreturnFrame.from_project(self.project)
frame.update(**registers)

syscall_num = self.arch.sigreturn_num # syscall(sigreturn)
chain = self.chain_builder.do_syscall(syscall_num, [], needs_return=False) # dummy args
if not chain or not chain._gadgets:
raise RopException("Fail to build sigreturn syscall chain")
frame_words = frame.to_words()

offset_words = self._execute_sp_delta(chain)
filler = self.chain_builder.roparg_filler
if filler is None:
filler = 0
if 0 < offset_words < len(chain._values): # should pad to offset(rsp at syscall)
chain._values = chain._values[:offset_words]
chain.payload_len = offset_words * self.project.arch.bytes
elif offset_words < 0: # drop values to fit offset.
l.warning("Negative offset, %d frame values would be dropped.",-offset_words)
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning message uses an incorrect format. The code has l.warning("Negative offset, %d frame values would be dropped.",-offset_words) but the comma should come after the closing quote and there should be a space after the comma. The correct format should be: l.warning("Negative offset, %d frame values would be dropped.", -offset_words).

Suggested change
l.warning("Negative offset, %d frame values would be dropped.",-offset_words)
l.warning("Negative offset, %d frame values would be dropped.", -offset_words)

Copilot uses AI. Check for mistakes.
frame_words = frame_words[-offset_words:]
elif offset_words > len(chain._values):
for _ in range(offset_words - len(chain._values)):
chain.add_value(filler)
Comment on lines +137 to +145
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential logic error: when offset_words equals 0, the frame_words are appended directly without any padding or truncation. However, this case is not explicitly handled in the if-elif chain. While the code will still work (falling through to the final loop), it would be clearer to explicitly handle the offset_words == 0 case or add a comment explaining that no adjustment is needed in this case.

Copilot uses AI. Check for mistakes.

# record the frame information before adding frame words
frame_start_offset = len(chain._values)

for word in frame_words:
chain.add_value(word)

# save sigreturn frame information for pretty printing
chain._sigreturn_frames.append((frame, frame_start_offset))

return chain
3 changes: 2 additions & 1 deletion angrop/chain_builder/sys_caller.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@ def do_syscall(self, syscall_num, args, needs_return=True, **kwargs):

try:
chain = self._func_call(gadget, cc, args, extra_regs=extra_regs,
needs_return=needs_return, preserve_regs=preserve_regs, **kwargs)
needs_return=needs_return, preserve_regs=preserve_regs,
**kwargs)
if self.verify(chain, registers, more):
return chain
except RopException:
Expand Down
27 changes: 25 additions & 2 deletions angrop/rop_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ def __init__(self, project, builder, state=None, badbytes=None):
self._pivoted = False
self._init_sp = None

# sigreturn frame information: list of (frame_object, start_offset_in_values)
self._sigreturn_frames = []

def __add__(self, other):
# need to add the values from the other's stack and the constraints to the result state
result = self.copy()
Expand All @@ -61,6 +64,12 @@ def __add__(self, other):
else:
result._values.extend(other._values)

# merge sigreturn frames: adjust offsets from other
word_count_before_merge = len(self._values) - (1 if idx is not None else 0)
for frame, offset in other._sigreturn_frames:
adjusted_offset = word_count_before_merge + offset
result._sigreturn_frames.append((frame, adjusted_offset))

# FIXME: cannot handle cases where a rop_block is used twice and have different constraints
# because right now symbolic values go with rop_blocks
if self._blank_state.solver._solver.variables.intersection(other._blank_state.solver._solver.variables):
Expand Down Expand Up @@ -220,6 +229,7 @@ def copy(self):
cp.payload_len = self.payload_len
cp._blank_state = self._blank_state.copy()
cp.badbytes = self.badbytes
cp._sigreturn_frames = list(self._sigreturn_frames)

cp._pivoted = self._pivoted
cp._init_sp = self._init_sp
Expand Down Expand Up @@ -395,17 +405,30 @@ def dstr(self):
bs = self._p.arch.bytes
prefix_len = bs*2+2
prefix = " "*prefix_len
for v in self._values:

sigreturn_map = {} # start_offset -> frame and end offset
for frame, start_offset in self._sigreturn_frames:
# iterate through frame registers to build a map
frame_words = frame.to_words()
sigreturn_map[start_offset] = (frame, start_offset + len(frame_words))
idx = 0
while idx < len(self._values):
v = self._values[idx]
if v.symbolic:
res += prefix + f" {v.ast}\n"
idx += 1
continue
for g in self._gadgets:
if g.addr == v.concreted:
fmt = f"%#0{prefix_len}x"
res += fmt % g.addr + f": {g.dstr()}\n"
break
else:
res += prefix + f" {v.concreted:#x}\n"
if idx in sigreturn_map:
sigframe, idx = sigreturn_map[idx]
res += sigframe.dstr(prefix=prefix)
continue
idx += 1
return res

def pp(self):
Expand Down
Loading
Loading