ESP32 Attack Surface Analysis: Practical Lab Guide

Before you can secure an embedded device you need to know what can be attacked. This lab builds a systematic ESP32 attack surface analysis: every hardware pin, wireless radio, software service and debug connector is catalogued, scored on a 1–10 risk scale and fed into a prioritised remediation plan. The complete Arduino code runs on an ESP32 DOIT DevKit V1 and reports its findings over Serial Monitor in under 30 seconds. A stock dev board with no hardening applied typically scores in the critical risk range – this lab makes that concrete. This is Lab 2 of the Embedded Systems Security course series.
Attack Surface vs. Vulnerability
An attack surface is the complete set of points where an attacker can attempt to enter, extract data from or disrupt a system. A vulnerability is a specific exploitable weakness at one of those points. The distinction matters for prioritisation: you reduce attack surface by disabling interfaces you do not need and you remediate vulnerabilities at the interfaces that remain. A device with a smaller attack surface gives attackers less to work with even before any vulnerabilities are patched.
The ESP32 exposes considerably more attack surface than a simpler 8-bit microcontroller. It has two wireless radios, a UART console accessible over USB, a JTAG debug interface, SPI-connected flash memory, 30 GPIO pins and a network protocol stack that can run HTTP, MQTT, OTA and mDNS simultaneously. Every one of those is an entry point. Measuring the risk of each one is the goal of this lab.
Equipment and Prerequisites
You need an ESP32 DOIT DevKit V1, a USB cable and Arduino IDE with the ESP32 board package installed. Open Serial Monitor at 115200 baud before uploading. No external hardware or additional libraries are required – the code uses only built-in ESP32 Arduino SDK headers: WiFi.h, esp_bt.h, esp_wifi.h, esp_system.h and esp_efuse.h.
Complete Lab Code
The scanner defines an AttackSurface struct and populates it across four scan functions covering hardware, wireless, software and debug interfaces. It then totals the risk scores, prints a category breakdown and outputs prioritised recommendations. The esp_flash_encryption_enabled() call reads the actual eFuse state, so the flash encryption risk score reflects your device’s real configuration.
/*
* ESP32 Attack Surface Scanner and Analyzer
* Maps all interfaces and calculates a quantified security risk score.
* Hardware: ESP32 DOIT DevKit V1
*/
#include <WiFi.h>
#include <esp_bt.h>
#include <esp_wifi.h>
#include <esp_system.h>
#include <esp_efuse.h>
#include <esp_mac.h>
#include <soc/efuse_reg.h>
const char* deviceName = "ESP32-LAB-DEVICE";
bool isFlashEncryptionEnabled() {
uint32_t efuse_bl0 = REG_READ(EFUSE_BLK0_RDATA2_REG);
return (efuse_bl0 & (1 << 15)) != 0;
}
struct AttackSurface {
String name;
String type; // Hardware | Wireless | Software | Debug
bool isExposed;
int riskLevel; // 1-10
String vulnerability;
String mitigation;
};
AttackSurface surfaces[15];
int surfaceCount = 0;
void addSurface(String name, String type, bool exposed, int risk,
String vuln, String mit) {
surfaces[surfaceCount].name = name;
surfaces[surfaceCount].type = type;
surfaces[surfaceCount].isExposed = exposed;
surfaces[surfaceCount].riskLevel = risk;
surfaces[surfaceCount].vulnerability = vuln;
surfaces[surfaceCount].mitigation = mit;
surfaceCount++;
}
void scanHardwareInterfaces() {
Serial.println("\n=== HARDWARE ATTACK SURFACES ===");
// 1. UART Console
Serial.println("\n1. UART/Serial Interface");
Serial.println(" Status: EXPOSED (via USB)");
Serial.println(" Risk: HIGH (8/10)");
Serial.println(" Vector: Debug console access, command injection");
Serial.println(" Mitigation: Disable in production; wrap in #ifdef DEBUG_SERIAL");
addSurface("UART0 Serial Console","Hardware",true,8,
"Unauthenticated debug access, command injection via serial",
"Wrap all Serial calls in #ifdef DEBUG_SERIAL; never ship with serial enabled");
// 2. GPIO Pins
Serial.println("\n2. GPIO Pins (30 pins, 6 input-only)");
Serial.println(" Status: EXPOSED (physical access required)");
Serial.println(" Risk: MEDIUM (6/10)");
Serial.println(" Vector: Voltage glitching, power analysis, SPI bus probing");
Serial.println(" Mitigation: Tamper-evident enclosure, disable unused pins");
addSurface("GPIO Pins","Hardware",true,6,
"Voltage glitching, power/EM side-channel, SPI bus probing",
"Physical enclosure with tamper detection; disable unused GPIO");
// 3. Flash Memory - risk depends on encryption state
bool flashEnc = isFlashEncryptionEnabled();
Serial.println("\n3. Flash Memory (SPI)");
Serial.printf(" Size: %u MB\n", ESP.getFlashChipSize()/(1024*1024));
Serial.print(" Encryption: ");
Serial.println(flashEnc ? "ENABLED" : "DISABLED <- CRITICAL");
Serial.println(flashEnc ? " Risk: LOW (3/10)" : " Risk: CRITICAL (10/10)");
Serial.println(" Vector: Complete firmware extraction via esptool.py");
Serial.println(" Mitigation: Enable flash encryption before production (permanent)");
addSurface("Flash Memory (SPI)","Hardware",!flashEnc,flashEnc?3:10,
"Full firmware + secrets extraction with free tools in under 10 minutes",
"Burn flash encryption eFuse in production - test firmware first; irreversible");
// 4. Boot Mode Pins
Serial.println("\n4. Boot Mode Pins (GPIO0)");
Serial.println(" Status: EXPOSED (physical access)");
Serial.println(" Risk: HIGH (7/10)");
Serial.println(" Vector: Force download mode, flash unsigned firmware");
Serial.println(" Mitigation: Enable secure boot; restrict physical GPIO0 access");
addSurface("Boot Mode Selection","Hardware",true,7,
"Force serial download mode; replace firmware without authentication",
"Enable secure boot (permanent); restrict physical access to GPIO0");
}
void scanWirelessInterfaces() {
Serial.println("\n=== WIRELESS ATTACK SURFACES ===");
// 5. WiFi
Serial.println("\n5. WiFi (802.11 b/g/n)");
Serial.println(" MAC: " + WiFi.macAddress());
Serial.println(" Risk: CRITICAL (9/10) when active");
Serial.println(" Vectors: Packet sniffing, evil twin, deauth, MITM, KRACK");
Serial.println(" Mitigation: WPA3, TLS for all protocols, certificate validation");
addSurface("WiFi Interface","Wireless",true,9,
"Packet sniffing, evil twin AP, deauth attacks, MITM, KRACK/DRAGONBLOOD",
"Enforce TLS at application layer; validate server certificates; use WPA3");
// 6. Bluetooth / BLE
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_BT);
Serial.println("\n6. Bluetooth Classic + BLE");
Serial.printf(" BT MAC: %02X:%02X:%02X:%02X:%02X:%02X\n",
mac[0],mac[1],mac[2],mac[3],mac[4],mac[5]);
Serial.println(" Risk: HIGH (8/10) when active");
Serial.println(" Vectors: BlueBorne, BLE injection, insecure pairing, MITM");
Serial.println(" Mitigation: Disable if unused via esp_bt_controller_disable()");
addSurface("Bluetooth/BLE","Wireless",true,8,
"BlueBorne vulnerabilities, BLE injection, insecure Just Works pairing",
"Call esp_bt_controller_disable() + esp_bt_controller_deinit() if not required");
}
void scanSoftwareInterfaces() {
Serial.println("\n=== SOFTWARE ATTACK SURFACES ===");
// 7. Network Protocols
Serial.println("\n7. Network Protocols (when WiFi active)");
Serial.println(" Services: HTTP, MQTT, mDNS, OTA, NTP");
Serial.println(" Risk: HIGH (8/10) without authentication");
Serial.println(" Vectors: Protocol exploits, injection, auth bypass, hijack");
Serial.println(" Mitigation: TLS, strong auth, input validation, rate limiting");
addSurface("Network Protocols","Software",false,8,
"Protocol-specific exploits, injection, authentication bypass, session hijack",
"Enforce TLS; authenticate every endpoint; validate all input; rate-limit");
// 8. Application Code
Serial.println("\n8. Application Code");
Serial.println(" Risk: VARIES (code-quality dependent)");
Serial.println(" Vectors: Buffer overflow, integer overflow, hardcoded credentials");
Serial.println(" Mitigation: Secure coding standards, cppcheck, code review");
addSurface("Application Code","Software",true,7,
"Buffer overflows, integer overflows, use-after-free, hardcoded credentials",
"Secure coding standards; static analysis (cppcheck); regular code review");
}
void scanDebugInterfaces() {
Serial.println("\n=== DEBUG ATTACK SURFACES ===");
// 9. JTAG
Serial.println("\n9. JTAG Interface");
Serial.println(" Risk: HIGH (8/10) when enabled");
Serial.println(" Vectors: Full debug: read/write memory, bypass security controls");
Serial.println(" Mitigation: espefuse.py burn_efuse JTAG_DISABLE before production");
addSurface("JTAG Interface","Debug",true,8,
"Full hardware debug: read/write arbitrary memory; bypass all software security",
"Burn JTAG_DISABLE eFuse (permanent); include in pre-shipment checklist");
// 10. USB Bootloader
Serial.println("\n10. USB Serial Bootloader");
Serial.println(" Risk: HIGH (7/10)");
Serial.println(" Vectors: Unauthenticated firmware replacement via USB");
Serial.println(" Mitigation: Enable secure boot + flash encryption together");
addSurface("USB Bootloader","Debug",true,7,
"Unauthenticated firmware replacement via USB serial download mode",
"Enable secure boot and flash encryption; both required for full protection");
}
void calculateRiskScore() {
Serial.println("\n=== ATTACK SURFACE RISK SCORE ===");
int total=0, exposed=0, critical=0, hw=0, wl=0, sw=0, db=0;
for (int i = 0; i < surfaceCount; i++) {
AttackSurface& s = surfaces[i];
if (s.isExposed) { exposed++; total += s.riskLevel; }
if (s.riskLevel >= 8 && s.isExposed) critical++;
if (s.type=="Hardware") hw += s.riskLevel;
else if (s.type=="Wireless") wl += s.riskLevel;
else if (s.type=="Software") sw += s.riskLevel;
else if (s.type=="Debug") db += s.riskLevel;
}
Serial.println("Interfaces Scanned: " + String(surfaceCount));
Serial.println("Currently Exposed: " + String(exposed));
Serial.println("Critical Risk (8-10): " + String(critical));
Serial.println("\nRisk by Category:");
Serial.println(" Hardware: " + String(hw));
Serial.println(" Wireless: " + String(wl));
Serial.println(" Software: " + String(sw));
Serial.println(" Debug: " + String(db));
Serial.println("\n================================");
Serial.println(" TOTAL RISK SCORE: " + String(total) + " / 100");
Serial.println("================================");
if (total >= 60) Serial.println("Status: CRITICAL RISK - Immediate remediation required");
else if (total >= 40) Serial.println("Status: HIGH RISK - Remediation needed");
else if (total >= 20) Serial.println("Status: MEDIUM RISK - Improvements recommended");
else Serial.println("Status: LOW RISK - Maintain current controls");
}
void printRecommendations() {
bool flashEnc = isFlashEncryptionEnabled();
Serial.println("\n=== SECURITY RECOMMENDATIONS ===");
Serial.println("\nIMMEDIATE (Critical):");
if (!flashEnc) {
Serial.println(" 1. Enable Flash Encryption (permanent eFuse burn)");
Serial.println(" Prevents firmware + secret extraction from physical access");
}
Serial.println(" 2. Enable Secure Boot (permanent eFuse burn)");
Serial.println(" Prevents unsigned firmware execution");
Serial.println(" 3. Disable Serial output in production builds");
Serial.println(" Use #ifdef DEBUG_SERIAL for all Serial calls");
Serial.println("\nSHORT-TERM:");
Serial.println(" 4. Burn JTAG_DISABLE eFuse before shipping");
Serial.println(" 5. Disable Bluetooth if application does not use it");
Serial.println(" 6. Enforce TLS on all network communication");
Serial.println("\nONGOING:");
Serial.println(" 7. Re-run attack surface scan after firmware changes");
Serial.println(" 8. Physical enclosure with tamper-evident seals");
}
void setup() {
Serial.begin(115200);
delay(2000);
Serial.println("\n=== ESP32 ATTACK SURFACE ANALYSIS ===");
Serial.println("Device: " + String(deviceName));
Serial.println("Chip: " + String(ESP.getChipModel()));
Serial.println("Flash: " + String(ESP.getFlashChipSize()/(1024*1024)) + " MB");
Serial.println("CPU: " + String(ESP.getCpuFreqMHz()) + " MHz");
scanHardwareInterfaces();
scanWirelessInterfaces();
scanSoftwareInterfaces();
scanDebugInterfaces();
calculateRiskScore();
printRecommendations();
Serial.println("\nPress 'R' to re-scan.");
}
void loop() {
if (Serial.available()) {
char cmd = Serial.read();
if (cmd == 'R' || cmd == 'r') ESP.restart();
}
delay(1000);
}
Hardware Attack Surfaces
UART/Serial Console (8/10). The UART0 interface connects directly to the USB-to-serial converter on dev boards. With serial active, any physical attacker reads all debug output in real time – typically including IP addresses, error messages with function names, sensor readings and authentication events. If a serial command interpreter is active, commands can be sent. The fix is a build flag: wrap every Serial call in #ifdef DEBUG_SERIAL and never define that flag in production builds. Production firmware should not call Serial.begin() at all.
GPIO Pins (6/10). Thirty accessible copper pads enable three hardware attack classes: voltage glitching (a timed voltage spike on VCC or RESET can corrupt the CPU’s instruction fetch pipeline, skipping security checks), power analysis (measuring current draw on VCC during cryptographic operations can leak key bits) and SPI bus probing (a logic analyser on the four SPI lines between MCU and flash reveals every byte transferred during firmware reads and writes). Physical enclosure with tamper-evident seals is the primary control.
Flash Memory (10/10 unencrypted, 3/10 encrypted). The single highest-risk interface on an unprotected ESP32. Running esptool.py read_flash 0 0x400000 firmware.bin extracts the complete 4 MB flash in 5–10 minutes using Espressif’s own toolchain. Running strings firmware.bin on the dump reveals every string literal: WiFi credentials, API keys, MQTT passwords, admin credentials, private keys. The attack leaves no trace on the device. Flash encryption changes this completely: the flash controller decrypts in hardware using a key stored in one-time-programmable eFuse. A raw flash read produces only unintelligible ciphertext. Lab 5 in this series demonstrates the full extraction attack and protection workflow.
Boot Mode Pins (7/10). Holding GPIO0 low at power-on puts the ESP32 into serial download mode, allowing unsigned firmware to be flashed via USB without any authentication – provided secure boot is not enabled. Secure boot closes this by requiring all firmware images to carry a cryptographic signature verifiable against a public key burned in eFuse. Unsigned firmware simply will not execute regardless of how it was installed.
Wireless Attack Surfaces
WiFi (9/10 when active). Five independent attack vectors apply. Passive packet capture: any unencrypted traffic is readable by anyone on the same network segment. Evil twin attack: a fake AP with the same SSID captures WPA handshakes or redirects application traffic. Deauthentication: a spoofed deauth frame forces reconnection into an attacker-controlled environment. MITM: without certificate validation at the application layer, any attacker who can intercept packets can silently relay and modify communication. Protocol attacks: KRACK exploits the WPA2 four-way handshake; DRAGONBLOOD targets WPA3. The layered defence is WPA3 for network authentication, TLS at the application layer so captured packets are unreadable and certificate validation so MITM at the network layer does not compromise the application layer. Labs 3 and 6 in this series demonstrate the before and after.
Bluetooth/BLE (8/10 when active). Both Bluetooth Classic and BLE are enabled in the ESP32 radio by default even if the sketch never calls a Bluetooth API. BlueBorne is a class of Bluetooth stack vulnerabilities exploitable without pairing from a nearby device. BLE advertising continuously broadcasts the device’s name and service UUIDs to every nearby Bluetooth scanner, enabling passive reconnaissance. Just Works pairing provides no authentication, making MITM trivial. If your application does not use Bluetooth, call esp_bt_controller_disable() and esp_bt_controller_deinit() in setup() and remove the entire radio from the attack surface.
Software and Debug Attack Surfaces
Network Protocols (8/10). Each protocol the device runs extends the software attack surface. An HTTP server without authentication allows anyone on the network to read sensor data and trigger control actions. MQTT without TLS exposes all published and subscribed messages to passive capture. The OTA update mechanism without password protection allows anyone on the network to push replacement firmware. mDNS broadcasts the device’s hostname and service list, giving an attacker a free inventory of all running services. Labs 6 and 9 cover MQTT TLS and OTA security respectively.
Application Code (7/10). Anywhere the firmware processes external data is a code-level attack surface: UART input, MQTT payloads, HTTP parameters, values received from sensors that a physical attacker could influence. Buffer overflows, integer overflows, hardcoded credentials and use-after-free bugs all live here. Lab 4 demonstrates buffer overflows on the ESP32. Lab 7 covers automated vulnerability detection with cppcheck.
JTAG Interface (8/10). JTAG is the most powerful hardware attack capability: read and write arbitrary memory addresses, set breakpoints, halt and single-step execution, read all CPU registers. On a production device it must be permanently disabled via espefuse.py burn_efuse JTAG_DISABLE. This operation is irreversible, so it belongs in the final pre-shipment eFuse sequence alongside flash encryption and secure boot, not during active development.
Risk Score Reference Table
| Interface | Type | Risk | Primary Reason |
|---|---|---|---|
| Flash Memory (unencrypted) | Hardware | 10/10 | Full extraction with free tools in <10 min; leaves no trace |
| WiFi (active) | Wireless | 9/10 | Remote access; five independent attack vectors |
| UART Console | Hardware | 8/10 | Real-time debug data; command injection if interpreter active |
| Bluetooth/BLE (active) | Wireless | 8/10 | BlueBorne; always-on advertisement; insecure pairing |
| JTAG Interface | Debug | 8/10 | Full hardware debug; bypasses all software security |
| Network Protocols | Software | 8/10 | Each service an independent attack surface; auth bypass risk |
| Application Code | Software | 7/10 | Memory safety bugs; hardcoded credentials; code-quality dependent |
| Boot Mode Pins | Hardware | 7/10 | Unsigned firmware replacement if secure boot not enabled |
| USB Bootloader | Debug | 7/10 | Same as boot mode; accessible via USB serial port |
| GPIO Pins | Hardware | 6/10 | Glitching and side-channel; physical access required |
| Flash Memory (encrypted) | Hardware | 3/10 | Raw read produces ciphertext; key in write-once eFuse |
A stock dev board with no hardening scores 69–75/100 (critical risk). The same board with flash encryption, secure boot, JTAG disable and Bluetooth disabled scores 20–30/100 (medium risk) – representing the unavoidable surface of a functional wireless device.
Execution Steps
Upload the code, open Serial Monitor at 115200 baud. The scan runs automatically. Step 1: watch the sequential interface scan and note the flash encryption status – on most unmodified dev boards it reads DISABLED at risk 10/10. Step 2: note the total risk score and category breakdown. Step 3: read the ordered recommendations and identify which require irreversible eFuse operations. Step 4: press ‘R’ to restart and rescan after making configuration changes, to confirm the score has improved.
Expected Serial Monitor Output
=== ESP32 ATTACK SURFACE ANALYSIS ===
Device: ESP32-LAB-DEVICE
Chip: ESP32-D0WDQ6
Flash: 4 MB
CPU: 240 MHz
3. Flash Memory (SPI)
Encryption: DISABLED <- CRITICAL
Risk: CRITICAL (10/10)
Vector: Complete firmware extraction via esptool.py
================================
TOTAL RISK SCORE: 69 / 100
================================
Status: CRITICAL RISK - Immediate remediation required
IMMEDIATE (Critical):
1. Enable Flash Encryption (permanent eFuse burn)
2. Enable Secure Boot (permanent eFuse burn)
3. Disable Serial output in production builds
Prioritising the Fixes
Flash encryption: fix first. Risk 10/10, exploit requires zero skill, extraction is silent. The fix is permanent – test encrypted firmware thoroughly on development hardware before burning the eFuse on any production unit. The ESP-IDF menuconfig workflow is the reference. Once burned, it cannot be undone.
Secure boot: fix second, part of the same eFuse burn session. Flash encryption stops firmware reading; secure boot stops firmware replacement. Both should be enabled together before production shipment. Signing keys must be generated, firmware must be signed and the eFuse burn is irreversible.
JTAG disable: plan for the pre-shipment sequence. Do not disable JTAG during active development – you need it for debugging. Include it in the final production eFuse burn checklist.
Bluetooth disable: one function call, do it now. If the application does not use Bluetooth, add esp_bt_controller_disable() and esp_bt_controller_deinit() to setup(). Reversible at any time. Removes a complete radio from the attack surface.
Serial console: use build flags. Wrap every Serial call in #ifdef DEBUG_SERIAL. Never define DEBUG_SERIAL in production builds. Zero cost; should be standard practice from the first line of production-bound firmware.
Discussion Questions
What is the difference between attack surface and vulnerability? The attack surface is the set of all possible entry points – UART, GPIO, WiFi, JTAG and so on. A vulnerability is a specific exploitable weakness at one of those entry points. A large attack surface creates more code paths, more protocols and more interfaces to contain bugs in.
Why is unencrypted flash the single highest-risk item? The attack requires no special skills, no expensive equipment and no exploit code. It uses Espressif’s own official firmware tool. The dump contains every string literal in the firmware: credentials, keys, passwords, certificates. The attack is completely silent – no log entries, no error output, no way to detect it after the fact.
Can the attack surface be eliminated completely? No. A useful device must expose some interfaces. The goal is to minimise it to what the application genuinely requires, authenticate every necessary interface, encrypt every data path and have a plan for when a surface is attacked.
What does leaving Bluetooth enabled cost? Two risk points and a permanent radio advertising the device’s identity to every nearby Bluetooth scanner, regardless of whether the application code uses Bluetooth. Disabling it is a two-function call.
Key Takeaways
Every interface is a potential attack vector: UART, GPIO, WiFi, BLE, JTAG, USB and every network protocol the device runs. Unencrypted flash is the highest-risk item because it is exploitable in minutes with free tools and reveals everything in the device. Attack surface scoring provides a concrete priority order for remediation. Defence in depth means layering flash encryption, secure boot, JTAG disable, TLS and access control – no single control is sufficient alone. Unused interfaces must be disabled at the code level rather than left enabled as dead code. The output of this lab is the foundation for every subsequent security decision across the rest of this course.