dwt/exploit.py: default to the computed dwt_prf answer -- echo is the known-failing mode and made a bare --peek look like a closed socket

This commit is contained in:
SDD committed 2026-10-18 11:28:00 +03:00
1 parent 0e4075958b
commit 2cc3ab1c79
1 file changed
+25 -18
+25 -18
View File
@@ -8,10 +8,12 @@ Protocol (see dwt/notes/rdebug-static.md and dwt/notes/EXPLOIT_ATTEMPT.md):
Before the first opcode >= 2 the dispatcher runs a 16-byte challenge
(dwt_prf, ROM bit 0x3F59FDC0): it sends one frame with flag 0x7F carrying 16
bytes and then reads exactly 16 raw bytes back. It compares the 16 bytes it
sent with the 16 bytes it received (the "transform" in between renders the
reply through the font code into a separate global buffer and does not modify
the reply buffer), so the answer is the challenge itself.
bytes, reads exactly 16 raw bytes back, rewrites them **in place** through a
fixed-key 16-round Feistel, and compares the result with the challenge. The
required wire reply is ``swap4(invert(swap4(challenge)))``; ``dwt/challenge.py``
implements it and is the default here. ``echo`` is kept only because it was
the historical first attempt -- it does **not** pass, and using it makes the
firmware close the session with no reply.
Examples:
@@ -46,6 +48,8 @@ import struct
import sys
import time
import challenge as _challenge # the reversed dwt_prf transform (same dir)
OP_NAMES = {
0: "pager (exits after)",
1: "printfs off",
@@ -99,7 +103,7 @@ class RDebug:
self.verbose = verbose
self.sock: socket.socket | None = None
self.challenged = False
self.challenge_type = "echo"
self.challenge_type = "computed"
self.challenge_raw: bytes | None = None
# ---- transport -------------------------------------------------------
@@ -151,25 +155,25 @@ class RDebug:
def challenge_answer(challenge: bytes, mode: str) -> bytes:
"""Compute the 16-byte answer.
The firmware stores four runtime words in host order at frame+0x20,
byte-swaps each one (`ntohl`, ROM 0x07F65828) into frame+0, and sends
*those* 16 bytes with flag 0x7F. After reading 16 bytes back it runs
them through a fixed-key font/cipher pipeline that rewrites the reply
buffer in place, and compares frame+0x20 (host order) with the result.
The firmware stores four runtime words in host order, byte-swaps each
one (`ntohl`, ROM 0x07F65828), sends *those* 16 bytes with flag 0x7F,
reads 16 bytes back, rewrites them in place through a fixed-key
16-round Feistel, and compares the result with the host-order
challenge. So the reply must be ``swap4(invert(swap4(challenge)))``,
which is what ``dwt/challenge.py`` implements.
`echo` and `computed` implement the two layers that were established
live: `echo` returns the challenge unchanged; `computed` byte-swaps
each 32-bit word, which fixes the host/network order layer. The
firmware's in-place transform is a second, nonlinear layer; on the
tested image neither mode passes it (memcmp returns -1 for echo and 1
for computed). `--challenge-raw` allows a fully custom answer once the
transform is reversed.
``computed`` (the default) uses that inverse and passes. ``echo``
returns the challenge unchanged and ``swap`` only fixes the byte
order; both fail and make the firmware kill the session, and are kept
for reference only.
"""
if len(challenge) != 16:
raise ProtocolError(f"challenge is {len(challenge)} bytes, not 16")
if mode == "echo":
return bytes(challenge)
if mode == "computed":
return _challenge.respond(bytes(challenge))
if mode == "swap": # historical: byte-order layer only, fails
out = bytearray()
for i in range(0, 16, 4):
out += challenge[i:i + 4][::-1]
@@ -360,7 +364,10 @@ def main(argv: list[str]) -> int:
ap.add_argument("--port", type=int, default=771)
ap.add_argument("--timeout", type=float, default=5.0,
help="socket timeout for ordinary command replies")
ap.add_argument("--challenge", choices=("echo", "computed"), default="echo")
ap.add_argument("--challenge", choices=("computed", "echo", "swap"),
default="computed",
help="challenge answer: computed (default, passes), or "
"echo/swap, which are historical and fail")
ap.add_argument("--challenge-raw", metavar="HEX",
help="override the 16-byte challenge answer (experiment)")
ap.add_argument("--byte-addr", action="store_true",