Files
vt1200/dwt/notes/CROSS.md
T

9.3 KiB

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.