commit 79612197a1586d483d2fdb9183eb931a7b27423a Author: Indiana Holmes Date: Thu Sep 10 18:12:42 2026 +0000 Card-Crack: Cardputer ESP32-S3 home-lab recon toolkit Manual-trigger firmware toolkit for pentesting owned devices: - Pin Scan: UART/JTAG/SWD detection (baud est + IDCODE reads) - V-Sense: target voltage probe w/ logic-family guess - UART Sniff: passive capture + manual-only frame replay - USB Enum: ESP32-S3 host, read-only descriptor fingerprinting - DefCred: single-host factory-default login check, rate-limited Modular shell (core/ + modules/), PlatformIO build, hardware/safety docs. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01AAhMHMRAQLQ9hSbBECKNfn diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..90d142e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.pio/ +.vscode/ +*.bin +*.elf +*.map +build/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..6fb3424 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# Card-Crack + +A manual-trigger hardware/network **recon toolkit** for the +[M5Stack Cardputer](https://docs.m5stack.com/en/core/Cardputer) (ESP32-S3), +built for **home-lab pentesting of devices you own** — your router, a car's +OBD/USB port, a PC, smart appliances. + +> ⚠️ **Authorized use only.** Every tool here is scoped for your own bench: +> single targets you enter by hand, curated default-credential lists (not +> brute-force), rate-limited network checks, and read-only descriptor/ID +> reads. Nothing scans, transmits, or replays on its own — you press the +> action key. Don't point it at anything you don't own or run. + +## Modules + +| Module | What it does | +|-------------|---------------------------------------------------------------------| +| **Pin Scan**| JTAGulator-style detection of **UART / JTAG / SWD** pins on the probe header. UART baud estimation; JTAG & SWD IDCODE reads. | +| **V-Sense** | Live target-voltage probe through a divider; guesses the logic family so you know what you're touching before driving a pin. | +| **UART Sniff** | Passive UART capture (hex+ascii, selectable baud); freeze a frame and **replay it on a keypress** (manual TX only). | +| **USB Enum**| ESP32-S3 as USB host — reads **descriptors** (VID/PID, class, strings, config) to fingerprint an unknown device. Read-only. | +| **DefCred** | Checks a single host (default: gateway) against a short list of **factory-default logins** over HTTP Basic Auth. Stops on first hit. | + +## Build & flash + +Requires [PlatformIO](https://platformio.org/). + +```bash +# set your lab Wi-Fi for the DefCred module (or edit defcred.cpp) +pio run -e cardputer \ + -a "--build-property build.flags=-DCC_WIFI_SSID='\"MyAP\"' -DCC_WIFI_PASS='\"secret\"'" +pio run -e cardputer -t upload +pio device monitor +``` + +## Controls + +- Menu: `;` up · `.` down · `Enter` open +- In a module: ` (backtick) back · per-module hints on the bottom bar + +## Layout + +``` +src/core/ shell, module interface, UI helpers, pin map +src/modules/ one file per recon tool +docs/ HARDWARE.md wiring + safety +``` + +See **[docs/HARDWARE.md](docs/HARDWARE.md)** before wiring — the S3 is a 3V3 +part and 5V on a bare GPIO will destroy it. diff --git a/docs/HARDWARE.md b/docs/HARDWARE.md new file mode 100644 index 0000000..f2118d7 --- /dev/null +++ b/docs/HARDWARE.md @@ -0,0 +1,41 @@ +# Hardware & wiring — read before probing + +The Cardputer's ESP32-S3 is a **3.3 V** part. Several targets you'll poke are +not: + +- Car OBD/USB, PC USB: **5 V** VBUS. +- Some debug headers idle at 1.8 V. + +**Always** go through protection. Never land a probe on a live target until +V-Sense has told you the voltage. + +## Probe header + +`src/core/pins.h` defines `pins::PROBE[]`. By default: + +- `G1`, `G2` — the side Grove connector (safe, use these first). +- `G8..G13` — StampS3 pads via a breakout ribbon (optional). + +## Minimum protection rig + +| Signal | Between probe and target | +|---------------|-------------------------------------------------------| +| Any GPIO in | 1 kΩ series + 3.3 V clamp (BAT54S to 3V3/GND) | +| V-Sense (G10) | resistor divider R1:R2 = 2:1 → 0–9.9 V range (ratio 3)| +| Level shift | bidirectional shifter (e.g. TXS0108) for 5 V buses | + +Adjust `pins::VSENSE_RATIO` if you change the divider. + +## USB host + +To use **USB Enum** the S3 must supply VBUS to the target as a host. Use an +OTG adapter on the S3 USB port, keep the CDC console on the built-in USB, and +never hot-plug a 5 V target onto a GPIO — it goes on the USB D+/D-/VBUS lines +only. + +## Safety checklist + +1. Target powered from its **own** supply, common ground with the Cardputer. +2. V-Sense the line first. If it reads ≥ 5 V, shift or stop. +3. Start passive (Pin Scan / UART Sniff). Only replay/TX when you mean to. +4. It's your device. Keep it that way. diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..182ef94 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,23 @@ +; Card-Crack — Cardputer (ESP32-S3) pentest recon toolkit +; Home-lab / authorized use only. +[platformio] +default_envs = cardputer +src_dir = src + +[env:cardputer] +platform = espressif32@6.9.0 +board = m5stack-stamps3 +framework = arduino +board_build.flash_mode = qio +board_build.f_flash = 80000000L +board_build.partitions = default_16MB.csv +monitor_speed = 115200 +upload_speed = 1500000 +build_flags = + -D CORE_DEBUG_LEVEL=1 + -D ARDUINO_USB_MODE=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D CARDCRACK_VERSION=\"0.1.0\" +lib_deps = + m5stack/M5Cardputer@^1.0.3 + m5stack/M5GFX@^0.2.3 diff --git a/src/core/module.h b/src/core/module.h new file mode 100644 index 0000000..d0ae7c8 --- /dev/null +++ b/src/core/module.h @@ -0,0 +1,39 @@ +#pragma once +#include + +// Card-Crack module interface. +// +// Every recon tool is a Module. The shell owns the screen and the +// keyboard; a module only gets called on the events below and draws +// into the content area the shell hands it. All modules are +// MANUAL-TRIGGER: nothing scans, transmits, or replays until the user +// presses the action key. This is deliberate — passive-by-default keeps +// you from poking a live bus you didn't mean to. + +class Module { +public: + virtual ~Module() {} + + // Short name shown in the main menu. + virtual const char* name() const = 0; + + // One-line description shown under the title bar. + virtual const char* blurb() const = 0; + + // Called once when the module becomes active. Set up peripherals here. + virtual void onEnter() {} + + // Called when leaving. Release GPIOs / buses back to hi-Z. + virtual void onExit() {} + + // Called every shell tick (~30 Hz). Do incremental work, never block + // longer than a frame; long scans must be chunked and resumable. + virtual void tick() {} + + // A key was pressed. Return true if the module consumed it. + // ` (backtick) is reserved by the shell for "back to menu". + virtual bool onKey(char c) { return false; } + + // Draw the module body. The shell has already drawn the chrome. + virtual void draw() {} +}; diff --git a/src/core/pins.h b/src/core/pins.h new file mode 100644 index 0000000..9ee7239 --- /dev/null +++ b/src/core/pins.h @@ -0,0 +1,28 @@ +#pragma once +// Card-Crack — Cardputer probe/target pin map. +// +// The Cardputer exposes a small set of free GPIOs on the bottom +// header (Grove-compatible G1/G2) plus the internal StampS3 pads. +// These are the lines the recon modules drive/read. Keep the count +// small: JTAGulator-style scanning is O(pins!) in the worst case. +// +// NOTE: only wire probes to the target through a level shifter / +// series resistors. The ESP32-S3 is a 3V3 part; 5V (car USB, PC USB +// VBUS) on a bare GPIO will kill it. See docs/HARDWARE.md. + +namespace pins { +// Grove port (safe, broken out on the side connector) +constexpr int GROVE_A = 1; // G1 +constexpr int GROVE_B = 2; // G2 + +// Extra probe channels (StampS3 exposed pads via ribbon/breakout). +// Order matters: index == logical channel number shown in the UI. +constexpr int PROBE[] = {1, 2, 8, 9, 10, 11, 12, 13}; +constexpr int PROBE_COUNT = sizeof(PROBE) / sizeof(PROBE[0]); + +// ADC channel used by the voltage-sense module (through a divider). +constexpr int VSENSE_ADC = 10; + +// Divider ratio: Vin = Vadc * ((R1+R2)/R2). Default 1:3 (0-9.9V). +constexpr float VSENSE_RATIO = 3.0f; +} diff --git a/src/core/shell.cpp b/src/core/shell.cpp new file mode 100644 index 0000000..2a7f2bc --- /dev/null +++ b/src/core/shell.cpp @@ -0,0 +1,71 @@ +#include "shell.h" +#include "ui.h" +#include + +// Module factories (defined in each module .cpp). +Module* makePinScan(); +Module* makeVSense(); +Module* makeUartSniff(); +Module* makeUsbEnum(); +Module* makeDefCred(); + +void Shell::begin() { + add(makePinScan()); + add(makeVSense()); + add(makeUartSniff()); + add(makeUsbEnum()); + add(makeDefCred()); + M5.Display.fillScreen(ui::bg()); + drawMenu(); +} + +void Shell::drawMenu() { + M5.Display.fillScreen(ui::bg()); + ui::titleBar("Card-Crack", CARDCRACK_VERSION); + for (int i = 0; i < nmods; i++) { + bool cur = i == sel; + ui::lineC(i, cur ? ui::accent() : ui::fg(), "%c %-10s %s", + cur ? '>' : ' ', mods[i]->name(), mods[i]->blurb()); + } + ui::hintBar("arrows move [enter]open home-lab only"); +} + +void Shell::enter(int i) { + active = i; + M5.Display.fillScreen(ui::bg()); + ui::titleBar(mods[i]->name(), mods[i]->blurb()); + mods[i]->onEnter(); + mods[i]->draw(); +} + +void Shell::back() { + if (active >= 0) mods[active]->onExit(); + active = -1; + drawMenu(); +} + +void Shell::loop() { + M5Cardputer.update(); + + if (M5Cardputer.Keyboard.isChange() && M5Cardputer.Keyboard.isPressed()) { + auto st = M5Cardputer.Keyboard.keysState(); + for (char c : st.word) { + if (c == '`') { if (active >= 0) back(); continue; } + if (active < 0) { + if (c == ';') { sel = (sel - 1 + nmods) % nmods; drawMenu(); } // up + else if (c == '.') { sel = (sel + 1) % nmods; drawMenu(); } // down + else if (c == '\r' || c == ' ') enter(sel); + } else { + mods[active]->onKey(c); + } + } + if (st.enter && active < 0) enter(sel); + } + + // ~30 Hz tick for the active module. + if (active >= 0 && millis() - lastTick > 33) { + lastTick = millis(); + mods[active]->tick(); + mods[active]->draw(); + } +} diff --git a/src/core/shell.h b/src/core/shell.h new file mode 100644 index 0000000..816c089 --- /dev/null +++ b/src/core/shell.h @@ -0,0 +1,20 @@ +#pragma once +#include "module.h" + +// The shell: owns the module list, the main menu, and event routing. +// Backtick (`) always returns to the menu from inside a module. +class Shell { +public: + void begin(); + void loop(); +private: + static constexpr int MAX = 12; + Module* mods[MAX]; int nmods = 0; + int sel = 0; // menu cursor + int active = -1; // -1 == in menu + uint32_t lastTick = 0; + void add(Module* m) { if (nmods < MAX) mods[nmods++] = m; } + void drawMenu(); + void enter(int i); + void back(); +}; diff --git a/src/core/ui.cpp b/src/core/ui.cpp new file mode 100644 index 0000000..227cc60 --- /dev/null +++ b/src/core/ui.cpp @@ -0,0 +1,53 @@ +#include "ui.h" +#include + +namespace ui { + +void titleBar(const char* title, const char* blurb) { + auto& d = M5.Display; + d.fillRect(0, 0, W, BODY_Y, d.color565(24, 28, 34)); + d.setTextColor(accent(), d.color565(24, 28, 34)); + d.setTextSize(1); + d.setCursor(3, 4); + d.print(title); + if (blurb && *blurb) { + d.setTextColor(fg(), d.color565(24, 28, 34)); + // Right-align-ish: just continue after a separator. + d.print(" "); + d.print(blurb); + } +} + +void hintBar(const char* hint) { + auto& d = M5.Display; + int y = H - 12; + d.fillRect(0, y, W, 12, d.color565(24, 28, 34)); + d.setTextColor(d.color565(150, 156, 164), d.color565(24, 28, 34)); + d.setTextSize(1); + d.setCursor(3, y + 2); + d.print(hint); +} + +void clearBody() { + M5.Display.fillRect(0, BODY_Y, W, BODY_H, bg()); +} + +static void vline(int row, uint16_t color, const char* fmt, va_list ap) { + char buf[64]; + vsnprintf(buf, sizeof(buf), fmt, ap); + auto& d = M5.Display; + int y = BODY_Y + row * 12; + d.fillRect(0, y, W, 12, bg()); + d.setTextColor(color, bg()); + d.setTextSize(1); + d.setCursor(3, y + 1); + d.print(buf); +} + +void line(int row, const char* fmt, ...) { + va_list ap; va_start(ap, fmt); vline(row, fg(), fmt, ap); va_end(ap); +} +void lineC(int row, uint16_t color, const char* fmt, ...) { + va_list ap; va_start(ap, fmt); vline(row, color, fmt, ap); va_end(ap); +} +} diff --git a/src/core/ui.h b/src/core/ui.h new file mode 100644 index 0000000..b5a39bd --- /dev/null +++ b/src/core/ui.h @@ -0,0 +1,25 @@ +#pragma once +#include + +// Tiny drawing helpers shared by all modules. The Cardputer screen is +// 240x135. We reserve a 16px title bar and a 12px hint bar, leaving a +// 240x107 body starting at y=16. + +namespace ui { +constexpr int W = 240, H = 135; +constexpr int BODY_Y = 16, BODY_H = 107; + +// Palette (RGB565 via M5GFX helpers). +inline uint16_t bg() { return M5.Display.color565(12, 14, 18); } +inline uint16_t fg() { return M5.Display.color565(210, 214, 220); } +inline uint16_t accent(){ return M5.Display.color565(80, 200, 120); } +inline uint16_t warn() { return M5.Display.color565(230, 170, 60); } +inline uint16_t bad() { return M5.Display.color565(220, 80, 80); } + +void titleBar(const char* title, const char* blurb); +void hintBar(const char* hint); +void clearBody(); +// Print a line at body row `row` (0-based, 12px pitch). +void line(int row, const char* fmt, ...); +void lineC(int row, uint16_t color, const char* fmt, ...); +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..1934b9e --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,21 @@ +#include +#include "core/shell.h" + +// Card-Crack — Cardputer (ESP32-S3) recon toolkit for home-lab hardware +// pentesting. Manual-trigger tools only; see README and docs/HARDWARE.md +// before wiring anything to a live target. + +static Shell shell; + +void setup() { + auto cfg = M5.config(); + M5Cardputer.begin(cfg, true); + M5.Display.setRotation(1); + M5.Display.setTextFont(&fonts::Font0); + shell.begin(); +} + +void loop() { + shell.loop(); + delay(2); +} diff --git a/src/modules/defcred.cpp b/src/modules/defcred.cpp new file mode 100644 index 0000000..af73cbb --- /dev/null +++ b/src/modules/defcred.cpp @@ -0,0 +1,114 @@ +#include "../core/module.h" +#include "../core/ui.h" +#include +#include +#include "mbedtls/base64.h" + +// Default-credential checker for devices on YOUR network. Point it at a +// host (default = the gateway) and it walks a short list of well-known +// vendor default logins, trying HTTP Basic Auth against the admin path. +// It reports which pair the device ACCEPTS so you can go change it. +// +// Scope guards, on purpose: +// * one host at a time, entered by you — no subnet sweeping; +// * a tiny curated default list — this is a "did you forget to change +// the factory password" check, not a brute-force/dictionary attack; +// * a delay between attempts so it can't hammer a device; +// * stops on the first success. +// +// Join your own AP first (creds compiled below, or reflash to change). + +#ifndef CC_WIFI_SSID +#define CC_WIFI_SSID "set-me" +#endif +#ifndef CC_WIFI_PASS +#define CC_WIFI_PASS "set-me" +#endif + +class DefCred : public Module { + struct Cred { const char* u; const char* p; }; + // Common factory defaults across consumer routers/appliances. + static constexpr Cred LIST[] = { + {"admin", "admin"}, {"admin", "password"}, {"admin", ""}, + {"admin", "1234"}, {"root", "root"}, {"root", "admin"}, + {"user", "user"}, {"admin", "12345678"}, {"support", "support"}, + }; + static constexpr int N = sizeof(LIST) / sizeof(LIST[0]); + + String host = "192.168.1.1"; + String path = "/"; + char status[3][40] = {{0},{0},{0}}; + bool running = false; + int ix = 0; + uint32_t lastTry = 0; + +public: + const char* name() const override { return "DefCred"; } + const char* blurb() const override { return "default login check"; } + + void onEnter() override { + running = false; ix = 0; + if (WiFi.status() != WL_CONNECTED) { + WiFi.mode(WIFI_STA); + WiFi.begin(CC_WIFI_SSID, CC_WIFI_PASS); + say("joining %s...", CC_WIFI_SSID); + } + if (WiFi.status() == WL_CONNECTED) host = WiFi.gatewayIP().toString(); + } + + bool onKey(char c) override { + if (c == ' ' || c == '\r') { + if (WiFi.status() != WL_CONNECTED) { say("wifi not up yet"); return true; } + running = !running; if (running) { ix = 0; say("target %s", host.c_str()); } + return true; + } + return false; + } + + void tick() override { + if (WiFi.status() == WL_CONNECTED && host == "192.168.1.1") + host = WiFi.gatewayIP().toString(); + if (!running) return; + if (millis() - lastTry < 600) return; // rate-limit + lastTry = millis(); + if (ix >= N) { running = false; say("-- no default creds worked --"); return; } + tryOne(LIST[ix]); + ix++; + } + + void draw() override { + bool up = WiFi.status() == WL_CONNECTED; + ui::lineC(0, up ? ui::accent() : ui::warn(), "wifi:%s host:%s", + up ? "up" : "down", host.c_str()); + ui::line(1, "trying %d/%d %s", ix, N, running ? "RUN" : "idle"); + for (int i = 0; i < 3; i++) ui::line(3 + i, "%s", status[i]); + ui::hintBar("[space]start/stop [`]back"); + } + +private: + void say(const char* fmt, ...) { + for (int i = 2; i > 0; i--) strncpy(status[i], status[i-1], 39); + va_list ap; va_start(ap, fmt); + vsnprintf(status[0], 40, fmt, ap); va_end(ap); + } + + void tryOne(const Cred& c) { + HTTPClient http; + String url = "http://" + host + path; + if (!http.begin(url)) { say("begin fail"); return; } + String token = String(c.u) + ":" + c.p; + unsigned char enc[128]; size_t olen = 0; + mbedtls_base64_encode(enc, sizeof(enc), &olen, + (const unsigned char*)token.c_str(), token.length()); + http.addHeader("Authorization", "Basic " + String((char*)enc).substring(0, olen)); + http.setConnectTimeout(1500); + int code = http.GET(); + if (code == 200) { say("OK! %s:%s <-- CHANGE IT", c.u, c.p[0] ? c.p : "(blank)"); running = false; } + else if (code == 401) say("401 %s:%s", c.u, c.p[0] ? c.p : "-"); + else say("%d %s:%s", code, c.u, c.p[0] ? c.p : "-"); + http.end(); + } +}; +constexpr DefCred::Cred DefCred::LIST[]; + +Module* makeDefCred() { return new DefCred(); } diff --git a/src/modules/pinscan.cpp b/src/modules/pinscan.cpp new file mode 100644 index 0000000..11ec302 --- /dev/null +++ b/src/modules/pinscan.cpp @@ -0,0 +1,192 @@ +#include "../core/module.h" +#include "../core/ui.h" +#include "../core/pins.h" + +// Pin-detection scanner. Three passive/semi-active discovery modes on +// the PROBE channels: +// +// UART : hold each candidate as input, watch for line activity, then +// bit-time the shortest low pulse to estimate baud. Optional +// TX-poke ("\r\n") on a paired pin to elicit a boot banner. +// JTAG : IDCODE scan — drive TCK/TMS on candidate pairs, shift TDO, +// look for a valid 32-bit IDCODE (bit0==1, != all-ones). +// SWD : line-reset + read IDCODE via SWD-DP (ARM ADIv5). Two wires +// (SWCLK/SWDIO), so the permutation space is small. +// +// All modes are manual-trigger and chunked across ticks so the UI stays +// live and you can abort. This is discovery only: it identifies pins and +// reads public ID registers. It does not halt cores or dump memory. + +class PinScan : public Module { + enum Mode { UART, JTAG, SWD } mode = UART; + bool running = false; + int step = 0; // permutation cursor + int found = 0; + char last[3][40] = {{0},{0},{0}}; + +public: + const char* name() const override { return "Pin Scan"; } + const char* blurb() const override { return "UART/JTAG/SWD detect"; } + + void onEnter() override { + for (int i = 0; i < pins::PROBE_COUNT; i++) + pinMode(pins::PROBE[i], INPUT); // hi-Z until we act + running = false; step = 0; found = 0; + } + void onExit() override { + for (int i = 0; i < pins::PROBE_COUNT; i++) + pinMode(pins::PROBE[i], INPUT); + } + + bool onKey(char c) override { + if (c == 'm') { mode = (Mode)((mode + 1) % 3); running = false; step = 0; found = 0; return true; } + if (c == ' ' || c == '\r') { running = !running; if (running) { step = 0; found = 0; } return true; } + return false; + } + + void tick() override { + if (!running) return; + // Do one permutation per tick (bounded work -> responsive UI). + switch (mode) { + case UART: scanUartStep(); break; + case JTAG: scanJtagStep(); break; + case SWD: scanSwdStep(); break; + } + } + + void draw() override { + const char* mn = mode == UART ? "UART" : mode == JTAG ? "JTAG" : "SWD"; + ui::lineC(0, ui::accent(), "mode:%-4s %s", mn, running ? "SCANNING" : "idle"); + ui::line(1, "chans:%d perm:%d hits:%d", pins::PROBE_COUNT, step, found); + for (int i = 0; i < 3; i++) + ui::line(3 + i, "%s", last[i]); + ui::hintBar("[m]ode [space]start/stop [`]back"); + } + +private: + void record(const char* s) { + for (int i = 2; i > 0; i--) strncpy(last[i], last[i-1], 39); + strncpy(last[0], s, 39); last[0][39] = 0; + } + + // --- UART --------------------------------------------------------- + // Sample a candidate RX pin, measure the narrowest pulse, map to baud. + void scanUartStep() { + int ch = step % pins::PROBE_COUNT; + int pin = pins::PROBE[ch]; + pinMode(pin, INPUT_PULLUP); + uint32_t minLow = measureMinPulse(pin, 3000 /*us window*/); + if (minLow) { + long baud = pulseToBaud(minLow); + char b[40]; snprintf(b, sizeof(b), "UART? G%d ~%ld 8N1", pin, baud); + record(b); found++; + } + if (++step >= pins::PROBE_COUNT) { running = false; record("-- uart scan done --"); } + } + + uint32_t measureMinPulse(int pin, uint32_t windowUs) { + uint32_t t0 = micros(), minLow = 0; + int last = digitalRead(pin); + uint32_t edge = micros(); + while (micros() - t0 < windowUs) { + int now = digitalRead(pin); + if (now != last) { + uint32_t w = micros() - edge; + if (last == LOW && (minLow == 0 || w < minLow)) minLow = w; + edge = micros(); last = now; + } + } + return minLow; + } + + long pulseToBaud(uint32_t us) { + static const long std[] = {9600, 19200, 38400, 57600, 115200, 230400, 460800}; + long est = 1000000L / (long)us; // one bit-time + long best = std[0]; long bd = 1L<<30; + for (long s : std) { long d = labs(s - est); if (d < bd) { bd = d; best = s; } } + return best; + } + + // --- JTAG (IDCODE) ------------------------------------------------ + // Try ordered (TCK,TMS,TDO) triples; TDI is optional for IDCODE. + void scanJtagStep() { + int n = pins::PROBE_COUNT; + int tck = step / (n * n) % n; + int tms = step / n % n; + int tdo = step % n; + if (tck != tms && tms != tdo && tck != tdo) { + uint32_t id = readJtagIdcode(pins::PROBE[tck], pins::PROBE[tms], pins::PROBE[tdo]); + if (id && id != 0xFFFFFFFF && (id & 1)) { + char b[40]; snprintf(b, sizeof(b), "JTAG id=%08lX T%d/%d/%d", + (unsigned long)id, pins::PROBE[tck], pins::PROBE[tms], pins::PROBE[tdo]); + record(b); found++; + } + } + if (++step >= n*n*n) { running = false; record("-- jtag scan done --"); } + } + + uint32_t readJtagIdcode(int tck, int tms, int tdo) { + pinMode(tck, OUTPUT); pinMode(tms, OUTPUT); pinMode(tdo, INPUT_PULLUP); + auto clk = [&](int tmsv){ digitalWrite(tms, tmsv); digitalWrite(tck, LOW); + delayMicroseconds(2); digitalWrite(tck, HIGH); delayMicroseconds(2); }; + for (int i = 0; i < 5; i++) clk(1); // -> Test-Logic-Reset (loads IDCODE) + clk(0); clk(0); // -> Run-Test/Idle -> Select-DR + clk(1); clk(0); clk(0); // -> Shift-DR + uint32_t id = 0; + for (int i = 0; i < 32; i++) { + digitalWrite(tck, LOW); delayMicroseconds(2); + if (digitalRead(tdo)) id |= (1UL << i); + digitalWrite(tck, HIGH); delayMicroseconds(2); + } + return id; + } + + // --- SWD (ARM ADIv5 DP IDCODE) ------------------------------------ + void scanSwdStep() { + int n = pins::PROBE_COUNT; + int clk = step / n % n, io = step % n; + if (clk != io) { + uint32_t id = readSwdIdcode(pins::PROBE[clk], pins::PROBE[io]); + if (id && id != 0xFFFFFFFF) { + char b[40]; snprintf(b, sizeof(b), "SWD id=%08lX CLK%d IO%d", + (unsigned long)id, pins::PROBE[clk], pins::PROBE[io]); + record(b); found++; + } + } + if (++step >= n*n) { running = false; record("-- swd scan done --"); } + } + + uint32_t readSwdIdcode(int swclk, int swdio) { + pinMode(swclk, OUTPUT); pinMode(swdio, OUTPUT); + auto wbit = [&](int b){ digitalWrite(swdio, b); digitalWrite(swclk, LOW); + delayMicroseconds(2); digitalWrite(swclk, HIGH); delayMicroseconds(2); }; + // Line reset: >=50 clocks with SWDIO high, then JTAG->SWD magic 0xE79E. + for (int i = 0; i < 56; i++) wbit(1); + uint16_t magic = 0xE79E; + for (int i = 0; i < 16; i++) wbit((magic >> i) & 1); + for (int i = 0; i < 56; i++) wbit(1); + for (int i = 0; i < 4; i++) wbit(0); + // Request: read DP reg 0 (IDCODE). Start=1 APnDP=0 RnW=1 A[2:3]=00, parity, stop=0, park=1. + uint8_t req = 0xA5; + for (int i = 0; i < 8; i++) wbit((req >> i) & 1); + // Turnaround, read 3-bit ACK. + pinMode(swdio, INPUT_PULLUP); + digitalWrite(swclk, LOW); delayMicroseconds(2); digitalWrite(swclk, HIGH); delayMicroseconds(2); + uint8_t ack = 0; + for (int i = 0; i < 3; i++) { + digitalWrite(swclk, LOW); delayMicroseconds(2); + if (digitalRead(swdio)) ack |= (1 << i); + digitalWrite(swclk, HIGH); delayMicroseconds(2); + } + if (ack != 0x1) return 0; // 0b001 = OK + uint32_t id = 0; + for (int i = 0; i < 32; i++) { + digitalWrite(swclk, LOW); delayMicroseconds(2); + if (digitalRead(swdio)) id |= (1UL << i); + digitalWrite(swclk, HIGH); delayMicroseconds(2); + } + return id; + } +}; + +Module* makePinScan() { return new PinScan(); } diff --git a/src/modules/uartsniff.cpp b/src/modules/uartsniff.cpp new file mode 100644 index 0000000..e8ee002 --- /dev/null +++ b/src/modules/uartsniff.cpp @@ -0,0 +1,96 @@ +#include "../core/module.h" +#include "../core/ui.h" +#include "../core/pins.h" + +// UART protocol sniff + replay. Passive by default: +// +// SNIFF : attach HardwareSerial to the Grove pins, dump a rolling +// hex/ascii view, and keep a small ring buffer of the last +// N bytes. Cycle common bauds with [b] until it reads clean. +// CAPTURE : freeze the current ring buffer as the "replay frame". +// REPLAY : re-transmit the captured frame ON A KEYPRESS ONLY. This is +// the one active operation in the module and it never fires +// on its own — you press [t] each time. +// +// Replay is for your own bench: confirming a command you already saw the +// device send/accept (e.g. a UART-controlled relay, an OBD ELM327 AT +// string). It is not a fuzzer and does not brute-force. + +class UartSniff : public Module { + static constexpr int RING = 256; + uint8_t ring[RING]; int head = 0, count = 0; + uint8_t frame[RING]; int frameLen = 0; + const long BAUDS[6] = {9600, 19200, 38400, 57600, 115200, 230400}; + int baudIx = 4; + bool live = false; + HardwareSerial& port = Serial1; + +public: + const char* name() const override { return "UART Sniff"; } + const char* blurb() const override { return "sniff / capture / replay"; } + + void onEnter() override { count = head = 0; frameLen = 0; startPort(); } + void onExit() override { port.end(); live = false; } + + bool onKey(char c) override { + if (c == 'b') { baudIx = (baudIx + 1) % 6; startPort(); return true; } + if (c == ' ') { live = !live; return true; } + if (c == 'c') { capture(); return true; } + if (c == 't') { replayOnce(); return true; } + return false; + } + + void tick() override { + if (!live) return; + while (port.available() && count < RING * 2) { // bound per tick + uint8_t b = port.read(); + ring[head] = b; head = (head + 1) % RING; + if (count < RING) count++; + } + } + + void draw() override { + ui::lineC(0, ui::accent(), "%ld 8N1 %s buf:%d frame:%d", + BAUDS[baudIx], live ? "LIVE" : "paused", count, frameLen); + // Last 3 rows = 8 bytes each of the tail of the ring, hex+ascii. + for (int r = 0; r < 3; r++) drawRow(2 + r, r); + ui::hintBar("[b]aud [space]live [c]apture [t]x-replay"); + } + +private: + void startPort() { + port.end(); + port.begin(BAUDS[baudIx], SERIAL_8N1, pins::GROVE_A /*RX*/, pins::GROVE_B /*TX*/); + live = true; + } + + void drawRow(int uiRow, int chunk) { + char line[40]; int p = 0; + int start = (head - count + (count - (chunk + 1) * 8) + RING * 4) % RING; + for (int i = 0; i < 8; i++) { + uint8_t b = ring[(start + i) % RING]; + p += snprintf(line + p, sizeof(line) - p, "%02X ", b); + } + p += snprintf(line + p, sizeof(line) - p, " "); + for (int i = 0; i < 8; i++) { + uint8_t b = ring[(start + i) % RING]; + line[p++] = (b >= 32 && b < 127) ? b : '.'; + } + line[p] = 0; + ui::line(uiRow, "%s", line); + } + + void capture() { + frameLen = count < RING ? count : RING; + int start = (head - frameLen + RING) % RING; + for (int i = 0; i < frameLen; i++) frame[i] = ring[(start + i) % RING]; + } + + void replayOnce() { // manual-only TX + if (frameLen == 0) return; + port.write(frame, frameLen); + port.flush(); + } +}; + +Module* makeUartSniff() { return new UartSniff(); } diff --git a/src/modules/usbenum.cpp b/src/modules/usbenum.cpp new file mode 100644 index 0000000..b52ede9 --- /dev/null +++ b/src/modules/usbenum.cpp @@ -0,0 +1,126 @@ +#include "../core/module.h" +#include "../core/ui.h" + +// USB descriptor enumeration. The ESP32-S3 has a USB-OTG core, so it can +// act as a host and read the standard descriptors a device advertises: +// VID/PID, device class, manufacturer/product strings, and the interface +// classes of each configuration. That's exactly what you want for +// fingerprinting an unknown dongle, a car's USB port, or a peripheral +// before you decide what it actually is. +// +// This is read-only enumeration. We open the device, pull descriptors, +// and close. We do NOT claim interfaces, send class requests, or drive +// endpoints — no HID injection, no mass-storage access. +// +// Requires the board's USB pins wired to the target as HOST (VBUS out). +// Build with ARDUINO_USB_MODE so the console stays on the CDC, and use a +// separate OTG connection for the target. See docs/HARDWARE.md. + +#if __has_include("usb/usb_host.h") +#include "usb/usb_host.h" +#define HAVE_USB_HOST 1 +#endif + +class UsbEnum : public Module { + char lines[6][40]; + int nlines = 0; + bool installed = false; +#ifdef HAVE_USB_HOST + usb_host_client_handle_t client = nullptr; + usb_device_handle_t dev = nullptr; + uint8_t devAddr = 0; +#endif + +public: + const char* name() const override { return "USB Enum"; } + const char* blurb() const override { return "descriptor readout"; } + + void onEnter() override { + nlines = 0; + say("host idle. plug target, press [space]"); +#ifndef HAVE_USB_HOST + say("build w/ ESP-IDF usb_host to enable"); +#endif + } + void onExit() override { teardown(); } + + bool onKey(char c) override { + if (c == ' ') { enumerate(); return true; } + if (c == 'x') { teardown(); nlines = 0; say("host released"); return true; } + return false; + } + + void draw() override { + ui::lineC(0, ui::accent(), "USB host %s", installed ? "up" : "down"); + for (int i = 0; i < nlines; i++) ui::line(1 + i, "%s", lines[i]); + ui::hintBar("[space]enumerate [x]release [`]back"); + } + +private: + void say(const char* fmt, ...) { + if (nlines >= 6) { for (int i = 1; i < 6; i++) strncpy(lines[i-1], lines[i], 39); nlines = 5; } + va_list ap; va_start(ap, fmt); + vsnprintf(lines[nlines], 40, fmt, ap); va_end(ap); + nlines++; + } + +#ifdef HAVE_USB_HOST + bool ensureHost() { + if (installed) return true; + usb_host_config_t hc = {}; hc.intr_flags = ESP_INTR_FLAG_LEVEL1; + if (usb_host_install(&hc) != ESP_OK) { say("usb_host_install fail"); return false; } + usb_host_client_config_t cc = {}; + cc.max_num_event_msg = 5; + if (usb_host_client_register(&cc, &client) != ESP_OK) { say("client reg fail"); return false; } + installed = true; return true; + } + + void enumerate() { + if (!ensureHost()) return; + // Pump the host lib so a freshly-plugged device gets an address. + for (int i = 0; i < 200; i++) { + usb_host_lib_handle_events(pdMS_TO_TICKS(5), nullptr); + usb_host_client_handle_events(client, pdMS_TO_TICKS(5)); + } + int num = 0; uint8_t list[8]; + if (usb_host_device_addr_list_fill(sizeof(list), list, &num) != ESP_OK || num == 0) { + say("no device detected"); return; + } + devAddr = list[0]; + if (usb_host_device_open(client, devAddr, &dev) != ESP_OK) { say("open @%d fail", devAddr); return; } + + const usb_device_desc_t* dd = nullptr; + if (usb_host_get_device_descriptor(dev, &dd) == ESP_OK && dd) { + say("VID:PID %04X:%04X", dd->idVendor, dd->idProduct); + say("class %02X sub %02X proto %02X", dd->bDeviceClass, dd->bDeviceSubClass, dd->bDeviceProtocol); + say("bcdUSB %04X cfgs %d", dd->bcdUSB, dd->bNumConfigurations); + readString(dd->iManufacturer, "mfr"); + readString(dd->iProduct, "prod"); + } else say("get dev desc fail"); + + const usb_config_desc_t* cd = nullptr; + if (usb_host_get_active_config_descriptor(dev, &cd) == ESP_OK && cd) + say("cfg len %d ifaces %d", cd->wTotalLength, cd->bNumInterfaces); + } + + void readString(uint8_t idx, const char* label) { + if (!idx) return; + usb_transfer_t* t = nullptr; + // Minimal control-in for a string descriptor; best-effort. + char out[24] = {0}; + if (usb_host_get_string_descriptor(dev, idx, 0x0409, (uint8_t*)out, sizeof(out)) == ESP_OK) + say("%s: %s", label, out); + } + + void teardown() { + if (dev) { usb_host_device_close(client, dev); dev = nullptr; } + if (client) { usb_host_client_deregister(client); client = nullptr; } + if (installed) { usb_host_uninstall(); installed = false; } + } +#else + void enumerate() { say("USB host stack not compiled in"); } + void teardown() {} +#endif +}; + +Module* makeUsbEnum() { return new UsbEnum(); } diff --git a/src/modules/vsense.cpp b/src/modules/vsense.cpp new file mode 100644 index 0000000..ff976c3 --- /dev/null +++ b/src/modules/vsense.cpp @@ -0,0 +1,62 @@ +#include "../core/module.h" +#include "../core/ui.h" +#include "../core/pins.h" + +// Voltage sensing. Reads the ADC behind an external divider and reports +// a live voltage plus a guess at the logic family, so you know what +// you're probing BEFORE you drive a pin into it. Min/max are held so a +// brief transient (e.g. a bus idling high) is captured. +// +// The ESP32-S3 ADC is non-linear near the rails; we use the Arduino +// analogReadMilliVolts() calibration path and apply the divider ratio. + +class VSense : public Module { + float v = 0, vmin = 99, vmax = 0; + uint32_t lastMs = 0; + +public: + const char* name() const override { return "V-Sense"; } + const char* blurb() const override { return "target voltage probe"; } + + void onEnter() override { + analogReadResolution(12); + pinMode(pins::VSENSE_ADC, INPUT); + vmin = 99; vmax = 0; + } + + bool onKey(char c) override { + if (c == 'r') { vmin = 99; vmax = 0; return true; } // reset hold + return false; + } + + void tick() override { + if (millis() - lastMs < 100) return; + lastMs = millis(); + uint32_t mv = analogReadMilliVolts(pins::VSENSE_ADC); + v = (mv / 1000.0f) * pins::VSENSE_RATIO; + if (v < vmin) vmin = v; + if (v > vmax) vmax = v; + } + + void draw() override { + ui::lineC(0, ui::accent(), "Vin = %5.2f V", v); + ui::line(1, "min %4.2f max %4.2f (ratio %.1f)", vmin, vmax, pins::VSENSE_RATIO); + ui::lineC(3, family(v).color, "logic: %s", family(v).name); + ui::line(5, "wire target -> divider -> G%d", pins::VSENSE_ADC); + ui::hintBar("[r]eset hold [`]back"); + } + +private: + struct Fam { const char* name; uint16_t color; }; + Fam family(float x) { + if (x < 0.3f) return {"floating / GND", ui::fg()}; + if (x < 1.5f) return {"1.2V core rail", ui::warn()}; + if (x < 2.1f) return {"1.8V logic", ui::warn()}; + if (x < 3.0f) return {"2.5V logic", ui::warn()}; + if (x < 3.9f) return {"3.3V logic OK", ui::accent()}; + if (x < 6.0f) return {"5V ! shift req", ui::bad()}; + return {"HIGH >6V DANGER", ui::bad()}; + } +}; + +Module* makeVSense() { return new VSense(); }