Skip to content

Commit f8bb3ef

Browse files
authored
Merge pull request #152 from angr/feat/concrete_setter
Feat/concrete setter
2 parents cce7be1 + b8c8dca commit f8bb3ef

File tree

9 files changed

+345
-301
lines changed

9 files changed

+345
-301
lines changed

angrop/chain_builder/mem_writer.py

Lines changed: 108 additions & 253 deletions
Large diffs are not rendered by default.

angrop/chain_builder/reg_setter.py

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from collections import defaultdict, Counter
44
from functools import cmp_to_key
55

6+
import claripy
67
import networkx as nx
78
from angr.errors import SimUnsatError
89

@@ -15,12 +16,76 @@
1516

1617
l = logging.getLogger(__name__)
1718

19+
class ConcreteRegSetter(Builder):
20+
"""
21+
a chain builder that aims to find best gadgets that can set registers to
22+
concrete values
23+
"""
24+
def __init__(self, chain_builder):
25+
super().__init__(chain_builder)
26+
self._concrete_setting_gadgets = None
27+
self._concrete_setting_cache = defaultdict(list)
28+
29+
def bootstrap(self):
30+
self._concrete_setting_gadgets = self.filter_gadgets(self.chain_builder.gadgets)
31+
for g in self._concrete_setting_gadgets:
32+
reg = list(g.concrete_regs)[0]
33+
self._concrete_setting_cache[reg].append(g)
34+
35+
def _effect_tuple(self, g):
36+
assert len(g.concrete_regs) == 1
37+
reg = list(g.concrete_regs)[0]
38+
return (reg, g.concrete_regs[reg])
39+
40+
def _comparison_tuple(self, g):
41+
assert len(g.concrete_regs) == 1
42+
reg = list(g.concrete_regs)[0]
43+
return (len(g.changed_regs-{reg}), g.stack_change, g.num_sym_mem_access,
44+
g.isn_count, int(g.has_conditional_branch is True))
45+
46+
def filter_gadgets(self, gadgets):
47+
gadgets = [g for g in gadgets if len(g.concrete_regs) == 1 and not g.num_sym_mem_access]
48+
return self._filter_gadgets(gadgets)
49+
50+
class ConcreteRegChanger(Builder):
51+
"""
52+
a chain builder that aims to craft register values using concrete value
53+
effect in gadgets
54+
"""
55+
def __init__(self, chain_builder):
56+
super().__init__(chain_builder)
57+
self._concrete_reg_changing_gadgets = None
58+
self._concrete_reg_changing_cache = defaultdict(list)
59+
60+
def bootstrap(self):
61+
self._concrete_reg_changing_gadgets = self.filter_gadgets(self.chain_builder.gadgets)
62+
for g in self._concrete_reg_changing_gadgets:
63+
reg = list(g.concrete_reg_changes)[0]
64+
self._concrete_reg_changing_cache[reg].append(g)
65+
66+
def _effect_tuple(self, g):
67+
reg = list(g.concrete_reg_changes.keys())[0]
68+
init_ast, final_ast = g.concrete_reg_changes[reg]
69+
val = claripy.algorithm.replace(expr=final_ast,
70+
old=init_ast,
71+
new=claripy.BVV(0, self.project.arch.bits))
72+
op = final_ast.op
73+
if op in ('ZeroExt', 'SignExt'):
74+
op = final_ast.args[1].op
75+
return (reg, op, val.concrete_value)
76+
77+
def _comparison_tuple(self, g):
78+
reg = list(g.concrete_reg_changes)[0]
79+
return (len(g.changed_regs-{reg}), g.stack_change, g.num_sym_mem_access,
80+
g.isn_count, int(g.has_conditional_branch is True))
81+
82+
def filter_gadgets(self, gadgets):
83+
gadgets = [g for g in gadgets if len(g.concrete_reg_changes) == 1 and not g.num_sym_mem_access]
84+
return self._filter_gadgets(gadgets)
85+
1886
class RegSetter(Builder):
1987
"""
20-
a chain builder that aims to set registers using different algorithms
21-
1. algo1: graph-search, fast, not reliable
22-
2. algo2: pop-only bfs search, fast, reliable, can generate chains to bypass bad-bytes
23-
3. algo3: riscy-rop inspired backward search, slow, can utilize gadgets containing conditional branches
88+
a chain builder that aims to set registers using the graph search algorithm
2489
"""
2590

2691
#### Inits ####
@@ -32,8 +97,12 @@ def __init__(self, chain_builder):
3297
# Estimate of how difficult it is to set each register.
3398
# all self-contained and not symbolic access
3499
self._reg_setting_dict: dict[str, list] = defaultdict(list)
100+
self._concrete_reg_setter = ConcreteRegSetter(chain_builder)
101+
self._concrete_reg_changer = ConcreteRegChanger(chain_builder)
35102

36103
def bootstrap(self):
104+
self._concrete_reg_setter.bootstrap()
105+
self._concrete_reg_changer.bootstrap()
37106
self._reg_setting_gadgets = self.filter_gadgets(self.chain_builder.gadgets)
38107

39108
# update reg_setting_dict
@@ -603,7 +672,7 @@ def _handle_hard_regs(self, gadgets, registers, preserve_regs) -> list[RopGadget
603672
if hard_chains:
604673
hard_chain = hard_chains[0]
605674
else:
606-
hard_chain = self._find_add_chain(gadgets, reg, val)
675+
hard_chain = self._find_add_chain(reg, val)
607676
if hard_chain:
608677
self.hard_chain_cache[key] = hard_chain # we cache the result even if it fails
609678

@@ -627,16 +696,22 @@ def _find_concrete_chains(gadgets, registers):
627696
chains.append([g])
628697
return chains
629698

630-
def _find_add_chain(self, gadgets, reg, val) -> list[RopGadget|RopBlock]:
699+
def _find_add_chain(self, reg, val) -> list[RopGadget|RopBlock]:
631700
"""
632701
find one chain to set one single register to a specific value using concrete values only through add/dec
633702
"""
634703
val = rop_utils.cast_rop_value(val, self.project)
635-
concrete_setter_gadgets = [ x for x in gadgets if reg in x.concrete_regs ]
636-
delta_gadgets = [ x for x in gadgets if len(x.reg_dependencies) == 1 and reg in x.reg_dependencies\
637-
and len(x.reg_dependencies[reg]) == 1 and reg in x.reg_dependencies[reg]]
704+
arch_bits = self.project.arch.bits
705+
concrete_setter_gadgets = self._concrete_reg_setter._concrete_setting_cache[reg]
706+
delta_gadgets = self._concrete_reg_changer._concrete_reg_changing_cache[reg]
638707
for g1 in concrete_setter_gadgets:
639708
for g2 in delta_gadgets:
709+
init_ast, final_ast = g2.concrete_reg_changes[reg]
710+
ast = claripy.algorithm.replace(expr=final_ast,
711+
old=init_ast,
712+
new=claripy.BVV(g1.concrete_regs[reg], arch_bits))
713+
if ast.concrete_value != val.concreted:
714+
continue
640715
try:
641716
chain = self._build_reg_setting_chain([g1, g2], {reg: val})
642717
state = chain.exec()
@@ -653,13 +728,9 @@ def _find_add_chain(self, gadgets, reg, val) -> list[RopGadget|RopBlock]:
653728

654729
def _effect_tuple(self, g):
655730
v1 = tuple(sorted(g.popped_regs))
656-
v2 = tuple(sorted(g.concrete_regs.items()))
657-
v3 = []
658-
for x,y in g.reg_dependencies.items():
659-
v3.append((x, tuple(sorted(y))))
660-
v3 = tuple(sorted(v3))
661-
v4 = g.transit_type
662-
return (v1, v2, v3, v4)
731+
v2 = tuple(sorted(g.reg_moves))
732+
v3 = g.transit_type
733+
return (v1, v2, v3)
663734

664735
def _comparison_tuple(self, g):
665736
return (len(g.changed_regs-g.popped_regs), g.stack_change, g.num_sym_mem_access,

angrop/chain_builder/sigreturn.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,6 @@ def sigreturn(self, **registers):
151151
chain.add_value(word)
152152

153153
# save sigreturn frame information for pretty printing
154-
chain._sigreturn_frames.append((frame, frame_start_offset))
154+
chain._sigreturn_frame = (frame, frame_start_offset)
155155

156156
return chain

angrop/gadget_finder/gadget_analyzer.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -596,25 +596,36 @@ def _check_reg_changes(self, final_state, _, gadget):
596596
else:
597597
gadget.changed_regs.add(reg)
598598

599-
def _check_reg_change_dependencies(self, symbolic_state, symbolic_p, gadget):
599+
def _check_reg_change_dependencies(self, init_state, final_state, gadget):
600600
"""
601601
Checks which registers affect register changes
602602
:param symbolic_state: the input state for testing
603603
:param symbolic_p: the stepped path, symbolic_state is an ancestor of it.
604604
:param gadget: the gadget to store the reg change dependencies in
605605
"""
606+
arch_bits = self.project.arch.bits
606607
for reg in gadget.changed_regs:
607608
# skip popped regs
608609
if reg in gadget.popped_regs:
609610
continue
610611
# check its dependencies and controllers
611-
dependencies = self._get_reg_dependencies(symbolic_p, reg)
612+
dependencies = self._get_reg_dependencies(final_state, reg)
612613
if len(dependencies) != 0:
613614
gadget.reg_dependencies[reg] = set(dependencies)
614-
controllers = self._get_reg_controllers(symbolic_state, symbolic_p, reg, dependencies)
615+
controllers = self._get_reg_controllers(init_state, final_state, reg, dependencies)
615616
if controllers:
616617
gadget.reg_controllers[reg] = set(controllers)
617618

619+
# record simple register changing effects
620+
init_reg = init_state.registers.load(reg)
621+
final_reg = final_state.registers.load(reg)
622+
if init_reg is final_reg:
623+
continue
624+
ast = claripy.algorithm.replace(expr=final_reg, old=init_reg, new=claripy.BVV(0, arch_bits))
625+
if ast.symbolic:
626+
continue
627+
gadget.concrete_reg_changes[reg] = (init_reg, final_reg)
628+
618629
def _check_pop_equal_set(self, gadget, final_state):
619630
"""
620631
identify the situation where the final registers are dependent on each other

angrop/rop_chain.py

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ def __init__(self, project, builder, state=None, badbytes=None):
4040
self._pivoted = False
4141
self._init_sp = None
4242

43-
# sigreturn frame information: list of (frame_object, start_offset_in_values)
44-
self._sigreturn_frames = []
43+
# sigreturn frame information: (frame_object, start_offset_in_values)
44+
self._sigreturn_frame = None
4545

4646
def __add__(self, other):
4747
# need to add the values from the other's stack and the constraints to the result state
@@ -66,9 +66,10 @@ def __add__(self, other):
6666

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

7374
# FIXME: cannot handle cases where a rop_block is used twice and have different constraints
7475
# because right now symbolic values go with rop_blocks
@@ -229,7 +230,7 @@ def copy(self):
229230
cp.payload_len = self.payload_len
230231
cp._blank_state = self._blank_state.copy()
231232
cp.badbytes = self.badbytes
232-
cp._sigreturn_frames = list(self._sigreturn_frames)
233+
cp._sigreturn_frame = self._sigreturn_frame
233234

234235
cp._pivoted = self._pivoted
235236
cp._init_sp = self._init_sp
@@ -406,29 +407,25 @@ def dstr(self):
406407
prefix_len = bs*2+2
407408
prefix = " "*prefix_len
408409

409-
sigreturn_map = {} # start_offset -> frame and end offset
410-
for frame, start_offset in self._sigreturn_frames:
411-
# iterate through frame registers to build a map
412-
frame_words = frame.to_words()
413-
sigreturn_map[start_offset] = (frame, start_offset + len(frame_words))
414-
idx = 0
415-
while idx < len(self._values):
416-
v = self._values[idx]
410+
# a chain may end with a sigreturn frame, in that case, we print up to the frame
411+
# and then print the frame differently
412+
top = len(self._values) if self._sigreturn_frame is None else self._sigreturn_frame[1]
413+
for i in range(top):
414+
v = self._values[i]
417415
if v.symbolic:
418416
res += prefix + f" {v.ast}\n"
419-
idx += 1
420417
continue
421418
for g in self._gadgets:
422419
if g.addr == v.concreted:
423420
fmt = f"%#0{prefix_len}x"
424421
res += fmt % g.addr + f": {g.dstr()}\n"
425422
break
426423
else:
427-
if idx in sigreturn_map:
428-
sigframe, idx = sigreturn_map[idx]
429-
res += sigframe.dstr(prefix=prefix)
430-
continue
431-
idx += 1
424+
res += prefix + f" {v.concreted:#x}\n"
425+
426+
# if there is a sigreturn frame, print it
427+
if self._sigreturn_frame:
428+
res += self._sigreturn_frame[0].dstr(prefix=prefix)
432429
return res
433430

434431
def pp(self):

angrop/rop_effect.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ def __init__(self):
130130
# Stores the stack variables that each register depends on.
131131
# Used to check for cases where two registers are popped from the same location.
132132
self.concrete_regs = {}
133+
self.concrete_reg_changes = {}
133134
self.reg_dependencies = {} # like rax might depend on rbx, rcx
134135
self.reg_controllers = {} # like rax might be able to be controlled by rbx (for any value of rcx)
135136
self.reg_pops = set()
@@ -231,6 +232,7 @@ def copy_effect(self, cp):
231232
cp.stack_change = self.stack_change
232233
cp.changed_regs = set(self.changed_regs)
233234
cp.concrete_regs = dict(self.concrete_regs)
235+
cp.concrete_reg_changes = dict(self.concrete_reg_changes)
234236
cp.reg_dependencies = dict(self.reg_dependencies)
235237
cp.reg_controllers = dict(self.reg_controllers)
236238
cp.reg_pops = set(self.reg_pops)

tests/test_badbytes.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import os
2+
import time
23
import logging
34

45
import angr
56
import angrop # pylint: disable=unused-import
7+
from angrop.errors import RopException
68

79
l = logging.getLogger(__name__)
810

@@ -87,9 +89,10 @@ def test_badbyte_transform():
8789
rop = proj.analyses.ROP()
8890

8991
if os.path.exists(cache_path):
90-
rop.load_gadgets(cache_path, optimize=False)
92+
rop.load_gadgets(cache_path)
9193
else:
9294
rop.find_gadgets()
95+
rop.save_gadgets(cache_path)
9396

9497
rop.set_badbytes([0x00, 0x0A])
9598
chain = rop.write_to_mem(0xdeadbeef, b"\x00", fill_byte=b"A")
@@ -106,24 +109,42 @@ def test_badbyte_multibyte():
106109
rop = proj.analyses.ROP()
107110

108111
if os.path.exists(cache_path):
109-
rop.load_gadgets(cache_path, optimize=False)
112+
rop.load_gadgets(cache_path)
110113
else:
111114
rop.find_gadgets()
115+
rop.save_gadgets(cache_path)
112116

113117
rop.set_badbytes([0x00, 0x0A])
114118
target = b"\x00\x00\x00\x42"
115119
chain = rop.write_to_mem(0xdeadbf00, target, fill_byte=b"\xff")
116120
state = chain.exec()
117-
endian = "little" if proj.arch.memory_endness == "Iend_LE" else "big"
118-
loaded = state.memory.load(
119-
0xdeadbf00, len(target), endness=proj.arch.memory_endness
120-
).concrete_value
121-
assert loaded == int.from_bytes(target, endian)
121+
data = state.solver.eval(state.memory.load(0xdeadbf00, len(target)), cast_to=bytes)
122+
assert data == target
122123

123124
payload = chain.payload_str()
124125
assert b"\x00" not in payload
125126
assert b"\x0A" not in payload
126127

128+
def test_hard_regs_loop():
129+
cache_path = os.path.join(data_dir, "bronze_ropchain")
130+
proj = angr.Project(os.path.join(tests_dir, "i386", "bronze_ropchain"), auto_load_libs=False)
131+
rop = proj.analyses.ROP()
132+
133+
if os.path.exists(cache_path):
134+
rop.load_gadgets(cache_path)
135+
else:
136+
rop.find_gadgets()
137+
rop.save_gadgets(cache_path)
138+
139+
rop.set_badbytes([0x00, 0x0A])
140+
start = time.time()
141+
try:
142+
rop.set_regs(eax=0x4200009a, edx=0xdeadbefc)
143+
except RopException:
144+
pass
145+
146+
# if should take less than 0.1s, but let's be lenient here
147+
assert time.time() - start < 2
127148

128149
def run_all():
129150
functions = globals()

0 commit comments

Comments
 (0)