Files
vt1200/dwt/notes/rdebug-static.md

524 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `rdebug_*` and the DWT remote-console path — static analysis
Ghidra session `d85b60d5e068489f8c43db59d69d907d`, program `vt1200_v22.bin`
(`tms34010:LE:32:default`). Address convention used throughout:
* **Ghidra address = `0x07E40000` + ROM file offset** (the `rom` block starts at
`0x07E40000`).
* **Firmware bit address = Ghidra address × 8.** Call targets, pointers and
table entries in this image are *bit* addresses, so a stored pointer
`0x3F5A12E0` means Ghidra `0x07EB425C`, i.e. the entry at that address.
Everything below that is *not* marked "runtime" comes from the listing /
raw bytes. Ghidra's decompiler reports `bad instruction data` over most of this
region and mis-decoded the challenge prologue by two bytes; where it mattered I
re-decoded the raw bytes by hand and cross-checked with
`reference/dis34-a-v22.txt.gz`. The decompiler output was used only for
orientation, never as evidence.
---
## Verdict
**Present, and architecturally a payload loader — but gated three times on this
image, and the third gate is not yet fully resolved.**
1. The service is a **TCP listener named `"DEBUG TCP Port"`** (descriptor port
field `0x0303` = 771; sibling descriptor in the same table is port `0x1770` =
6000 = X11, which confirms the field layout). Accept handler
`0x07EB15AC`.
2. `accept_debug_conn` logs `Debug connection from IP host %s`, then consults a
**16-bit configuration word at bit `0xAECD2`**. If it is **non-zero the
connection is rejected** (`" rejected"`, return `-1`) before any protocol
byte is exchanged. On the emulated machine
(`VT1200_SKIP_LEVEL1=1`, fresh NVRAM) that word reads **`0x8070`** at every
frame from 200 to 4000 — i.e. **the gate is closed in this configuration**.
3. If the gate were open, the accepted socket becomes a process named
`"remote debug"` whose entry is the dispatcher `0x07EB3E64`. Before the
first opcode ≥ 2 the dispatcher runs a **16-byte request/response handshake**
(`dwt_prf`, `0x07EB3FB8`); if it returns 0 the process exits. What exactly
makes that handshake succeed is **not established** (see §6).
So: *not reachable on a default emulated boot*, and the rdebug opcode set
(including `poke` and `jump`) is only reachable after (a) the config word is
zero **and** (b) the handshake is satisfied.
**Confidence:** high that the code path exists, its addresses and wire format
are as stated (they were confirmed against a live RAM dump of the dispatch
table); medium on the accept gate's polarity/meaning (derived from the listing,
and the observed value); **low/unresolved** on the handshake's success
condition.
---
## 1. Function table
| role | Ghidra | bit address | found from |
|---|---|---|---|
| `accept_debug_conn` (TCP accept + gate) | `0x07EB15AC` | `0x3F58AD60` | service descriptor, ROM `0x1A64A4` |
| service setup twin (X11 accept) | `0x07EB1706` | `0x3F58B830` | descriptor ROM `0x1A6470` |
| shared setup helper | `0x07EB1B42` | `0x3F58DA10` | descriptors `0x1A646C`, `0x1A64A0` |
| **dispatcher / session loop** | `0x07EB3E64` | `0x3F59F320` | process entry passed to `create_process` |
| **challenge / `dwt_prf`** | `0x07EB3FB8` | `0x3F59FDC0` | `CALLA 0x07EB3FB8` at `0x07EB3F62` |
| `write_message` (framing + send) | `0x07EB4164` | `0x3F5A0B20` | every handler |
| opcode 0 — pager/`--More--` runner | `0x07EB4216` | `0x3F5A10B0` | table[0] |
| opcode 1 — dump_printfs (off) | `0x07EB44D4` | `0x3F5A26A0` | table[1] |
| **opcode 2 — `rdebug_peek`** | `0x07EB425C` | `0x3F5A12E0` | table[2] |
| **opcode 3 — `rdebug_poke`** | `0x07EB436A` | `0x3F5A1B50` | table[3] |
| **opcode 4 — `rdebug_jump`** | `0x07EB4712` | `0x3F5A3890` | table[4] |
| opcode 5 — dump_printfs (on) | `0x07EB44B0` | `0x3F5A2580` | table[5] |
| **opcode 6 — `rdebug_start_telnet`** | `0x07EB4E02` | `0x3F5A7010` | table[6] |
| opcode 7 — no-op (`"Done\n"`) | `0x07EB3F8C` | `0x3F59FC60` | table[7] |
| opcode 8 — `rdebug_display_message` | `0x07EB4EAE` | `0x3F5A7570` | table[8] |
| opcode 9 — `rdebug_gather_usage_stats` | `0x07EB5042` | `0x3F5A8210` | table[9] |
| `ntohl` helper | `0x07F65828` | `0x3FB2C140` | called on every received word |
| `memcpy` via `PIXBLT` | `0x07F68880` | `0x3FB44400` | peek/poke copy |
| socket read wrapper | `0x07EBB1CE` | `0x3F5D8E70` | every handler |
### Caller chain
```
"DEBUG TCP Port" service descriptor (ROM 0x1A6490)
name = 0x07EB21A2 "DEBUG TCP Port"
port = 0x0303 (network order) [sibling descriptor: 0x1770 = 6000/X11]
setup = 0x07EB1B42
accept= 0x07EB15AC
|
v
accept_debug_conn 0x07EB15AC
getsock/accept -> printf("Debug connection from IP host %s", peer)
if (*(u16 *)0xAECD2 != 0) { print " rejected"; return -1; }
create_process(name="remote debug", entry=0x07EB3E64, stack=0x3000)
|
v
dispatcher 0x07EB3E64
loop { recv 4 bytes; op = ntohl(); if (op >= 2 && !challenged) { if (!dwt_prf()) exit(); }
call table[op] } table at RAM bit 0x1BB830 (byte 0x37706)
```
Evidence, `listing_disassemble_function` at `0x07EB3E64` (abridged to the
dispatch):
```
rom:07eb3eba CALLA 0x07eb0898 ; open/attach session -> *A13(0x40)
rom:07eb3ed8 ... loop head ...
rom:07eb3ee8 CALLA 0x07ebb1ce ; recv(session, A13, 4, 0)
rom:07eb3efc MOVI 0x3f5b66a8,A8 ; (error text for bad opcode)
rom:07eb3f02 CALLA 0x07f65828 ; ntohl
rom:07eb3f08 MOVE A8,*A13,0x1 ; A13[0] = opcode
rom:07eb3f0e SUBK 0xa,A8 ; opcode - 10
rom:07eb3f10 JRLS 0x07eb3f50 ; <= 10 (unsigned) -> challenge/dispatch
rom:07eb3f50 MOVE *A13,A9,0x1
rom:07eb3f52 SUBK 0x2,A9
rom:07eb3f54 JRLT 0x07eb3f76 ; opcode < 2 -> dispatch, no challenge
rom:07eb3f56 MOVE *A13(0x60),A9,0x1
rom:07eb3f5a JRNE 0x07eb3f76 ; already challenged -> dispatch
rom:07eb3f62 CALLA 0x07eb3fb8 ; dwt_prf challenge
rom:07eb3f6e JRNE 0x07eb3f76 ; non-zero -> dispatch
rom:07eb3f70 CALLA 0x07ee0f46 ; zero -> kill the session
rom:07eb3f76 MOVE *A13(0x40),*A14+,0x1 ; arg0 = session
rom:07eb3f7c MOVE *A13,A9,0x1
rom:07eb3f7e SLL 0x5,A9 ; opcode * 32 bits = opcode * 4 bytes
rom:07eb3f80 ADDI 0x1bb830,A9
rom:07eb3f86 MOVE *A9,A9,0x1
rom:07eb3f88 CALL A9
rom:07eb3f8a JR 0x07eb3ed8 ; loop
```
Runtime confirmation (`dwt/probe_ram.lua`, `VT1200_SKIP_LEVEL1=1`, frame 2600,
reading RAM through MAME's debugger):
```
--- rdebug command table @ bit 0x1bb830 bit=0x001BB830 byte=0x037706
[ 0] = 0x3F5A10B0 [ 1] = 0x3F5A26A0 [ 2] = 0x3F5A12E0 [ 3] = 0x3F5A1B50
[ 4] = 0x3F5A3890 [ 5] = 0x3F5A2580 [ 6] = 0x3F5A7010 [ 7] = 0x3F59FC60
[ 8] = 0x3F5A7570 [ 9] = 0x3F5A8210 [10] = 0x00000001 [11] = 0x3F5AE800
```
`[0]..[9]` are exactly the ten pointers in the ROM initialiser at
`0x1A6678`, in the same order. The table is **10 entries**, but the dispatcher
accepts `opcode <= 10`, so **opcode 10 indexes one entry past the table and
calls `0x00000001`** (bit address 1 = byte 0). That is an out-of-bounds call,
not a command; whether byte 0 is executable depends on the RAM-init image, so
treat it as a crash, not a primitive. Worth noting as a defect.
---
## 2. Wire format (as far as it can be reconstructed)
### Requests (host → terminal)
Unframed, big-endian:
```
u32 opcode # ntohl()'d at 0x07EB3F02
<opcode-specific arguments> # each 32-bit field is ntohl()'d by the handler
```
The dispatcher reads exactly 4 bytes (`MOVK 0x8`→ push 8? no: `MOVK 0x8` then
`MOVE A13` … see §3 for the exact call shape) and byte-swaps them. The handlers
then read their own fixed-size headers raw and `ntohl` each word. There is no
length prefix on requests and no request checksum.
### Responses (terminal → host)
Every response is framed by `write_message` `0x07EB4164`:
```
u32 be length = payload_len + 1
u8 flag
u8[] payload (payload_len bytes)
```
`write_message(session, flag, buf, len)`; the argument order is fixed by its
prologue, which reads `*A13(0xffc0)` as a **byte** into `*A13(0x20)` (offset
+4), `*A13(0xff80)` as the length (clamped to 0x400), and `*A13(0xffa0)` as the
payload pointer:
```
rom:07eb416e MOVB *A13(0xffc0),*A13(0x20) ; flag byte at buf+4
rom:07eb4178 CMPI 0x400,A11 ; length clamp
rom:07eb4196 MOVE A8,*A13,0x1 ; ntohl(len+1) at buf+0
rom:07eb41aa CALLA 0x07f68880 ; memcpy(buf+5, payload, len)
rom:07eb41b6 ADDK 0x4,A11
rom:07eb41c0 CALLA 0x07eba95e ; send(session, buf, len+5)
```
Flags observed in the handlers: `0` = text (`"Done\n"`), `1` = binary data,
`0x7F` = the challenge frame. `"Done\n"` is the literal 6-byte string at
`0x07EB6CE9` and terminates every successful command.
### Per-opcode arguments (from the listings)
| op | request after opcode | response |
|---|---|---|
| 0 | *(none read; runs pager)* | text/`"Done\n"`, then **exits** |
| 1 | *(none)* | `"Done\n"` |
| 2 peek | `u32 addr; u32 len` (both be) | `len` raw bytes (flag 1) + `"Done\n"` |
| 3 poke | `u32 addr; u32 len` then `len` raw bytes | `"Done\n"` |
| 4 jump | `u32 addr; u32 nargs` then `nargs`×`u32` | `u32 be retval` (flag 1) + `"Done\n"` |
| 5 | *(none)* | stream of captured console text |
| 6 telnet | up to 255 bytes hostname | `"Done\n"` |
| 7 | *(none)* | `"Done\n"` |
| 8 display | `u32 ?, u32 len` then `len` bytes text | `"Done\n"` |
| 9 stats | 28 bytes (`0x1C`) request block | stats block + `"Done\n"` |
---
## 3. `rdebug_peek` — arbitrary read
`0x07EB425C`:
```
rom:07eb4266 MOVE A14,-*SP,0x1 ; SUB A11,A11 ; MOVK 0x8,A11 ; MOVE A13
rom:07eb4276 CALLA 0x07ebb1ce ; recv(session, A13, 8, 0)
rom:07eb4282 JRGE 0x07eb42ae ; <0 -> "rdebug_peek: recv error %d"
rom:07eb42b4 CALLA 0x07f65828 ; ntohl(*A13(0x20)) = length
rom:07eb42c4 CALLA 0x07f65828 ; ntohl(*A13(0x00)) = address
rom:07eb42cc MOVE *A13(0x20),A11,0x1
rom:07eb42d0 CMPI 0x400,A11
rom:07eb42d4 JRLS 0x07eb431a ; length <= 1024 -> copy
rom:07eb42d8 MOVI 0x400,A9
rom:07eb42de MOVI 0x3f5b6a50,A8 ; "rdebug_peek: maximum peek size is %d bytes"
rom:07eb430e CALLA 0x07eb4164 ; send error text
rom:07eb4314 CALLA 0x07ee0f46 ; and exit the process
rom:07eb431a ... memcpy(buf, addr, len) ...
rom:07eb433e CALLA 0x07eb4164 ; send(buf, len, flag=1)
rom:07eb435a CALLA 0x07eb4164 ; send("Done\n", 6, flag=0)
```
The address is taken verbatim from the request and used as a **bit address**;
there is **no validation at all** — not even "is this in RAM". Any address the
CPU can read is readable: ROM, I/O registers, VRAM, NVRAM. Length is capped at
0x400 (1024) per request, but the command can be repeated.
## 4. `rdebug_poke` — arbitrary write
`0x07EB436A`:
```
rom:07eb4374 ... recv(session, A13, 8, 0) ; {addr, len}
rom:07eb4390 JRGE 0x07eb43bc
rom:07eb43c2 CALLA 0x07f65828 ; ntohl(addr)
rom:07eb43d0 CALLA 0x07f65828 ; ntohl(len)
rom:07eb43da CMPI 0x400,A8
rom:07eb43de JRLS 0x07eb4424 ; len <= 1024
rom:07eb43e2 MOVI 0x400,A11
rom:07eb43e8 MOVI 0x3f5b6c88,A8 ; "rdebug_poke: maximum poke size is %d bytes"
...
rom:07eb4424 ... recv(session, A13+0x2310, len, 0) ; payload into a stack buffer
rom:07eb4446 JRGE 0x07eb4472
rom:07eb445a MOVI 0x3f5b6de0,A8 ; "rdebug_poke: recv error %d (2)"
rom:07eb4472 ... memcpy(addr, A13+0x2310, len) ...
rom:07eb44a0 CALLA 0x07eb4164 ; send("Done\n")
```
* **Maximum poke size = 0x400 (1024) bytes** per request; repeat as needed.
* Destination = the requested **bit address**, unchecked.
* ROM is *not* write-protected by the handler: whether a write to the ROM
window sticks is a property of the memory map, not of this code. On the
TMS34010 the ROM region is mapped read-only in the driver, so ROM writes
would fault/discard; **RAM at bit `0x00000000`–`0x001FFFFF` and I/O at
`0x0C000000`-range are writable** and reachable by address alone.
* The address is a **bit** address: to write byte `B` of main RAM, send
`B*8`. A host that sends a byte address writes to the wrong place — this is
the single most likely mistake when testing.
## 5. `rdebug_jump` — arbitrary call
`0x07EB4712`:
```
rom:07eb471c ... recv(session, A13, 8, 0) ; {addr, nargs}
rom:07eb4738 JRGE 0x07eb4764
rom:07eb474c MOVI 0x3f5b70f0,A8 ; "rdebug_jump: recv error %d"
rom:07eb476a CALLA 0x07f65828 ; ntohl(addr)
rom:07eb4778 CALLA 0x07f65828 ; ntohl(nargs)
rom:07eb4786 SUBK 0x14,A8 ; nargs - 20
rom:07eb4788 JRLE 0x07eb47ac ; <= 20 args accepted
rom:07eb4790 MOVI 0x3f5b71d0,A8 ; (too-many-args text)
rom:07eb479e CALLA 0x07eb4164 ; JR 0x07eb4df8
rom:07eb47ac MOVE A9,A8
rom:07eb47ae JRNE 0x07eb47bc ; nargs != 0 -> malloc args
rom:07eb47b0 MOVE *A13,A11,0x1
rom:07eb47b2 CALL A11 ; nargs == 0: bare call
rom:07eb47b4 MOVE A8,*A13(0x60),0x1
rom:07eb47bc ... malloc(nargs*4); recv(session, buf, nargs*4, 0) ...
rom:07eb4814 JRGE 0x07eb484c
rom:07eb4828 MOVI 0x3f5b7298,A8 ; "rdebug_jump: recv error %d args"
rom:07eb485e JRGE 0x07eb4d3c ; ntohl each arg
rom:07eb4d3c MOVE *A13(0x20),A8,0x1
rom:07eb4d40 DEC A8
rom:07eb4d42 CMPI 0x13,A8
rom:07eb4d46 JRHI 0x07eb4da4
rom:07eb4d48 SLL 0x5,A8
rom:07eb4d4a ADDI 0x3f5a6aa0,A8 ; jump table of 20 call stubs
rom:07eb4d50 MOVE *A8,A8,0x1
rom:07eb4d52 JUMP A8
```
So `rdebug_jump` is **`call(bit_address, args...)`**, not a PC assignment:
the address is called as a function with 0–20 arguments taken from the wire.
The 20 stubs push 1..20 words and then `CALL *A13(0)`. The return value is
`ntohl`'d and sent back as a 4-byte message (flag 1), followed by `"Done\n"`.
There is no "args" structure and no argument type checking; each argument is a
raw 32-bit word. This is the payload loader: `poke` the code into RAM, then
`jump` to it (or `jump` to an existing ROM routine with chosen arguments).
Because `jump` never returns to the dispatcher on a non-returning target,
a payload can take over the `"remote debug"` process cleanly.
## 6. The `dwt_prf` challenge — what is actually checked
`0x07EB3FB8`, called once per session before the first opcode ≥ 2. Hand-decoded
from the raw bytes (Ghidra's decode is off by two bytes at `0x3FC8`):
```
3fc2 CALLA 0x07eb31a0 ; ticks() = REFCNT + global
3fc8 MOVE A8,*A13(0x100) ; +32 bytes
3fcc CALLA 0x07f67f38 ; *0x1E93C0 (clock high word / counter)
3fd2 MOVE A8,*A13(0x120) ; +36
3fd6 CALLA 0x07eb905c ; *0x1DA830 (another global)
3fdc MOVE A8,*A13(0x140) ; +40
3fe0 CALLA 0x07eb31a0 ; ticks() again
3fe6 MOVE A8,*A13(0x160) ; +44
3fea..4024 ntohl() each of the four words in place
4028 push 0x10 ; push A13 ; push 0x7F ; push session
403a CALLA 0x07eb4164 ; write_message(flag=0x7F, A13, 16)
4040 push 0 ; push 0x10 ; push A13+0x80 ; push session
4056 CALLA 0x07ebb1ce ; recv(session, frame+16, 16, 0)
4062 JRGE 0x07eb4082
4066..4078 printf("dwt_prf: challenge failed - size error (expecting %d, got %d)", 16, ret)
4082..40be ntohl() the four received words in place
40c2 CALLA 0x07f6670e ; disable interrupts, save state
40d2 CALLA 0x07eb52c4 (1) ; allocate 0x800 + 0x80 buffers
40e2 CALLA 0x07eb5458 (0x1be810); expand 128 bytes of the block at 0x1BE810
40f6 CALLA 0x07eb4138 (frame+16, 16) ; process the 16 received bytes (2 x 8-byte records)
40fc CALLA 0x07eb53f4 ; free buffers
4108 CALLA 0x07f6671a ; restore interrupt state
411e CALLA 0x07f28446 ; memcmp(frame+32, A11, 16)
4126 JREQ -> return 1 else 0
```
Findings:
* It is **not a password**: nothing is compared against a ROM constant, NVRAM
string, or stored key. There is no MAC/CRC routine here either.
* It **is** a length check *plus* a 16-byte content check. A short read is
reported with the quoted `"challenge failed - size error (expecting %d, got
%d)"` string and the function returns the negative read count (the dispatcher
treats non-zero as failure and exits).
* The compared bytes are produced **at run time**: the reply is fed through the
terminal's own bitmap/plot routines (`0x07EB52C4`, `0x07EB5458`,
`0x07EB4138`, `0x07EB5748`) and the result is `memcmp`'d with the 16 bytes
the terminal itself sent. `0x07EB5748` treats each 8-byte half of the reply
as an `{x, y}`-style plotting record; `0x1BE810` is a font/bitmap block, not
a secret (runtime dump shows `55 F8 61 C7 E5 13 11 D4 C8 00 …`).
* Because the "expected" value is derived from local state and the reply, a
reply that merely echoes the 16 bytes is **plausible** to pass, but
**I did not establish it** — the plot/transform path between the `recv` and
the `memcmp` is not fully resolved, and `A11` at the `memcmp` is the
internal buffer pointer (`@0x1C0260 + 0x5C0`), not a stored key.
**What would settle it (single experiment):** run the challenge outside the
network. With a MAME debugger script, set `PC = 0x3F59FDC0`, make the session
handle in `A13(-0x20)` a connected socketpair (or patch `0x07EBB1CE` to return
a canned 16-byte reply from RAM), feed the reply `= the 16 bytes just sent`,
and break at `0x07EB4124` to read the `memcmp` result. If `A8 == 1`, the gate
is an echo and the whole console is reachable by anyone who can connect.
## 7. Mode gate
* There is **no** build-time or jumper gate visible: the service is in the
production service table, and the accept function is reachable from it.
* The actual gate is the **16-bit configuration word at bit `0xAECD2`**.
`accept_debug_conn` reads it with `SETF 2,0,0` and rejects when it is
non-zero. It is written by the configuration loader at `0x07E653E0` (and
`0x07E653E0`-neighbouring code sets `0xAECD0` the same way) from a config
descriptor, so it is a **persistent setting**, not a compile-time constant.
* Runtime observation, `VT1200_SKIP_LEVEL1=1`, fresh NVRAM:
`@0xAECD0 = 0x8070`, `@0xAECD2 = 0x8070` at frames 200, 600, 1200, 2000,
3000, 4000 — stable, i.e. **the debug TCP service refuses connections in this
configuration**. I did not determine which user-visible setting drives it,
nor the real terminal's factory value.
* Independently, the service can only be reached if the terminal's own TCP/IP
stack is up and listening — i.e. after a successful boot. The MAME driver
currently attaches **no netdev to the LANCE**, so a host cannot connect at
all in the emulator (see §10).
## 8. `rdebug_dump_printfs` — console capture (opcode 5/1)
`0x07EB44B0` sets `@0x1BB810 = 1` then calls `0x07EB44F8`; `0x07EB44D4` clears
it and calls the same function. `0x07EB44F8`:
```
rom:07eb4500 MOVE @0x1bb730,A11 ; already streaming -> error text + exit
rom:07eb453c CALLA 0x07ee0246 ; malloc(0x400)
rom:07eb4588 MOVK 1,A7 ; MOVE A7,@0x1bb730
rom:07eb4590 MOVE 0x1b3900,@0x1bb770 ; install a printf sink struct
rom:07eb45a8 CALLA 0x07edf58e
rom:07eb45f4 CALLA 0x07eb4164 ; write_message(flag=1, captured text, len)
```
The remote host gets a **stream of the terminal's own printf/console output**.
That is an information leak on its own (messages, error text, possibly
filenames and user data) and a useful reconnaissance primitive before `poke`.
## 9. The documented console (INIT / SHOW / TEST / HELP)
This is a **different dispatcher** from rdebug. It lives on the console input
path, not on the debug socket:
* Parser: `0x07E59186(char *buf, int len)` in Ghidra (`0x3F2C8C30`), called
from `0x07F5E646` with the character-ring buffer at bit `0xF4D10`
(byte `0x1E9A2`) and length `*(u16 *)0x…(0x70) - 2`.
* Command table: **RAM bit `0xA5C20` (byte `0x14B84`)**, 12-byte entries
`{ char *name; void (*handler)(); u32 0 }`, four entries. Preceded at
`0xA5C00` by a pointer to `"\r\n"`.
* Runtime dump (frame 2600) confirms:
```
0xA5C20 "TEST" 0x3FAD3900 = 0x07F5A720
0xA5C80 "INIT" 0x3F2C7620 = 0x07E58EC4
0xA5CE0 "HELP" 0x3F2C71C0 = 0x07E58E38
0xA5D40 "SHOW" 0x3F2C7840 = 0x07E58F08
```
* Dispatcher `0x07E5945C` does `stricmp(entry->name, argv[0])` via
`0x07E59526` and returns 2 on match; `0x07E592B8` parses the rest of the line,
then `0x07E592DC` loads `entry->handler` (`*(entry+0x20 bits)` = +4 bytes) and
calls it.
* Strings: `"DWT>>> "` at `0x07E5975A`, banner
`"\r\n DECwindows Terminal Remote Console\r\n"` at `0x07E59777`,
`" Commands available:"` at `0x07E597A0`, per-command help text from
`0x07E597F6`.
**Complete command-name list (documented path): `TEST`, `INIT`, `HELP`, `SHOW`.**
There is no fifth entry — the table is exactly four records, and the
`"Commands available:"` help block lists the same four.
Relationship to rdebug: none. The DWT console takes *text lines* from the
console character stream; rdebug takes *binary opcodes* from a TCP socket. They
share only the output helper (`0x07E59154`).
## 10. `rdebug_start_telnet` — outbound dial
`0x07EB4E02`:
```
rom:07eb4e0c push 0 ; MOVI 0xff,A11 ; push ; push A13 ; push session
rom:07eb4e1e CALLA 0x07ebb1ce ; recv(session, A13, 255, 0) -> hostname
rom:07eb4e2a JRGE 0x07eb4e56
rom:07eb4e3e MOVI 0x3f5b73a0,A8 ; "rdebug_start_telnet: recv error %d"
rom:07eb4e56 MOVI 0x3f5b74c0,A8 ; "Starting telnet session to %s by request of remote host."
rom:07eb4e6a CALLA 0x07f66886 ; sprintf(buf, fmt, hostname)
rom:07eb4e78 CALLA 0x07e6126a ; log it
rom:07eb4e7e push A13
rom:07eb4e82 CALLA 0x07eda7a0 ; start the terminal's telnet client on that name
rom:07eb4e9e CALLA 0x07eb4164 ; write_message("Done\n")
```
The host string is up to 255 bytes and is used **verbatim** as the telnet
destination. The remote host therefore controls an **outbound TCP connection
from the terminal** — a reverse channel / SSRF, and an exfiltration path
(connect to an attacker host and the terminal's telnet client speaks to it).
No allow-list and no confirmation dialog were found on this path.
## 11. `rdebug_display_message` and `gather_usage_stats`
* `0x07EB4EAE`: reads 8 bytes `{u32 ?, u32 len}`, rejects `len > 0x400`
(`"rdebug_display_message: len (%d) > 1024 || < 1"`), `malloc(len+2)`, reads
`len` bytes, then calls `0x07E6126A(level, text)` to put the text on the
terminal's screen, then `"Done\n"`. Up to 1024 bytes of **attacker-chosen text
displayed on the terminal** — social engineering / UI spoofing.
* `0x07EB5042`: reads a 28-byte request, copies a 28-byte block from
`0x1BE860`, `ntohl`s four fields, checks a duration
(`"rdebug_gather_usage_stats: duration < 1"`), calls `malloc`/`calloc`
(`"calloc failed"`) and spawns a collector. Returns terminal usage data to
the remote host.
## 12. Honest gaps
1. **Handshake pass condition** — see §6. Not resolved; the one experiment that
resolves it is given there.
2. **Accept gate meaning** — `0xAECD2` is a 16-bit config word; I know its
polarity (non-zero ⇒ reject) and its emulated value (`0x8070`), but not
which user-visible setting writes it or what the real terminal ships with.
3. **Service table layout** — the descriptor at `0x1A6490` reads
`{name="DEBUG TCP Port", 0x0002, 0x0303, 0, 1, setup, accept}`; the sibling
`{…, 0x0002, 0x1770, …}` is X11 on 6000, which is what fixes `0x0303` as a
port (771). The exact struct offsets were inferred, not proven.
4. **`recv` semantics** — the wrapper `0x07EBB1CE` is a stateful read with
timeout handling (`0x07EE11BC`, `0x07EE1268`); I did not trace whether it
can return short reads. Each handler treats a negative return as fatal, so a
short read is at worst a session abort.
5. **Reachability in MAME is currently zero** because the driver binds no
netdev to the AM79C90. A host cannot open the TCP connection at all; the
firmware's own stack has no link. Any test of this path needs either a
driver-side netdev (out of scope for this note) or a debugger-driven call
into `accept_debug_conn`/the dispatcher.
## 13. Shortest path to a test
1. Boot `VT1200_SKIP_LEVEL1=1` (the OS must be up for the TCP stack).
2. At frame ~2500, set the gate word to zero:
`space:write_u16(0xAECD2, 0)` (bit address; MAME Lua, debugger enabled).
3. Give the LANCE a host network (a `netdev`/`pcap` attachment on the driver is
required; without it there is nothing to connect to).
4. Connect to TCP port **771** and send `u32 be 7` (`0x00000007`) — opcode 7 is
the no-op. A correct reply proves the accept path and framing:
`00 00 00 07 | 00 | 44 6F 6E 65 0A 00` (11 bytes; the 6-byte payload
includes the NUL of `"Done\n"` and the length field counts payload+flag).
5. If the handshake fires (`dwt_prf`) and replies with 16 bytes, capture them
and try echoing them back; if the session survives, send
`u32 be 2` + `u32 be <addr*8>` + `u32 be 4` and check for 4 bytes of ROM
(e.g. address `0x07E40000*8 = 0x3F200000` returns the first ROM bytes).
6. Only then `poke` (`opcode 3`) and `jump` (`opcode 4`) — see `harness/hello-world`
for a payload that is known to run on this CPU.
---
*ROM untouched (`md5 b46a3e442ccb86134d2356bd70d22fd2`). Runtime probes used:
`dwt/probe_ram.lua`, `dwt/probe_cfg.lua` (read-only memory dumps).*