snikrflow 0.1.0__py3-none-any.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.
Files changed (47) hide show
  1. snikrflow/__init__.py +0 -0
  2. snikrflow/analysis/__init__.py +0 -0
  3. snikrflow/analysis/backends/__init__.py +0 -0
  4. snikrflow/analysis/backends/angr/__init__.py +0 -0
  5. snikrflow/analysis/backends/angr/audit.py +145 -0
  6. snikrflow/analysis/backends/angr/callgraph.py +40 -0
  7. snikrflow/analysis/backends/angr/cfg.py +49 -0
  8. snikrflow/analysis/backends/angr/dataflow.py +205 -0
  9. snikrflow/analysis/backends/angr/functions.py +99 -0
  10. snikrflow/analysis/backends/angr/jni.py +45 -0
  11. snikrflow/analysis/backends/angr/reachability.py +84 -0
  12. snikrflow/analysis/backends/angr/sinks.py +74 -0
  13. snikrflow/analysis/backends/angr/symbolic.py +152 -0
  14. snikrflow/analysis/interfaces/__init__.py +0 -0
  15. snikrflow/analysis/interfaces/apk_audit.py +97 -0
  16. snikrflow/analysis/interfaces/audit.py +14 -0
  17. snikrflow/analysis/interfaces/callgraph.py +13 -0
  18. snikrflow/analysis/interfaces/cfg.py +9 -0
  19. snikrflow/analysis/interfaces/dataflow.py +31 -0
  20. snikrflow/analysis/interfaces/functions.py +20 -0
  21. snikrflow/analysis/interfaces/jni.py +23 -0
  22. snikrflow/analysis/interfaces/reachability.py +14 -0
  23. snikrflow/analysis/interfaces/sinks.py +22 -0
  24. snikrflow/analysis/interfaces/symbolic.py +30 -0
  25. snikrflow/android/__init__.py +0 -0
  26. snikrflow/android/apk.py +78 -0
  27. snikrflow/cli.py +590 -0
  28. snikrflow/core/__init__.py +0 -0
  29. snikrflow/core/exceptions.py +36 -0
  30. snikrflow/core/models.py +326 -0
  31. snikrflow/elf/__init__.py +0 -0
  32. snikrflow/elf/jni.py +54 -0
  33. snikrflow/elf/loader.py +105 -0
  34. snikrflow/elf/metadata.py +84 -0
  35. snikrflow/reporting/__init__.py +0 -0
  36. snikrflow/reporting/sarif.py +122 -0
  37. snikrflow/reporting/terminal.py +412 -0
  38. snikrflow/sinks/__init__.py +0 -0
  39. snikrflow/sinks/database.py +30 -0
  40. snikrflow/sinks/signatures.py +40 -0
  41. snikrflow/utils/__init__.py +0 -0
  42. snikrflow-0.1.0.dist-info/METADATA +240 -0
  43. snikrflow-0.1.0.dist-info/RECORD +47 -0
  44. snikrflow-0.1.0.dist-info/WHEEL +5 -0
  45. snikrflow-0.1.0.dist-info/entry_points.txt +2 -0
  46. snikrflow-0.1.0.dist-info/licenses/LICENSE +21 -0
  47. snikrflow-0.1.0.dist-info/top_level.txt +1 -0
snikrflow/__init__.py ADDED
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,145 @@
1
+ """
2
+ SnikrFlow angr backend -- audit orchestration.
3
+
4
+ Combines every prior analysis stage into one pipeline, per the doc's
5
+ performance philosophy: cheap triage on everything, expensive symbolic/
6
+ data-flow analysis only on the most promising candidates.
7
+
8
+ Pipeline:
9
+ 1. Discover functions, JNI candidates, sinks (cheap -- one CFGFast run,
10
+ reused across all three thanks to the caching guard in functions.py).
11
+ 2. For every (JNI entry, sink) pair, check STATIC call-graph
12
+ reachability (cheap -- no symbolic execution).
13
+ 3. Rank the statically-reachable pairs by call-chain length (shorter =
14
+ more direct = higher priority) and take the top N for deep analysis.
15
+ 4. Run data-flow analysis (symbolic, bounded by a short per-pair
16
+ timeout) ONLY on that top-N subset.
17
+ 5. Score risk transparently (see _score_risk) and sort findings by risk.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from typing import Optional
23
+
24
+ from snikrflow.analysis.backends.angr.dataflow import check_data_flow
25
+ from snikrflow.analysis.backends.angr.functions import discover_functions
26
+ from snikrflow.analysis.backends.angr.jni import discover_jni_functions
27
+ from snikrflow.analysis.backends.angr.reachability import check_static_reachability
28
+ from snikrflow.analysis.backends.angr.sinks import discover_sinks
29
+ from snikrflow.core.models import (
30
+ AnalysisFinding,
31
+ AuditResult,
32
+ DataFlowStatus,
33
+ ReachabilityType,
34
+ RiskLevel,
35
+ )
36
+ from snikrflow.elf.loader import LoadedBinary
37
+ from snikrflow.elf.metadata import extract_binary_info
38
+
39
+ # Cap on how many statically-reachable pairs get the expensive data-flow
40
+ # treatment. Keeps `audit` bounded on binaries with many JNI x sink
41
+ # combinations -- deep analysis is a scarce resource, spent on the most
42
+ # direct (shortest) paths first.
43
+ MAX_DEEP_ANALYSIS_PAIRS = 5
44
+ DEFAULT_DEEP_TIMEOUT_SECONDS = 30
45
+
46
+ # Rough, documented, and DELIBERATELY simple severity weighting per sink
47
+ # category. This is a transparent prioritization heuristic, not a
48
+ # vulnerability database -- edit freely.
49
+ _CATEGORY_SEVERITY = {
50
+ "process_execution": 3,
51
+ "dynamic_loading": 3,
52
+ "memory": 2,
53
+ "networking": 2,
54
+ "filesystem": 1,
55
+ }
56
+
57
+
58
+ def _score_risk(
59
+ reachability_type: ReachabilityType,
60
+ data_flow_status: Optional[DataFlowStatus],
61
+ category: str,
62
+ ) -> RiskLevel:
63
+ severity = _CATEGORY_SEVERITY.get(category, 1)
64
+
65
+ if reachability_type != ReachabilityType.STATICALLY_REACHABLE:
66
+ return RiskLevel.LOW
67
+
68
+ if data_flow_status == DataFlowStatus.POSSIBLE:
69
+ return RiskLevel.CRITICAL if severity >= 3 else RiskLevel.HIGH
70
+
71
+ # Reachable via call graph, but data flow wasn't confirmed possible
72
+ # (either not checked, or checked and came back inconclusive).
73
+ return RiskLevel.HIGH if severity >= 3 else RiskLevel.MEDIUM
74
+
75
+
76
+ def run_audit(binary: LoadedBinary, deep_timeout_seconds: int = DEFAULT_DEEP_TIMEOUT_SECONDS) -> AuditResult:
77
+ binary_info = extract_binary_info(binary)
78
+
79
+ all_functions = discover_functions(binary) # runs CFGFast once
80
+ jni_functions = discover_jni_functions(binary) # reuses it
81
+ sinks = discover_sinks(binary) # reuses it
82
+
83
+ # Step 1: cheap static reachability across every JNI x sink pair.
84
+ reachable_pairs = []
85
+ for jni_func in jni_functions:
86
+ for sink in sinks:
87
+ path = check_static_reachability(binary, jni_func.address, sink.address)
88
+ if path.reachable and path.reachability_type == ReachabilityType.STATICALLY_REACHABLE:
89
+ reachable_pairs.append((jni_func, sink, path))
90
+
91
+ # Step 2: prioritize shortest (most direct) call chains for deep analysis.
92
+ reachable_pairs.sort(key=lambda item: len(item[2].function_path))
93
+ deep_pairs = reachable_pairs[:MAX_DEEP_ANALYSIS_PAIRS]
94
+ deep_keys = {(j.address, s.address) for j, s, _ in deep_pairs}
95
+
96
+ # Step 3: build findings, running data-flow only on the selected subset.
97
+ findings: list[AnalysisFinding] = []
98
+ for jni_func, sink, path in reachable_pairs:
99
+ data_flow_status: Optional[DataFlowStatus] = None
100
+ confidence = path.confidence
101
+ notes: list[str] = [path.note] if path.note else []
102
+
103
+ if (jni_func.address, sink.address) in deep_keys:
104
+ try:
105
+ df_result = check_data_flow(
106
+ binary, jni_func.address, sink.address,
107
+ timeout_seconds=deep_timeout_seconds,
108
+ )
109
+ data_flow_status = df_result.status
110
+ confidence = df_result.confidence
111
+ if df_result.note:
112
+ notes.append(df_result.note)
113
+ except Exception as e:
114
+ notes.append(f"Data-flow analysis failed: {e}")
115
+
116
+ risk = _score_risk(path.reachability_type, data_flow_status, sink.category)
117
+
118
+ findings.append(
119
+ AnalysisFinding(
120
+ title=f"JNI -> {sink.category}",
121
+ source_address=jni_func.address,
122
+ source_name=jni_func.name,
123
+ sink_address=sink.address,
124
+ sink_name=sink.name,
125
+ sink_category=sink.category,
126
+ reachability_type=path.reachability_type,
127
+ data_flow_status=data_flow_status,
128
+ risk_level=risk,
129
+ confidence=confidence,
130
+ notes=notes,
131
+ )
132
+ )
133
+
134
+ risk_order = {RiskLevel.CRITICAL: 0, RiskLevel.HIGH: 1, RiskLevel.MEDIUM: 2, RiskLevel.LOW: 3}
135
+ findings.sort(key=lambda f: risk_order.get(f.risk_level, 4))
136
+
137
+ return AuditResult(
138
+ binary_info=binary_info,
139
+ function_count=len(all_functions),
140
+ jni_count=len(jni_functions),
141
+ sink_count=len(sinks),
142
+ interesting_path_count=len(reachable_pairs),
143
+ symbolic_analysis_count=len(deep_pairs),
144
+ findings=findings,
145
+ )
@@ -0,0 +1,40 @@
1
+ """
2
+ SnikrFlow angr backend -- callers/callees lookup.
3
+
4
+ Reuses discover_functions() (runs CFGFast, builds kb.callgraph) so
5
+ callers/callees work off the same function set as `functions`/`jni`/
6
+ `sinks` -- no separate, possibly-inconsistent analysis pass.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from snikrflow.analysis.backends.angr.functions import discover_functions
12
+ from snikrflow.core.models import Function
13
+ from snikrflow.elf.loader import LoadedBinary
14
+
15
+
16
+ def _resolve(binary: LoadedBinary):
17
+ all_functions = discover_functions(binary)
18
+ by_addr = {f.address: f for f in all_functions}
19
+ callgraph = binary.project.kb.callgraph
20
+ return by_addr, callgraph
21
+
22
+
23
+ def find_callers(binary: LoadedBinary, address: int) -> list[Function]:
24
+ """Direct callers of the function at `address`. Indirect/unresolved calls are not included."""
25
+ by_addr, callgraph = _resolve(binary)
26
+ try:
27
+ caller_addrs = sorted(callgraph.predecessors(address))
28
+ except Exception:
29
+ caller_addrs = []
30
+ return [by_addr[a] for a in caller_addrs if a in by_addr]
31
+
32
+
33
+ def find_callees(binary: LoadedBinary, address: int) -> list[Function]:
34
+ """Direct callees of the function at `address`. Indirect/unresolved calls are not included."""
35
+ by_addr, callgraph = _resolve(binary)
36
+ try:
37
+ callee_addrs = sorted(callgraph.successors(address))
38
+ except Exception:
39
+ callee_addrs = []
40
+ return [by_addr[a] for a in callee_addrs if a in by_addr]
@@ -0,0 +1,49 @@
1
+ """
2
+ SnikrFlow angr backend -- CFG analysis.
3
+
4
+ Supports CFGFast (default, scalable) and CFGEmulated (deeper, slower).
5
+ CFGEmulated has been deprecated/removed in some recent angr versions --
6
+ if it's unavailable here, we fail gracefully with a clear message rather
7
+ than crashing, per SnikrFlow's error-handling philosophy.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from snikrflow.core.exceptions import AnalysisIncompleteError
13
+ from snikrflow.core.models import CFGSummary
14
+ from snikrflow.elf.loader import LoadedBinary
15
+
16
+
17
+ def build_cfg_summary(binary: LoadedBinary, mode: str = "fast") -> CFGSummary:
18
+ project = binary.project
19
+
20
+ if mode == "fast":
21
+ try:
22
+ cfg = project.analyses.CFGFast(normalize=True)
23
+ except Exception as e:
24
+ raise AnalysisIncompleteError(f"CFGFast failed: {e}") from e
25
+
26
+ elif mode == "emulated":
27
+ try:
28
+ cfg = project.analyses.CFGEmulated(keep_state=False)
29
+ except AttributeError as e:
30
+ raise AnalysisIncompleteError(
31
+ "CFGEmulated is not available in this angr version. "
32
+ "Use --cfg-mode fast instead."
33
+ ) from e
34
+ except Exception as e:
35
+ raise AnalysisIncompleteError(f"CFGEmulated failed: {e}") from e
36
+
37
+ else:
38
+ raise AnalysisIncompleteError(
39
+ f"Unknown CFG mode '{mode}'. Use 'fast' or 'emulated'."
40
+ )
41
+
42
+ graph = cfg.graph
43
+
44
+ return CFGSummary(
45
+ function_count=len(project.kb.functions),
46
+ basic_block_count=graph.number_of_nodes(),
47
+ edge_count=graph.number_of_edges(),
48
+ mode=mode,
49
+ )
@@ -0,0 +1,205 @@
1
+ """
2
+ SnikrFlow angr backend -- data flow (heuristic taint) analysis.
3
+
4
+ Reuses the same symbolic-buffer setup as symbolic.py's run_symbolic_search,
5
+ but instead of just checking whether the sink's address is reached, we
6
+ inspect the VALUE sitting in the sink's own first argument register at
7
+ the moment execution enters it. If that value's Claripy expression
8
+ contains our "sym_input" symbolic variable, the source's data possibly
9
+ flows into what the sink operates on -- not just "control flow got
10
+ there," but "the tainted value got there."
11
+
12
+ This is a HEURISTIC, single-argument-position check, not a sound full
13
+ taint analysis (per the doc's terminology rules). It can only ever
14
+ report POSSIBLE or INCONCLUSIVE, never CONFIRMED.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import time
20
+ from typing import Optional
21
+
22
+ import claripy
23
+
24
+ from snikrflow.analysis.backends.angr.symbolic import (
25
+ DEFAULT_INPUT_SIZE,
26
+ DEFAULT_MAX_ACTIVE_STATES,
27
+ DEFAULT_MAX_STEPS,
28
+ DEFAULT_TIMEOUT_SECONDS,
29
+ )
30
+ from snikrflow.core.exceptions import AnalysisIncompleteError
31
+ from snikrflow.core.models import Confidence, DataFlowResult, DataFlowStatus
32
+ from snikrflow.elf.loader import LoadedBinary
33
+
34
+
35
+ def check_data_flow(
36
+ binary: LoadedBinary,
37
+ source: int,
38
+ sink: int,
39
+ avoid: Optional[list[int]] = None,
40
+ input_size: int = DEFAULT_INPUT_SIZE,
41
+ max_active_states: int = DEFAULT_MAX_ACTIVE_STATES,
42
+ max_steps: int = DEFAULT_MAX_STEPS,
43
+ timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS,
44
+ ) -> DataFlowResult:
45
+ project = binary.project
46
+ avoid = avoid or []
47
+
48
+ sym_input = claripy.BVS("sym_input", input_size * 8)
49
+
50
+ try:
51
+ state = project.factory.call_state(source, 0)
52
+ except Exception as e:
53
+ raise AnalysisIncompleteError(
54
+ f"Failed to build a call state at {hex(source)}: {e}"
55
+ ) from e
56
+
57
+ try:
58
+ buf_addr = state.heap.allocate(input_size)
59
+ state.memory.store(buf_addr, sym_input)
60
+ cc = project.factory.cc()
61
+ arg_reg = cc.ARG_REGS[0]
62
+ setattr(state.regs, arg_reg, buf_addr)
63
+ except Exception as e:
64
+ raise AnalysisIncompleteError(
65
+ f"Failed to set up the symbolic argument buffer: {e}"
66
+ ) from e
67
+
68
+ simgr = project.factory.simgr(state)
69
+
70
+ start_time = time.time()
71
+ steps_taken = 0
72
+ stop_reason = "unknown"
73
+
74
+ while simgr.active:
75
+ if time.time() - start_time > timeout_seconds:
76
+ stop_reason = "timeout"
77
+ break
78
+ if steps_taken >= max_steps:
79
+ stop_reason = "max_steps_reached"
80
+ break
81
+
82
+ try:
83
+ simgr.step()
84
+ except Exception:
85
+ stop_reason = "step_error"
86
+ break
87
+ steps_taken += 1
88
+
89
+ if avoid:
90
+ simgr.move(
91
+ from_stash="active", to_stash="avoided",
92
+ filter_func=lambda s: s.addr in avoid,
93
+ )
94
+
95
+ simgr.move(
96
+ from_stash="active", to_stash="found",
97
+ filter_func=lambda s: s.addr == sink,
98
+ )
99
+
100
+ if simgr.found:
101
+ stop_reason = "sink_reached"
102
+ break
103
+
104
+ if len(simgr.active) > max_active_states:
105
+ simgr.active = simgr.active[:max_active_states]
106
+
107
+ if stop_reason == "unknown":
108
+ if simgr.found:
109
+ stop_reason = "sink_reached"
110
+ elif not simgr.active:
111
+ stop_reason = "no_active_states"
112
+
113
+ if not simgr.found:
114
+ return DataFlowResult(
115
+ source=source, sink=sink,
116
+ status=DataFlowStatus.INCONCLUSIVE,
117
+ confidence=Confidence.INCONCLUSIVE,
118
+ states_explored=steps_taken, stopped_reason=stop_reason,
119
+ note="Sink was not reached within the configured limits. This is inconclusive, not proof of safety.",
120
+ )
121
+
122
+ found_state = simgr.found[0]
123
+
124
+ if not found_state.solver.satisfiable():
125
+ return DataFlowResult(
126
+ source=source, sink=sink,
127
+ status=DataFlowStatus.INCONCLUSIVE,
128
+ confidence=Confidence.LOW,
129
+ states_explored=steps_taken, stopped_reason=stop_reason,
130
+ note="Sink was reached, but the accumulated path constraints are UNSAT.",
131
+ )
132
+
133
+ # Check the sink's first few argument registers for taint. Two checks
134
+ # per argument, since attacker data can show up either way:
135
+ # 1. The register VALUE itself is symbolic (integer/value args, or
136
+ # a pointer computed via symbolic arithmetic).
137
+ # 2. The register holds a concrete-ish pointer to memory that
138
+ # itself contains tainted bytes (buffer/string args -- e.g.
139
+ # memcpy's `src` -- where the taint lives in the pointed-to
140
+ # memory, not the pointer value itself).
141
+ # We check multiple argument positions (not just arg 1) because for
142
+ # many real sinks (memcpy, strcpy, sprintf...) the tainted data is
143
+ # arg 2, not arg 1.
144
+ try:
145
+ cc = project.factory.cc()
146
+ arg_regs = cc.ARG_REGS[:4]
147
+ except Exception as e:
148
+ return DataFlowResult(
149
+ source=source, sink=sink,
150
+ status=DataFlowStatus.INCONCLUSIVE,
151
+ confidence=Confidence.INCONCLUSIVE,
152
+ states_explored=steps_taken, stopped_reason=stop_reason,
153
+ note=f"Sink was reached but its calling convention could not be resolved: {e}",
154
+ )
155
+
156
+ tainted = False
157
+ tainted_arg_index: Optional[int] = None
158
+
159
+ for idx, reg_name in enumerate(arg_regs):
160
+ try:
161
+ reg_value = getattr(found_state.regs, reg_name)
162
+ except Exception:
163
+ continue
164
+
165
+ if any(v.startswith("sym_input") for v in reg_value.variables):
166
+ tainted, tainted_arg_index = True, idx
167
+ break
168
+
169
+ if not reg_value.symbolic:
170
+ try:
171
+ addr = found_state.solver.eval(reg_value)
172
+ probe = found_state.memory.load(addr, 16)
173
+ if any(v.startswith("sym_input") for v in probe.variables):
174
+ tainted, tainted_arg_index = True, idx
175
+ break
176
+ except Exception:
177
+ continue
178
+
179
+ if tainted:
180
+ return DataFlowResult(
181
+ source=source, sink=sink,
182
+ status=DataFlowStatus.POSSIBLE,
183
+ confidence=Confidence.MEDIUM,
184
+ states_explored=steps_taken, stopped_reason=stop_reason,
185
+ note=(
186
+ f"Sink argument #{tainted_arg_index + 1} appears tainted by the "
187
+ "source's symbolic input (checked both the argument value "
188
+ "itself and, for pointer arguments, the memory it points "
189
+ "to). Heuristic result, not a sound full taint analysis."
190
+ ),
191
+ )
192
+
193
+ return DataFlowResult(
194
+ source=source, sink=sink,
195
+ status=DataFlowStatus.INCONCLUSIVE,
196
+ confidence=Confidence.LOW,
197
+ states_explored=steps_taken, stopped_reason=stop_reason,
198
+ note=(
199
+ "The sink was reached, but none of its first 4 arguments "
200
+ "(register value or pointed-to memory) appear derived from "
201
+ "the source's symbolic input. Note: a sink that takes no "
202
+ "arguments (a bare marker/flag function) will always show "
203
+ "this result -- there's nothing for it to receive."
204
+ ),
205
+ )
@@ -0,0 +1,99 @@
1
+ """
2
+ SnikrFlow angr backend -- function discovery.
3
+
4
+ This is one of the few modules allowed to touch angr's analysis objects
5
+ directly (project.analyses.CFGFast, project.kb.functions). It translates
6
+ angr's Function/CFG knowledge-base objects into SnikrFlow-owned Function
7
+ models before handing anything back up the stack.
8
+
9
+ We run CFGFast rather than CFGEmulated here -- it's the scalable option
10
+ suitable for an initial triage pass. Deeper/emulated CFG analysis is a
11
+ later milestone with its own --cfg-mode flag.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from snikrflow.core.exceptions import AnalysisIncompleteError
17
+ from snikrflow.core.models import Function, FunctionSource
18
+ from snikrflow.elf.loader import LoadedBinary
19
+
20
+ _JNI_PREFIXES = ("Java_",)
21
+ _JNI_EXACT = ("JNI_OnLoad", "JNI_OnUnload")
22
+
23
+
24
+ def _looks_like_jni(name: str) -> bool:
25
+ if not name:
26
+ return False
27
+ if name in _JNI_EXACT:
28
+ return True
29
+ return name.startswith(_JNI_PREFIXES)
30
+
31
+
32
+ def discover_functions(binary: LoadedBinary) -> list[Function]:
33
+ project = binary.project
34
+ main_obj = project.loader.main_object
35
+
36
+ # Cross-reference lookup: address -> symbol, so we can tell whether an
37
+ # angr-discovered function also has a name/export/import status.
38
+ symbol_by_addr = {
39
+ sym.rebased_addr: sym for sym in main_obj.symbols if sym.rebased_addr
40
+ }
41
+
42
+ try:
43
+ if len(project.kb.functions) == 0:
44
+ project.analyses.CFGFast(normalize=True)
45
+ # else: CFGFast has already populated kb.functions/kb.callgraph on
46
+ # this project (e.g. an earlier call in the same process, such as
47
+ # `audit` chaining functions -> jni -> sinks) -- reuse it instead
48
+ # of re-running the full analysis.
49
+ except Exception as e:
50
+ raise AnalysisIncompleteError(f"CFGFast analysis failed: {e}") from e
51
+
52
+ functions: list[Function] = []
53
+
54
+ for addr, kb_func in project.kb.functions.items():
55
+ # angr creates synthetic placeholder functions in its own internal
56
+ # "extern" address space to model imported functions we don't have
57
+ # the real code for (since auto_load_libs=False). These aren't
58
+ # real locations inside the analyzed binary, and they show up as
59
+ # noise (e.g. appearing to call themselves) if not filtered out.
60
+ if not main_obj.contains_addr(addr):
61
+ continue
62
+
63
+ name = kb_func.name or f"sub_{addr:x}"
64
+ size = kb_func.size if kb_func.size else None
65
+
66
+ is_plt = bool(getattr(kb_func, "is_plt", False))
67
+ is_simprocedure = bool(getattr(kb_func, "is_simprocedure", False))
68
+
69
+ sym = symbol_by_addr.get(addr)
70
+ has_symbol = sym is not None
71
+ is_imported = bool(sym.is_import) if sym else is_simprocedure
72
+ is_exported = bool(getattr(sym, "is_export", False)) if sym else False
73
+
74
+ if is_plt:
75
+ source = FunctionSource.PLT_STUB
76
+ elif is_imported:
77
+ source = FunctionSource.IMPORT
78
+ elif has_symbol:
79
+ source = FunctionSource.SYMBOL_TABLE
80
+ else:
81
+ source = FunctionSource.ANGR_DISCOVERED
82
+
83
+ functions.append(
84
+ Function(
85
+ address=addr,
86
+ name=name,
87
+ size=size,
88
+ source=source,
89
+ has_symbol=has_symbol,
90
+ is_imported=is_imported,
91
+ is_exported=is_exported,
92
+ is_plt_stub=is_plt,
93
+ is_thunk=False, # thunk detection lands in a later milestone
94
+ is_jni_candidate=_looks_like_jni(name),
95
+ )
96
+ )
97
+
98
+ functions.sort(key=lambda f: f.address)
99
+ return functions
@@ -0,0 +1,45 @@
1
+ """
2
+ SnikrFlow angr backend -- JNI function discovery.
3
+
4
+ Reuses discover_functions() from functions.py (which runs CFGFast) rather
5
+ than re-running CFG analysis from scratch. CFGFast populates
6
+ project.kb.callgraph as a side effect, which we use here for callers/
7
+ callees -- so this stays cheap on top of an already-discovered function
8
+ set.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from snikrflow.analysis.backends.angr.functions import discover_functions
14
+ from snikrflow.core.models import Function
15
+ from snikrflow.elf.jni import demangle_jni_name
16
+ from snikrflow.elf.loader import LoadedBinary
17
+
18
+
19
+ def discover_jni_functions(binary: LoadedBinary) -> list[Function]:
20
+ all_functions = discover_functions(binary) # also builds kb.callgraph
21
+
22
+ callgraph = binary.project.kb.callgraph
23
+
24
+ jni_functions: list[Function] = []
25
+ for f in all_functions:
26
+ if not f.is_jni_candidate:
27
+ continue
28
+
29
+ java_class, java_method = demangle_jni_name(f.name)
30
+ f.likely_java_class = java_class
31
+ f.likely_java_method = java_method
32
+
33
+ try:
34
+ f.callers = sorted(callgraph.predecessors(f.address))
35
+ except Exception:
36
+ f.callers = []
37
+
38
+ try:
39
+ f.callees = sorted(callgraph.successors(f.address))
40
+ except Exception:
41
+ f.callees = []
42
+
43
+ jni_functions.append(f)
44
+
45
+ return jni_functions
@@ -0,0 +1,84 @@
1
+ """
2
+ SnikrFlow angr backend -- static reachability.
3
+
4
+ Static reachability here means call-graph reachability: is there a chain
5
+ of DIRECT calls from source to sink? This only sees direct calls -- it
6
+ does NOT track indirect calls (function pointers, vtables, etc.), so a
7
+ "not reachable" result is never proof of safety, only "no direct call
8
+ chain found." Symbolic/data-flow reachability (later milestones) goes
9
+ further; runtime verification goes further still. These are never
10
+ treated as equivalent, per SnikrFlow's terminology rules.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import networkx as nx
16
+
17
+ from snikrflow.analysis.backends.angr.functions import discover_functions
18
+ from snikrflow.core.models import AnalysisPath, Confidence, ReachabilityType
19
+ from snikrflow.elf.loader import LoadedBinary
20
+
21
+
22
+ def check_static_reachability(binary: LoadedBinary, source: int, sink: int) -> AnalysisPath:
23
+ all_functions = discover_functions(binary)
24
+ by_addr = {f.address: f for f in all_functions}
25
+ callgraph = binary.project.kb.callgraph
26
+
27
+ if source not in by_addr:
28
+ return AnalysisPath(
29
+ source=source, sink=sink, reachable=False,
30
+ reachability_type=ReachabilityType.INCONCLUSIVE,
31
+ confidence=Confidence.INCONCLUSIVE,
32
+ note=f"Source address {hex(source)} is not a known function start.",
33
+ )
34
+ if sink not in by_addr:
35
+ return AnalysisPath(
36
+ source=source, sink=sink, reachable=False,
37
+ reachability_type=ReachabilityType.INCONCLUSIVE,
38
+ confidence=Confidence.INCONCLUSIVE,
39
+ note=f"Sink address {hex(sink)} is not a known function start.",
40
+ )
41
+
42
+ if source == sink:
43
+ label = f"{by_addr[source].name} @ {hex(source)}"
44
+ return AnalysisPath(
45
+ source=source, sink=sink, reachable=True,
46
+ reachability_type=ReachabilityType.STATICALLY_REACHABLE,
47
+ function_path=[source], path_labels=[label],
48
+ confidence=Confidence.HIGH,
49
+ note="Source and sink are the same function.",
50
+ )
51
+
52
+ try:
53
+ path = nx.shortest_path(callgraph, source=source, target=sink)
54
+ except nx.NetworkXNoPath:
55
+ return AnalysisPath(
56
+ source=source, sink=sink, reachable=False,
57
+ reachability_type=ReachabilityType.NOT_REACHABLE,
58
+ confidence=Confidence.MEDIUM,
59
+ note=(
60
+ "No direct call-graph path found. This does NOT prove the "
61
+ "sink is unreachable -- indirect calls (function pointers, "
62
+ "vtables) aren't tracked by this analysis."
63
+ ),
64
+ )
65
+ except nx.NodeNotFound:
66
+ return AnalysisPath(
67
+ source=source, sink=sink, reachable=False,
68
+ reachability_type=ReachabilityType.INCONCLUSIVE,
69
+ confidence=Confidence.INCONCLUSIVE,
70
+ note="Source or sink address not present in the call graph.",
71
+ )
72
+
73
+ labels = [
74
+ f"{by_addr[a].name if a in by_addr else f'sub_{a:x}'} @ {hex(a)}"
75
+ for a in path
76
+ ]
77
+
78
+ return AnalysisPath(
79
+ source=source, sink=sink, reachable=True,
80
+ reachability_type=ReachabilityType.STATICALLY_REACHABLE,
81
+ function_path=path, path_labels=labels,
82
+ confidence=Confidence.MEDIUM,
83
+ note="Based on direct call-graph edges only; indirect/unresolved calls not tracked.",
84
+ )