Skip to content

Commit

Permalink
Merge pull request #105 from angr/fix/ret_n
Browse files Browse the repository at this point in the history
Fix/ret n
  • Loading branch information
Kyle-Kyle authored Apr 30, 2024
2 parents 8c7d117 + 95499fd commit ffc9135
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 16 deletions.
1 change: 0 additions & 1 deletion angrop/chain_builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,6 @@ def _build_reg_setting_chain(self, gadgets, modifiable_memory_range, register_di
for i in range(stack_change // bytes_per_pop):
sym_word = test_symbolic_state.memory.load(sp + bytes_per_pop*i, bytes_per_pop,
endness=self.project.arch.memory_endness)

val = test_symbolic_state.solver.eval(sym_word)
if len(gadgets) > 0 and val == gadgets[0].addr:
chain.add_gadget(gadgets[0])
Expand Down
9 changes: 7 additions & 2 deletions angrop/chain_builder/func_caller.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging

import angr
import claripy
from angr.calling_conventions import SimRegArg, SimStackArg

from .builder import Builder
Expand Down Expand Up @@ -51,8 +52,11 @@ def _func_call(self, func_gadget, cc, args, extra_regs=None, preserve_regs=None,

# invoke the function
chain.add_gadget(func_gadget)
for _ in range(func_gadget.stack_change//arch_bytes-1):
chain.add_value(self._get_fill_val())
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:
Expand Down Expand Up @@ -105,4 +109,5 @@ def func_call(self, address, args, **kwargs):
)(self.project.arch)
func_gadget = RopGadget(address)
func_gadget.stack_change = self.project.arch.bytes
func_gadget.pc_offset = 0
return self._func_call(func_gadget, cc, args, **kwargs)
10 changes: 8 additions & 2 deletions angrop/gadget_finder/gadget_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ def _identify_transit_type(self, final_state, ctrl_type):
return ctrl_type

if ctrl_type == 'pivot':
# FIXME: this logic feels wrong
variables = list(final_state.ip.variables)
if all(x.startswith("sreg_") for x in variables):
return "jmp_reg"
Expand All @@ -261,7 +262,7 @@ def _identify_transit_type(self, final_state, ctrl_type):
if sols[0] != final_state.arch.bytes:
continue
return "ret"
return "jmp_mem"
return "pop_pc"

assert ctrl_type == 'stack'

Expand All @@ -271,7 +272,7 @@ def _identify_transit_type(self, final_state, ctrl_type):
if v is final_state.ip:
return "ret"

return "jmp_mem"
return "pop_pc"

def _create_gadget(self, addr, init_state, final_state, ctrl_type):
transit_type = self._identify_transit_type(final_state, ctrl_type)
Expand Down Expand Up @@ -319,6 +320,11 @@ def _create_gadget(self, addr, init_state, final_state, ctrl_type):
#FIXME: technically, it can be negative, e.g. call instructions
return None

# record pc_offset
if type(gadget) is not PivotGadget and transit_type in ['pop_pc', 'ret']:
idx = list(final_state.ip.variables)[0].split('_')[2]
gadget.pc_offset = int(idx) * self.project.arch.bytes

l.info("... checking for controlled regs")
self._check_reg_changes(final_state, init_state, gadget)

Expand Down
44 changes: 38 additions & 6 deletions angrop/rop_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,18 @@ def __add__(self, other):
o_stack = o_state.memory.load(o_state.regs.sp, other.payload_len)
result._blank_state.memory.store(result._blank_state.regs.sp + self.payload_len, o_stack)
result._blank_state.add_constraints(*o_state.solver.constraints)
if not other._values:
return result
# add the other values and gadgets
result._values.extend(other._values)
result._gadgets.extend(other._gadgets)
idx = self.next_pc_idx()
result.payload_len = self.payload_len + other.payload_len
if idx is None:
result._values.extend(other._values)
else:
result._values[idx] = other._values[0]
result._values.extend(other._values[1:])
result.payload_len -= self._p.arch.bytes
return result

def set_timeout(self, timeout):
Expand All @@ -64,14 +72,31 @@ def add_gadget(self, gadget):
value = RopValue(value, self._p)
if self._pie:
value._rebase = True
self.add_value(value)

idx = self.next_pc_idx()
if idx is None:
self.add_value(value)
else:
self._values[idx] = value

def add_constraint(self, cons):
"""
helpful if the chain contains variables
"""
self._blank_state.add_constraints(cons)

def next_pc_idx(self):
"""
in some gadgets, we have this situation:
pop pc,r1, which means pc is not the last popped value like ret (retn is another example)
in these case, the value will be presented as symbolic "next_pc" in _values.
it will be concretized when adding new gadgets or doing chain concatenation
"""
for idx, x in enumerate(self._values):
if x.symbolic and any(y.startswith("next_pc_") for y in x.ast.variables):
return idx
return None

def __concretize_chain_values(self, constraints=None):
"""
with the flexibilty of chains to have symbolic values, this helper function
Expand Down Expand Up @@ -107,13 +132,20 @@ def __concretize_chain_values(self, constraints=None):

return concrete_vals

def _concretize_chain_values(self, constraints=None, timeout=None):
def _concretize_chain_values(self, constraints=None, timeout=None, preserve_next_pc=False):
"""
concretize chain values with a timeout
"""
if timeout is None:
timeout = self._timeout
return rop_utils.timeout(timeout)(self.__concretize_chain_values)(constraints=constraints)
values = rop_utils.timeout(timeout)(self.__concretize_chain_values)(constraints=constraints)
if not preserve_next_pc:
return values
idx = self.next_pc_idx()
if idx is None:
return values
values[idx] = (self._values[idx].ast, None)
return values

def payload_str(self, constraints=None, base_addr=None, timeout=None):
"""
Expand Down Expand Up @@ -205,8 +237,8 @@ def exec(self, max_steps=None, timeout=None):
state = self._blank_state.copy()
state.solver.reload_solver([]) # remove constraints
state.regs.pc = self._values[0].concreted
concrete_vals = self._concretize_chain_values(timeout=timeout)
# the assumps that the first value in the chain is a code address
concrete_vals = self._concretize_chain_values(timeout=timeout, preserve_next_pc=True)
# the assumption is that the first value in the chain is a code address
# it sounds like a reasonable assumption to me. But I can be wrong.
for value, _ in reversed(concrete_vals[1:]):
state.stack_push(value)
Expand Down
8 changes: 7 additions & 1 deletion angrop/rop_gadget.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ def __eq__(self, other):
return False
return True


class RopRegMove:
"""
Holds information about Register moves
Expand Down Expand Up @@ -111,10 +110,17 @@ def __init__(self, addr):
self.mem_writes = []
self.mem_changes = []

# TODO: pc shouldn't be treated differently from other registers
# it is just a register. With the register setting framework, we will be able to
# utilize gadgets like `call qword ptr [rax+rbx]` because we have the dependency information.
# transition information, i.e. how to pass the control flow to the next gadget
self.transit_type = None
# TODO: what's the difference between jump_reg and pc_reg?
self.jump_reg = None
self.pc_reg = None
# pc_offset is exclusively used when transit_type is "pop_pc",
# when pc_offset==stack_change-arch_bytes, transit_type is basically ret
self.pc_offset = None

@property
def num_mem_access(self):
Expand Down
28 changes: 24 additions & 4 deletions tests/test_chainbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,18 +266,18 @@ def test_shifter():
rop.find_gadgets()
rop.save_gadgets(cache_path)

chain = rop.shift(0x54, preserve_regs=['ebx'])
chain = rop.shift(0x50, preserve_regs=['ebx'])
init_sp = chain._blank_state.regs.sp.concrete_value - len(chain._values) * proj.arch.bytes
state = chain.exec()
assert state.regs.sp.concrete_value == init_sp + 0x54 + proj.arch.bytes
assert state.regs.sp.concrete_value == init_sp + 0x50 + proj.arch.bytes

chain = rop.set_regs(ebx=0x41414141)
chain += rop.shift(0x54, preserve_regs=['ebx'])
chain += rop.shift(0x50, preserve_regs=['ebx'])
state = chain.exec()
assert state.regs.ebx.concrete_value == 0x41414141

chain = rop.set_regs(eax=0x41414141)
chain += rop.shift(0x54, preserve_regs=['eax'])
chain += rop.shift(0x50, preserve_regs=['eax'])
state = chain.exec()
assert state.regs.eax.concrete_value == 0x41414141

Expand Down Expand Up @@ -354,6 +354,26 @@ def test_retsled():
chain = rop.retsled(0x40)
assert len(chain.payload_str()) == 0x40

def test_pop_pc_syscall_chain():
proj = angr.Project(os.path.join(BIN_DIR, "tests", "x86_64", "angrop_retn_test"), auto_load_libs=False)
rop = proj.analyses.ROP(fast_mode=False, only_check_near_rets=False)
rop.find_gadgets_single_threaded(show_progress=False)

chain1 = rop.do_syscall(3, [0])
chain2 = rop.do_syscall(0x27, [0])
final_chain = chain1 + chain2
state = final_chain.exec()
assert state.regs.rax.concrete_value == 1337
assert 0 not in state.posix.fd

chain = rop.do_syscall(3, [0])
gadget = rop.analyze_gadget(0x0000000000401138) # pop rdi; ret
chain.add_gadget(gadget)
chain.add_value(0x41414141)
state = chain.exec()
assert state.regs.rdi.concrete_value == 0x41414141
assert 0 not in state.posix.fd

def run_all():
functions = globals()
all_functions = {x:y for x, y in functions.items() if x.startswith('test_')}
Expand Down
12 changes: 12 additions & 0 deletions tests/test_find_gadgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def test_syscall_gadget():
assert all(gadget_exists(rop, x) for x in [0x0806f860, 0x0806f85e, 0x080939e3, 0x0806f2f1])

def test_shift_gadget():
# pylint: disable=pointless-string-statement
"""
438a91 pop es
438a92 add esp, 0x9c
Expand Down Expand Up @@ -154,6 +155,7 @@ def test_shift_gadget():
assert all(gadget_exists(rop, x) for x in [0x454e75, 0x5622d5, 0x490058])

def test_i386_syscall():
# pylint: disable=pointless-string-statement
proj = angr.Project(os.path.join(tests_dir, "i386", "angrop_syscall_test"), auto_load_libs=False)

rop = proj.analyses.ROP()
Expand All @@ -177,6 +179,16 @@ def test_i386_syscall():
"""
assert all(not gadget_exists(rop, x) for x in [0x8049189, 0x804918f])

def test_gadget_timeout():
# pylint: disable=pointless-string-statement
proj = angr.Project(os.path.join(tests_dir, "x86_64", "datadep_test"), auto_load_libs=False)
rop = proj.analyses.ROP()
"""
0x4005d5 ret 0xc148
"""
gadget = rop.analyze_gadget(0x4005d5)
assert gadget

def run_all():
functions = globals()
all_functions = {x:y for x, y in functions.items() if x.startswith('test_')}
Expand Down
14 changes: 14 additions & 0 deletions tests/test_gadgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,20 @@ def test_syscall_gadget():
assert not gadget.can_return
assert len(gadget.concrete_regs) == 1 and gadget.concrete_regs.pop('rsi') == 0x81

def test_pop_pc_gadget():
proj = angr.Project(os.path.join(BIN_DIR, "tests", "mipsel", "darpa_ping"), auto_load_libs=False)
rop = proj.analyses.ROP()
gadget = rop.analyze_gadget(0x404e98)
assert gadget.transit_type == 'pop_pc'
assert gadget.pc_offset == 0x28

proj = angr.Project(os.path.join(BIN_DIR, "tests", "x86_64", "angrop_retn_test"), auto_load_libs=False)
rop = proj.analyses.ROP(fast_mode=False, only_check_near_rets=False)
gadget = rop.analyze_gadget(0x40113a)
assert gadget.transit_type == 'pop_pc'
assert gadget.pc_offset == 0
assert gadget.stack_change == 0x18

def run_all():
functions = globals()
all_functions = {x:y for x, y in functions.items() if x.startswith('test_')}
Expand Down

0 comments on commit ffc9135

Please sign in to comment.