Files
vt1200/dwt/notes/DISPLAY_MESSAGE.md
T

161 lines
7.8 KiB
Markdown
Raw 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.
# Attacker-chosen text in the Terminal Manager Messages box (rdebug opcode 8)
Status: **DONE — opcode 8 (`rdebug_display_message`) puts attacker-chosen text
in the Terminal Manager's Messages box.** No payload, no code injection and no
debugger breakpoint are involved. The visible result is the same pane the OS
uses for `System defaults loaded.`, `TCP/IP is now up.` and
`Debug connection from IP host …`.
ROM untouched (`md5 b46a3e442ccb86134d2356bd70d22fd2`). No driver edits.
`VT1200_SKIP_LEVEL1=1` (documented divergence), private `-nvram_directory` and
BOOTP/TFTP as in the other OS-path runs.
## 1. The wire request
```
request = u32be 8 | u32be level | u32be len | len raw bytes
```
`len` counts the raw bytes that follow and must be 1..1024. **The NUL
terminator is the sender's job.** The handler `malloc(len + 2)` and copies
`len` bytes, but it never writes a terminator. A message sent without one is
followed on screen by stale heap bytes (first experiment: the 29-byte
`RDEBUG MSG ONE - LEVEL ZERO` rendered as `RDEBUG MSG ONE - LEVEL ZEROUUUUUU@`).
Sending `text + NUL` with `len = len(text)+1` renders cleanly. So the largest
clean text is 1023 bytes, not 1024.
Exact requests used (dry run, `dwt/exploit.py --dry-run`):
```
# level 0, "ATTACKER MESSAGE ONE" + NUL (len 0x15)
00 00 00 08 00 00 00 00 00 00 00 15
41 54 54 41 43 4b 45 52 20 4d 45 53 53 41 47 45 20 4f 4e 45 00
# level 3, "LEVEL THREE TERTIARY" + NUL (len 0x15)
00 00 00 08 00 00 00 03 00 00 00 15
4c 45 56 45 4c 20 54 48 52 45 45 20 54 45 52 54 49 41 52 59 00
```
The reply is the usual `"Done\n"` frame. `dwt/exploit.py` now has
`RDebug.display_message(level, text, terminate=True)` and a `--message TEXT`
option; `terminate=True` appends the NUL (and therefore counts it in `len`).
## 2. What `level` does
`level` is a syslog-style message class, not an offset or a length. The append
routine is `0x07E6126A` (bit `0x3F309350`) — the same routine `accept_debug_conn`
calls for `Debug connection from IP host %s`. On entry it checks per-class
enable bits in RAM:
| level | check (bit address) | on this boot | visible in Messages |
|---|---|---|---|
| 0 | `@0xAECE2` (byte `0xAECE0` bit 2) | **1** | **yes** |
| 1 | `@0xAECE3` (bit 3) | 0 | no |
| 2 | `@0xAECE4` (bit 4) | 0 | no |
| 3 | no check — always accepted | — | **yes** |
Measured byte at `0xAECE0` on the live OS (`opcode 2` peek): **`05`** =
bit 2 set, bits 3 and 4 clear. That matches the observed result exactly:
levels 0 and 3 render; 1 and 2 are silently dropped. Levels 0 and 3 write to
the same Messages list — the level chooses *whether* the line is admitted, not
*where* it goes.
Disassembly of the filter: `3F3093B0` (bit `0xAECE2` / `jreq` exit when the
class is 0), `3F309430` (`0xAECE3`), `3F3094C0` (`0xAECE4`, `subk 2` /
`jreq` exit). Level 3 falls through all of them.
## 3. Where it appears, and what the pane does
* **The Terminal Manager Messages box**, below the `Messages` heading and above
the Ethernet/IP/LAT block. Nothing else on the screen changes: no dialog, no
popup, no status line.
* It is **not modal**. The TM keeps running, the rdebug session stays usable
(`opcode 7` answers after the messages) and the message is just another line
in the list.
* The pane is a **5-line viewport**. The append routine computes the top line
as `count - 5` (`3F30A940` / `3F30AA40`), which is why sending eight
`SCROLL LINE 01…08` messages leaves `SCROLL LINE 04…08` visible. Older lines
go out of view (the OS's own `System defaults loaded.` / `Using local fonts.`
scroll off the same way); the scrollbar is present. A hard cap on retained
lines was not established; the only verified per-message bound is the
1024-byte `len` check.
* **Persistence:** after the three demo messages the frame at 944 and the frame
at 1938 (≈1000 frames later) are byte-identical
(`4bf8e2299d4ee704848a0b107bf2a31d`). The line persists and survives the OS's
repaints.
* **Repaint timing (honest caveat):** in the controlled run the text appeared in
the next snapshots (5–14 frames after the send). In one earlier run the very
first message was buffered but not drawn for more than 130 frames, until the
next message/connection caused a pane redraw. So the list update itself is
immediate; when it becomes visible depends on the TM redrawing the pane.
## 4. Why no payload is needed
The firmware already ships the primitive. There is no reason to call the
append routine from a `jump` payload: `dwt/visual/msg.asm` was **not** built,
because opcode 8 reaches the same routine with an attacker-controlled string and
returns `Done`. It composes trivially with the visual work — the same rdebug
connection can `poke`/`jump` a payload and then send opcode 8; no hook, no
resident code, no breakpoint.
## 5. Reproduce
The host tap `tap-mess-<uid>-0` is fixed by MAME's taptun provider, so a second
MAME cannot share it. This run used a private network namespace with its own
`tap-mess-1000-0`, bridge and veth to the host, while another MAME held the
default tap. Nothing about the result depends on that choice; on a free tap the
ordinary harness command works.
```sh
# terminal (private NVRAM; 192.168.100.2 comes from harness/serve.py --bootp)
VT1200_SKIP_LEVEL1=1 \
/home/arti/code/k-space/mame0289/vt1200 vt1200 -rompath harness/mame/roms \
-sound none -skip_gameinfo -networkprovider taptun -video none -nothrottle \
-cfg_directory <cfg> -nvram_directory <nvram> \
-debug -debugger none -autoboot_script dwt/visual/msg_snap.lua \
-autoboot_delay 0
# host: one connection, challenge answered by dwt/challenge.respond
python3 dwt/visual/msg_demo.py --host 192.168.100.2 --wait 40 \
--message '0:ATTACKER MESSAGE ONE' \
--message '0:RDEBUG TEXT TWO' \
--message '3:LEVEL THREE TERTIARY'
```
`msg_snap.lua` is read-only with respect to the emulated machine: it reads the
frame counter and takes `screen:snapshot()` PNGs when the host writes a trigger
file (`"<prefix> <count> <stride>"`). No breakpoints, no firmware writes.
## 6. Evidence
| file | what it shows |
|---|---|
| `dwt/evidence/visual/msg_before_frame00824.png` | before: five OS lines, no attacker text |
| `dwt/evidence/visual/msg_after_frame00944.png` | after: `ATTACKER MESSAGE ONE`, `RDEBUG TEXT TWO`, `LEVEL THREE TERTIARY` in the Messages box |
| `dwt/evidence/visual/msg_persist_frame01938.png` | ≈1000 frames later: identical frame (`md5 4bf8e229…`) |
| `dwt/evidence/visual/msg_scroll_frame07979.png` | eight lines sent; viewport shows the last five (`SCROLL LINE 04…08`) |
| `dwt/evidence/visual/msg_levels_frame15265.png` | level 1 and 2 sent just before: invisible; level 3 visible |
| `dwt/evidence/visual/msg_*_crop.png` | the Messages box crops of the above |
| `dwt/evidence/visual/msg_client.log` | challenge, reply and every opcode-8 body on the wire |
The log was captured as `msg2_client.log`; only the PNG prefix was renamed
(`msg2_*` → `msg_*`) when the duplicate run-1 frames were dropped.
Also `dwt/visual/msg_demo.py` (new host driver) and `dwt/visual/msg_snap.lua`
(new snapshot trigger). `dwt/exploit.py` gained only additive support
(`display_message`, `--message`, `--message-level`); existing behaviour is
unchanged.
## 7. Caveats
* The challenge (`dwt_prf`) still has to be answered; `msg_demo.py` uses
`dwt/challenge.respond`, the established offline answer. This note is about
what the service does once a session is open, not about bypassing the
challenge.
* Level 1/2 are filtered on this boot because their enable bits are clear.
Writing `0xAECE0` to set bits 3/4 would presumably enable them, but that is a
runtime config change and was not needed; 0 and 3 are both usable.
* The 5-line viewport is measured and matches the `count-5` code. Scrollback
beyond the viewport and any ring cap were not exercised (no pointer-input run
for the scrollbar).