angr 9.2.124__py3-none-manylinux2014_aarch64.whl → 9.2.125__py3-none-manylinux2014_aarch64.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 (30) hide show
  1. angr/__init__.py +1 -1
  2. angr/analyses/__init__.py +9 -1
  3. angr/analyses/codecave.py +77 -0
  4. angr/analyses/decompiler/clinic.py +31 -1
  5. angr/analyses/decompiler/decompiler.py +4 -0
  6. angr/analyses/decompiler/optimization_passes/__init__.py +3 -0
  7. angr/analyses/decompiler/optimization_passes/inlined_string_transformation_simplifier.py +6 -0
  8. angr/analyses/decompiler/optimization_passes/tag_slicer.py +41 -0
  9. angr/analyses/decompiler/peephole_optimizations/constant_derefs.py +2 -2
  10. angr/analyses/patchfinder.py +137 -0
  11. angr/analyses/pathfinder.py +282 -0
  12. angr/analyses/smc.py +159 -0
  13. angr/angrdb/models.py +1 -2
  14. angr/engines/vex/heavy/heavy.py +2 -0
  15. angr/exploration_techniques/spiller_db.py +1 -2
  16. angr/knowledge_plugins/functions/function.py +4 -0
  17. angr/knowledge_plugins/functions/function_manager.py +18 -9
  18. angr/knowledge_plugins/functions/function_parser.py +1 -1
  19. angr/knowledge_plugins/functions/soot_function.py +1 -0
  20. angr/misc/ux.py +2 -2
  21. angr/project.py +17 -1
  22. angr/state_plugins/history.py +6 -4
  23. angr/utils/bits.py +4 -0
  24. angr/utils/tagged_interval_map.py +112 -0
  25. {angr-9.2.124.dist-info → angr-9.2.125.dist-info}/METADATA +6 -6
  26. {angr-9.2.124.dist-info → angr-9.2.125.dist-info}/RECORD +30 -24
  27. {angr-9.2.124.dist-info → angr-9.2.125.dist-info}/LICENSE +0 -0
  28. {angr-9.2.124.dist-info → angr-9.2.125.dist-info}/WHEEL +0 -0
  29. {angr-9.2.124.dist-info → angr-9.2.125.dist-info}/entry_points.txt +0 -0
  30. {angr-9.2.124.dist-info → angr-9.2.125.dist-info}/top_level.txt +0 -0
angr/__init__.py CHANGED
@@ -2,7 +2,7 @@
2
2
  # pylint: disable=wrong-import-position
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "9.2.124"
5
+ __version__ = "9.2.125"
6
6
 
7
7
  if bytes is str:
8
8
  raise Exception(
angr/analyses/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- # pylint:disable=wrong-import-position
1
+ # " pylint:disable=wrong-import-position
2
2
  from __future__ import annotations
3
3
 
4
4
  from .analysis import Analysis, AnalysesHub
@@ -49,6 +49,10 @@ from .flirt import FlirtAnalysis
49
49
  from .s_propagator import SPropagatorAnalysis
50
50
  from .s_reaching_definitions import SReachingDefinitionsAnalysis
51
51
  from .s_liveness import SLivenessAnalysis
52
+ from .codecave import CodeCaveAnalysis
53
+ from .patchfinder import PatchFinderAnalysis
54
+ from .pathfinder import Pathfinder
55
+ from .smc import SelfModifyingCodeAnalysis
52
56
 
53
57
 
54
58
  __all__ = (
@@ -101,4 +105,8 @@ __all__ = (
101
105
  "SPropagatorAnalysis",
102
106
  "SReachingDefinitionsAnalysis",
103
107
  "SLivenessAnalysis",
108
+ "CodeCaveAnalysis",
109
+ "PatchFinderAnalysis",
110
+ "Pathfinder",
111
+ "SelfModifyingCodeAnalysis",
104
112
  )
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+ import logging
3
+ from enum import Enum, auto
4
+ from typing import TYPE_CHECKING
5
+ from dataclasses import dataclass
6
+
7
+ from angr.analyses import Analysis, AnalysesHub
8
+
9
+
10
+ if TYPE_CHECKING:
11
+ from angr.knowledge_plugins import Function
12
+
13
+
14
+ log = logging.getLogger(__name__)
15
+
16
+
17
+ class CodeCaveClassification(Enum):
18
+ """
19
+ Type of code caves.
20
+ """
21
+
22
+ ALIGNMENT = auto()
23
+ UNREACHABLE = auto()
24
+
25
+
26
+ @dataclass
27
+ class CodeCave:
28
+ """
29
+ Describes a code cave in a binary.
30
+ """
31
+
32
+ func: Function | None
33
+ addr: int
34
+ size: int
35
+ classification: CodeCaveClassification
36
+
37
+
38
+ class CodeCaveAnalysis(Analysis):
39
+ """
40
+ Best-effort static location of potential vacant code caves for possible code injection:
41
+ - Padding functions
42
+ - Unreachable code
43
+ """
44
+
45
+ codecaves: list[CodeCave]
46
+
47
+ def __init__(self):
48
+ self.codecaves = []
49
+
50
+ if len(self.project.kb.functions) == 0 and self.project.kb.cfgs.get_most_accurate() is None:
51
+ log.warning("Please run CFGFast analysis first, to identify functions")
52
+ return
53
+
54
+ # Alignment functions
55
+ for func in self.project.kb.functions.values():
56
+ if func.is_alignment:
57
+ for block in func.blocks:
58
+ self.codecaves.append(CodeCave(func, block.addr, block.size, CodeCaveClassification.ALIGNMENT))
59
+
60
+ # Unreachable code
61
+ for func in self.project.kb.functions.values():
62
+ if func.is_alignment or func.is_plt or func.is_simprocedure or func.addr in self.project.kb.labels:
63
+ continue
64
+
65
+ in_degree = self.project.kb.callgraph.in_degree(func.addr)
66
+ if in_degree == 0 or (
67
+ in_degree == 1
68
+ and self.project.kb.functions[next(self.project.kb.callgraph.predecessors(func.addr))].is_alignment
69
+ ):
70
+ for block in func.blocks:
71
+ self.codecaves.append(CodeCave(func, block.addr, block.size, CodeCaveClassification.UNREACHABLE))
72
+
73
+ # FIXME: find dead blocks with argument propagation
74
+ # FIXME: find dead blocks with external coverage info
75
+
76
+
77
+ AnalysesHub.register_default("CodeCaves", CodeCaveAnalysis)
@@ -42,6 +42,7 @@ from .optimization_passes import (
42
42
  OptimizationPassStage,
43
43
  RegisterSaveAreaSimplifier,
44
44
  StackCanarySimplifier,
45
+ TagSlicer,
45
46
  DUPLICATING_OPTS,
46
47
  CONDENSING_OPTS,
47
48
  )
@@ -111,6 +112,7 @@ class Clinic(Analysis):
111
112
  inlining_parents: set[int] | None = None,
112
113
  vvar_id_start: int = 0,
113
114
  optimization_scratch: dict[str, Any] | None = None,
115
+ desired_variables: set[str] | None = None,
114
116
  ):
115
117
  if not func.normalized and mode == ClinicMode.DECOMPILE:
116
118
  raise ValueError("Decompilation must work on normalized function graphs.")
@@ -154,6 +156,7 @@ class Clinic(Analysis):
154
156
  self._inline_functions = inline_functions
155
157
  self._inlined_counts = {} if inlined_counts is None else inlined_counts
156
158
  self._inlining_parents = inlining_parents or ()
159
+ self._desired_variables = desired_variables
157
160
 
158
161
  self._register_save_areas_removed: bool = False
159
162
 
@@ -220,6 +223,9 @@ class Clinic(Analysis):
220
223
  ail_graph = self._inline_child_functions(ail_graph)
221
224
 
222
225
  ail_graph = self._decompilation_simplifications(ail_graph)
226
+
227
+ if self._desired_variables:
228
+ ail_graph = self._slice_variables(ail_graph)
223
229
  self.graph = ail_graph
224
230
 
225
231
  def _decompilation_graph_recovery(self):
@@ -279,6 +285,27 @@ class Clinic(Analysis):
279
285
 
280
286
  return ail_graph
281
287
 
288
+ def _slice_variables(self, ail_graph):
289
+ nodes_index = {(n.addr, n.idx): n for n in ail_graph.nodes()}
290
+
291
+ vfm = self.variable_kb.variables.function_managers[self.function.addr]
292
+ for v_name in self._desired_variables:
293
+ v = next(iter(vv for vv in vfm._unified_variables if vv.name == v_name))
294
+ for va in vfm.get_variable_accesses(v):
295
+ nodes_index[(va.location.block_addr, va.location.block_idx)].statements[va.location.stmt_idx].tags[
296
+ "keep_in_slice"
297
+ ] = True
298
+
299
+ a = TagSlicer(
300
+ self.function,
301
+ graph=ail_graph,
302
+ variable_kb=self.variable_kb,
303
+ )
304
+ if a.out_graph:
305
+ # use the new graph
306
+ ail_graph = a.out_graph
307
+ return ail_graph
308
+
282
309
  def _inline_child_functions(self, ail_graph):
283
310
  for blk in ail_graph.nodes():
284
311
  for idx, stmt in enumerate(blk.statements):
@@ -909,13 +936,16 @@ class Clinic(Analysis):
909
936
  Convert a VEX block to an AIL block.
910
937
 
911
938
  :param block_node: A BlockNode instance.
912
- :return: An converted AIL block.
939
+ :return: A converted AIL block.
913
940
  :rtype: ailment.Block
914
941
  """
915
942
 
916
943
  if type(block_node) is not BlockNode:
917
944
  return block_node
918
945
 
946
+ if block_node.size == 0:
947
+ return ailment.Block(block_node.addr, 0, statements=[])
948
+
919
949
  block = self.project.factory.block(block_node.addr, block_node.size, cross_insn_opt=False)
920
950
  if block.vex.jumpkind not in {"Ijk_Call", "Ijk_Boring", "Ijk_Ret"} and not block.vex.jumpkind.startswith(
921
951
  "Ijk_Sys"
@@ -66,6 +66,7 @@ class Decompiler(Analysis):
66
66
  decompile=True,
67
67
  regen_clinic=True,
68
68
  inline_functions=frozenset(),
69
+ desired_variables=frozenset(),
69
70
  update_memory_data: bool = True,
70
71
  generate_code: bool = True,
71
72
  use_cache: bool = True,
@@ -103,6 +104,7 @@ class Decompiler(Analysis):
103
104
  self._update_memory_data = update_memory_data
104
105
  self._generate_code = generate_code
105
106
  self._inline_functions = inline_functions
107
+ self._desired_variables = desired_variables
106
108
  self._cache_parameters = (
107
109
  {
108
110
  "cfg": self._cfg,
@@ -118,6 +120,7 @@ class Decompiler(Analysis):
118
120
  "ite_exprs": self._ite_exprs,
119
121
  "binop_operators": self._binop_operators,
120
122
  "inline_functions": self._inline_functions,
123
+ "desired_variables": self._desired_variables,
121
124
  }
122
125
  if use_cache
123
126
  else None
@@ -226,6 +229,7 @@ class Decompiler(Analysis):
226
229
  cache=cache,
227
230
  progress_callback=progress_callback,
228
231
  inline_functions=self._inline_functions,
232
+ desired_variables=self._desired_variables,
229
233
  optimization_scratch=self._optimization_scratch,
230
234
  **self.options_to_params(self.options_by_class["clinic"]),
231
235
  )
@@ -26,6 +26,7 @@ from .cross_jump_reverter import CrossJumpReverter
26
26
  from .code_motion import CodeMotionOptimization
27
27
  from .switch_default_case_duplicator import SwitchDefaultCaseDuplicator
28
28
  from .deadblock_remover import DeadblockRemover
29
+ from .tag_slicer import TagSlicer
29
30
  from .inlined_string_transformation_simplifier import InlinedStringTransformationSimplifier
30
31
  from .const_prop_reverter import ConstPropOptReverter
31
32
  from .call_stmt_rewriter import CallStatementRewriter
@@ -59,6 +60,7 @@ ALL_OPTIMIZATION_PASSES = [
59
60
  FlipBooleanCmp,
60
61
  InlinedStringTransformationSimplifier,
61
62
  CallStatementRewriter,
63
+ TagSlicer,
62
64
  ]
63
65
 
64
66
  # these passes may duplicate code to remove gotos or improve the structure of the graph
@@ -118,6 +120,7 @@ __all__ = (
118
120
  "ConstPropOptReverter",
119
121
  "CallStatementRewriter",
120
122
  "DuplicationReverter",
123
+ "TagSlicer",
121
124
  "ALL_OPTIMIZATION_PASSES",
122
125
  "DUPLICATING_OPTS",
123
126
  "CONDENSING_OPTS",
@@ -240,6 +240,12 @@ class InlinedStringTransformationAILEngine(SimEngineLightAILMixin):
240
240
  return None
241
241
 
242
242
  def _handle_Neg(self, expr: UnaryOp):
243
+ v = self._expr(expr.operand)
244
+ if isinstance(v, claripy.ast.Bits):
245
+ return -v
246
+ return None
247
+
248
+ def _handle_BitwiseNeg(self, expr: UnaryOp):
243
249
  v = self._expr(expr.operand)
244
250
  if isinstance(v, claripy.ast.Bits):
245
251
  return ~v
@@ -0,0 +1,41 @@
1
+ # pylint:disable=too-many-boolean-expressions
2
+ from __future__ import annotations
3
+ import logging
4
+
5
+ from ailment.statement import ConditionalJump, Jump, Label
6
+ from .optimization_pass import OptimizationPass, OptimizationPassStage
7
+
8
+
9
+ _l = logging.getLogger(name=__name__)
10
+
11
+
12
+ class TagSlicer(OptimizationPass):
13
+ """
14
+ Removes unmarked statements from the graph.
15
+ """
16
+
17
+ ARCHES = None
18
+ PLATFORMS = None
19
+ STAGE = OptimizationPassStage.AFTER_VARIABLE_RECOVERY
20
+ NAME = "Remove unmarked statements from the graph."
21
+ DESCRIPTION = __doc__.strip()
22
+
23
+ def __init__(self, func, **kwargs):
24
+ super().__init__(func, **kwargs)
25
+ self.analyze()
26
+
27
+ def _check(self):
28
+ return True, {}
29
+
30
+ def _analyze(self, cache=None):
31
+ for n in self._graph.nodes():
32
+ for i, s in enumerate(n.statements):
33
+ if isinstance(s, (ConditionalJump, Jump, Label)):
34
+ continue
35
+ if not s.tags.get("keep_in_slice", False):
36
+ n.statements[i] = None
37
+
38
+ for n in self._graph.nodes():
39
+ n.statements = [s for s in n.statements if s is not None]
40
+
41
+ self.out_graph = self._graph
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
  from ailment.expression import Load, Const
3
- from cle.backends import Blob
3
+ from cle.backends import Blob, Hex
4
4
 
5
5
  from .base import PeepholeOptimizationExprBase
6
6
 
@@ -32,7 +32,7 @@ class ConstantDereferences(PeepholeOptimizationExprBase):
32
32
 
33
33
  # is it loading from a blob?
34
34
  obj = self.project.loader.find_object_containing(expr.addr.value)
35
- if obj is not None and isinstance(obj, Blob):
35
+ if obj is not None and isinstance(obj, (Blob, Hex)):
36
36
  # do we know the value that it's reading?
37
37
  try:
38
38
  val = self.project.loader.memory.unpack_word(expr.addr.value, size=self.project.arch.bytes)
@@ -0,0 +1,137 @@
1
+ # pylint:disable=missing-class-docstring
2
+ from __future__ import annotations
3
+ import logging
4
+ from typing import TYPE_CHECKING
5
+ from collections import defaultdict
6
+ from dataclasses import dataclass
7
+
8
+ from sortedcontainers import SortedDict
9
+
10
+ from angr.analyses import Analysis, AnalysesHub
11
+ from angr.utils.bits import ffs
12
+
13
+ if TYPE_CHECKING:
14
+ from angr.knowledge_plugins import Function
15
+
16
+
17
+ log = logging.getLogger(__name__)
18
+
19
+
20
+ class OverlappingFunctionsAnalysis(Analysis):
21
+ """
22
+ Identify functions with interleaved blocks.
23
+ """
24
+
25
+ overlapping_functions: dict[int, list[int]]
26
+
27
+ def __init__(self):
28
+ self.overlapping_functions = defaultdict(list)
29
+ addr_to_func_max_addr = SortedDict()
30
+
31
+ for func in self.project.kb.functions.values():
32
+ if func.is_alignment:
33
+ continue
34
+ func_max_addr = max((block.addr + block.size) for block in func.blocks)
35
+ addr_to_func_max_addr[func.addr] = (func, func_max_addr)
36
+
37
+ for idx, (addr, (func, max_addr)) in enumerate(addr_to_func_max_addr.items()):
38
+ for other_addr in addr_to_func_max_addr.islice(idx + 1):
39
+ if other_addr >= max_addr:
40
+ break
41
+
42
+ self.overlapping_functions[addr].append(other_addr)
43
+
44
+
45
+ class FunctionAlignmentAnalysis(Analysis):
46
+ """
47
+ Determine typical function alignment
48
+ """
49
+
50
+ alignment: int | None
51
+
52
+ def __init__(self):
53
+ self.alignment = None
54
+
55
+ if len(self.project.kb.functions) == 0:
56
+ if self.project.kb.cfgs.get_most_accurate() is None:
57
+ log.warning("Please run CFGFast analysis first, to identify functions")
58
+ return
59
+
60
+ alignment_bins = defaultdict(int)
61
+ count = 0
62
+ for func in self.project.kb.functions.values():
63
+ if not (func.is_alignment or func.is_plt or func.is_simprocedure):
64
+ alignment_bins[ffs(func.addr)] += 1
65
+ count += 1
66
+
67
+ # FIXME: Higher alignment values will be naturally aligned
68
+
69
+ typical_alignment = max(alignment_bins, key=lambda k: alignment_bins[k])
70
+ if count > 10 and alignment_bins[typical_alignment] >= count / 4: # XXX: cutoff
71
+ self.alignment = 1 << max(typical_alignment, 0)
72
+ log.debug("Function alignment appears to be %d bytes", self.alignment)
73
+
74
+
75
+ @dataclass
76
+ class AtypicallyAlignedFunction:
77
+ function: Function
78
+ expected_alignment: int
79
+
80
+
81
+ @dataclass
82
+ class PatchedOutFunctionality:
83
+ patched_function: Function
84
+ patched_out_function: Function
85
+
86
+
87
+ class PatchFinderAnalysis(Analysis):
88
+ """
89
+ Looks for binary patches using some basic heuristics:
90
+ - Looking for interleaved functions
91
+ - Looking for unaligned functions
92
+ """
93
+
94
+ # FIXME: Possible additional heuristics:
95
+ # - Jumps out to end of function, then back
96
+ # - Looking for patch jumps, e.g. push <addr>; ret
97
+ # - Looking for instruction partials broken by a patch (nodecode)
98
+ # - Unusual stack manipulation
99
+
100
+ atypical_alignments: list[Function]
101
+ possibly_patched_out: list[PatchedOutFunctionality]
102
+
103
+ def __init__(self):
104
+ self.atypical_alignments = []
105
+ self.possibly_patched_out = []
106
+
107
+ if len(self.project.kb.functions) == 0:
108
+ if self.project.kb.cfgs.get_most_accurate() is None:
109
+ log.warning("Please run CFGFast analysis first, to identify functions")
110
+ return
111
+
112
+ # In CFGFast with scanning enabled, a function may be created from unreachable blocks within another function.
113
+ # Search for interleaved/overlapping functions to identify possible patches.
114
+ overlapping_functions = self.project.analyses.OverlappingFunctions().overlapping_functions
115
+ for addr, overlapping_func_addrs in overlapping_functions.items():
116
+ func = self.project.kb.functions[addr]
117
+
118
+ # Are the overlapping functions reachable?
119
+ for overlapping_addr in overlapping_func_addrs:
120
+ overlapping_func = self.project.kb.functions[overlapping_addr]
121
+ if self.project.kb.callgraph.in_degree(overlapping_addr) == 0:
122
+ self.possibly_patched_out.append(PatchedOutFunctionality(func, overlapping_func))
123
+ # FIXME: What does the patch do?
124
+
125
+ # Look for unaligned functions
126
+ expected_alignment = self.project.analyses.FunctionAlignment().alignment
127
+ if expected_alignment is not None and expected_alignment > self.project.arch.instruction_alignment:
128
+ for func in self.project.kb.functions.values():
129
+ if not (func.is_alignment or func.is_plt or func.is_simprocedure) and func.addr & (
130
+ expected_alignment - 1
131
+ ):
132
+ self.atypical_alignments.append(AtypicallyAlignedFunction(func, expected_alignment))
133
+
134
+
135
+ AnalysesHub.register_default("OverlappingFunctions", OverlappingFunctionsAnalysis)
136
+ AnalysesHub.register_default("FunctionAlignment", FunctionAlignmentAnalysis)
137
+ AnalysesHub.register_default("PatchFinder", PatchFinderAnalysis)