angr 9.2.160__cp310-abi3-macosx_11_0_arm64.whl → 9.2.162__cp310-abi3-macosx_11_0_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of angr might be problematic. Click here for more details.

Files changed (58) hide show
  1. angr/__init__.py +4 -1
  2. angr/analyses/analysis.py +0 -1
  3. angr/analyses/cfg/cfg_base.py +5 -1
  4. angr/analyses/decompiler/ail_simplifier.py +101 -2
  5. angr/analyses/decompiler/block_simplifier.py +13 -8
  6. angr/analyses/decompiler/clinic.py +1 -0
  7. angr/analyses/decompiler/condition_processor.py +24 -0
  8. angr/analyses/decompiler/counters/call_counter.py +11 -1
  9. angr/analyses/decompiler/decompiler.py +3 -1
  10. angr/analyses/decompiler/graph_region.py +11 -2
  11. angr/analyses/decompiler/optimization_passes/const_prop_reverter.py +1 -1
  12. angr/analyses/decompiler/optimization_passes/lowered_switch_simplifier.py +1 -0
  13. angr/analyses/decompiler/optimization_passes/optimization_pass.py +31 -11
  14. angr/analyses/decompiler/optimization_passes/return_duplicator_low.py +2 -0
  15. angr/analyses/decompiler/peephole_optimizations/__init__.py +4 -4
  16. angr/analyses/decompiler/peephole_optimizations/eager_eval.py +53 -0
  17. angr/analyses/decompiler/peephole_optimizations/modulo_simplifier.py +89 -0
  18. angr/analyses/decompiler/peephole_optimizations/{const_mull_a_shift.py → optimized_div_simplifier.py} +139 -25
  19. angr/analyses/decompiler/peephole_optimizations/remove_redundant_bitmasks.py +18 -9
  20. angr/analyses/decompiler/region_simplifiers/goto.py +3 -3
  21. angr/analyses/decompiler/region_simplifiers/if_.py +2 -2
  22. angr/analyses/decompiler/region_simplifiers/loop.py +2 -2
  23. angr/analyses/decompiler/structured_codegen/c.py +3 -3
  24. angr/analyses/decompiler/structuring/dream.py +1 -1
  25. angr/analyses/decompiler/structuring/phoenix.py +138 -99
  26. angr/analyses/decompiler/structuring/recursive_structurer.py +3 -2
  27. angr/analyses/decompiler/structuring/sailr.py +51 -43
  28. angr/analyses/decompiler/structuring/structurer_base.py +2 -3
  29. angr/analyses/deobfuscator/string_obf_opt_passes.py +1 -1
  30. angr/analyses/disassembly.py +1 -1
  31. angr/analyses/reaching_definitions/function_handler.py +1 -0
  32. angr/analyses/s_propagator.py +2 -2
  33. angr/analyses/s_reaching_definitions/s_rda_model.py +1 -0
  34. angr/analyses/s_reaching_definitions/s_reaching_definitions.py +5 -2
  35. angr/analyses/variable_recovery/engine_base.py +17 -1
  36. angr/analyses/variable_recovery/variable_recovery_base.py +30 -2
  37. angr/analyses/variable_recovery/variable_recovery_fast.py +11 -2
  38. angr/emulator.py +143 -0
  39. angr/engines/concrete.py +66 -0
  40. angr/engines/icicle.py +66 -30
  41. angr/exploration_techniques/driller_core.py +2 -2
  42. angr/knowledge_plugins/functions/function.py +1 -1
  43. angr/knowledge_plugins/functions/function_manager.py +1 -2
  44. angr/project.py +7 -0
  45. angr/rustylib.abi3.so +0 -0
  46. angr/sim_type.py +16 -8
  47. angr/simos/javavm.py +1 -1
  48. angr/unicornlib.dylib +0 -0
  49. angr/utils/graph.py +48 -13
  50. angr/utils/library.py +13 -12
  51. angr/utils/ssa/__init__.py +57 -5
  52. {angr-9.2.160.dist-info → angr-9.2.162.dist-info}/METADATA +5 -5
  53. {angr-9.2.160.dist-info → angr-9.2.162.dist-info}/RECORD +57 -55
  54. angr/analyses/decompiler/peephole_optimizations/a_sub_a_div_const_mul_const.py +0 -57
  55. {angr-9.2.160.dist-info → angr-9.2.162.dist-info}/WHEEL +0 -0
  56. {angr-9.2.160.dist-info → angr-9.2.162.dist-info}/entry_points.txt +0 -0
  57. {angr-9.2.160.dist-info → angr-9.2.162.dist-info}/licenses/LICENSE +0 -0
  58. {angr-9.2.160.dist-info → angr-9.2.162.dist-info}/top_level.txt +0 -0
@@ -26,7 +26,9 @@ class SAILRStructurer(PhoenixStructurer):
26
26
 
27
27
  NAME = "sailr"
28
28
 
29
- def __init__(self, region, improve_phoenix=True, **kwargs):
29
+ def __init__(self, region, improve_phoenix=True, postdom_max_edges=10, postdom_max_graph_size=50, **kwargs):
30
+ self._postdom_max_edges = postdom_max_edges
31
+ self._postdom_max_graph_size = postdom_max_graph_size
30
32
  super().__init__(
31
33
  region,
32
34
  improve_algorithm=improve_phoenix,
@@ -46,44 +48,50 @@ class SAILRStructurer(PhoenixStructurer):
46
48
  except StopIteration:
47
49
  entry_node = None
48
50
 
51
+ edges = sorted(edges, key=lambda edge: (edge[0].addr, edge[1].addr))
49
52
  best_edges = edges
50
53
  if entry_node is not None:
51
- # the first few heuristics are based on the post-dominator count of the edge
52
- # so we collect them for each candidate edge
53
- edge_postdom_count = {}
54
+ # collect sibling counts for edges
54
55
  edge_sibling_count = {}
55
56
  for edge in edges:
56
57
  _, dst = edge
57
- graph_copy = networkx.DiGraph(graph)
58
- graph_copy.remove_edge(*edge)
59
- sibling_cnt = graph_copy.in_degree(dst)
58
+ sibling_cnt = graph.in_degree[dst] - 1
60
59
  if sibling_cnt == 0:
61
60
  continue
62
-
63
61
  edge_sibling_count[edge] = sibling_cnt
64
- post_dom_graph = PostDominators(graph_copy, entry_node).post_dom
65
- post_doms = set()
66
- for postdom_node, dominatee in post_dom_graph.edges():
67
- if not isinstance(postdom_node, TemporaryNode) and not isinstance(dominatee, TemporaryNode):
68
- post_doms.add((postdom_node, dominatee))
69
- edge_postdom_count[edge] = len(post_doms)
70
-
71
- # H1: the edge that has the least amount of sibling edges should be virtualized first
72
- # this is believed to reduce the amount of virtualization needed in future rounds and increase
73
- # the edges that enter a single outer-scope if-stmt
74
- if edge_sibling_count:
75
- min_sibling_count = min(edge_sibling_count.values())
76
- best_edges = [edge for edge, cnt in edge_sibling_count.items() if cnt == min_sibling_count]
77
- if len(best_edges) == 1:
78
- return best_edges
79
62
 
80
- # create the next heuristic based on the best edges from the previous heuristic
81
- filtered_edge_postdom_count = edge_postdom_count.copy()
82
- for edge in list(edge_postdom_count.keys()):
83
- if edge not in best_edges:
84
- del filtered_edge_postdom_count[edge]
85
- if filtered_edge_postdom_count:
86
- edge_postdom_count = filtered_edge_postdom_count
63
+ # H1: the edge that has the least amount of sibling edges should be virtualized first
64
+ # this is believed to reduce the amount of virtualization needed in future rounds and increase
65
+ # the edges that enter a single outer-scope if-stmt
66
+ if edge_sibling_count:
67
+ min_sibling_count = min(edge_sibling_count.values())
68
+ best_edges = [edge for edge, cnt in edge_sibling_count.items() if cnt == min_sibling_count]
69
+ if len(best_edges) == 1:
70
+ return best_edges
71
+
72
+ if len(edges) <= self._postdom_max_edges and len(graph) <= self._postdom_max_graph_size:
73
+ # dominator analysis is expensive, so we only do it for small graphs
74
+ edge_postdom_count = {}
75
+ graph_copy = networkx.DiGraph(graph)
76
+ for edge in edges:
77
+ _, dst = edge
78
+ graph_copy.remove_edge(*edge)
79
+ post_dom_graph = PostDominators(graph_copy, entry_node).post_dom
80
+ post_doms = set()
81
+ for postdom_node, dominatee in post_dom_graph.edges():
82
+ if not isinstance(postdom_node, TemporaryNode) and not isinstance(dominatee, TemporaryNode):
83
+ post_doms.add((postdom_node, dominatee))
84
+ edge_postdom_count[edge] = len(post_doms)
85
+ # add back the edge
86
+ graph_copy.add_edge(*edge)
87
+
88
+ # create the next heuristic based on the best edges from the previous heuristic
89
+ filtered_edge_postdom_count = edge_postdom_count.copy()
90
+ for edge in list(edge_postdom_count.keys()):
91
+ if edge not in best_edges:
92
+ del filtered_edge_postdom_count[edge]
93
+ if filtered_edge_postdom_count:
94
+ edge_postdom_count = filtered_edge_postdom_count
87
95
 
88
96
  # H2: the edge, when removed, that causes the most post-dominators of the graph should be virtualized
89
97
  # first. this is believed to make the code more linear looking be reducing the amount of scopes.
@@ -94,19 +102,19 @@ class SAILRStructurer(PhoenixStructurer):
94
102
  if len(best_edges) == 1:
95
103
  return best_edges
96
104
 
97
- # H3: the edge that goes directly to a return statement should be virtualized first
98
- # this is believed to be good because it can be corrected in later optimization by duplicating
99
- # the return
100
- candidate_edges = best_edges
101
- best_edges = []
102
- for src, dst in candidate_edges:
103
- if graph.has_node(dst) and structured_node_is_simple_return(dst, graph):
104
- best_edges.append((src, dst))
105
-
106
- if len(best_edges) == 1:
107
- return best_edges
108
- if not best_edges:
109
- best_edges = candidate_edges
105
+ # H3: the edge that goes directly to a return statement should be virtualized first
106
+ # this is believed to be good because it can be corrected in later optimization by duplicating
107
+ # the return
108
+ candidate_edges = best_edges
109
+ best_edges = []
110
+ for src, dst in candidate_edges:
111
+ if graph.has_node(dst) and structured_node_is_simple_return(dst, graph):
112
+ best_edges.append((src, dst))
113
+
114
+ if len(best_edges) == 1:
115
+ return best_edges
116
+ if not best_edges:
117
+ best_edges = candidate_edges
110
118
 
111
119
  # if we have another tie, or we never used improved heuristics, then we do the default ordering.
112
120
  return super()._order_virtualizable_edges(graph, best_edges, node_seq)
@@ -6,9 +6,9 @@ import logging
6
6
 
7
7
  import networkx
8
8
 
9
- import angr.ailment as ailment
10
9
  import claripy
11
10
 
11
+ from angr import ailment
12
12
  from angr.analyses import Analysis
13
13
  from angr.analyses.decompiler.condition_processor import ConditionProcessor
14
14
  from angr.analyses.decompiler.sequence_walker import SequenceWalker
@@ -971,8 +971,7 @@ class StructurerBase(Analysis):
971
971
  new_sequences.append(new_seq_)
972
972
  self._new_sequences = new_sequences
973
973
 
974
- @staticmethod
975
- def replace_nodes(graph, old_node_0, new_node, old_node_1=None, self_loop=True):
974
+ def replace_nodes(self, graph, old_node_0, new_node, old_node_1=None, self_loop=True): # pylint:disable=no-self-use
976
975
  in_edges = list(graph.in_edges(old_node_0, data=True))
977
976
  out_edges = list(graph.out_edges(old_node_0, data=True))
978
977
  if old_node_1 is not None:
@@ -90,7 +90,7 @@ class StringObfType3Rewriter(OptimizationPass):
90
90
  else:
91
91
  new_stmt = new_call
92
92
 
93
- statements = block.statements[:-1] + [new_stmt]
93
+ statements = [*block.statements[:-1], new_stmt]
94
94
 
95
95
  # remove N-2 continuous stack assignment
96
96
  if len(deobf_content) > 2:
@@ -922,7 +922,7 @@ class Value(OperandPiece):
922
922
  if func is not None and lbl == func.name and func.name != func.demangled_name:
923
923
  # see if lbl == func.name and func.demangled_name != func.name. if so, we prioritize the
924
924
  # demangled name
925
- normalized_name = get_cpp_function_name(func.demangled_name, specialized=False, qualified=True)
925
+ normalized_name = get_cpp_function_name(func.demangled_name)
926
926
  return [normalized_name]
927
927
  return [("+" if self.render_with_sign else "") + lbl]
928
928
  if func is not None:
@@ -584,6 +584,7 @@ class FunctionHandler:
584
584
  # migrate data from sub_rda to its parent
585
585
  state.analysis.function_calls.update(sub_rda.function_calls)
586
586
  state.analysis.model.observed_results.update(sub_rda.model.observed_results)
587
+ state.all_definitions |= sub_rda.all_definitions
587
588
 
588
589
  sub_ld = get_exit_livedefinitions(data.function, sub_rda.model)
589
590
  if sub_ld is not None:
@@ -33,7 +33,7 @@ from angr.utils.ssa import (
33
33
  is_const_vvar_load_assignment,
34
34
  is_const_vvar_load_dirty_assignment,
35
35
  is_const_vvar_tmp_assignment,
36
- is_vvar_eliminatable,
36
+ is_vvar_propagatable,
37
37
  get_tmp_uselocs,
38
38
  get_tmp_deflocs,
39
39
  phi_assignment_get_src,
@@ -246,7 +246,7 @@ class SPropagatorAnalysis(Analysis):
246
246
  self.model.dead_vvar_ids.add(vvar.varid)
247
247
  continue
248
248
 
249
- if is_vvar_eliminatable(vvar, stmt):
249
+ if is_vvar_propagatable(vvar, stmt):
250
250
  if len(vvar_uselocs_set) == 1:
251
251
  vvar_used, vvar_useloc = next(iter(vvar_uselocs_set))
252
252
  if (
@@ -25,6 +25,7 @@ class SRDAModel:
25
25
  self.all_tmp_definitions: dict[CodeLocation, dict[atoms.Tmp, int]] = defaultdict(dict)
26
26
  self.all_tmp_uses: dict[CodeLocation, dict[atoms.Tmp, set[tuple[Tmp, int]]]] = defaultdict(dict)
27
27
  self.phi_vvar_ids: set[int] = set()
28
+ self.phivarid_to_varids_with_unknown: dict[int, set[int | None]] = {}
28
29
  self.phivarid_to_varids: dict[int, set[int]] = {}
29
30
  self.vvar_uses_by_loc: dict[CodeLocation, list[int]] = {}
30
31
 
@@ -63,7 +63,7 @@ class SReachingDefinitionsAnalysis(Analysis):
63
63
  case _:
64
64
  raise NotImplementedError
65
65
 
66
- phi_vvars: dict[int, set[int]] = {}
66
+ phi_vvars: dict[int, set[int | None]] = {}
67
67
  # find all vvar definitions
68
68
  vvar_deflocs = get_vvar_deflocs(blocks.values(), phi_vvars=phi_vvars)
69
69
  # find all explicit vvar uses
@@ -87,7 +87,10 @@ class SReachingDefinitionsAnalysis(Analysis):
87
87
  self.model.phi_vvar_ids = set(phi_vvars)
88
88
  self.model.phivarid_to_varids = {}
89
89
  for vvar_id, src_vvars in phi_vvars.items():
90
- self.model.phivarid_to_varids[vvar_id] = src_vvars
90
+ self.model.phivarid_to_varids_with_unknown[vvar_id] = src_vvars
91
+ self.model.phivarid_to_varids[vvar_id] = ( # type: ignore
92
+ {vvar_id for vvar_id in src_vvars if vvar_id is not None} if None in src_vvars else src_vvars
93
+ )
91
94
 
92
95
  if self.mode == "function":
93
96
 
@@ -178,6 +178,14 @@ class SimEngineVRBase(
178
178
  for var_stack_offset, var in self.state.extract_variables(v):
179
179
  existing_vars.append((var, var_stack_offset))
180
180
 
181
+ if not existing_vars:
182
+ existing_vars = [
183
+ (v, 0)
184
+ for v in self.state.variable_manager[self.func_addr].find_variables_by_stack_offset(
185
+ stack_offset
186
+ )
187
+ ]
188
+
181
189
  if not existing_vars:
182
190
  # no variables exist
183
191
  lea_size = 1
@@ -525,6 +533,10 @@ class SimEngineVRBase(
525
533
  def _store_to_stack(
526
534
  self, stack_offset, data: RichR[claripy.ast.BV | claripy.ast.FP], size, offset=0, atom=None, endness=None
527
535
  ):
536
+ """
537
+ Store data to a stack location. We limit the size of the data to store to 256 bytes for performance reasons.
538
+ """
539
+
528
540
  if atom is None:
529
541
  existing_vars = self.state.variable_manager[self.func_addr].find_variables_by_stmt(
530
542
  self.block.addr, self.stmt_idx, "memory"
@@ -550,7 +562,11 @@ class SimEngineVRBase(
550
562
  variable, variable_offset = next(iter(existing_vars))
551
563
 
552
564
  if isinstance(stack_offset, int):
553
- expr = self.state.annotate_with_variables(data.data, [(variable_offset, variable)])
565
+ expr = data.data
566
+ if isinstance(expr, claripy.ast.Bits) and expr.size() > 1024:
567
+ # we don't write more than 256 bytes to the stack at a time for performance reasons
568
+ expr = expr[expr.size() - 1 : expr.size() - 1024]
569
+ expr = self.state.annotate_with_variables(expr, [(variable_offset, variable)])
554
570
  stack_addr = self.state.stack_addr_from_offset(stack_offset)
555
571
  self.state.stack_region.store(stack_addr, expr, endness=endness)
556
572
 
@@ -5,6 +5,8 @@ from collections.abc import Generator, Iterable
5
5
  import logging
6
6
  from collections import defaultdict
7
7
 
8
+ import networkx
9
+
8
10
  import archinfo
9
11
  import claripy
10
12
  from claripy.annotation import Annotation
@@ -86,8 +88,18 @@ class VariableRecoveryBase(Analysis):
86
88
  The base class for VariableRecovery and VariableRecoveryFast.
87
89
  """
88
90
 
89
- def __init__(self, func, max_iterations, store_live_variables: bool, vvar_to_vvar: dict[int, int] | None = None):
91
+ def __init__(
92
+ self,
93
+ func,
94
+ max_iterations,
95
+ store_live_variables: bool,
96
+ vvar_to_vvar: dict[int, int] | None = None,
97
+ func_graph: networkx.DiGraph | None = None,
98
+ entry_node_addr: int | tuple[int, int | None] | None = None,
99
+ ):
90
100
  self.function = func
101
+ self.func_graph = func_graph
102
+ self.entry_node_addr = entry_node_addr
91
103
  self.variable_manager = self.kb.variables
92
104
 
93
105
  self._max_iterations = max_iterations
@@ -120,7 +132,23 @@ class VariableRecoveryBase(Analysis):
120
132
 
121
133
  def initialize_dominance_frontiers(self):
122
134
  # Computer the dominance frontier for each node in the graph
123
- df = self.project.analyses.DominanceFrontier(self.function)
135
+ func_entry = None
136
+ if self.func_graph is not None:
137
+ entry_node_addr = self.entry_node_addr if self.entry_node_addr is not None else self.function.addr
138
+ assert entry_node_addr is not None
139
+ if isinstance(entry_node_addr, int):
140
+ func_entry = next(iter(node for node in self.func_graph if node.addr == entry_node_addr))
141
+ elif isinstance(entry_node_addr, tuple):
142
+ func_entry = next(
143
+ iter(
144
+ node
145
+ for node in self.func_graph
146
+ if node.addr == entry_node_addr[0] and node.idx == entry_node_addr[1]
147
+ )
148
+ )
149
+ else:
150
+ raise TypeError(f"Unsupported entry node address type: {type(entry_node_addr)}")
151
+ df = self.project.analyses.DominanceFrontier(self.function, func_graph=self.func_graph, entry=func_entry)
124
152
  self._dominance_frontiers = defaultdict(set)
125
153
  for b0, domfront in df.frontiers.items():
126
154
  for d in domfront:
@@ -237,6 +237,7 @@ class VariableRecoveryFast(ForwardAnalysis, VariableRecoveryBase): # pylint:dis
237
237
  self,
238
238
  func: Function | str | int,
239
239
  func_graph: networkx.DiGraph | None = None,
240
+ entry_node_addr: int | tuple[int, int | None] | None = None,
240
241
  max_iterations: int = 2,
241
242
  low_priority=False,
242
243
  track_sp=True,
@@ -259,10 +260,18 @@ class VariableRecoveryFast(ForwardAnalysis, VariableRecoveryBase): # pylint:dis
259
260
  function_graph_visitor = visitors.FunctionGraphVisitor(func, graph=func_graph)
260
261
 
261
262
  # Make sure the function is not empty
262
- if not func.block_addrs_set or func.startpoint is None:
263
+ if (not func.block_addrs_set or func.startpoint is None) and not func_graph:
263
264
  raise AngrVariableRecoveryError(f"Function {func!r} is empty.")
264
265
 
265
- VariableRecoveryBase.__init__(self, func, max_iterations, store_live_variables, vvar_to_vvar=vvar_to_vvar)
266
+ VariableRecoveryBase.__init__(
267
+ self,
268
+ func,
269
+ max_iterations,
270
+ store_live_variables,
271
+ vvar_to_vvar=vvar_to_vvar,
272
+ func_graph=func_graph_with_calls,
273
+ entry_node_addr=entry_node_addr,
274
+ )
266
275
  ForwardAnalysis.__init__(
267
276
  self, order_jobs=True, allow_merging=True, allow_widening=False, graph_visitor=function_graph_visitor
268
277
  )
angr/emulator.py ADDED
@@ -0,0 +1,143 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from enum import Enum
5
+
6
+ from angr.engines.concrete import ConcreteEngine, HeavyConcreteState
7
+ from angr.errors import AngrError
8
+
9
+
10
+ log = logging.getLogger(name=__name__)
11
+
12
+
13
+ class EmulatorException(AngrError):
14
+ """Base class for exceptions raised by the Emulator."""
15
+
16
+
17
+ class EngineException(EmulatorException):
18
+ """Exception raised when the emulator encounters an unhandlable error in the engine."""
19
+
20
+
21
+ class StateDivergedException(EmulatorException):
22
+ """Exception raised when an engine returns multiple successors."""
23
+
24
+
25
+ class EmulatorStopReason(Enum):
26
+ """
27
+ Enum representing the reason for stopping the emulator.
28
+ """
29
+
30
+ INSTRUCTION_LIMIT = "instruction_limit"
31
+ BREAKPOINT = "breakpoint"
32
+ NO_SUCCESSORS = "no_successors"
33
+ MEMORY_ERROR = "memory_error"
34
+ FAILURE = "failure"
35
+ EXIT = "exit"
36
+
37
+
38
+ class Emulator:
39
+ """
40
+ Emulator is a utility that adapts an angr `ConcreteEngine` to a more
41
+ user-friendly interface for concrete execution. It only supports concrete
42
+ execution and requires a ConcreteEngine.
43
+
44
+ Saftey: This class is not thread-safe. It should only be used in a
45
+ single-threaded context. It can be safely shared between multiple threads,
46
+ provided that only one thread is using it at a time.
47
+ """
48
+
49
+ _engine: ConcreteEngine
50
+ _state: HeavyConcreteState
51
+
52
+ def __init__(self, engine: ConcreteEngine, init_state: HeavyConcreteState):
53
+ """
54
+ :param engine: The `ConcreteEngine` to use for emulation.
55
+ :param init_state: The initial state to use for emulation.
56
+ """
57
+ self._engine = engine
58
+ self._state = init_state
59
+
60
+ @property
61
+ def state(self) -> HeavyConcreteState:
62
+ """
63
+ The current state of the emulator.
64
+ """
65
+ return self._state
66
+
67
+ @property
68
+ def breakpoints(self) -> set[int]:
69
+ """
70
+ The set of currently set breakpoints.
71
+ """
72
+ return self._engine.get_breakpoints()
73
+
74
+ def add_breakpoint(self, addr: int) -> None:
75
+ """
76
+ Add a breakpoint at the given address.
77
+
78
+ :param addr: The address to set the breakpoint at.
79
+ """
80
+ self._engine.add_breakpoint(addr)
81
+
82
+ def remove_breakpoint(self, addr: int) -> None:
83
+ """
84
+ Remove a breakpoint at the given address, if present.
85
+
86
+ :param addr: The address to remove the breakpoint from.
87
+ """
88
+ self._engine.remove_breakpoint(addr)
89
+
90
+ def run(self, num_inst: int | None = None) -> EmulatorStopReason:
91
+ """
92
+ Execute the emulator.
93
+ """
94
+ completed_engine_execs = 0
95
+ num_inst_executed: int = 0
96
+ while self._state.history.jumpkind != "Ijk_Exit":
97
+ # Check if there is a breakpoint at the current address
98
+ if completed_engine_execs > 0 and self._state.addr in self._engine.get_breakpoints():
99
+ return EmulatorStopReason.BREAKPOINT
100
+
101
+ # Check if we've already executed the requested number of instructions
102
+ if num_inst is not None and num_inst_executed >= num_inst:
103
+ return EmulatorStopReason.INSTRUCTION_LIMIT
104
+
105
+ # Calculate remaining instructions for this engine execution
106
+ remaining_inst: int | None = None
107
+ if num_inst is not None:
108
+ remaining_inst = num_inst - num_inst_executed
109
+
110
+ # Run the engine to get successors
111
+ try:
112
+ successors = self._engine.process(self._state, num_inst=remaining_inst)
113
+ except EngineException as e:
114
+ raise EngineException(f"Engine encountered an error: {e}") from e
115
+
116
+ # Handle cases with an unexpected number of successors
117
+ if len(successors.successors) == 0:
118
+ return EmulatorStopReason.NO_SUCCESSORS
119
+ if len(successors.successors) > 1:
120
+ log.warning("Concrete engine returned multiple successors")
121
+
122
+ # Set the state before raising further exceptions
123
+ self._state = successors.successors[0]
124
+
125
+ # Track the number of instructions executed using the state's history
126
+ if self._state.history.recent_instruction_count > 0:
127
+ num_inst_executed += self._state.history.recent_instruction_count
128
+
129
+ if successors.successors[0].history.jumpkind == "Ijk_SigSEGV":
130
+ return EmulatorStopReason.MEMORY_ERROR
131
+
132
+ completed_engine_execs += 1
133
+
134
+ return EmulatorStopReason.EXIT
135
+
136
+
137
+ __all__ = (
138
+ "Emulator",
139
+ "EmulatorException",
140
+ "EmulatorStopReason",
141
+ "EngineException",
142
+ "StateDivergedException",
143
+ )
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import typing
5
+ from abc import abstractmethod, ABCMeta
6
+ from typing_extensions import override
7
+
8
+ import claripy
9
+ from angr.engines.successors import SimSuccessors, SuccessorsEngine
10
+ from angr.sim_state import SimState
11
+
12
+ log = logging.getLogger(__name__)
13
+
14
+
15
+ HeavyConcreteState = SimState[int, int]
16
+
17
+
18
+ class ConcreteEngine(SuccessorsEngine, metaclass=ABCMeta):
19
+ """
20
+ ConcreteEngine extends SuccessorsEngine and adds APIs for managing breakpoints.
21
+ """
22
+
23
+ @abstractmethod
24
+ def get_breakpoints(self) -> set[int]:
25
+ """Return the set of currently set breakpoints."""
26
+
27
+ @abstractmethod
28
+ def add_breakpoint(self, addr: int) -> None:
29
+ """Add a breakpoint at the given address."""
30
+
31
+ @abstractmethod
32
+ def remove_breakpoint(self, addr: int) -> None:
33
+ """Remove a breakpoint at the given address, if present."""
34
+
35
+ @abstractmethod
36
+ def process_concrete(self, state: HeavyConcreteState, num_inst: int | None = None) -> HeavyConcreteState:
37
+ """
38
+ Process the concrete state and return a HeavyState object.
39
+
40
+ :param state: The concrete state to process.
41
+ :return: A HeavyState object representing the processed state.
42
+ """
43
+
44
+ @override
45
+ def process_successors(
46
+ self, successors: SimSuccessors, *, num_inst: int | None = None, **kwargs: dict[str, typing.Any]
47
+ ):
48
+ if len(kwargs) > 0:
49
+ log.warning("ConcreteEngine.process_successors received unknown kwargs: %s", kwargs)
50
+
51
+ # TODO: Properly error here when the state is not a HeavyConcreteState
52
+ # Alternatively, we could make SimSuccessors generic over the state type too
53
+ concrete_state = typing.cast(HeavyConcreteState, self.state)
54
+
55
+ concrete_successor = self.process_concrete(concrete_state, num_inst=num_inst)
56
+ successors.add_successor(
57
+ concrete_successor,
58
+ concrete_successor.ip,
59
+ claripy.true(),
60
+ concrete_successor.history.jumpkind,
61
+ add_guard=False,
62
+ )
63
+ successors.processed = True
64
+
65
+
66
+ __all__ = ["ConcreteEngine", "HeavyConcreteState"]