summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/docs/14-ADVERSARIAL-INTEROP-GUIDE.md
blob: d310f2223aabb2ae16156bd6a675393382751ca9 (plain)
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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# 14 — Firmware adversarial interoperability research guide (green-eye + Ghidra)

> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.

## 1. Purpose and scope

Open-IOV's hard problem is not writing the GSP emulation once; it is keeping
`open-iov-vfio` accurate while the firmware changes underneath it. New GSP
images, device pairings and architectures arrive stripped, signed and
hardware-coupled, and some actively resist analysis. This guide is the
practical companion to `GREEN-EYE-ROADMAP.md` H4: how to move from "the new
image does something different" to a reviewed product change with evidence.

Ghidra is the **static map**; green-eye is the **dynamic ground truth**;
recorded sessions are the **shared evidence**. Recipes use only the v0.1
surface, plus items explicitly labelled roadmap; see also docs/06-ACCURACY.md.

| Capability used here | State |
|---|---|
| `probe`/`record`/`analyze`/`compare`/`symbols`/`inject`/`ghidra`, hooks, watchpoints, snapshots, sessions, `export_hits` | working (v0.1) |
| Ghidra control bridge (`run_until`, breakpoints, hits) | working (v0.1) |
| Full TCG access trace (`qemu/greeneye_trace.c`) | source + optional build; not folded into sessions yet (R4) |
| Mutation harness (AFL++ class), device model, MCP | roadmap (R5, H2/R1, H7/R3) |

## 2. The adversary model (a moving target, not a malicious party)

This guide assumes no threat actor. The "adversary" is **unplanned firmware
change plus deliberate opacity**: new images on a hidden
schedule; stripped symbols, signed images and non-recoverable boot states;
anti-analysis behaviour (timing checks, self-consistency checks, hardware-
dependent branches) and **decoys** (code or data not on the live path); and
hardware coupling a simulator cannot yet exercise.

1. Any single run is an anecdote. A conclusion needs at least an A/B pair.
2. A static reading is a hypothesis. It becomes evidence only when execution
   reaches it in a recorded session.
3. Absence of a hit is a fact about *that* run at *that* fidelity (docs/06),
   not proof of dead code.
4. Every claim carries its fidelity level (L0–L3, docs/06) and provenance.

## 3. The joint workflow: Ghidra, green-eye, sessions

```
   question > Ghidra (static): decompile, dispatcher tables, symbols
            > green-eye (dynamic): same stimulus, hooks, watchpoints, trace
            > sessions + hits TSV: greeneye compare -> triage -> evidence pack
            > open-iov-vfio change (with evidence)
```

| Question | Ghidra answers | green-eye answers | Shared artifact |
|---|---|---|---|
| What is this code? | decompilation, call graph, xrefs | — | symbol DB (docs/05) |
| Did it execute here? | hypothesis from xrefs | `bp_hit`/`watch_hit` events | `greeneye_hits.tsv` |
| What changed between versions? | corpus/symbol diff | `compare` hit/watch/write deltas | symbol DB per version + report |
| What can this service touch? | object references | watchpoint/TCG access map | capability table |

References: docs/05-GHIDRA-BRIDGE.md, docs/13-GHIDRA-CONTROL.md, docs/07-PORTABILITY-PLAYBOOKS.md.

## 4. Method catalogue

Run all commands from `open-iov/debug/risc-v-gsp-rm-vm/`; angle-bracket
placeholders are task-supplied (never hardcode private paths in docs).

### 4.1 Semantic diff across firmware versions (H4.1)

Goal: separate "the code changed" from "the behaviour moved" before the
emulation is touched.

1. Build a symbol DB per version. `ghidra/ExportForGreenEye.java` emits
   `addr<TAB>name<TAB>size`, which `symbols export --index` accepts:

   ```bash
   ./greeneye.sh symbols export --index <new-export.tsv> --bias 0 -o symbols-new.json
   ./greeneye.sh symbols export --index <old-export.tsv> --bias 0 -o symbols-old.json
   ```

2. Probe each image for first-touch structure changes (docs/07 P1.2):

   ```bash
   ./greeneye.sh probe <old.fwimage> --symbols symbols-old.json --record
   ./greeneye.sh probe <new.fwimage> --symbols symbols-new.json --record
   ```

3. Run the **same stimulus** against the re-hosted build of each version. Put
   it in a script so it is identical across runs:

   ```python
   # run_stimulus.py -- ./greeneye.sh run run_stimulus.py --kernel <rehosted.elf>
   import greeneye as ge
   s = ge.launch(kernel="<rehosted.elf>", record=True, name="stim")
   ctrl = s.symbols.lookup("<control-symbol>")  # demo: gsp_ctrl
   s.watch(ctrl + 0x10, size=8, kind="w")
   s.run(duration=0.5)                          # settle/self-test
   s.memory.write_u64(ctrl + 0x20, 1)           # mailbox PING
   s.memory.write_u64(ctrl + 0x28, 0x1234)
   s.run(duration=0.5)
   s.note("stimulus: PING 0x1234")
   s.close()
   ```

   Demo control-block offsets are demo-specific; use the schema
   (`firmware/uproc_demo/gsp_ctrl.schema.json`) or SDK constants rather than
   hardcoding them (docs/12 §4).

4. Compare the sessions symbolically — breakpoint profile delta, watchpoint
   delta and written-page delta:

   ```bash
   ./greeneye.sh compare dumps/<old-run> dumps/<new-run> --symbols
   ```

5. Triage with a decision table rather than intuition:

   | Static code changed? | Hit/write profile changed? | Reading |
   |---|---|---|
   | no | no | unchanged on this stimulus |
   | no | yes | behaviour moved: inputs, ordering, environment or state |
   | yes | no | changed code off this stimulus path (or dead/decoy: §4.4) |
   | yes | yes | new logic on a live path — prioritise |
   | n/a | symbol never hit in either run | coverage gap: widen the stimulus first |

6. Annotate the result back into Ghidra (`greeneye_hits.tsv` +
   `MarkGreenEyeHits.java`). Behaviour that moved without a code change is
   usually a state/ordering fix for the emulation, not new logic.

### 4.2 Capability inference from watchpoints and TCG traces (H4.2)

Goal: derive what a service/engine actually reads and writes, then compare it
with the capability model the emulation assumes.

1. Instrument the object ranges the service owns (kinds `w`, `r`, `a`):

   ```python
   # capability_probe.py -- ./greeneye.sh run capability_probe.py --kernel <elf>
   import greeneye as ge
   s = ge.launch(kernel="<rehosted.elf>", record=True, name="caps")
   base = s.symbols.lookup("<object-symbol>")
   for off in (0x0, 0x10, 0x20, 0x28, 0x38, 0x40):
       for kind in ("r", "w"):
           s.watch(base + off, size=8, kind=kind,
                   callback=lambda h, k=kind: s.note("cap-" + k, pc=h.pc))
   s.run(duration=1.0)
   s.close()
   ```

2. For access coverage **between stops**, enable the TCG plugin with a PC
   filter for the service's load range:

   ```bash
   make -C qemu
   qemu-system-riscv64 -machine virt -m 256M -display none \
       -serial file:/tmp/uart.log -bios none -kernel <rehosted.elf> -S \
       -gdb tcp:127.0.0.1:4444 \
       -plugin qemu/greeneye-trace.so,path=/tmp/ge-trace.jsonl,start=0x80000000,end=0x81000000,exec=on
   ./greeneye.sh attach --port 4444 --kernel <rehosted.elf> --plugin <capability-plugin.py>
   ```

   `start=`/`end=` bound the trace, `exec=on` adds per-instruction records and
   `path=` selects the JSONL output (`qemu/greeneye_trace.c`; qemu/README.md).
   Folding that JSONL into sessions is roadmap R4 — today it is a separate
   evidence blob.

3. Reduce to a capability table and compare with the emulation's model:

   | Observed access | Address/range | Kind | PC/symbol | Emulated capability | Finding |
   |---|---|---|---|---|---|
   | write | `0x…` | w | `func+0x…` | not modelled | least-privilege candidate |
   | read | `0x…` | r | `func+0x…` | denied in emulation | potential fail-closed gap |

   Least-privilege readings matter for `open-iov-vfio`, but a simulator
   observation is evidence about the firmware (docs/06 §6); confirm before it
   changes the product.

### 4.3 Mutation of construction/control inputs (H4.3)

Goal: drive error paths and asserts deliberately, capture them with symbols,
and build a corpus for the emulation's fail-closed rules. The full harness is
roadmap R5; until then, bounded mutation over existing primitives is honest.

1. Mutate single fields deterministically and record each attempt:

   ```python
   # mutate.py -- ./greeneye.sh run mutate.py --kernel <rehosted.elf>
   import greeneye as ge
   s = ge.launch(kernel="<rehosted.elf>", record=True, name="mutate")
   ctrl = s.symbols.lookup("<control-symbol>")
   for value in (0, 1, 0xFFFFFFFFFFFFFFFF, 0x8000000000000000):
       s.memory.write_u64(ctrl + 0x20, value)
       s.run(duration=0.2, max_stops=200)
       s.note("mutation", field="mailbox", value=value)
   s.close()
   ```

2. Mutate injected-service calls on both paths (direct and mailbox):

   ```python
   from greeneye.inject import load_service, invoke_remote, invoke_via_firmware
   svc = load_service(s, "sdk/build/injected_demo.elf", slot=6)
   invoke_remote(s, svc, cmd=0xFFFF, a0=0, a1=0)        # malformed cmd
   s.run(duration=0.3)
   invoke_via_firmware(s, svc, cmd=0xFFFF, a0=0, a1=0)  # malformed mailbox
   ```

3. Capture error paths with hooks/watchpoints on the dispatcher, fault
   handlers and assert sites (symbol DB); `./greeneye.sh demo` records a trap.
4. Compare clean vs mutated sessions, keeping the mutation value in a `note`
   so the pack is self-describing:

   ```bash
   ./greeneye.sh compare dumps/<clean-run> dumps/<mutated-run> --symbols
   ```

   Every corpus entry needs the firmware hash; never run mutation against
   hardware (docs/12 §9).

### 4.4 Decoy and anti-analysis handling (H4.4)

Goal: distinguish signal from noise when the image branches on environment,
timing or integrity state, or contains bogus code paths.

1. **Deterministic replay first.** Run the identical stimulus twice and
   require the comparison to be empty:

   ```bash
   ./greeneye.sh compare dumps/<run1> dumps/<run2> --symbols
   ```

   Any delta is a non-determinism candidate (scheduler, uninitialised read, time); record it.

2. **Two-run differential.** Flip one input bit; attribute every delta to it;
   unrelated movement suggests a time/integrity check:

   ```python
   s.memory.write_u64(ctrl + 0x28, original ^ (1 << 0))   # one bit
   ```

3. **Coverage-based dead-code detection.** Use the TCG trace with `exec=on`
   (or a bounded `s.step(n)` pass) to build a PC set, then annotate it into
   Ghidra:

   ```python
   from greeneye.ghidra import export_hits
   from greeneye.symbols import SymbolDB
   db = SymbolDB(); db.load_json("<symbols.json>")
   export_hits("dumps/<run>", "greeneye_hits.tsv", symbols=db)
   ```
   ```bash
   $GHIDRA/support/analyzeHeadless <proj> <name> -process <program> -noanalysis \
       -scriptPath debug/risc-v-gsp-rm-vm/ghidra \
       -postScript MarkGreenEyeHits.java /abs/path/greeneye_hits.tsv
   ```

   Zero hits across every stimulus makes a function a **decoy candidate**, not proof of dead code (docs/06).

4. **Trap-on-register-read experiments.** Arm a read watchpoint on a register
   the firmware might be probing, and record which PC reads it:

   ```python
   s.watch("<reg-or-mmio-symbol>", size=8, kind="r",
           callback=lambda hit: s.note("anti-analysis-read", pc=hit.pc))
   ```

   On today's `qemu virt` an unmodelled GPU register faults rather than
   returning a value (`greeneye probe` reports that first read, docs/06 §3);
   the value-return experiment waits on the H2 device model — log, don't claim.
5. Maintain the catalogue of observed techniques in
   docs/09-GSP-FIRMWARE-NOTES.md (roadmap H4.4), one session pair per technique.

### 4.5 Differential execution: reference vs simulator vs injected service (H4.5)

Goal: measure, not narrate. The contract is the M1–M6 table from docs/06:

| # | Metric | How it is measured here |
|---|---|---|
| M1 | Inventory coverage | observed controls/RPCs vs emulated handled set |
| M2 | Reply agreement | field-by-field reply diff (canonical size, consumed fields) |
| M3 | Ordering agreement | sequence alignment of the control/event stream (LCS/edit distance) |
| M4 | State agreement | firmware state transitions vs emulation state model |
| M5 | Error-path agreement | injected faults take the same status/notifier path |
| M6 | Memory side-effects | read/write map vs emulation shadow state; snapshot hashes |

Procedure: (1) capture the reference on hardware/VFIO (docs/07 P4); (2) capture
the simulator side with `record`/scripted `run`, hooks, watchpoints and
snapshots; (3) `./greeneye.sh compare dumps/<reference-dir> dumps/<greeneye-dir>
--symbols`, with a script or plugin for per-control M2/M3 payload events;
(4) repeat with the injected-service path to test the service boundary;
(5) escalate — divergence that would change emulation behaviour must be
reproduced physically; simulator-only divergence becomes a fidelity gap.

### 4.6 From observation to reimplementation

The canonical accuracy loop (roadmap H4.5): (1) observe on hardware/VFIO;
(2) observe in green-eye (replayable session); (3) `compare` + M1–M6;
(4) implement in `open-iov-vfio` with the session directories attached as
evidence; (5) re-run the same stimulus and require the metric table to improve
or stay equal.

Practical rules: one session pair per behaviour change; the PR links the
`compare` report and session hashes plus the symbol DB diff if the static map
changed; decoy or low-fidelity observations never justify a product change.

## 5. Ghidra integration specifics

Start the live bridge and drive it from the decompiler:

```bash
./greeneye.sh ghidra --kernel <rehosted.elf> --bridge-port 7000
$GHIDRA/support/analyzeHeadless <proj> <name> -process <program> -noanalysis \
    -scriptPath debug/risc-v-gsp-rm-vm/ghidra \
    -postScript GreenEyeControl.java 7000 0x<decompiler-address>
```

`GreenEyeControl.java` operations (docs/13): `setBreakpointAt/Here`,
`runUntil/Here`, `readMemoryHere`, `readRegister`/`writeRegister`,
`callFunction`, `snapshotHere`, `listHits`, `exportHits`. Watchpoints exist on
the bridge (`watch_set`) without a Java wrapper: `send("watch_set", {...})`.

Address conventions and bias pitfalls (docs/05 §1):

* Ghidra shows **program addresses**; green-eye resolves **runtime addresses**.
  For firmware linked at its load address they coincide; otherwise supply
  `--bias` when building the symbol DB (`ExportForGreenEye.java` emits program
  addresses).
* The classic mistake is passing a program address to `hook()` when the image
  base is non-zero; prefer `s.hook("<name>")` and pass raw numbers through
  `resolve`. `decompiled_index.tsv` carries both spaces; green-eye keeps the
  program address in `symbol.extras["program_addr"]` for `bucket:line` mapping.
* Wrapper-generated functions are re-laid-out between versions: transfer
  names, never raw wrapper addresses (docs/05 §6).
* Bridge `run_until` timeouts mean "not executed under this stimulus at this
  fidelity"; they do not prove unreachability.

## 6. Metrics, evidence pack and report template

A finding is only a finding when the pack is complete. The evidence pack for
one adversarial-navigation task contains:

* [ ] firmware identity (version, hash, pairing) and symbol DB(s) with provenance and hash;
* [ ] baseline/candidate session dirs (or sha256 inventory) and the exact stimulus;
* [ ] `compare` report and relevant `analyze` reports;
* [ ] M1–M6 table with the reference-capture method and explicit L2/L3 limits;
* [ ] `greeneye_hits.tsv` plus any Ghidra annotations produced;
* [ ] unexplained-delta list (empty is a claim, not a default).

Short report template:

```markdown
# Firmware delta: <old> -> <new> (<device>)
* firmware: <old sha256> -> <new sha256>; symbol DB: <old hash> -> <new hash>
  (provenance: <export>, bias <n>)
* sessions: <baseline dir>, <candidate dir> (greeneye-session/1); stimulus:
  <script/spec + mutation values>

## Observed delta
* changed on live path: <symbols>; behaviour moved without code change:
  <symbols, evidence>; coverage gaps/decoys: <symbols, why>

## Metrics (docs/06)
| M1 | M2 | M3 | M4 | M5 | M6 |
|----|----|----|----|----|----|

## Fidelity statement
L0/L1 in simulator; L2 not claimed; L3 absent for <registers>.
Physical reproduction required before product change: yes/no, why.

## Product action
open-iov-vfio change (or explicit "no change"): <link/notes>
```

## 7. Boundaries, safety and ethics

* **Development-only tooling.** Never shipped, never a dependency of the emulation, never in a guest (docs/00).
* **No guest modification.** Instrument the firmware under study in the
  simulator; do not patch or observe a guest driver (project rule; V4).
* **No confidentiality claims.** The simulator is a study environment, never a
  security or confidentiality boundary (docs/06, V4/H6).
* **Clean-room discipline.** Do not copy vendor code into product code; use
  the study material to understand behaviour, then write the emulation from
  observation. Tracked docs cite behaviour generically and never name source
  locations.
* **Fuzzing is gated.** Mutation runs only against the simulator or re-hosted
  code, behind explicit harnesses, with session evidence (docs/12 §9, R5).
  Never use injection to defeat a hardware security boundary (docs/12 §1).
* **Escalate to physical.** Any conclusion that would change `open-iov-vfio`
  must be confirmed against a physical capture (docs/06 §6, docs/07 P4.5).

## 8. Roadmap hooks

| Method | Roadmap item | What it adds | Gate |
|---|---|---|---|
| 4.1 semantic diff | H4.1 | versioned symbol corpora, automated delta report | H1+H2 |
| 4.2 capability inference | H4.2 | access map vs capability model, least-privilege findings | watchpoints (done); TCG-session fold (R4) |
| 4.3 mutation/fuzzing | H4.3, R5 | AFL++ class harness, corpus of error paths | R1/R2 |
| 4.4 decoy handling | H4.4 | technique catalogue in docs/09, deterministic replay | H2 for register-value experiments |
| 4.5 differential exec | H4.5, docs/06 | scripted consensus harness, M1–M6 automation | reference capture pipeline |
| 4.6 reimplementation | H4.5, V1 | golden sessions, CI comparison | H1 stable |

Program-level success criterion served here: a new GSP version can be triaged
(inventory + behaviour diff + metric table) in **hours**, with evidence.