1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
|
#!/usr/bin/env python3
"""s09 - grab a Ghidra address: did the firmware actually execute it?
Real scenario (docs/05-GHIDRA-BRIDGE.md, docs/13, docs/14 5; and
Handoff-vGPU-IO.md PART 91 ADDENDUM 9 cycle 24, 2026-10-01): a static
hypothesis (the GPFIFO GET encodes entry VA ``0x101000``) was only accepted
after the decoded status-queue fault showed the engine actually fetched
``0x40000`` because the GET field dropped the high VA bits (fix ``6f9cbfa``).
The static/dynamic join is exactly this: Ghidra owns addresses and
decompilation, risc-v-gsp-rm-debugger answers "did it execute here".
Analog: export a Ghidra-style ``names.tsv`` from the demo ELF *with a base
offset* (the classic program-address vs runtime-address trap), load it with
the right bias, hook the raw Ghidra address, trigger the path, and write the
resulting ``greeneye_hits.tsv`` annotation back for Ghidra.
"""
from __future__ import annotations
import os
try: # package import (tests) or direct script execution (run_all)
from . import _common as C
except ImportError:
import sys as _sys
_sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import _common as C
#: Pretend the Ghidra project was based at runtime - 0x1000 (the bias pitfall).
BIAS = 0x1000
def _ghidra_tsv(ge, out_path: str) -> dict:
image = ge.load_elf(C.DEMO_ELF)
rows = []
for symbol in image.symbols:
if not getattr(symbol, "is_function", False) or not symbol.name:
continue
program = symbol.value - BIAS
rows.append((program, symbol.name, symbol.size))
rows.sort()
with open(out_path, "w", encoding="utf-8") as fh:
for program, name, size in rows:
fh.write(f"{program:#x}\t{name}\tsize={size}\n")
return {"functions": len(rows), "bias": BIAS}
def run() -> dict:
import greeneye as ge
from greeneye.ghidra import export_hits
backend = C.choose_backend("qemu")
out = C.artifact_dir("s09_grabbing_a_ghidra_address")
session_dir = C.session_dir(out)
names_tsv = os.path.join(session_dir, "ghidra_names.tsv")
facts = _ghidra_tsv(ge, names_tsv)
runtime_rpc_fault = ge.load_elf(C.DEMO_ELF)
addr_rpc_fault = None
for symbol in runtime_rpc_fault.symbols:
if symbol.name == "rpc_fault":
addr_rpc_fault = symbol.value
if addr_rpc_fault is None:
raise C.DemoError("rpc_fault not found in demo ELF")
ghidra_addr = addr_rpc_fault - BIAS
commands = [
f'write Ghidra names.tsv from ELF (program = runtime - {BIAS:#x})',
f'SymbolDB.load_names_tsv(names, bias={BIAS:#x})',
f'hook(ghidra_addr {ghidra_addr:#x} + bias) # raw static address',
"trigger mailbox cmd=6; hits recorded; export_hits -> greeneye_hits.tsv",
]
scenario = C.scenario(
ids=[
"docs/05-GHIDRA-BRIDGE.md / docs/13: static/dynamic address join",
"Handoff-vGPU-IO.md PART 91 ADDENDUM 9 cycle 24 (2026-10-01): GET fault at 0x40000 vs hypothesis 0x101000",
"docs/14-ADVERSARIAL-INTEROP-GUIDE.md 5: program vs runtime addresses and bias",
],
baseline=(
"cycle 24's decoded status-queue fault ended a run of eight "
"supervised cycles (18-25, ~1 h 41 m); the static GET-encoding "
"hypothesis was unproven until a live PC/fault address existed"
),
projection=(
"for the real image the same names.tsv + bias + hook loop runs once "
"H2.2 exposes execution of the .fwimage; the bias arithmetic demoed "
"here is exactly what docs/05 warns about"
),
blocked=True,
device="H2.2 device model to execute the real .fwimage",
)
C.render_banner(
"s09_grabbing_a_ghidra_address",
"Ghidra gave an address: does the firmware execute exactly there, and can the hit be annotated back?",
"static/dynamic join for the GPFIFO GET fault (0x40000 vs 0x101000)",
scenario["static_baseline"],
commands,
)
session_dir = C.session_dir(out)
session = ge.launch(kernel=C.DEMO_ELF, backend=backend, record=session_dir, name="vd-s09")
try:
session.symbols.load_names_tsv(names_tsv, bias=BIAS)
ctrl = session.symbols.lookup("gsp_ctrl")
resolved = session.resolve(ghidra_addr + BIAS)
hits = []
session.hook(ghidra_addr + BIAS, on_enter=lambda hit: hits.append(hit.addr))
session.run(duration=0.25)
session.memory.write_u64(ctrl + C.CTRL_MAILBOX, 6)
session.run(duration=0.25)
hits_tsv = export_hits(session_dir, os.path.join(session_dir, "greeneye_hits.tsv"), symbols=session.symbols)
payload = {
"success": len(hits) >= 1 and resolved == addr_rpc_fault,
"backend": backend,
"ghidra_functions": facts["functions"],
"ghidra_names_tsv": os.path.relpath(names_tsv, C.TOOL_ROOT),
"bias": BIAS,
"ghidra_addr": f"{ghidra_addr:#x}",
"runtime_addr": f"{resolved:#x}",
"program_addr_used_without_bias": f"{ghidra_addr:#x}",
"hits": hits,
"hits_tsv": os.path.relpath(hits_tsv, C.TOOL_ROOT),
"question": "join a static Ghidra address to a runtime hit, with the bias handled explicitly",
"answer": (
f"Ghidra addr {ghidra_addr:#x} + bias {BIAS:#x} = runtime {resolved:#x}; "
f"the hook fired {len(hits)}x at that exact address; export_hits wrote "
"the annotated greeneye_hits.tsv - the static hypothesis is now a "
"recorded dynamic fact (and using the program address raw would miss by 0x1000)"
),
"success_criteria": {
"pass": "the biased address resolves to the real function and the hook records the hit; TSV annotates it",
"fail": "the hit lands at a different address or the TSV is empty",
},
"real_case": scenario,
}
with open(hits_tsv, "r", encoding="utf-8") as fh:
payload["greeneye_hits_tsv"] = fh.read()
C.write_json(os.path.join(out, "ghidra-join.json"), payload)
finally:
session.close()
return payload
def main() -> int:
return 0 if C.timed_demo(run, C.artifact_dir("s09_grabbing_a_ghidra_address"))["success"] else 1
if __name__ == "__main__":
raise SystemExit(main())
|