Loki Phase 1: BLE identity cycling countermeasure

16 fake device profiles (Galaxy S24, iPhone, Pixel 9, AirPods Pro, etc.)
cycling every 2s with randomized MACs and realistic manufacturer data.
Targets ESP32-C3 Super Mini with NimBLE 1.4.3 on ESP-IDF 4.4.x.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
rpriven 2026-02-23 00:03:07 -07:00
commit a7f26f88e4
Signed by: djedi
GPG key ID: D04DED574622EF45
3 changed files with 132 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.pio
.vscode

11
platformio.ini Normal file
View file

@ -0,0 +1,11 @@
[env:seeed_xiao_esp32c3]
platform = espressif32@6.9.0
framework = arduino
board = seeed_xiao_esp32c3
monitor_speed = 115200
upload_speed = 115200
build_flags =
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1
lib_deps =
h2zero/NimBLE-Arduino@^1.4.3

119
src/main.cpp Normal file
View file

@ -0,0 +1,119 @@
// Loki / Chaff — BLE Countermeasure (Phase 1)
// Broadcasts fake BLE device advertisements with randomized MACs
// to confuse passive BLE surveillance scanners.
//
// Hardware: ESP32-C3 Super Mini
// Framework: Arduino + NimBLE 1.4.x (ESP-IDF 4.4.x required for C3 BLE)
#include <Arduino.h>
#include <NimBLEDevice.h>
// ── Device profiles ──
struct DeviceProfile {
const char* name;
uint16_t appearance;
uint8_t mfgId[2]; // Company ID (little-endian)
uint8_t mfgData[4];
uint8_t mfgDataLen;
};
static const DeviceProfile PROFILES[] = {
{"Galaxy S24", 0x0040, {0x75, 0x00}, {0x42, 0x04, 0x01, 0x80}, 4},
{"iPhone", 0x0040, {0x4C, 0x00}, {0x02, 0x15, 0x06, 0x01}, 4},
{"Pixel 9", 0x0040, {0xE0, 0x00}, {0x01, 0x03, 0x00, 0x22}, 4},
{"OnePlus 12", 0x0040, {0x75, 0x00}, {0x42, 0x04, 0x02, 0x10}, 4},
{"iPad", 0x0080, {0x4C, 0x00}, {0x02, 0x15, 0x05, 0x03}, 4},
{"Galaxy Watch6", 0x00C0, {0x75, 0x00}, {0x42, 0x04, 0x03, 0x60}, 4},
{"Apple Watch", 0x00C0, {0x4C, 0x00}, {0x02, 0x15, 0x0C, 0x01}, 4},
{"Mi Band 8", 0x02C0, {0x10, 0x03}, {0x01, 0x02, 0x08, 0x00}, 4},
{"Fitbit Charge 6", 0x00C0, {0xE0, 0x00}, {0x01, 0x01, 0x06, 0x04}, 4},
{"AirPods Pro", 0x0941, {0x4C, 0x00}, {0x07, 0x19, 0x01, 0x0E}, 4},
{"Galaxy Buds3", 0x0941, {0x75, 0x00}, {0x42, 0x04, 0x05, 0x21}, 4},
{"Surface Laptop", 0x0080, {0x06, 0x00}, {0x01, 0x09, 0x02, 0x00}, 4},
{"Nothing Phone (2)", 0x0040, {0x75, 0x00}, {0x42, 0x04, 0x01, 0x44}, 4},
{"Bose QC Ultra", 0x0941, {0x0F, 0x00}, {0x02, 0x01, 0x04, 0x08}, 4},
{"JBL Flip 6", 0x0941, {0x0F, 0x00}, {0x02, 0x01, 0x06, 0x12}, 4},
{"Sony WH-1000XM5", 0x0941, {0x0F, 0x00}, {0x02, 0x01, 0x05, 0x20}, 4},
};
static const int NUM_PROFILES = sizeof(PROFILES) / sizeof(PROFILES[0]);
static const uint32_t ADV_DURATION_MS = 2000;
static const uint32_t ADV_GAP_MS = 100;
static int currentProfile = 0;
static NimBLEAdvertising* pAdvertising = nullptr;
static void generateRandomMAC(uint8_t* mac) {
uint32_t r1 = esp_random();
uint32_t r2 = esp_random();
mac[0] = (r1 & 0xFF) | 0xC0; // Random static address bits
mac[1] = (r1 >> 8) & 0xFF;
mac[2] = (r1 >> 16) & 0xFF;
mac[3] = r2 & 0xFF;
mac[4] = (r2 >> 8) & 0xFF;
mac[5] = (r2 >> 16) & 0xFF;
}
static void advertiseProfile(int idx) {
const DeviceProfile& p = PROFILES[idx];
pAdvertising->stop();
// Set new random MAC
uint8_t mac[6];
generateRandomMAC(mac);
ble_hs_id_set_rnd(mac);
// Build advertisement data
NimBLEAdvertisementData advData;
advData.setFlags(BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP);
advData.setName(p.name);
advData.setAppearance(p.appearance);
// Manufacturer-specific data
std::string mfg;
mfg += (char)p.mfgId[0];
mfg += (char)p.mfgId[1];
for (int i = 0; i < p.mfgDataLen; i++) {
mfg += (char)p.mfgData[i];
}
advData.setManufacturerData(mfg);
pAdvertising->setAdvertisementData(advData);
pAdvertising->setAdvertisementType(BLE_GAP_CONN_MODE_NON);
pAdvertising->start();
Serial.printf("[%4lus] %-20s MAC %02X:%02X:%02X:%02X:%02X:%02X\n",
millis() / 1000, p.name,
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
void setup() {
Serial.begin(115200);
delay(500);
Serial.println();
Serial.println("=============================");
Serial.println(" LOKI — BLE Countermeasure");
Serial.println(" Phase 1: Identity Cycling");
Serial.printf(" Profiles: %d devices\n", NUM_PROFILES);
Serial.println("=============================");
Serial.println();
NimBLEDevice::init("");
NimBLEDevice::setOwnAddrType(BLE_OWN_ADDR_RANDOM);
pAdvertising = NimBLEDevice::getAdvertising();
Serial.println("Broadcasting...\n");
}
void loop() {
advertiseProfile(currentProfile);
delay(ADV_DURATION_MS);
pAdvertising->stop();
delay(ADV_GAP_MS);
currentProfile = (currentProfile + 1) % NUM_PROFILES;
}