Add theme engine, animated UI, and Bus Dump / Crypto / Exfil modules
UI: - Theme engine (5 palettes) persisted to NVS; live Theme switcher module - Animated shell: boot splash, CRT scanlines, breathing menu cursor, wipe-in transitions, spinners, progress bars New recon modules: - Bus Dump: SPI-NOR + I2C EEPROM readout to microSD (read-only) - Crypto Lab: entropy, magic-byte file-ID, XOR brute, AES-128 known-key - Exfil: SD browser + base64 dump export over USB serial, manifest writer README updated with the new module table and look-and-feel notes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AAhMHMRAQLQ9hSbBECKNfn
This commit is contained in:
10
README.md
10
README.md
@@ -20,6 +20,16 @@ OBD/USB port, a PC, smart appliances.
|
||||
| **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. |
|
||||
| **Bus Dump**| Reads **SPI-NOR (25-series)** and **I2C EEPROM (24-series)** chips on your own boards to `/dump/*.bin` on microSD. Read-only. |
|
||||
| **Crypto Lab** | Offline analysis of a dumped blob: **Shannon entropy**, magic-byte **file-ID**, single-byte **XOR** brute, **AES-128-ECB** known-key decrypt. |
|
||||
| **Exfil** | Browse the SD card and **stream a dump to the host** over USB serial as base64 (`base64 -d`); writes a manifest of what you pulled. |
|
||||
| **Theme** | Live **UI theme switcher** — 5 palettes, saved to NVS, restyles everything instantly. |
|
||||
|
||||
## Look & feel
|
||||
|
||||
Boot splash with a sweeping-glow logo, CRT scanlines, a breathing menu
|
||||
cursor, wipe-in transitions, spinners and progress bars. Switch palettes any
|
||||
time in the **Theme** module.
|
||||
|
||||
## Build & flash
|
||||
|
||||
|
||||
@@ -1,39 +1,56 @@
|
||||
#include "shell.h"
|
||||
#include "ui.h"
|
||||
#include "theme.h"
|
||||
#include <M5Cardputer.h>
|
||||
|
||||
// Module factories (defined in each module .cpp).
|
||||
Module* makePinScan();
|
||||
Module* makeVSense();
|
||||
Module* makeUartSniff();
|
||||
Module* makeUsbEnum();
|
||||
Module* makeDefCred();
|
||||
Module* makeBusDump();
|
||||
Module* makeCrypto();
|
||||
Module* makeExfil();
|
||||
Module* makeThemePicker();
|
||||
|
||||
void Shell::begin() {
|
||||
theme::load();
|
||||
add(makePinScan());
|
||||
add(makeVSense());
|
||||
add(makeUartSniff());
|
||||
add(makeUsbEnum());
|
||||
add(makeDefCred());
|
||||
M5.Display.fillScreen(ui::bg());
|
||||
add(makeBusDump());
|
||||
add(makeCrypto());
|
||||
add(makeExfil());
|
||||
add(makeThemePicker());
|
||||
ui::bootSplash();
|
||||
drawMenu();
|
||||
}
|
||||
|
||||
void Shell::drawMenu() {
|
||||
M5.Display.fillScreen(ui::bg());
|
||||
auto& d = M5.Display;
|
||||
d.fillScreen(ui::bg());
|
||||
ui::titleBar("Card-Crack", CARDCRACK_VERSION);
|
||||
// animated selection glow bar
|
||||
int y = ui::BODY_Y + sel * 12;
|
||||
uint16_t sb = ui::mix(theme::active().panel, theme::active().accent, 0.25f + 0.25f * ui::pulse(900));
|
||||
d.fillRect(0, y, ui::W, 12, sb);
|
||||
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());
|
||||
uint16_t c = cur ? ui::accent() : ui::fg();
|
||||
d.setTextColor(c, cur ? sb : ui::bg());
|
||||
d.setCursor(5, ui::BODY_Y + i * 12 + 1);
|
||||
d.printf("%c %-9s %s", cur ? char(0x10) : ' ', mods[i]->name(), mods[i]->blurb());
|
||||
}
|
||||
ui::hintBar("arrows move [enter]open home-lab only");
|
||||
ui::hintBar("; up . down 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());
|
||||
ui::wipeIn();
|
||||
mods[i]->onEnter();
|
||||
mods[i]->draw();
|
||||
}
|
||||
@@ -52,8 +69,8 @@ void Shell::loop() {
|
||||
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
|
||||
if (c == ';') { sel = (sel - 1 + nmods) % nmods; drawMenu(); }
|
||||
else if (c == '.') { sel = (sel + 1) % nmods; drawMenu(); }
|
||||
else if (c == '\r' || c == ' ') enter(sel);
|
||||
} else {
|
||||
mods[active]->onKey(c);
|
||||
@@ -62,8 +79,12 @@ void Shell::loop() {
|
||||
if (st.enter && active < 0) enter(sel);
|
||||
}
|
||||
|
||||
// ~30 Hz tick for the active module.
|
||||
if (active >= 0 && millis() - lastTick > 33) {
|
||||
if (active < 0) {
|
||||
// keep the menu selection breathing
|
||||
if (millis() - lastTick > 60) { lastTick = millis(); drawMenu(); }
|
||||
return;
|
||||
}
|
||||
if (millis() - lastTick > 33) {
|
||||
lastTick = millis();
|
||||
mods[active]->tick();
|
||||
mods[active]->draw();
|
||||
|
||||
33
src/core/theme.cpp
Normal file
33
src/core/theme.cpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#include "theme.h"
|
||||
#include <Preferences.h>
|
||||
|
||||
namespace theme {
|
||||
// 0xRRGGBB packed; ui converts to 565.
|
||||
const Theme LIST[] = {
|
||||
{"Matrix", 0x0A0E0A, 0x10201410 & 0xFFFFFF, 0xB8F0B8, 0x2E6B2E, 0x39FF6A, 0xE6C23C, 0xE05050, 0x1FBF4F},
|
||||
{"Vapor", 0x140A1E, 0x241436, 0xF0D8FF, 0x6E4E8E, 0xFF5FD0, 0xFFD25F, 0xFF5F87, 0x9F5FFF},
|
||||
{"Amber", 0x140F04, 0x2A2008, 0xFFD79A, 0x8A6A2E, 0xFFB020, 0xFFE070, 0xFF6030, 0xFF8A1F},
|
||||
{"Ice", 0x061018, 0x0E2030, 0xCDE8FF, 0x3E6E90, 0x40C8FF, 0xC8E060, 0xFF6A8A, 0x1F9FE0},
|
||||
{"Blood", 0x140606, 0x2A0E0E, 0xFFC8C8, 0x8A3030, 0xFF3B3B, 0xFFB020, 0xFF5F5F, 0xE01F1F},
|
||||
};
|
||||
const int COUNT = sizeof(LIST) / sizeof(LIST[0]);
|
||||
|
||||
static int cur = 0;
|
||||
static Preferences prefs;
|
||||
|
||||
int current() { return cur; }
|
||||
const Theme& active() { return LIST[cur]; }
|
||||
|
||||
void set(int i) {
|
||||
cur = (i % COUNT + COUNT) % COUNT;
|
||||
prefs.begin("cardcrack", false);
|
||||
prefs.putInt("theme", cur);
|
||||
prefs.end();
|
||||
}
|
||||
void load() {
|
||||
prefs.begin("cardcrack", true);
|
||||
cur = prefs.getInt("theme", 0);
|
||||
prefs.end();
|
||||
if (cur < 0 || cur >= COUNT) cur = 0;
|
||||
}
|
||||
}
|
||||
20
src/core/theme.h
Normal file
20
src/core/theme.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
// A theme is a named palette. Modules never hardcode colors — they pull
|
||||
// from the active theme via ui::th(). Switch themes in the Theme module;
|
||||
// the choice persists to NVS so it survives a reboot.
|
||||
|
||||
struct Theme {
|
||||
const char* name;
|
||||
uint32_t bg, panel, fg, dim, accent, warn, bad, glow;
|
||||
};
|
||||
|
||||
namespace theme {
|
||||
extern const Theme LIST[];
|
||||
extern const int COUNT;
|
||||
int current();
|
||||
void set(int i); // clamps + saves to NVS
|
||||
const Theme& active();
|
||||
void load(); // restore from NVS at boot
|
||||
}
|
||||
116
src/core/ui.cpp
116
src/core/ui.cpp
@@ -1,53 +1,125 @@
|
||||
#include "ui.h"
|
||||
#include <stdarg.h>
|
||||
#include <math.h>
|
||||
|
||||
namespace ui {
|
||||
|
||||
uint16_t rgb(uint32_t c) {
|
||||
return M5.Display.color565((c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF);
|
||||
}
|
||||
|
||||
uint16_t mix(uint32_t a, uint32_t b, float t) {
|
||||
if (t < 0) t = 0; if (t > 1) t = 1;
|
||||
int ar=(a>>16)&0xFF, ag=(a>>8)&0xFF, ab=a&0xFF;
|
||||
int br=(b>>16)&0xFF, bg2=(b>>8)&0xFF, bb=b&0xFF;
|
||||
return M5.Display.color565(ar+(br-ar)*t, ag+(bg2-ag)*t, ab+(bb-ab)*t);
|
||||
}
|
||||
|
||||
float pulse(uint32_t periodMs) {
|
||||
float ph = (millis() % periodMs) / (float)periodMs;
|
||||
return 0.5f - 0.5f * cosf(ph * 2.0f * PI); // smooth 0..1..0
|
||||
}
|
||||
|
||||
static uint16_t titleBg() { return rgb(theme::active().panel); }
|
||||
|
||||
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.fillRect(0, 0, W, BODY_Y, titleBg());
|
||||
// pulsing accent tick on the left
|
||||
d.fillRect(0, 0, 3, BODY_Y, mix(theme::active().accent, theme::active().glow, pulse(1400)));
|
||||
d.setTextSize(1);
|
||||
d.setCursor(3, 4);
|
||||
d.print(title);
|
||||
d.setTextColor(accent(), titleBg());
|
||||
d.setCursor(7, 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);
|
||||
d.setTextColor(dim(), titleBg());
|
||||
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);
|
||||
d.fillRect(0, y, W, 12, titleBg());
|
||||
d.setTextColor(dim(), titleBg());
|
||||
d.setTextSize(1); d.setCursor(3, y + 2); d.print(hint);
|
||||
}
|
||||
|
||||
void clearBody() {
|
||||
M5.Display.fillRect(0, BODY_Y, W, BODY_H, bg());
|
||||
}
|
||||
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);
|
||||
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);
|
||||
d.setTextSize(1); d.setCursor(5, 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);
|
||||
}
|
||||
|
||||
void bar(int row, float frac, uint16_t col, const char* label) {
|
||||
if (frac < 0) frac = 0; if (frac > 1) frac = 1;
|
||||
auto& d = M5.Display;
|
||||
int y = BODY_Y + row * 12, x = 5, w = W - 60, h = 8;
|
||||
d.fillRect(0, y, W, 12, bg());
|
||||
d.drawRect(x, y + 1, w, h, dim());
|
||||
d.fillRect(x + 1, y + 2, (int)((w - 2) * frac), h - 2, col);
|
||||
d.setTextColor(fg(), bg()); d.setCursor(x + w + 4, y + 1);
|
||||
d.printf("%3d%% %s", (int)(frac * 100), label ? label : "");
|
||||
}
|
||||
|
||||
void spinner(int x, int y, uint16_t col) {
|
||||
static const char* fr = "|/-\\";
|
||||
M5.Display.setTextColor(col, bg());
|
||||
M5.Display.setCursor(x, y);
|
||||
M5.Display.print(fr[(millis() / 90) % 4]);
|
||||
}
|
||||
|
||||
// CRT-ish scanlines over the body — cheap, drawn every other row.
|
||||
void scanlines() {
|
||||
auto& d = M5.Display;
|
||||
for (int y = BODY_Y; y < BODY_Y + BODY_H; y += 3)
|
||||
d.drawFastHLine(0, y, W, mix(theme::active().bg, 0x000000, 0.35f));
|
||||
}
|
||||
|
||||
void bootSplash() {
|
||||
auto& d = M5.Display;
|
||||
const char* logo = "CARD-CRACK";
|
||||
d.fillScreen(bg());
|
||||
// sweep the glow across the logo
|
||||
for (int t = 0; t <= 24; t++) {
|
||||
d.setTextSize(2);
|
||||
int lw = strlen(logo) * 12, x = (W - lw) / 2, y = 44;
|
||||
for (int i = 0; logo[i]; i++) {
|
||||
float d2 = fabsf((i * 12) - (t * (lw / 24.0f)));
|
||||
uint16_t c = mix(theme::active().dim, theme::active().glow, 1.0f - d2 / 40.0f);
|
||||
d.setTextColor(c, bg()); d.setCursor(x + i * 12, y); d.print(logo[i]);
|
||||
}
|
||||
d.setTextSize(1);
|
||||
d.setTextColor(dim(), bg());
|
||||
d.setCursor((W - 20 * 6) / 2 + 20, 74);
|
||||
d.print("home-lab recon toolkit");
|
||||
// loading ticks
|
||||
int bw = 140, bx = (W - bw) / 2;
|
||||
d.drawRect(bx, 92, bw, 6, dim());
|
||||
d.fillRect(bx + 1, 93, (bw - 2) * t / 24, 4, accent());
|
||||
delay(28);
|
||||
}
|
||||
delay(180);
|
||||
scanlines();
|
||||
delay(120);
|
||||
}
|
||||
|
||||
void wipeIn() {
|
||||
auto& d = M5.Display;
|
||||
for (int x = 0; x <= W; x += 16) {
|
||||
d.drawFastVLine(x, BODY_Y, BODY_H, accent());
|
||||
delay(4);
|
||||
d.drawFastVLine(x, BODY_Y, BODY_H, bg());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,40 @@
|
||||
#pragma once
|
||||
#include <M5Cardputer.h>
|
||||
#include "theme.h"
|
||||
|
||||
// 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.
|
||||
// Drawing + animation helpers. All color comes from the active theme so
|
||||
// the whole UI restyles instantly when you switch themes. Screen is
|
||||
// 240x135; 16px title bar, 12px hint bar, 240x107 body 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); }
|
||||
uint16_t rgb(uint32_t c); // 0xRRGGBB -> 565
|
||||
inline uint16_t bg() { return rgb(theme::active().bg); }
|
||||
inline uint16_t panel() { return rgb(theme::active().panel); }
|
||||
inline uint16_t fg() { return rgb(theme::active().fg); }
|
||||
inline uint16_t dim() { return rgb(theme::active().dim); }
|
||||
inline uint16_t accent() { return rgb(theme::active().accent); }
|
||||
inline uint16_t warn() { return rgb(theme::active().warn); }
|
||||
inline uint16_t bad() { return rgb(theme::active().bad); }
|
||||
inline uint16_t glow() { return rgb(theme::active().glow); }
|
||||
|
||||
// Interpolate a->b (0xRRGGBB) by t[0..1], return 565. For pulses/fades.
|
||||
uint16_t mix(uint32_t a, uint32_t b, float t);
|
||||
// A 0..1 breathing value derived from millis() at the given period(ms).
|
||||
float pulse(uint32_t periodMs);
|
||||
|
||||
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, ...);
|
||||
|
||||
// Animations -------------------------------------------------------
|
||||
void bootSplash(); // one-shot boot intro
|
||||
void wipeIn(); // transition when entering a module
|
||||
void scanlines(); // subtle CRT overlay on the body
|
||||
void spinner(int x, int y, uint16_t col); // small animated activity dot
|
||||
void bar(int row, float frac, uint16_t col, const char* label); // progress bar
|
||||
}
|
||||
|
||||
126
src/modules/busdump.cpp
Normal file
126
src/modules/busdump.cpp
Normal file
@@ -0,0 +1,126 @@
|
||||
#include "../core/module.h"
|
||||
#include "../core/ui.h"
|
||||
#include "../core/pins.h"
|
||||
#include <SPI.h>
|
||||
#include <Wire.h>
|
||||
#include <SD.h>
|
||||
|
||||
// Bus Dump — read the contents of memory chips on YOUR boards.
|
||||
// SPI : 25-series NOR flash. JEDEC ID, then 0x03 READ the whole chip
|
||||
// to /dump/spi_<id>.bin on microSD.
|
||||
// I2C : scan the bus, dump a 24-series EEPROM to /dump/i2c_<addr>.bin.
|
||||
// Read-only; never writes the target. [space] start/stop.
|
||||
|
||||
class BusDump : public Module {
|
||||
enum Mode { SPI_NOR, I2C_EE } mode = SPI_NOR;
|
||||
bool running = false;
|
||||
uint32_t addr = 0, size = 0, jedec = 0;
|
||||
uint8_t i2cAddr = 0x50;
|
||||
char msg[3][40] = {{0},{0},{0}};
|
||||
File out;
|
||||
bool sdOk = false;
|
||||
static constexpr int CS = pins::PROBE[2];
|
||||
public:
|
||||
const char* name() const override { return "Bus Dump"; }
|
||||
const char* blurb() const override { return "SPI/I2C chip readout"; }
|
||||
|
||||
void onEnter() override {
|
||||
running = false; addr = 0; size = 0; jedec = 0;
|
||||
sdOk = SD.begin();
|
||||
say(sdOk ? "SD ready" : "no SD (dump->serial)");
|
||||
}
|
||||
void onExit() override { if (out) out.close(); running = false; SPI.end(); Wire.end(); }
|
||||
|
||||
bool onKey(char c) override {
|
||||
if (c == 'm') { mode = mode==SPI_NOR?I2C_EE:SPI_NOR; running=false; say("mode switched"); return true; }
|
||||
if (c == 'i') { probeId(); return true; }
|
||||
if (c == 's') { scanI2C(); return true; }
|
||||
if (c == '+') { i2cAddr = (i2cAddr+1)&0x7F; return true; }
|
||||
if (c == ' ' || c == '\r') { running ? stop() : start(); return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
void tick() override {
|
||||
if (!running) return;
|
||||
uint8_t buf[64];
|
||||
uint32_t n = (size-addr) < sizeof(buf) ? (size-addr) : sizeof(buf);
|
||||
if (n == 0) { stop(); say("-- dump complete --"); return; }
|
||||
if (mode == SPI_NOR) spiRead(addr, buf, n); else i2cRead(addr, buf, n);
|
||||
if (out) out.write(buf, n); else dumpSerial(buf, n);
|
||||
addr += n;
|
||||
}
|
||||
|
||||
void draw() override {
|
||||
ui::lineC(0, ui::accent(), "%s %s", mode==SPI_NOR?"SPI-NOR":"I2C-EE",
|
||||
running?"DUMPING":"idle");
|
||||
if (mode == SPI_NOR) ui::line(1, "JEDEC %06lX size %luKB",
|
||||
(unsigned long)jedec, (unsigned long)(size/1024));
|
||||
else ui::line(1, "addr 0x%02X size %luB", i2cAddr, (unsigned long)size);
|
||||
if (running && size) ui::bar(2, addr/(float)size, ui::glow(), "read");
|
||||
for (int i=0;i<3;i++) ui::line(4+i, "%s", msg[i]);
|
||||
if (running) ui::spinner(228, ui::BODY_Y+1, ui::glow());
|
||||
ui::hintBar("[m]ode [i]d [s]can [+]addr [space]go");
|
||||
}
|
||||
private:
|
||||
void say(const char* fmt, ...) {
|
||||
for (int i=2;i>0;i--) strncpy(msg[i], msg[i-1], 39);
|
||||
va_list ap; va_start(ap,fmt); vsnprintf(msg[0],40,fmt,ap); va_end(ap);
|
||||
}
|
||||
void spiBegin() { SPI.begin(pins::PROBE[3], pins::PROBE[4], pins::PROBE[5], CS);
|
||||
pinMode(CS, OUTPUT); digitalWrite(CS, HIGH); }
|
||||
void probeId() {
|
||||
if (mode != SPI_NOR) { say("id: SPI mode only"); return; }
|
||||
spiBegin();
|
||||
SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
|
||||
digitalWrite(CS, LOW); SPI.transfer(0x9F);
|
||||
uint8_t m=SPI.transfer(0), t=SPI.transfer(0), c=SPI.transfer(0);
|
||||
digitalWrite(CS, HIGH); SPI.endTransaction();
|
||||
jedec = (m<<16)|(t<<8)|c;
|
||||
size = 1UL << c;
|
||||
if (c < 16 || c > 26) size = 0;
|
||||
say("JEDEC %06lX %s", (unsigned long)jedec, size?"":"(unknown cap)");
|
||||
}
|
||||
void spiRead(uint32_t a, uint8_t* b, uint32_t n) {
|
||||
SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
|
||||
digitalWrite(CS, LOW); SPI.transfer(0x03);
|
||||
SPI.transfer((a>>16)&0xFF); SPI.transfer((a>>8)&0xFF); SPI.transfer(a&0xFF);
|
||||
for (uint32_t i=0;i<n;i++) b[i]=SPI.transfer(0);
|
||||
digitalWrite(CS, HIGH); SPI.endTransaction();
|
||||
}
|
||||
void scanI2C() {
|
||||
Wire.begin(pins::GROVE_A, pins::GROVE_B);
|
||||
int found = 0;
|
||||
for (uint8_t a=0x08; a<0x78; a++) {
|
||||
Wire.beginTransmission(a);
|
||||
if (Wire.endTransmission()==0) { i2cAddr=a; found++; }
|
||||
}
|
||||
say("i2c: %d dev, sel 0x%02X", found, i2cAddr);
|
||||
}
|
||||
void i2cRead(uint32_t a, uint8_t* b, uint32_t n) {
|
||||
Wire.beginTransmission(i2cAddr);
|
||||
Wire.write((a>>8)&0xFF); Wire.write(a&0xFF);
|
||||
Wire.endTransmission();
|
||||
Wire.requestFrom((int)i2cAddr, (int)n);
|
||||
for (uint32_t i=0;i<n && Wire.available();i++) b[i]=Wire.read();
|
||||
}
|
||||
void start() {
|
||||
if (mode == SPI_NOR) { probeId(); if (!size) { say("probe chip first"); return; } }
|
||||
else { Wire.begin(pins::GROVE_A, pins::GROVE_B); size = 8192; }
|
||||
addr = 0;
|
||||
if (sdOk) {
|
||||
SD.mkdir("/dump");
|
||||
char path[40];
|
||||
if (mode==SPI_NOR) snprintf(path,sizeof(path),"/dump/spi_%06lX.bin",(unsigned long)jedec);
|
||||
else snprintf(path,sizeof(path),"/dump/i2c_%02X.bin", i2cAddr);
|
||||
out = SD.open(path, FILE_WRITE);
|
||||
say("-> %s", path);
|
||||
} else say("-> serial (hex)");
|
||||
running = true;
|
||||
}
|
||||
void stop() { running = false; if (out) out.close(); }
|
||||
void dumpSerial(const uint8_t* b, uint32_t n) {
|
||||
for (uint32_t i=0;i<n;i++) Serial.printf("%02X", b[i]);
|
||||
}
|
||||
};
|
||||
|
||||
Module* makeBusDump() { return new BusDump(); }
|
||||
129
src/modules/crypto.cpp
Normal file
129
src/modules/crypto.cpp
Normal file
@@ -0,0 +1,129 @@
|
||||
#include "../core/module.h"
|
||||
#include "../core/ui.h"
|
||||
#include <SD.h>
|
||||
#include <math.h>
|
||||
#include "mbedtls/aes.h"
|
||||
|
||||
// Crypto Lab — offline analysis of a blob you dumped (Bus Dump output on
|
||||
// the SD card). Helps you tell encrypted from packed from plaintext, and
|
||||
// try the obvious cheap keys on firmware you own.
|
||||
// ENTROPY : Shannon bits/byte over the sample (8.0 ~ encrypted/packed).
|
||||
// IDENT : magic-byte sniff (gzip, ELF, PNG, JFFS2, uImage, ...).
|
||||
// XOR : single-byte XOR brute; scores printable ratio, shows best.
|
||||
// AES : AES-128-ECB decrypt with a key in aesKey[] (known-key).
|
||||
// Loads /dump/*.bin — first file found, or cycle with [f].
|
||||
|
||||
class Crypto : public Module {
|
||||
enum Mode { ENTROPY, IDENT, XORB, AESD } mode = ENTROPY;
|
||||
static constexpr int CAP = 1024;
|
||||
uint8_t buf[CAP]; int len = 0;
|
||||
char fname[32] = "";
|
||||
char msg[4][40] = {{0},{0},{0},{0}};
|
||||
uint8_t aesKey[16] = {0}; bool haveKey = false;
|
||||
public:
|
||||
const char* name() const override { return "Crypto Lab"; }
|
||||
const char* blurb() const override { return "entropy/id/xor/aes"; }
|
||||
|
||||
void onEnter() override { if (!SD.begin()) say("no SD"); else loadFirst(); }
|
||||
|
||||
bool onKey(char c) override {
|
||||
if (c == 'm') { mode = (Mode)((mode+1)%4); analyse(); return true; }
|
||||
if (c == 'f') { loadNext(); return true; }
|
||||
if (c == 'g') { analyse(); return true; }
|
||||
return false;
|
||||
}
|
||||
void draw() override {
|
||||
const char* mn[] = {"ENTROPY","IDENT","XOR","AES"};
|
||||
ui::lineC(0, ui::accent(), "%s file:%s (%dB)", mn[mode],
|
||||
fname[0]?fname:"none", len);
|
||||
for (int i=0;i<4;i++) ui::line(2+i, "%s", msg[i]);
|
||||
ui::hintBar("[m]ode [f]ile [g]o (dumps in /dump)");
|
||||
}
|
||||
private:
|
||||
void say(const char* fmt, ...) {
|
||||
for (int i=3;i>0;i--) strncpy(msg[i], msg[i-1], 39);
|
||||
va_list ap; va_start(ap,fmt); vsnprintf(msg[0],40,fmt,ap); va_end(ap);
|
||||
}
|
||||
void clearMsg(){ for(int i=0;i<4;i++) msg[i][0]=0; }
|
||||
|
||||
void loadFrom(File dir, const char* want) {
|
||||
File f;
|
||||
while ((f = dir.openNextFile())) {
|
||||
if (!f.isDirectory()) {
|
||||
const char* nm = f.name();
|
||||
if (!want || strcmp(nm, want) > 0) {
|
||||
strncpy(fname, nm, 31);
|
||||
len = f.read(buf, CAP); f.close();
|
||||
analyse(); return;
|
||||
}
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
say("no more files");
|
||||
}
|
||||
void loadFirst(){ File d=SD.open("/dump"); if(d){ loadFrom(d, nullptr); d.close(); } else say("mkdir /dump first"); }
|
||||
void loadNext(){ File d=SD.open("/dump"); if(d){ char cur[32]; strncpy(cur,fname,31); loadFrom(d, cur); d.close(); } }
|
||||
|
||||
void analyse() {
|
||||
clearMsg();
|
||||
if (!len) { say("load a dump first"); return; }
|
||||
switch (mode) {
|
||||
case ENTROPY: doEntropy(); break;
|
||||
case IDENT: doIdent(); break;
|
||||
case XORB: doXor(); break;
|
||||
case AESD: doAes(); break;
|
||||
}
|
||||
}
|
||||
void doEntropy() {
|
||||
int cnt[256] = {0};
|
||||
for (int i=0;i<len;i++) cnt[buf[i]]++;
|
||||
double H = 0;
|
||||
for (int i=0;i<256;i++) if (cnt[i]) { double p=(double)cnt[i]/len; H -= p*log2(p); }
|
||||
say("Shannon: %.2f bits/byte", H);
|
||||
say(H>7.5 ? "-> encrypted/compressed" : H>5 ? "-> code/packed?" : "-> low: text/config");
|
||||
}
|
||||
void doIdent() {
|
||||
struct M { int off; const char* sig; int n; const char* name; };
|
||||
static const M sigs[] = {
|
||||
{0,"\x1f\x8b",2,"gzip"}, {0,"\x7f""ELF",4,"ELF"},
|
||||
{0,"\x89PNG",4,"PNG"}, {0,"BZh",3,"bzip2"},
|
||||
{0,"\x27\x05\x19\x56",4,"uImage"}, {0,"\x85\x19",2,"JFFS2"},
|
||||
{0,"hsqs",4,"squashfs"}, {0,"UBI#",4,"UBI"},
|
||||
{0,"\xff\xd8\xff",3,"JPEG"},
|
||||
};
|
||||
for (auto& s : sigs)
|
||||
if (len>=s.off+s.n && memcmp(buf+s.off, s.sig, s.n)==0) { say("magic: %s", s.name); return; }
|
||||
say("no known magic (raw/enc?)");
|
||||
}
|
||||
void doXor() {
|
||||
int bestKey=0; double bestScore=-1;
|
||||
for (int k=0;k<256;k++) {
|
||||
int print=0;
|
||||
for (int i=0;i<len;i++){ uint8_t v=buf[i]^k; if(v>=32&&v<127) print++; }
|
||||
double s=(double)print/len;
|
||||
if (s>bestScore){ bestScore=s; bestKey=k; }
|
||||
}
|
||||
say("best XOR key 0x%02X", bestKey);
|
||||
say("printable %.0f%%", bestScore*100);
|
||||
char prev[24]; int j=0;
|
||||
for (int i=0;i<20 && i<len;i++){ uint8_t v=buf[i]^bestKey; prev[j++]=(v>=32&&v<127)?v:'.'; }
|
||||
prev[j]=0; say("%s", prev);
|
||||
}
|
||||
void doAes() {
|
||||
say(haveKey ? "AES-128-ECB, custom key" : "AES-128-ECB, key=00..00");
|
||||
if (!haveKey) say("(edit aesKey[] for real key)");
|
||||
mbedtls_aes_context ctx; mbedtls_aes_init(&ctx);
|
||||
mbedtls_aes_setkey_dec(&ctx, aesKey, 128);
|
||||
uint8_t o[16];
|
||||
int blocks = len/16 < 2 ? len/16 : 2;
|
||||
for (int b=0;b<blocks;b++) {
|
||||
mbedtls_aes_crypt_ecb(&ctx, MBEDTLS_AES_DECRYPT, buf+b*16, o);
|
||||
char h[40]; int p=0;
|
||||
for (int i=0;i<16;i++) p+=snprintf(h+p,sizeof(h)-p,"%02X",o[i]);
|
||||
say("%s", h);
|
||||
}
|
||||
mbedtls_aes_free(&ctx);
|
||||
}
|
||||
};
|
||||
|
||||
Module* makeCrypto() { return new Crypto(); }
|
||||
104
src/modules/exfil.cpp
Normal file
104
src/modules/exfil.cpp
Normal file
@@ -0,0 +1,104 @@
|
||||
#include "../core/module.h"
|
||||
#include "../core/ui.h"
|
||||
#include <SD.h>
|
||||
#include "mbedtls/base64.h"
|
||||
|
||||
// Exfil / Export — get captured data OFF the Cardputer for analysis on
|
||||
// your workstation. Browse the SD card and stream a selected dump out
|
||||
// over USB serial as base64 (paste into `base64 -d > file` on the host).
|
||||
// Also writes a session manifest of what you pulled. A data-offload
|
||||
// helper for your own captures — not a network channel.
|
||||
|
||||
class Exfil : public Module {
|
||||
static constexpr int MAXF = 32;
|
||||
char files[MAXF][32]; uint32_t sizes[MAXF]; int nf = 0, sel = 0;
|
||||
bool sending = false; File cur; uint32_t sent = 0, total = 0;
|
||||
char status[2][40] = {{0},{0}};
|
||||
public:
|
||||
const char* name() const override { return "Exfil"; }
|
||||
const char* blurb() const override { return "export dumps->host"; }
|
||||
|
||||
void onEnter() override { sending=false; if (!SD.begin()) say("no SD"); else scan(); }
|
||||
void onExit() override { if (cur) cur.close(); sending=false; }
|
||||
|
||||
bool onKey(char c) override {
|
||||
if (sending) { if (c==' ') stop(); return true; }
|
||||
int m = nf ? nf : 1;
|
||||
if (c == ';') { sel = (sel-1+m)%m; return true; }
|
||||
if (c == '.') { sel = (sel+1)%m; return true; }
|
||||
if (c == 'r') { scan(); return true; }
|
||||
if (c == '\r' || c == ' ') { startSend(); return true; }
|
||||
if (c == 'x') { writeManifest(); return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
void tick() override {
|
||||
if (!sending) return;
|
||||
uint8_t in[48]; int n = cur.read(in, sizeof(in));
|
||||
if (n <= 0) { Serial.println(); say("done: %s", files[sel]); stop(); return; }
|
||||
unsigned char out[80]; size_t olen = 0;
|
||||
mbedtls_base64_encode(out, sizeof(out), &olen, in, n);
|
||||
Serial.write(out, olen);
|
||||
sent += n;
|
||||
}
|
||||
|
||||
void draw() override {
|
||||
ui::lineC(0, ui::accent(), "SD export %s", sending ? "STREAMING" : "browse");
|
||||
if (sending) {
|
||||
ui::bar(1, total ? sent/(float)total : 0, ui::glow(), "b64->usb");
|
||||
} else {
|
||||
for (int i = 0; i < nf && i < 5; i++)
|
||||
ui::lineC(1+i, i==sel ? ui::glow() : ui::fg(), "%c %-16s %luB",
|
||||
i==sel?char(0x10):' ', files[i], (unsigned long)sizes[i]);
|
||||
if (!nf) ui::line(1, "no files in /dump");
|
||||
}
|
||||
for (int i = 0; i < 2; i++) ui::line(6+i, "%s", status[i]);
|
||||
ui::hintBar("; . pick enter send [x]manifest [`]back");
|
||||
}
|
||||
|
||||
private:
|
||||
void say(const char* fmt, ...) {
|
||||
strncpy(status[1], status[0], 39);
|
||||
va_list ap; va_start(ap,fmt); vsnprintf(status[0],40,fmt,ap); va_end(ap);
|
||||
}
|
||||
void scan() {
|
||||
nf = 0; sel = 0;
|
||||
File d = SD.open("/dump");
|
||||
if (!d) { say("no /dump dir"); return; }
|
||||
File f;
|
||||
while ((f = d.openNextFile()) && nf < MAXF) {
|
||||
if (!f.isDirectory()) {
|
||||
strncpy(files[nf], f.name(), 31);
|
||||
sizes[nf] = f.size();
|
||||
nf++;
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
d.close();
|
||||
say("%d file(s)", nf);
|
||||
}
|
||||
void startSend() {
|
||||
if (!nf) { say("nothing to send"); return; }
|
||||
char path[48]; snprintf(path, sizeof(path), "/dump/%s", files[sel]);
|
||||
cur = SD.open(path, FILE_READ);
|
||||
if (!cur) { say("open fail"); return; }
|
||||
total = sizes[sel]; sent = 0; sending = true;
|
||||
Serial.printf("\n----- BEGIN %s (base64) -----\n", files[sel]);
|
||||
say("streaming %s", files[sel]);
|
||||
}
|
||||
void stop() {
|
||||
if (cur) cur.close();
|
||||
Serial.printf("\n----- END -----\n");
|
||||
sending = false;
|
||||
}
|
||||
void writeManifest() {
|
||||
File m = SD.open("/dump/MANIFEST.txt", FILE_WRITE);
|
||||
if (!m) { say("manifest fail"); return; }
|
||||
m.printf("Card-Crack export manifest\n");
|
||||
for (int i = 0; i < nf; i++) m.printf("%-20s %lu B\n", files[i], (unsigned long)sizes[i]);
|
||||
m.close();
|
||||
say("wrote MANIFEST.txt");
|
||||
}
|
||||
};
|
||||
|
||||
Module* makeExfil() { return new Exfil(); }
|
||||
43
src/modules/themepick.cpp
Normal file
43
src/modules/themepick.cpp
Normal file
@@ -0,0 +1,43 @@
|
||||
#include "../core/module.h"
|
||||
#include "../core/ui.h"
|
||||
#include "../core/theme.h"
|
||||
#include <M5Cardputer.h>
|
||||
|
||||
// Live theme switcher. Preview swatches, [space]/arrows to cycle, choice
|
||||
// saved to NVS so it sticks across reboots.
|
||||
class ThemePicker : public Module {
|
||||
public:
|
||||
const char* name() const override { return "Theme"; }
|
||||
const char* blurb() const override { return "restyle the UI"; }
|
||||
|
||||
bool onKey(char c) override {
|
||||
if (c == '.' || c == ' ') { theme::set(theme::current() + 1); repaint(); return true; }
|
||||
if (c == ';') { theme::set(theme::current() - 1); repaint(); return true; }
|
||||
return false;
|
||||
}
|
||||
void onEnter() override { repaint(); }
|
||||
|
||||
void draw() override {
|
||||
auto& d = M5.Display;
|
||||
ui::lineC(0, ui::accent(), "theme: %s (%d/%d)",
|
||||
theme::active().name, theme::current() + 1, theme::COUNT);
|
||||
// swatch row
|
||||
int y = ui::BODY_Y + 20;
|
||||
for (int i = 0; i < theme::COUNT; i++) {
|
||||
const Theme& t = theme::LIST[i];
|
||||
int x = 6 + i * 30;
|
||||
d.fillRoundRect(x, y, 24, 24, 4, ui::rgb(t.bg));
|
||||
d.fillRect(x + 4, y + 4, 16, 5, ui::rgb(t.accent));
|
||||
d.fillRect(x + 4, y + 11, 10, 4, ui::rgb(t.warn));
|
||||
d.fillRect(x + 4, y + 16, 14, 4, ui::rgb(t.bad));
|
||||
d.drawRoundRect(x, y, 24, 24, 4, i == theme::current() ? ui::glow() : ui::dim());
|
||||
}
|
||||
ui::line(5, "everything restyles instantly.");
|
||||
ui::hintBar("; / . cycle [`]back");
|
||||
}
|
||||
private:
|
||||
void repaint() { M5.Display.fillScreen(ui::bg());
|
||||
ui::titleBar(name(), blurb()); draw(); }
|
||||
};
|
||||
|
||||
Module* makeThemePicker() { return new ThemePicker(); }
|
||||
Reference in New Issue
Block a user