Files
vt1200/lat/notes/lat-receive-audit.md
T

195 lines
8.4 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.
# VT1200 LAT client — static memory-safety audit (receive path)
Image: `vt1200_v22.bin` (address-ordered, file offset == physical byte; ROM base
`0x07E40000`). All addresses below are **file/byte offsets**. The cached
disassembly (`reference/dis34-a-v22.txt.gz`) and `unidasm` both print **bit**
addresses, where `bit = byte * 8` (verified: reset vector target `0x3F200040`
-> byte 8 -> file `0x07E40008`).
Helper scripts used are in this directory:
| script | purpose |
|---|---|
| `findrefs.py` | find 32-bit bit-address constants for a string target |
| `near_refs.py` | find any 32-bit constant whose `>>3` lands in a byte range (catches leading-space strings) |
| `scan_str.py` | regex over ROM strings + refs |
| `ua.py` | dump `unidasm` lines for a byte range (address = byte*8) |
| `dd.py` | same for the `dis34` output |
| `mapfuncs.py` | map every string ref to its enclosing function |
## Wire layout (confirmed)
LAT ethertype `0x6004`. Message = 8-byte header + slots:
```
byte 0 bits: [0]=RRF [1]=master [6..7..2]=msg type
byte 1 nbr_slots
byte 2-3 dst_cir_id
byte 4-5 src_cir_id
byte 6 msg_seq
byte 7 msg_ack
slots: byte0=dst_slot, byte1=src_slot, byte2=byte_count, byte3=hi-nibble type,
byte4.. = data ; next slot at +(byte_count+4), padded to even
```
Confirmed three ways: the wireshark dissector (`lat/code/wireshark-lat/packet-lat.c`),
the captured session (`lat/local/vt-session.pcap`, e.g. RUN message
`0103 6054 0100 0201 01 01 16 9f...` = 3 slots), and the firmware's own message
builders (e.g. byte `0x44A0`, which writes bit0/bit1 then a 6-bit type field at
bit 2: `MOVE A9,*A11`; `MOVE A9,*A11(1h)`; `SETF 6h`; `MOVE A9,*A11(2h)`).
Slot types: 0=Data_A, 9=Start, 10(0xA)=Data_B, 11(0xB)=Attention,
12(0xC)=Reject, 13(0xD)=Stop.
## Key functions
| function | byte offset | bit addr (physical) |
|---|---|---|
| R_run (RUN handler) | `0xFEEA` | `0x3F27F750` |
| message dispatcher (AST) | `0x11634` | `0x3F28B1A0` |
| LAT demultiplexer | `0x16C44` | `0x3F2B6220` |
| Data_a delivery (to X/VTE) | `0xA1A2A` | `0x3F70D150` |
| fragment flattener | `0xFC1BA` | `0x3F9E0DD0` |
| insert_latjob | `0xF316` | `0x3F2798B0` |
Demux is called from R_run at byte `0x101A8` (`CALLA 3F2B6220`); its argument
`count` is the wire `nbr_slots`, read in R_run at byte `0x10140`
(`MOVE *A10(8h),A11,0`), i.e. bit offset 8 = header byte 1.
## FINDING 1 — demux walks `nbr_slots` slots with no length bound
Demux entry `0x16C44`. Per-slot dispatch (byte `0x17318`) reads the 4-bit type
from the high nibble of header byte 3:
```
0b98c0: MOVE *A10(1Ch),A8,0 ; 4-bit field at bit 28 -> type
0b98e0: SUBK 9h,A8 ; JREQ -> Start
0b9910: DEC ; JREQ -> Data_B
... Attn, Reject, Stop, else -> BAD RCV
```
Loop tail (bytes `0x1733E`–`0x1736E`):
```
0b99f0: MOVE *A10(10h),A7,0 ; A7 = slot byte_count (wire byte 2)
0b9a10: ADDK 4h,A7
0b9a20: SLL 3h,A7 ; A7 = (byte_count+4) * 8 bits
0b9a30: MOVE *A13,A5,1
0b9a40: ADD A5,A7 ; current slot ptr += (byte_count+4) bytes
0b9a50: MOVE A7,*A13,1
... ; round up for odd byte_count
0b9ad0: INC A0 ; slot index++
0b9b00: CMP A5,A0 ; A5 = count argument (nbr_slots)
0b9b10: JRNC B9B70h ; only exits when index >= nbr_slots
```
There is **no check** that the accumulated slot bytes stay inside the received
message, and the demux receives no length argument at all (its three args are
the slot buffer, `nbr_slots`, and an interface/session-table pointer).
`nbr_slots` is a full byte (0..255) and
`byte_count` is a full byte (0..255), so the walk can run up to ~66 KB past the
buffer.
Effect per pushed "slot": Data_a slots (`0x16D32` -> `3F70D150`) deliver
`byte_count` bytes from an out-of-bounds source into the X/VTE socket, so the
out-of-bounds bytes are handed to the LAT client's own X session; Start/Reject/
Stop slots mutate SCB fields from out-of-bounds bytes. This is an
out-of-bounds **read** (info leak + session-state corruption), not a write.
Confidence: **MEDIUM**. The missing self-bound is certain; exploitability
depends on whether the layer that copies the received frame into the demux
buffer already validates `nbr_slots`/`byte_count` (see Blockers).
## FINDING 2 — `host_credits < 0` checks are present as strings but dead
Data_a path stores a single byte at SCB bit-offset `0x1E0`:
```
0b6660: MOVE *A11(1E0h),A5,0 ; host_credits
0b6690: MOVE *A10(18h),A3,0 ; wire byte 3
0b66b0: ADD A3,A5
0b66c0: MOVB A5,*A11(1E0h) ; byte store — no sign handling
```
The guard messages exist but no 32-bit constant references them:
* `DMUX Data_A scb->host_credits=%d scb=0x%x < 0` @ `0x175CF` — no ref
* `dmux Data_B scb->host_credits = %d < 0` @ `0x17626` — no ref
* `Illegal slot received by remote node.` @ `0x17600` — no ref
(checked with `near_refs.py`, which would also catch a leading-space pointer.)
The same pattern in Data_b at bytes `0x16EE2`–`0x16F2C` and the multiplex/flow
control code read `*A11(1E0h)` as an unsigned byte elsewhere, so this is at
minimum a flow-control desync and a possible signedness/length bug downstream.
Confidence: **LOW–MEDIUM** (dead-check fact is solid; memory-safety impact not
proven).
## FINDING 3 — service-announcement / LAD parsing (not fully audited)
Dispatcher byte `0x11634` branches on message type; type `0xA` (service
announcement) goes to byte `0x118C0` and calls the LAD update (func `0x10660` /
`0x103CA`). That code `memcpy`s (callee `3FB44400`) with lengths derived from
the announcement and has a zero-length check (`Node=%s has zero for service
class len` @ `0x121D8`, ref byte `0x1088E`) but no confirmed upper bound. The
host controls announcements, so this is a real attack surface. Confidence:
**LOW**; needs a dedicated pass.
## Ruled out / checks that ARE present
* **Data_a delivery copy is clamped.** `3F70D150` (byte `0xA1A2A`) computes
`min(len, scb->avail)` (bytes `0xA1A4A`–`0xA1A54`) before the `memcpy` at byte
`0xA1A94`. The write into the X/VTE ring buffer cannot overflow.
* **Data_b / Attention** are ignored unless the SCB service class is 1:
`DSJ`/`DSJS` against SCB bit-offset `0x218` (bytes `0x16EF4`, `0x16FF6`), with
messages `DATA_B ignored on service class 3 or 4` (`0x1764F`) and
`ATTN_SLOT ignored on service class 3 or 4` (`0x17687`).
* **Start slot** validates the service class is 1, 3 or 4 (bytes `0x16E48`–
`0x16E58`) before accepting.
* **Unknown slot types** fall to the default branch -> `DMUL : BAD RCV`
(`0x172A2` / string `0x1773A`) and the session is torn down.
* **Frame size** is bounded in the network layer: `MAXSIZE_RCVPKT` = 1518
(`packet_size = %d exceeds MAXSIZE_RCVPKT` @ `0x81872`) and `SIZE_OF_RCV_BUFS`
= 550 (`0x8189B`).
* **insert_latjob ring** has an overflow guard ("insert_latjob : lat job ring
overflow", string `0xFC30`, ref byte `0xF364`) and wraps the index at 500
(`CMPI 0x1F3`).
## Reproduce
```sh
cd /home/arti/code/k-space/vt1200
# 1. clean linear disassembly (bit addr = byte*8)
unidasm vt1200_v22.bin -arch tms34010 -basepc 0 > /tmp/opencode/v22.asm
# 2. locate the LAT strings / their code refs
python3 lat/notes/scan_str.py vt1200_v22.bin "DMUL|DMUX|demultiplex|host_credits|insert_latjob"
python3 lat/notes/near_refs.py vt1200_v22.bin 0x175C0 0x17650 # dead "<0" checks
# 3. read the demultiplexer
python3 lat/notes/ua.py 0x16C44 0x177D2 > /tmp/opencode/demux.asm
# 4. read R_run and the Data_a delivery copy
python3 lat/notes/ua.py 0xFEEA 0x103CA > /tmp/opencode/run.asm
python3 lat/notes/ua.py 0xA19F0 0xA1B80
# 5. cross-check wire format
tshark -r lat/local/vt-session.pcap -Y 'eth.type==0x6004'
```
## Blockers / needs dynamic confirmation
1. The **frame-receive -> demux buffer** copy and the buffer size were not
located. Whether `nbr_slots` is validated against the received length
determines if Finding 1 is exploitable. The received message arrives as a
fragment chain (R_run arg4; counted by `3F9E0B80`, flattened by `3F9E0DD0`
into a small stack buffer), suggesting the demux walks a stack/message
buffer; the 3-fragment * 550-byte bound does not constrain `nbr_slots`.
2. Register/argument mapping was inferred from stack offsets (`A13(FFE0)` =
last pushed arg, spacing 4 bytes); a MAME TMS34010 run or live trace would
confirm the buffer base and size.
3. Finding 2 needs a downstream data-flow check of the signed `host_credits`
consumers (multiplex flow control, SCB bit-offset `0x1E0`).
4. Finding 3 (LAD) was only spot-checked.