Files
vt1200/dwt/visual/msg_demo.py
T

140 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""Send opcode-8 messages over rdebug and take frame snapshots around them.
This is the host driver for the Messages-box experiments in
``dwt/notes/DISPLAY_MESSAGE.md``. It imports ``dwt/exploit.py`` (which owns
the protocol) and patches the challenge answer with ``dwt/challenge.respond``
exactly like ``dwt/visual/inject.py`` and ``dwt/solve.py`` -- no debugger, no
breakpoints, no RAM patch.
python3 dwt/visual/msg_demo.py --wait 30 \
--message '0:ATTACKER MESSAGE ONE' \
--message '0:RDEBUG TEXT TWO' \
--message '3:LEVEL THREE TERTIARY' \
--before-prefix msg_before --persist-frames 1200 \
--persist-prefix msg_persist
The trigger file is consumed by ``dwt/visual/msg_snap.lua``:
``"<prefix> <count> <stride>"`` snapshots ``count`` frames, ``stride`` apart.
"""
from __future__ import annotations
import argparse
import os
import sys
import time
HERE = os.path.dirname(os.path.abspath(__file__))
DWT = os.path.dirname(HERE)
sys.path.insert(0, DWT)
import challenge # noqa: E402
import exploit # noqa: E402
_orig_answer = exploit.RDebug.answer_challenge
def _answer(self, challenge_bytes: bytes) -> None:
self.challenge_raw = challenge.respond(challenge_bytes)
print(f"[+] challenge {challenge_bytes.hex(' ')}", flush=True)
print(f"[+] reply {self.challenge_raw.hex(' ')}", flush=True)
return _orig_answer(self, challenge_bytes)
exploit.RDebug.answer_challenge = _answer
def trigger(path: str, prefix: str, count: int, stride: int) -> None:
with open(path, "w") as fh:
fh.write(f"{prefix} {count} {stride}")
print(f"[.] snapshot trigger {prefix} x{count} every {stride}", flush=True)
def parse_message(spec: str) -> tuple[int, bytes]:
level_text, sep, text = spec.partition(":")
if not sep:
raise argparse.ArgumentTypeError("message must be LEVEL:TEXT")
return int(level_text, 0), text.encode("latin-1")
def main(argv: list[str]) -> int:
ap = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--host", default="192.168.100.2")
ap.add_argument("--port", type=int, default=771)
ap.add_argument("--timeout", type=float, default=8.0)
ap.add_argument("--wait", type=float, default=0.0,
help="seconds to keep retrying connect (0 = one attempt)")
ap.add_argument("--trigger", default=None,
help="msg_snap.lua trigger file")
ap.add_argument("--message", type=parse_message, action="append",
default=[], metavar="LEVEL:TEXT",
help="send one opcode-8 message (repeatable); a NUL byte "
"is appended because the handler does not terminate")
ap.add_argument("--before-prefix", default=None)
ap.add_argument("--after-prefix", default="msg_send")
ap.add_argument("--after-frames", type=int, default=4)
ap.add_argument("--after-stride", type=int, default=2)
ap.add_argument("--persist-frames", type=int, default=0,
help="wait this many emulated frames after the last send")
ap.add_argument("--persist-prefix", default="msg_persist")
ap.add_argument("--keepalive", action="store_true",
help="send opcode 7 after the messages (proves the "
"session/process still answers)")
a = ap.parse_args(argv[1:])
deadline = time.monotonic() + a.wait
rd = exploit.RDebug(a.host, a.port, a.timeout, False)
while True:
try:
rd.connect()
break
except OSError as exc:
if time.monotonic() >= deadline:
print(f"error: no TCP at {a.host}:{a.port}: {exc}",
file=sys.stderr)
return 2
time.sleep(1.0)
try:
flag, payload = rd.liveness(7)
print(f"opcode 7 -> flag={flag:#04x} payload={payload.hex(' ')} "
f"({payload!r})", flush=True)
if a.before_prefix and a.trigger:
trigger(a.trigger, a.before_prefix, 1, 0)
time.sleep(0.5) # let the frame hook consume it before the send
for i, (level, text) in enumerate(a.message, 1):
# The handler allocates len+2 but never writes the terminator, so
# the sender must include the NUL; otherwise stale heap bytes are
# printed after the text.
wire = text + b"\x00"
body = exploit.be32(level) + exploit.be32(len(wire)) + wire
print(f"[>] opcode 8 body {body.hex(' ')}", flush=True)
if a.trigger:
trigger(a.trigger, f"{a.after_prefix}{i}",
a.after_frames, a.after_stride)
rd.display_message(level, wire)
print(f"[.] message {i} level={level} {text!r}: Done", flush=True)
if a.keepalive:
flag, payload = rd.liveness(7)
print(f"post-message opcode 7 -> flag={flag:#04x} "
f"payload={payload.hex(' ')} ({payload!r})", flush=True)
if a.persist_frames and a.trigger:
# ~60 fps emulated; the clock file is not used here, so this is
# wall time at whatever speed MAME runs. The caller picks a value
# that covers the intended span.
time.sleep(a.persist_frames / 300.0)
trigger(a.trigger, a.persist_prefix, 4, 30)
time.sleep(0.5)
return 0
except (OSError, exploit.ProtocolError, ValueError) as exc:
print(f"error: {exc}", file=sys.stderr)
return 2
finally:
rd.close()
if __name__ == "__main__":
raise SystemExit(main(sys.argv))