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:
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