From c0f142300b4f043ce9f74744bd53818697856900 Mon Sep 17 00:00:00 2001 From: SDD Date: Sun, 18 Oct 2026 13:49:19 +0300 Subject: [PATCH] dwt/screenshot.py: network screenshot over rdebug peek (VRAM -> PNG), verified against the live OS --- dwt/evidence/shot-canvas.png | Bin 0 -> 8628 bytes dwt/evidence/shot-screen.png | Bin 0 -> 9656 bytes dwt/screenshot.py | 216 +++++++++++++++++++++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 dwt/evidence/shot-canvas.png create mode 100644 dwt/evidence/shot-screen.png create mode 100644 dwt/screenshot.py diff --git a/dwt/evidence/shot-canvas.png b/dwt/evidence/shot-canvas.png new file mode 100644 index 0000000000000000000000000000000000000000..20b2312fdf8a6680ac7ef5dd9503073efed91b73 GIT binary patch literal 8628 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 j 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 tuple[int, int]: + if ":" in text: + a, b = text.split(":", 1) + start, end = int(a, 0), int(b, 0) + else: + start, end = 0, int(text, 0) + if not (0 <= start < end <= HEIGHT): + raise argparse.ArgumentTypeError( + f"rows must satisfy 0 <= start < end <= {HEIGHT}") + return start, end + + +def decode(data: bytes, width: int = WIDTH) -> bytearray: + """LSB-first bits -> 8-bit grayscale (bit 1 = white).""" + rows = len(data) // ROW_BYTES + out = bytearray(rows * width) + for r in range(rows): + src = r * ROW_BYTES + dst = r * width + for x in range(width): + v = (data[src + (x >> 3)] >> (x & 7)) & 1 + out[dst + x] = 255 if v else 0 + return out + + +def compose_screen(gray: bytearray, rows: int, sw: int, sh: int, + ox: int, oy: int) -> bytearray: + canvas = bytearray(sw * sh) + for r in range(rows): + y = r + oy + if 0 <= y < sh: + xs = max(0, ox) + xe = min(sw, ox + WIDTH) + if xe > xs: + canvas[y * sw + xs:y * sw + xe] = \ + gray[r * WIDTH + (xs - ox):r * WIDTH + (xe - ox)] + return canvas + + +def write_png(path: str, width: int, height: int, gray: bytes) -> None: + raw = bytearray() + for y in range(height): + raw.append(0) # filter: none + raw += gray[y * width:(y + 1) * width] + comp = zlib.compress(bytes(raw), 9) + + def chunk(tag: bytes, data: bytes) -> bytes: + return (struct.pack(">I", len(data)) + tag + data + + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)) + + png = b"\x89PNG\r\n\x1a\n" + png += chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 0, 0, 0, 0)) + png += chunk(b"IDAT", comp) + png += chunk(b"IEND", b"") + with open(path, "wb") as f: + f.write(png) + + +def grab(rd: exploit.RDebug, row_start: int, row_end: int, + quiet: bool) -> bytes: + start = row_start * ROW_BYTES + end = row_end * ROW_BYTES + data = bytearray() + pos = start + nchunks = (end - start + CHUNK - 1) // CHUNK + done = 0 + while pos < end: + n = min(CHUNK, end - pos) + data += rd.peek(VRAM_BASE_BITS + pos * 8, n) + pos += n + done += 1 + if not quiet and nchunks > 8 and (done % 16 == 0 or done == nchunks): + print(f"\r[.] {done}/{nchunks} chunks", end="", file=sys.stderr, + flush=True) + if not quiet and nchunks > 8: + print(file=sys.stderr) + return bytes(data) + + +def one(rd: exploit.RDebug, a: argparse.Namespace, tag: str) -> None: + data = grab(rd, a.rows[0], a.rows[1], a.quiet) + rows = len(data) // ROW_BYTES + gray = decode(data) + if a.invert: + gray = bytearray(255 - v for v in gray) + + out = a.out + if a.repeat > 1: + root, dot, ext = out.rpartition(".") + out = f"{root}-{tag}.{ext}" if dot else f"{out}-{tag}" + if a.screen: + canvas = compose_screen(gray, rows, a.screen_width, a.screen_height, + a.offset_x, a.offset_y) + write_png(out, a.screen_width, a.screen_height, canvas) + print(f"[+] {out}: {a.screen_width}x{a.screen_height} " + f"(canvas {WIDTH}x{HEIGHT}, rows {a.rows[0]}..{a.rows[1]-1})") + else: + write_png(out, WIDTH, rows, gray) + print(f"[+] {out}: {WIDTH}x{rows} (rows {a.rows[0]}..{a.rows[1]-1})") + + if a.raw: + rp = a.raw + if a.repeat > 1: + root, dot, ext = rp.rpartition(".") + rp = f"{root}-{tag}.{ext}" if dot else f"{rp}-{tag}" + with open(rp, "wb") as f: + f.write(data) + print(f"[+] {rp}: {len(data)} bytes of VRAM") + + +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("--out", default="shot.png") + ap.add_argument("--rows", type=parse_rows, default=(0, HEIGHT), + metavar="A:B", help=f"row range to read (default 0:{HEIGHT})") + ap.add_argument("--screen", action="store_true", + help="compose the visible screen instead of the raw canvas") + ap.add_argument("--screen-width", type=int, default=SCREEN_W) + ap.add_argument("--screen-height", type=int, default=SCREEN_H) + ap.add_argument("--offset-x", type=int, default=OFFSET_X) + ap.add_argument("--offset-y", type=int, default=OFFSET_Y) + ap.add_argument("--invert", action="store_true", help="white <-> black") + ap.add_argument("--raw", metavar="FILE", help="also write the raw VRAM bytes") + ap.add_argument("--repeat", type=int, default=1, help="grab N times") + ap.add_argument("--interval", type=float, default=0.0, + help="seconds between repeats") + ap.add_argument("--quiet", action="store_true") + ap.add_argument("--verbose", action="store_true") + a = ap.parse_args(argv[1:]) + + rd = exploit.RDebug(a.host, a.port, a.timeout, a.verbose) + rd.connect() + try: + rd.liveness(7) # cheap reachability check + for i in range(max(1, a.repeat)): + if i: + time.sleep(a.interval) + one(rd, a, time.strftime("%H%M%S") if a.repeat > 1 else "1") + finally: + rd.close() + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main(sys.argv)) + except exploit.ProtocolError as exc: + print(f"error: {exc}", file=sys.stderr) + sys.exit(1) + except OSError as exc: + print(f"error: {exc}", file=sys.stderr) + sys.exit(2)