diff --git a/dwt/evidence/real-unit-nvram-raw.bin b/dwt/evidence/real-unit-nvram-raw.bin new file mode 100644 index 0000000..7a15053 Binary files /dev/null and b/dwt/evidence/real-unit-nvram-raw.bin differ diff --git a/dwt/evidence/real-unit-nvram-settings.bin b/dwt/evidence/real-unit-nvram-settings.bin new file mode 100644 index 0000000..0d100f2 Binary files /dev/null and b/dwt/evidence/real-unit-nvram-settings.bin differ diff --git a/dwt/notes/REAL_UNIT.md b/dwt/notes/REAL_UNIT.md index bf180c3..bbb3764 100644 --- a/dwt/notes/REAL_UNIT.md +++ b/dwt/notes/REAL_UNIT.md @@ -251,3 +251,34 @@ The first attempt hung the Terminal Manager; this one completed. The hang is not deterministic. The mailbox watcher's reconnect storm during boot is one possible co-factor (it spawns a debug process per accepted connection), but the diagnostic reset is the other; a single success does not settle which. + +### NVRAM and LANCE address ROM, read over rdebug + +`dwt/nvram_dump.py` reads 4 KiB at bit `0x6000000` and extracts the 0x800 +settings bytes (low byte of each 16-bit word). Evidence: +`real-unit-nvram-settings.bin`, `real-unit-nvram-raw.bin`. + +Printable settings runs: + +``` +0x26f 100.103.0.10 0x28f 172.20.51.0 +0x3cf 1.1.1.1 0x3ef 8.8.8.8 +0x40f retro.k-space.ee +0x50f 100.103.0.1 +0x597 ROM 0x5e7 ROM +0x5eb 103.255.254:69 +0x637 100.103.100.103 0x657 255.255.0.0 +0x677 Messages 0x6ca / 0x6ea VTE 1 +``` + +Both font-path fields persist: the selected `ROM` and the old TFTP +`103.255.254:69`. + +The LANCE address ROM at bit `0x6800000` reads (low bytes of the words): + +``` +08 00 2b 16 4b 5a c1 86 +``` + +byte-identical to the MAME model. The `Ethernet Address ROM Test` passing is +consistent with this. diff --git a/dwt/nvram_dump.py b/dwt/nvram_dump.py new file mode 100644 index 0000000..d168f3e --- /dev/null +++ b/dwt/nvram_dump.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +"""Dump the VT1200 NVR (user settings) over rdebug `peek`. + +The NVR is at bit `0x6000000`, a 16-bit-wide region. The firmware's +`io_settings_copy_expand` copies **0x800 settings bytes, one per 16-bit word** +(and `io_settings_copy_linear` writes them back the same way). So: + + raw region 4 KiB (0x800 words x 2 bytes) + settings 2 KiB (the low byte of each word) + +The low byte of a little-endian word is the even byte of the linear byte +stream, so `raw[0::2]` is the settings block. + + python3 dwt/nvram_dump.py --host 100.103.100.103 + python3 dwt/nvram_dump.py --host HOST --out nvram.bin --raw nvram-raw.bin +""" +from __future__ import annotations + +import argparse +import sys + +import exploit + +NVR_BASE = 0x6000000 +NVR_BYTES = 0x1000 # 0x800 words x 2 bytes +SETTINGS_LEN = 0x800 # one byte per word + + +def main(argv): + ap = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--host", default="100.103.100.103") + ap.add_argument("--port", type=int, default=771) + ap.add_argument("--timeout", type=float, default=8.0) + ap.add_argument("--out", default="dwt/evidence/nvram-settings.bin") + ap.add_argument("--raw", default=None, help="also write the 4 KiB raw region") + ap.add_argument("--ascii", action="store_true", default=True, + help="print strings/ASCII (default)") + a = ap.parse_args(argv[1:]) + + rd = exploit.RDebug(a.host, a.port, a.timeout, False) + rd.connect() + raw = bytearray() + try: + for off in range(0, NVR_BYTES, exploit.MAX_XFER): + n = min(exploit.MAX_XFER, NVR_BYTES - off) + raw += rd.peek(NVR_BASE + off * 8, n) + finally: + rd.close() + + settings = bytes(raw[0::2]) + with open(a.out, "wb") as fh: + fh.write(settings) + print(f"[+] raw {len(raw)} bytes at bit {NVR_BASE:#010x}") + print(f"[+] settings {len(settings)} bytes -> {a.out}") + if a.raw: + with open(a.raw, "wb") as fh: + fh.write(raw) + print(f"[+] raw copy -> {a.raw}") + + # Known landmarks from the NVRAM report and the unit's settings. + for needle in (b"ROM", b"100.103", b"LAT_", b"DECW$FONT", b"192.168", b":69"): + idx = settings.find(needle) + if idx >= 0: + print(f" {needle!r} at settings offset 0x{idx:03x}") + + # Printable ASCII runs, useful for reading the settings block. + runs, cur = [], bytearray() + for i, b in enumerate(settings): + if 32 <= b < 127: + cur.append(b) + else: + if len(cur) >= 3: + runs.append((i - len(cur), bytes(cur))) + cur.clear() + if len(cur) >= 3: + runs.append((len(settings) - len(cur), bytes(cur))) + print(f"[+] {len(runs)} printable runs:") + for off, s in runs[:80]: + print(f" 0x{off:03x} {s.decode('latin-1')}") + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main(sys.argv)) + except (exploit.ProtocolError, OSError) as exc: + print(f"error: {exc}", file=sys.stderr) + sys.exit(1)