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

25 KiB
Raw Blame History

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, ntohls 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).