mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-10-30 12:03:54 +02:00
Static: the VT1200 GUI is an X11 dix server in ROM plus Xlib clients (SERVER, SESSIONMGR/Terminal Manager, WM, VTE). Internal clients use the local service "xserver" via the display string "0::0"; TCP 6000 is a separate authorised listener. Function table and the 4-call hello-window sequence are in dwt/notes/GUI_APPS.md. Dynamic: a poke+jump payload proved the firmware ABI (strlen=4) and then called XOpenDisplay; it never returns (stage 2, display 0, X-server client watermark unchanged) and the rdebug service dies. Verdict: hard for a self-contained window; shared-RAM UI mutation (opcode 8) is the working route.
376 lines
21 KiB
Markdown
376 lines
21 KiB
Markdown
# DWT / rdebug investigation — cross-agent notes
|
||
|
||
Two agents run in parallel:
|
||
|
||
| agent | owns | file |
|
||
|---|---|---|
|
||
| static (Ghidra) | `rdebug_*` functions, command dispatcher, gates | `dwt/notes/rdebug-static.md` |
|
||
| protocol (MOP) | MOP Remote Console framing + `dwt/mop_console.py` | `dwt/notes/mop-protocol.md` |
|
||
|
||
**Append only, with a timestamp header.** Facts that change the other agent's
|
||
next step go here; speculation stays in your own file. Do not rewrite another
|
||
agent's section.
|
||
|
||
---
|
||
|
||
|
||
## 2026-10-18 01:48 EEST — protocol agent: framing is known, client exists
|
||
|
||
Facts (sources in `dwt/notes/mop-protocol.md`):
|
||
|
||
* Ethernet II, **ethertype 0x6002** = MOP Remote Console (0x6001 = Dump/Load).
|
||
Destination = terminal unicast MAC `08:00:2b:16:4b:5a` (verified; multicast
|
||
`ab:00:00:02:00:00` also defined by the spec).
|
||
* MOP payload: `len_lo len_hi | code | body…`, length is **little-endian and
|
||
counts the bytes after the length field** (code + body). Pad the payload to the
|
||
**46-byte Ethernet minimum**; the length field, not the frame size, delimits.
|
||
* Codes we need: **13** Reserve Console (body = 8-byte verification), **5**
|
||
Request ID (body = `00` + 2-byte receipt), **7** System ID (reply), **17**
|
||
Console Command and Poll (body = flags + console bytes), **19** Console
|
||
Response and Acknowledge (reply; body = flags + console bytes), **15**
|
||
Release Console.
|
||
* Session: send 13 then 5; wait for 7. Function bitmap in System ID info TLV
|
||
type 2, **bit 5 = console carrier** (0x20). Data is **not** framed with a
|
||
session id or sequence beyond CCP flag bit 0 (1-bit message number, toggles
|
||
per command); bit 1 = send break. Flag bit 0 of CRA acknowledges the command.
|
||
* No checksum in Remote Console (unlike Dump/Load), no version byte on the wire.
|
||
* Password: the 8-byte Reserve-Console verification is the MOP service
|
||
password, default `00 00 00 00 00 00 00 00`. Wrong value = terminal silently
|
||
ignores the request (documented NCP behaviour: "Target does not respond").
|
||
NCL byte order = leftmost hex pair first; NCP integer order = reversed.
|
||
VT1200 has no documented maintenance password, so start with zeros.
|
||
* Console data is a raw byte stream. To type a command send `HELP\r`; the
|
||
terminal echoes `DWT>>> ` and CRLF text. Use `--raw HEX` for arbitrary bytes.
|
||
|
||
Client: `dwt/mop_console.py`. `--dry-run` prints frames without a socket;
|
||
`--raw HEX` sends bytes inside CCP; `--raw-frame HEX` sends a complete MOP
|
||
payload if a bare rdebug opcode ever has to bypass the console stream.
|
||
|
||
## 2026-10-18 — static agent: rdebug is a *separate TCP service*, and MAME has no netdev
|
||
|
||
Facts that change the transport assumption (details + quotes in
|
||
`dwt/notes/rdebug-static.md`):
|
||
|
||
1. **The `rdebug_*` family is not reached over MOP.** It is a TCP listener
|
||
registered in the terminal's service table as **`"DEBUG TCP Port"`**, port
|
||
field `0x0303` (network order = **771**). The sibling entry in the same
|
||
table is port `0x1770` = 6000 = X11, which is what fixes the field layout.
|
||
Accept handler `0x07EB15AC` (bit `0x3F58AD60`); it spawns a process named
|
||
`"remote debug"` whose entry is the dispatcher `0x07EB3E64`
|
||
(bit `0x3F59F320`).
|
||
2. **Wire format is binary, not text.** Request = `u32 be opcode` + opcode
|
||
arguments (each word big-endian). Response framing =
|
||
`u32 be (payload_len+1)` + `u8 flag` + payload. Opcode 7 is a no-op that
|
||
answers 11 bytes: `00 00 00 07 | 00 | 44 6F 6E 65 0A 00` (len field = 7 =
|
||
payload 6 + flag 1; payload is `"Done\n"` **plus its NUL**, because the
|
||
handler passes the 6-byte C string at `0x07EB6CE9`). The cheapest liveness
|
||
test.
|
||
3. **There is a gate before any command**: `accept_debug_conn` reads a 16-bit
|
||
config word at **bit address `0xAECD2`** and rejects the connection when it
|
||
is non-zero (logs `Debug connection from IP host %s` then ` rejected`).
|
||
On the emulated machine (`VT1200_SKIP_LEVEL1=1`, fresh NVRAM) it reads
|
||
**`0x8070`** at frames 200/600/1200/2000/3000/4000, i.e. **closed**. Setting
|
||
`space:write_u16(0xAECD2, 0)` from a debugger script opens it.
|
||
4. **A second gate follows**: before the first opcode >= 2 the dispatcher runs a
|
||
16-byte request/response exchange (`dwt_prf`, `0x07EB3FB8`). It sends 16
|
||
bytes with message flag `0x7F` and requires exactly 16 bytes back (short
|
||
read => `dwt_prf: challenge failed - size error`), then returns 1/0 and the
|
||
session is killed on 0. What passes is **not established**; it is not a
|
||
password (no ROM/NVRAM constant is compared). Echoing the 16 bytes back is
|
||
the first thing to try.
|
||
5. **MAME cannot accept the connection today.** `harness/mame/vt1200.cpp` binds
|
||
the AM79C90 with no `netdev`, so the firmware's TCP/IP stack has no link.
|
||
A "MOP console client" run against MAME will not reach any socket unless a
|
||
host network is attached or the debugger drives
|
||
`accept_debug_conn`/the dispatcher directly.
|
||
6. The **documented console** (`INIT`/`TEST`/`HELP`/`SHOW`) is a *different*
|
||
path: a 4-entry text-command table at RAM bit `0xA5C20` (byte `0x14B84`),
|
||
parsed by `0x07E59186(char*, int)` from the console character ring at bit
|
||
`0xF4D10`. There are exactly four names; no undocumented fifth entry.
|
||
|
||
## 2026-10-18 02:05 EEST — protocol agent: MOP console WORKS against the emulator
|
||
|
||
Runtime facts, `VT1200_SKIP_LEVEL1=1`, `-cfg_directory cfg-net -networkprovider
|
||
taptun`, private NVRAM. Capture `dwt/evidence/mop-hello.pcap`, log
|
||
`dwt/evidence/hello.log`.
|
||
|
||
* `Reserve Console` (13, verification = 8 zero bytes) + `Request ID` (5) gets a
|
||
**System ID (7)** in ~0.5 ms. Decode of the VT1200 reply:
|
||
maintenance 3.0.0; functions `0xE1` (loop, console carrier, counters,
|
||
reservation); console user = host MAC; timer 12 s; command buffer 32 B;
|
||
response buffer 600 B; hardware address `08:00:2b:16:4b:5a`.
|
||
* `HELP` (sent as `HELP\r` in a CCP, code 17) returns **425 bytes** in one
|
||
CRA (code 19, length 427, frame 443). `SHOW` returns **509 bytes** (frame
|
||
527). Full text in `dwt/evidence/dwt-help.txt` and
|
||
`dwt/evidence/dwt-show.txt`.
|
||
* **No challenge/password on this path.** Reserve with zeros works; Reserve
|
||
with the arbitrary non-zero value `0102030405060708` also works
|
||
(`dwt/evidence/wrong-password.log`). The `dwt_prf` challenge is the **TCP
|
||
debug listener** per `rdebug-static.md` §6, not MOP.
|
||
* **DWT is OS-only.** Bare boot (no skip) holds `PC=3FDCE400` from frame 100 to
|
||
3600 (`dwt/evidence/mame-noos-probe.log`) and 8 Reserve/RequestID pairs over
|
||
8 s get **zero** frames back (`noos-attempt3.log`, `mop-noos.pcap`).
|
||
* `TEST 0 0` and `TEST 12` are accepted and print `…Testing...`, then the MOP
|
||
server stops answering: a following Reserve gets no System ID on two fresh
|
||
boots (`script3.log`, `test00b.log`, `test12.log`, `help-after-test*.log`).
|
||
This matches the service guide's precondition "no active work (no open
|
||
windows)"; we could not meet it because DWT lives in the running session
|
||
manager.
|
||
* `INIT` gets **no reply** and the terminal stops answering MOP for >90 s
|
||
(`init.log`, `help-after-init2.log`) — consistent with a reset/reboot.
|
||
|
||
**Important for the rdebug work:** the MOP console is the *text* path into the
|
||
four-entry DWT parser (`TEST/INIT/HELP/SHOW`, `rdebug-static.md` §9). It is
|
||
**not** the rdebug binary-opcode path. `--raw HEX` sends bytes into that text
|
||
parser, so it cannot reach `rdebug_peek`/`poke`/`jump`; those are TCP port 771
|
||
and gated (`rdebug-static.md` §1/§7). Use this client for the documented
|
||
console, not as an rdebug transport.
|
||
|
||
## 2026-10-18 02:07 EEST — exploit agent: gate A is OPEN; the 0x8070 reading was a field-size mistake
|
||
|
||
**This changes the static note's verdict.** `accept_debug_conn` does **not** read a
|
||
16-bit word. The listing is:
|
||
|
||
```
|
||
3f58b280: setf 2, 0, 0 ; field size = 2 BITS (MAME prints 1..0x20 = size)
|
||
3f58b290: move @0xaecd2, a7, 0 ; read the 2-bit field at bit 0xAECD2
|
||
3f58b2c0: jreq L3F58B430 ; zero -> ACCEPT path
|
||
```
|
||
|
||
`SETF`'s first operand is the field size in bits (`34010dsm.cpp` prints
|
||
`(op & 0x1f) ? op & 0x1f : 0x20`); `setf 2` = 2 bits. The compared value is
|
||
therefore **bits 2..3 of the word at 0xAECD0**, not the whole word. The word is
|
||
`0x8070` from frame ~600 onward (and `0x0000` before), and in both cases
|
||
bits 2..3 are **0**.
|
||
|
||
Verified on the emulated CPU: jumping the live firmware into `0x3F58B280` with a
|
||
breakpoint on both targets hits **`3F58B430` (ACCEPT)** with `a7=0`; the reject
|
||
target is never reached. Evidence: `dwt/evidence/run-branch/`.
|
||
|
||
The two 2-bit fields in that word are written by `3F329F00` (@0xAECD0) and
|
||
`3F32A050` (@0xAECD2) from config lookups; those stores did **not** execute in
|
||
the `VT1200_SKIP_LEVEL1=1` boot. The `0x8070` itself arrives via the PIXBLT
|
||
memcpy at PC `3FB446D0` (a boot-time settings/bitmap block copy), which is why a
|
||
watchpoint saw it. Do not use the whole-word value as a gate indicator.
|
||
|
||
**Consequence for the MOP agent:** the rdebug path is not blocked by the config
|
||
word on this image; the remaining gate is the 16-byte echo challenge (`dwt_prf`),
|
||
which the static reading now also resolves in favour of echo (the transform
|
||
writes to `@0x1C0240`, not to the reply buffer, and the memcmp is
|
||
`reply` vs `sent`). Do not patch `0xAECD2` to "open" the port; it is already 0
|
||
in the field that matters.
|
||
|
||
**Tap:** no vt1200 MAME instance or tcpdump is running as of 02:07. I am about
|
||
to use `tap-mess-1000-0` / bridge `vt1200` for a short live TCP test. If you
|
||
start a MAME instance, please leave the tap alone for a few minutes or note it
|
||
here.
|
||
|
||
## 2026-10-18 02:40 EEST — exploit agent: gate B is real; args must follow the challenge
|
||
|
||
Live result against the emulated terminal (`VT1200_SKIP_LEVEL1=1`, LANCE on the
|
||
tap, BOOTP address 192.168.99.2):
|
||
|
||
* **Gate A is not a gate.** A TCP connection to 771 reached
|
||
`accept_debug_conn`, the dispatcher and `dwt_prf` (breakpoints fired). The
|
||
`0xAECD2` compare is a 2-bit field read by `SETF 2`; it is 0. Do not patch
|
||
the 16-bit word at `0xAECD0`.
|
||
* **Gate B is real.** `dwt_prf` sends 16 bytes (flag 0x7F) and requires exactly
|
||
16 back. Echo gives `memcmp = 0xFFFFFFFF` and the session is killed. The
|
||
expected value is the wire challenge with each **32-bit word byte-swapped**
|
||
(the code ntohl's the host words into the send buffer and memcmp's the
|
||
host-order copy), but even that ("computed") gives `memcmp = 0x00000001`:
|
||
the received buffer is first rewritten in place by a fixed-key font/cipher
|
||
pipeline (`0x3F5A09C0` → two 8-byte records → `0x3F5ABA40`, tables at
|
||
`@0x1C0220`/`@0x1C0240`, cipher `0x3F5ACEC0`). Not inverted here.
|
||
* **Opcode ≥ 2 all require the challenge, including 7.** Only 0 and 1 skip it,
|
||
and neither reaches poke/jump.
|
||
* **Protocol ordering matters:** send the opcode alone, read and answer the
|
||
challenge, *then* send the handler's arguments. If the args go with the
|
||
opcode, `dwt_prf` eats their first 16 bytes as the answer and the handler
|
||
reads shifted fields (a peek length of 8 was reported as "maximum peek size").
|
||
* With the challenge comparison forced to 0 **by a debugger breakpoint** (not
|
||
remotely), the real `poke`/`jump` handlers delivered and ran
|
||
`harness/hello-world/payload.bin`: `MAGIC1=DEAD MAGIC2=BEEF COUNT=0400`,
|
||
VRAM 1024/1024. That is the debugger-assisted loader proof, not a remote
|
||
bypass. Poke/peek addresses are bit addresses; ROM writes are ignored.
|
||
|
||
The tap (`tap-mess-1000-0` / bridge `vt1200`) is free again as of 02:40; the
|
||
MAME instance and the BOOTP server are stopped. Full write-up:
|
||
`dwt/notes/EXPLOIT_ATTEMPT.md`; transcript and logs under `dwt/evidence/`.
|
||
|
||
## 2026-10-18 02:42 EEST — oracle agent: dwt_prf transform ground truth is captured
|
||
|
||
Full method and caveats: `dwt/notes/CHALLENGE_ORACLE.md`.
|
||
|
||
**Vectors:** `dwt/challenge_vectors.json` — a top-level JSON **array** of 180
|
||
instances (177 distinct inputs). Load with
|
||
`vectors = json.load(open("dwt/challenge_vectors.json"))`; each entry is
|
||
`{"input_hex": <32 hex>, "output_hex": <32 hex>, "meta": {...}}`.
|
||
`input_hex` is the 16-byte buffer **after** dwt_prf's per-word `ntohl` (the
|
||
transform's actual input); `output_hex` is `f(input)` in place. The full
|
||
128-byte key schedule and seed-block dumps are in
|
||
`dwt/evidence/oracle/runA/stdout.log`.
|
||
|
||
**Direction (confirmed):** the firmware transforms the reply, not the
|
||
challenge. With host-order challenge words `W`, wire challenge
|
||
`C = ntohl(W)` (per-32-bit-word byte swap), wire reply `R`:
|
||
`X = ntohl(R)`, `Y = f(X)`, and dwt_prf memcmp's `Y` against `W`.
|
||
Pass condition: `f(ntohl(R)) == W`, i.e.
|
||
`R = ntohl(f^-1(W)) = ntohl(f^-1(ntohl(C)))`. The vectors give `f`, so the
|
||
implementation needs the inverse (and the outer word swap).
|
||
|
||
**Determinism:** yes. 177 distinct inputs give identical outputs on two
|
||
fresh-NVRAM boots, and on a third boot that hijacks at a different frame.
|
||
Seed block `0x1BE810` and the key schedule `@0x1C0260` are byte-identical
|
||
across boots — the key is genuinely fixed for this image.
|
||
|
||
**Per-8-byte independence:** yes. Records are processed separately: changing
|
||
only bytes 0–7 never changes output bytes 8–15 (0/64 single-bit trials), and
|
||
32 independence-grid pairs agree. Output for a doubled record is the doubled
|
||
output. Permutation on samples: 152 distinct inputs → 152 distinct outputs, no
|
||
fixed points. Avalanche: one input bit flips **32.66 of 64** first-record
|
||
output bits on average (23–42).
|
||
|
||
**Two reversing shortcuts found while building the oracle:**
|
||
|
||
1. **Argument order for a direct call.** `0x3F5A09C0` expects `(len, buf)` in
|
||
source order, i.e. `buf` must be the **last** argument written (the callee
|
||
reads `*a13(0xffe0)` as the pointer and `*a13(0xffc0)` as the length).
|
||
Writing `(buf, 16)` gives a pointer of 16 and will not work.
|
||
2. **The two "font render" calls are identity copies on this path.** dwt_prf
|
||
calls INIT with mode **1** (`0x3F5A9620(1)`); that mode leaves
|
||
`@0x1C0220`/`@0x1C0240` NULL, so `0x3F5ABA40`'s two render calls take their
|
||
memcpy branch. Captured per-record intermediates
|
||
(`dwt/evidence/oracle/stages.txt`) show `feistel_in == input` and
|
||
`pre_final_render == output` for all six samples. The effective transform is
|
||
the 16-round Feistel over the key schedule expanded from `0x1BE810` (+ the
|
||
`0x1BED40/0x1BEF00/0x1BEF80` auxiliary blocks).
|
||
|
||
Reproduce: `python3 dwt/oracle.py` (~35 s, two boots + stage capture). No ROM,
|
||
driver or `dwt_prf` code was modified; no socket was used for the vectors.
|
||
|
||
## 2026-10-18 — challenge reversed: the remote path is open (supersedes the echo reading)
|
||
|
||
**The challenge no longer blocks anything.** `dwt/challenge.py` answers it from
|
||
the challenge bytes alone; `dwt/solve.py` did so over TCP with no debugger:
|
||
|
||
* The firmware **transforms our reply** and compares it with the host-order copy
|
||
of the challenge: `memcmp(frame+0x20, transform(frame+0x10), 16)`. The reply
|
||
must therefore be `swap4(inverse_transform(swap4(challenge)))` — the inverse
|
||
direction, not echo. Live bytes at the comparison are in
|
||
`dwt/evidence/live-run/debug-live.log` (`expecting == got`, `a8=0`).
|
||
* The transform is a **16-round Feistel per independent 8-byte record**, keys
|
||
`key[15]..key[0]` (128-byte schedule at `@0x1C0260`), round function
|
||
`0x3F5ACEC0` = eight 6-bit SP-table lookups OR'd (`@0x1C0200`, 2048 bytes).
|
||
* The "font renders" at `@0x1C0220`/`@0x1C0240` are **memcpy in this path**:
|
||
`dwt_prf` calls `0x3F5A9620(1)`, which allocates only the SP table and the
|
||
key. The font pointers stay NULL (`0,0,0,0` at frame 2000; `init(1)` leaves
|
||
them zero), and the renderer's table==0 branch is a memcpy. Earlier attempts
|
||
tried to invert a font stage that never runs here.
|
||
* Key and SP table are fixed: identical across two fresh-NVRAM boots and
|
||
identical to the earlier live capture. They are embedded in
|
||
`dwt/challenge.py`; provenance and disassembly are in
|
||
`dwt/notes/CHALLENGE_REVERSE.md`.
|
||
* Validation: 70/70 + 70/70 emulator forward vectors (140 records each),
|
||
180/180 in `dwt/challenge_vectors.json`, 6/6 against the firmware's own
|
||
inverse `0x3F5AADA0`, and live sessions that ran `peek`, `poke` and `jump`
|
||
after the computed answer.
|
||
* **Remote execution demonstrated:** the 82-byte `harness/hello-world` payload
|
||
was poked to bit `0x01800000` and jumped to over TCP; the emulator read back
|
||
`MAGIC1=DEAD MAGIC2=BEEF COUNT=0400` and 1024/1024 VRAM words `FFFF`.
|
||
`dwt/evidence/live-run2/` is that run with **no breakpoints at all** (the Lua
|
||
script only polls RAM). Only `VT1200_SKIP_LEVEL1=1` is needed for TCP.
|
||
|
||
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.
|
||
|
||
## 2026-10-18 11:01 EEST — opcode 8 writes the TM Messages box (no payload)
|
||
|
||
Facts, all measured over rdebug on the live OS (`VT1200_SKIP_LEVEL1=1`); details
|
||
and frames in `dwt/notes/DISPLAY_MESSAGE.md`.
|
||
|
||
* **Opcode 8 is `rdebug_display_message` and it lands in the Terminal Manager
|
||
Messages box**, not a popup. Body: `u32be 8 | u32be level | u32be len | len
|
||
bytes`; reply `Done\n`. `len` is 1..1024.
|
||
* **The handler does not NUL-terminate.** It `malloc(len+2)` and copies `len`
|
||
bytes; a message sent without a NUL is followed by stale heap bytes on screen
|
||
(`…LEVEL ZEROUUUUUU@`). Send `text + NUL` and count the NUL in `len`; max clean
|
||
text is 1023 bytes.
|
||
* **Level is a class filter, not a destination.** The append routine is
|
||
`0x07E6126A` (bit `0x3F309350`), the same one `accept_debug_conn` calls.
|
||
Per-class enable bits at bit `0xAECE2/3/4` (byte `0xAECE0` = `0x05` on this
|
||
boot): level 0 enabled, level 1/2 disabled, level 3 unchecked. Observed:
|
||
levels 0 and 3 render; 1 and 2 are silently dropped.
|
||
* The pane is a **5-line viewport** (`count-5` at `3F30A940`/`3F30AA40`); older
|
||
lines scroll out (eight sent → 04..08 visible). Lines persist across
|
||
repaints (frames 944 and 1938 byte-identical). Not modal; opcode 7 still
|
||
answers after the messages.
|
||
* No `dwt/visual/msg.asm` was built: the firmware already provides the
|
||
primitive. Same connection can poke/jump a payload, then opcode 8.
|
||
* `dwt/exploit.py` gained only additive `RDebug.display_message()` /
|
||
`--message` / `--message-level`; `dwt/visual/msg_demo.py` and
|
||
`dwt/visual/msg_snap.lua` are new host tooling.
|
||
* Harness note: MAME's taptun name is fixed (`tap-mess-<uid>-0`), so a second
|
||
MAME needs its own net namespace; this run used one (`vtmsg`) while another
|
||
MAME held the default tap.
|
||
|
||
|
||
---
|
||
|
||
|
||
## 2026-10-18 14:10 EEST — GUI apps are X clients; a payload can call Xlib but not get a display
|
||
|
||
Facts for anyone tempted to "just create a window" (details in
|
||
`dwt/notes/GUI_APPS.md`):
|
||
|
||
* The GUI is a **real X11 dix server in ROM** (process `SERVER`, entry bit
|
||
`0x3F362910`) plus Xlib in ROM. The DWT Window Manager (`WM`, `0x3FAB8160`),
|
||
the terminal emulator (`VTE`, `0x3F4DB050`) and the **Terminal Manager**
|
||
(`SESSIONMGR_PROC`, `0x3F2E6DE0`) are ordinary X clients, not a private
|
||
window API. The TM *is* the session manager process; `main()` (bit
|
||
`0x3F34DB50`) creates SERVER and SESSIONMGR, and the session manager creates
|
||
WM and VTE.
|
||
* Internal clients open the display with the literal **`"0::0"`** (TM ROM
|
||
`0x7E61B80`, WM `0x7F59D29`). `XParseDisplay` resolves a **named local
|
||
service `"xserver"`** through the 32-entry service table at RAM bit `0x1E8D70`
|
||
(`0x7EE160C` / lookup `0x7EE1B70`). TCP 6000 is a separate, user/node
|
||
authorised listener.
|
||
* Xlib entry points (bit addresses): `XOpenDisplay` `0x3F9C2570`,
|
||
`XCreateSimpleWindow` `0x3FD41B90`, `XMapWindow` `0x3FD645B0`,
|
||
`XSync` `0x3FD9F6B0`, `XCreateGC` `0x3F997000`, `XDrawString` `0x3FDA0000`.
|
||
* **Live result:** a 390-byte payload (poke+jump, no breakpoints) called
|
||
`strlen("0::0")` successfully (returned 4) using the firmware ABI, then
|
||
`XOpenDisplay` **hung**: stage 2, display 0, X-server client watermark
|
||
unchanged, rdebug service dead afterwards. The CPU sits in Xlib's select
|
||
wrapper. So the ROM is callable, but the `"xserver"` service connect from the
|
||
injected process never reaches the server. Do not assume a self-contained X
|
||
client is "one call away".
|
||
* Shared-RAM UI mutation still works (opcode 8 / `0x3F309350`), and that is the
|
||
realistic route for making the Terminal Manager draw something.
|