forked from arti/vt1200
dwt: visual injection experiments via the returning rdebug jump
This commit is contained in:
1 parent
1e5c095b72
commit
05f1895bf5
80 files changed
+3252
No files matched your search
@@ -285,3 +285,28 @@ Files: `dwt/challenge.py`, `dwt/solve.py`, `dwt/notes/CHALLENGE_REVERSE.md`,
|
||||
`dwt/evidence/oracle-*.log`, `dwt/evidence/VALIDATION.txt`,
|
||||
`dwt/evidence/live-run{,2}/`, `dwt/evidence/scripts/{oracle_call,live_verify,live_check}.lua`.
|
||||
`dwt/exploit.py` and `dwt/mop_console.py` were not modified.
|
||||
|
||||
## 2026-10-18 — visual injection via the returning rdebug `jump`
|
||||
|
||||
**`jump` is a real `call`.** A 30-byte `rets` payload writes markers, returns
|
||||
`0`, and the same TCP session plus a later connection still answer opcode 7
|
||||
(`dwt/evidence/visual/step1_ret_test.log`).
|
||||
|
||||
* **Static VRAM draw lands but is repainted.** A three-box payload's writes are
|
||||
visible in an immediate `peek` but frames before/after are identical; a
|
||||
spinning control shows the same boxes, so the coordinates are right.
|
||||
* **Resident DI hook:** `RLONG(0xFFFFFEA0) = 00000DB0h`, stub `JA 3F7146D0h`.
|
||||
Patching the stub works (pre-handler draw is repainted). A return-PC
|
||||
trampoline (draw after the handler's `reti`) produced a visible blink:
|
||||
frames 3450 (box on) / 3480 (off) / 3500 (on), TCP answered at 3450 and 3500.
|
||||
It is **not durable** — only the first entry takes the `reti` path; later
|
||||
entries take the `jauc 3F579880` full path and stop using the patched PC
|
||||
(counter sticks at 1). A one-shot post-handler box persists for 600+ frames.
|
||||
* **Window:** port 6000 is open, but `xdpyinfo` is rejected (“Client is not
|
||||
authorized”). Opening a window by injection was not attempted; it is an
|
||||
X-structure project.
|
||||
|
||||
Details, addresses, frames and limits: `dwt/notes/VISUAL_INJECTION.md`,
|
||||
`dwt/evidence/visual/`. ROM md5 unchanged; no driver edits; only
|
||||
`VT1200_SKIP_LEVEL1=1` (documented divergence) and a private nvram.
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
# Visual injection through the rdebug loader
|
||||
|
||||
Status: **the loader returns, a one-shot post-handler draw persists, and a
|
||||
resident display-interrupt hook produced a visible blink** (black box on at
|
||||
frames 3450/3500, off at 3480; a repeat run gives off at 3440/3450 and on from
|
||||
3455). The blink is **not durable and TCP survival is flaky**: in the best run
|
||||
the OS answered TCP at 3450 and 3500, in the repeat run it had lost TCP by
|
||||
3450. The stock display handler's full path stops returning through the
|
||||
patched saved PC, so the resident hook draws once and then only the box stays.
|
||||
A durable blink needs the full display path (or an X-level callback).
|
||||
|
||||
ROM untouched (`md5 b46a3e442ccb86134d2356bd70d22fd2`). No driver edits.
|
||||
Emulator only; private `-nvram_directory`; `VT1200_SKIP_LEVEL1=1` is the
|
||||
documented harness divergence that makes the OS reach the GUI and TCP.
|
||||
|
||||
All payloads are GSPA/GSPLNK output, built by `dwt/visual/build.sh`
|
||||
(the `harness/hello-world` flow). The host driver is `dwt/visual/inject.py`,
|
||||
which imports `dwt/exploit.py` and patches `answer_challenge` with
|
||||
`dwt/challenge.respond` exactly like `dwt/solve.py`; it adds `--jump-wait`,
|
||||
post-jump liveness and peek, so a return can be observed.
|
||||
|
||||
## 1. `jump` is a call, and it returns
|
||||
|
||||
`ret_test.asm` (30 bytes) writes `0xC0DE`/`0xB007` at bit `01850000h` and `rets`.
|
||||
|
||||
```
|
||||
jump 0x01840000: returned 0x00000000
|
||||
after-peek 0x01850000 -> de c0 07 b0
|
||||
post-jump opcode 7 -> Done
|
||||
second connection at frame 3526 -> Done, markers still de c0 07 b0
|
||||
```
|
||||
|
||||
So the stub table's `call *a13` has a real return address, our `rets` returns to
|
||||
the dispatcher, and the same TCP connection (and a new one 88 frames later)
|
||||
still works. Evidence: `dwt/evidence/visual/step1_ret_test.log`,
|
||||
`step1_alive_3500.png`.
|
||||
|
||||
Two bit-address traps were found here and are worth remembering:
|
||||
|
||||
* A 32-bit cell is **+0x20 bits**, not +4. Early payloads put scratch cells at
|
||||
+2/+4 bits and they overlapped, which looked like an OS crash.
|
||||
* A 16-bit store to VRAM at x not a multiple of 16 does not produce the pixels
|
||||
you expect. Word-aligned x (multiple of 16) works; the hello-world block and
|
||||
`control_spin_box_3480.png` prove it.
|
||||
|
||||
## 2. Static VRAM draws are repainted
|
||||
|
||||
`drawal.asm` draws three black 32x32 boxes at frame-buffer (48,48), (256,400)
|
||||
and (928,752), then returns. The writes land — an immediate `peek` of
|
||||
`0400C030h` returns `00 00 00 00 ff ff ...` — but frames 3420 (before the draw)
|
||||
and 3430 (after it) are byte-identical, and the three box regions never change
|
||||
through 3600. The OS re-composites the screen before scanout.
|
||||
|
||||
Control: `boxalign.asm` draws the same kind of box and then spins. The next
|
||||
frame shows the black box (`control_spin_box_3480.png`), and the stock
|
||||
hello-world payload run at OS time shows its 256x64 block
|
||||
(`control_hello_3480.png`). So the draw and the renderer mapping are correct;
|
||||
persistence is what fails.
|
||||
|
||||
## 3. Resident display interrupt
|
||||
|
||||
### The vector and stub
|
||||
|
||||
The CPU vector table is in ROM, but the DI entry is a pointer to low RAM:
|
||||
|
||||
```
|
||||
RLONG(0xFFFFFEA0) = 00000DB0h
|
||||
00000DB0h: 80 c0 d0 46 71 3f = JA 3F7146D0h (stock OS DI handler)
|
||||
```
|
||||
|
||||
`install` rewrites only the 32-bit target:
|
||||
|
||||
```
|
||||
00000DB0h: 80 c0 90 01 84 01 = JA 01840190h (our entry)
|
||||
```
|
||||
|
||||
The opcode word stays `C080h` (`jauc`); the target is written as one 32-bit
|
||||
store under `dint`, so no interrupt can see a half-updated address.
|
||||
|
||||
### Why the draw has to be after the handler
|
||||
|
||||
* `blink.asm` (draw at handler entry, then `jauc 3F7146D0h`) ran: its counter
|
||||
incremented, the TCP session stayed usable, but **no box ever appeared**.
|
||||
* The stock handler tail-calls `3F579880` (the display work) and returns with
|
||||
`reti`. A **return-PC trampoline** gets control after that:
|
||||
`entry` reads the interrupt frame's saved PC at `SP+0x20`, stores it, replaces
|
||||
it with `ret_hook`, and tail-jumps to `3F7146D0h`; `ret_hook` draws and returns
|
||||
to the original PC with `rets 2` (the `2` skips the temporary a0 slot so the
|
||||
interrupted stack is exactly balanced). `ret_hook` saves/restores a0-a5 and
|
||||
ST, and `dint` is its first instruction.
|
||||
|
||||
This produced the blink. The payload kept for the evidence is
|
||||
`dwt/visual/build/blinkev.bin` (252 bytes; same return-PC trampoline, scratch
|
||||
at `01840800h`):
|
||||
|
||||
| frame | box region (fb 256..319 x 400..463) | state |
|
||||
|---|---|---|
|
||||
| 3450 | 56.2% lit | black box ON |
|
||||
| 3480 | 100.0% lit | OFF (white body) |
|
||||
| 3500 | 56.2% lit | black box ON |
|
||||
|
||||
TCP answered at 3450 and 3500 in that run (`post-jump opcode 7 -> Done`).
|
||||
The region is 56.2% rather than 0% because the box overlaps the Memory bar; the
|
||||
crops show the black square clearly (`blink_on_3450_crop.png`,
|
||||
`blink_off_3480_crop.png`).
|
||||
|
||||
A repeat run with the same binary reproduces the visual blink (5-frame dumps:
|
||||
3440/3450 OFF, 3455..3520 ON) but **loses TCP**: a new connection at 3450 got
|
||||
the challenge and then timed out, and by 3470 the terminal's IP stack was gone.
|
||||
So the blink is reproducible; keeping the OS fully alive with it is not yet.
|
||||
|
||||
**The limit:** only the first trampoline entry draws. A breakpoint log shows
|
||||
`ENTRY 1`, `RET 1`, then the stock handler continues 34+ times
|
||||
(`hook_breakpoints.log`), and the counter sticks at 1
|
||||
(`di_one_shot_state.log`). The handler's depth counter at `0x1e9420` makes the
|
||||
first entry take the `reti` path and later entries take the `jauc 0x3F579880`
|
||||
full path, whose return does not use our patched saved PC. The one-shot draw
|
||||
does persist (the OS does not repaint that box while idle):
|
||||
`posthandler_persist_3600.png` and the settled run answer TCP through frame
|
||||
4000 with the box still there (`settled_hook_client.log`).
|
||||
|
||||
What would close this: hook `0x3F579880` (or its return) instead of the vector,
|
||||
or hook a RAM function pointer the X server calls each frame. Both need the
|
||||
full display path's calling convention; that is the next experiment.
|
||||
|
||||
INT2 was tried as a faster periodic source (stub `0F30h`, handler `3F714530h`):
|
||||
at idle the handler never takes the trampoline (counter 0), so it does not help.
|
||||
|
||||
## 4. Windows
|
||||
|
||||
* Port **6000 is open**; `6001` is closed (`dwt/evidence/visual/xcheck.log`).
|
||||
* `xdpyinfo -display 192.168.99.2:0` is rejected: **“Client is not authorized
|
||||
to access server.”** An ordinary X client cannot connect without the
|
||||
server's auth cookie, so a legitimate window is not reachable in this setup
|
||||
(this is a baseline, not injection).
|
||||
* Creating a window by injection was **not attempted**: it needs the X
|
||||
server's screen/window structures, resource IDs and callbacks. That is a
|
||||
project, not an experiment.
|
||||
|
||||
## 5. Caveats
|
||||
|
||||
* The blink evidence is a **short-lived resident blink**, not a durable one.
|
||||
The durable result is a persistent one-shot box.
|
||||
* `VT1200_SKIP_LEVEL1=1` is required for the OS/TCP; the level-1 POST path is
|
||||
not involved.
|
||||
* The DI stub address (`00000DB0h`) and handler (`3F7146D0h`) are properties of
|
||||
**this firmware image**. A different image must read `RLONG(0xFFFFFEA0)` and
|
||||
the stub bytes first.
|
||||
* The real unit would need the rdebug service reachable (the challenge is
|
||||
solved offline by `dwt/challenge.py`) and the same RAM patch. On real
|
||||
hardware the display interrupt timing and the RAM vector page should be
|
||||
verified before writing; the patch itself is RAM-only and survives until
|
||||
reset.
|
||||
|
||||
## 6. Files
|
||||
|
||||
| path | contents |
|
||||
|---|---|
|
||||
| `dwt/visual/*.asm` | `ret_test`, `drawal`, `draw_box`/`drawspin`, `boxalign`, `blink` (pre-handler), `blinkev` (return-PC trampoline, blink evidence), `blinkpc` (settled one-shot), `blink2` (INT2), `probe_*` |
|
||||
| `dwt/visual/build.sh` | GSPA/GSPLNK build wrapper (same flow as hello-world) |
|
||||
| `dwt/visual/build/` | linked `.bin` + maps/listings |
|
||||
| `dwt/visual/inject.py` | challenge-solving client with `--jump-wait`, liveness, peek |
|
||||
| `dwt/visual/*.lua` | frame clock / interrupt probe / PC and hook probes |
|
||||
| `dwt/evidence/visual/` | frames, crops, client and MAME logs, `summary.txt` |
|
||||
Reference in new issue
Block a user