This shows you the differences between two versions of the page.
|
iothings:laboratoare:2025:lab8 [2025/11/15 10:52] dan.tudose [Step 5 – Try to tamper with the data] |
iothings:laboratoare:2025:lab8 [2025/11/15 13:31] (current) dan.tudose [Lab 8. Security in IoT] |
||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | ====== Lab 8. Security in IoT ====== | + | ====== Lab 8. IoT Security and Attacks ====== |
| ===== Breaking an insecure IoT sensor over HTTP ===== | ===== Breaking an insecure IoT sensor over HTTP ===== | ||
| Line 29: | Line 29: | ||
| - | Let's write a (horrible) but functional code that connects to a HTTP server and sends periodically sensor data. This is the "bad" example in which we are using plaintext messaging over an unsecured connection. | + | Let's write a (horribly unsafe) but functional code that connects to a HTTP server and sends periodically sensor data. This is the "bad" example in which we are using plaintext messaging over an unsecured connection. |
| - | + | ||
| - | <code C main.cpp> | + | |
| - | #include <Arduino.h> | + | |
| - | #include <WiFi.h> | + | |
| - | #include <HTTPClient.h> | + | |
| - | + | ||
| - | // ==== TODO: fill these in for the lab ==== | + | |
| - | const char* WIFI_SSID = "LAB_WIFI_SSID"; | + | |
| - | const char* WIFI_PASSWORD = "LAB_WIFI_PASSWORD"; | + | |
| - | const char* SERVER_URL = "http://192.168.1.100:8080/ingest"; // replace with your server IP | + | |
| - | // ========================================= | + | |
| - | + | ||
| - | const char* DEVICE_ID = "sparrow-01"; | + | |
| - | const char* API_KEY = "SUPER_SECRET_API_KEY_123"; // intentionally bad design | + | |
| - | + | ||
| - | void connectToWiFi() { | + | |
| - | Serial.printf("Connecting to WiFi SSID: %s\n", WIFI_SSID); | + | |
| - | WiFi.mode(WIFI_STA); | + | |
| - | WiFi.begin(WIFI_SSID, WIFI_PASSWORD); | + | |
| - | + | ||
| - | uint8_t retries = 0; | + | |
| - | while (WiFi.status() != WL_CONNECTED && retries < 30) { | + | |
| - | delay(500); | + | |
| - | Serial.print("."); | + | |
| - | retries++; | + | |
| - | } | + | |
| - | + | ||
| - | if (WiFi.status() == WL_CONNECTED) { | + | |
| - | Serial.println("\nWiFi connected!"); | + | |
| - | Serial.print("IP address: "); | + | |
| - | Serial.println(WiFi.localIP()); | + | |
| - | } else { | + | |
| - | Serial.println("\nFailed to connect to WiFi"); | + | |
| - | } | + | |
| - | } | + | |
| - | + | ||
| - | void setup() { | + | |
| - | Serial.begin(115200); | + | |
| - | delay(2000); // give serial time to come up | + | |
| - | + | ||
| - | connectToWiFi(); | + | |
| - | randomSeed(esp_random()); | + | |
| - | } | + | |
| - | + | ||
| - | void loop() { | + | |
| - | if (WiFi.status() != WL_CONNECTED) { | + | |
| - | Serial.println("WiFi disconnected, reconnecting..."); | + | |
| - | connectToWiFi(); | + | |
| - | } | + | |
| - | + | ||
| - | if (WiFi.status() == WL_CONNECTED) { | + | |
| - | // Fake sensor data | + | |
| - | float tempC = 20.0 + (random(0, 1000) / 100.0f); // 20.00 to 29.99 | + | |
| - | float humidity = 40.0 + (random(0, 1000) / 50.0f); // 40.0 to 59.9 | + | |
| - | int battery = random(60, 100); // 60-99 % | + | |
| - | + | ||
| - | // Build JSON manually to avoid extra libraries | + | |
| - | String payload = "{"; | + | |
| - | payload += "\"device_id\":\"" + String(DEVICE_ID) + "\","; | + | |
| - | payload += "\"temp_c\":" + String(tempC, 2) + ","; | + | |
| - | payload += "\"humidity\":" + String(humidity, 1) + ","; | + | |
| - | payload += "\"battery\":" + String(battery); | + | |
| - | payload += "}"; | + | |
| - | + | ||
| - | Serial.println("Sending HTTP POST to server..."); | + | |
| - | Serial.println("URL: " + String(SERVER_URL)); | + | |
| - | Serial.println("Payload: " + payload); | + | |
| - | Serial.println("API key (also sent!): " + String(API_KEY)); | + | |
| - | + | ||
| - | HTTPClient http; | + | |
| - | http.begin(SERVER_URL); // HTTP, not HTTPS | + | |
| - | http.addHeader("Content-Type", "application/json"); | + | |
| - | http.addHeader("X-API-Key", API_KEY); // Intentionally leaked secret | + | |
| - | + | ||
| - | int httpCode = http.POST(payload); | + | |
| - | String response = http.getString(); | + | |
| - | + | ||
| - | Serial.printf("HTTP response code: %d\n", httpCode); | + | |
| - | Serial.println("Response body: " + response); | + | |
| - | Serial.println("-----------------------------"); | + | |
| - | + | ||
| - | http.end(); | + | |
| - | } | + | |
| - | + | ||
| - | delay(5000); // send every 5 seconds | + | |
| - | } | + | |
| - | + | ||
| - | </code> | + | |
| + | Get the code from [[iothings:laboratoare:2025_code:lab8_1|here]]. | ||
| ==== Simple Flask Server ==== | ==== Simple Flask Server ==== | ||
| Line 142: | Line 55: | ||
| if __name__ == "__main__": | if __name__ == "__main__": | ||
| # Listen on all interfaces, port 8080 | # Listen on all interfaces, port 8080 | ||
| - | app.run(host="0.0.0.0", port=8080) | + | app.run(host="0.0.0.0", port=8080) #replace the IP address with the server's IP |
| </code> | </code> | ||
| Line 220: | Line 133: | ||
| Let's modify the device to use HTTPS instead of HTTP and add a shared secret + simple MAC (message authentication code) to detect tampering. This will prevent passive sniffers from reading the payload and there will also be a complete failure of injecting spoofed data to the server. | Let's modify the device to use HTTPS instead of HTTP and add a shared secret + simple MAC (message authentication code) to detect tampering. This will prevent passive sniffers from reading the payload and there will also be a complete failure of injecting spoofed data to the server. | ||
| + | |||
| + | {{ :iothings:laboratoare:2025:http-vs-https-1024x533.jpeg?600 |}} | ||
| First, let's generate a self-signed cert for Flask. In the same folder you have the two previous python scripts, run: | First, let's generate a self-signed cert for Flask. In the same folder you have the two previous python scripts, run: | ||
| Line 306: | Line 221: | ||
| Now, let's upgrade also the code on the node itself. Key changes we will need to make are URL is now ''https://...:8443/ingest'', we use ''WiFiClientSecure + setInsecure()'' (so: encrypted but not properly authenticated), and we add a mac field based on the same simple MAC function as the server. | Now, let's upgrade also the code on the node itself. Key changes we will need to make are URL is now ''https://...:8443/ingest'', we use ''WiFiClientSecure + setInsecure()'' (so: encrypted but not properly authenticated), and we add a mac field based on the same simple MAC function as the server. | ||
| - | <code C++ main.cpp> | + | Get the new main.cpp code from [[iothings:laboratoare:2025_code:lab8_2|here]]. |
| - | #include <Arduino.h> | + | |
| - | #include <WiFi.h> | + | |
| - | #include <WiFiClientSecure.h> | + | |
| - | #include <HTTPClient.h> | + | |
| - | // ====== LAB CONFIG: fill these in per-lab ====== | ||
| - | const char* WIFI_SSID = "LAB_WIFI_SSID"; | ||
| - | const char* WIFI_PASSWORD = "LAB_WIFI_PASSWORD"; | ||
| - | |||
| - | // Replace with your server's IP | ||
| - | const char* SERVER_URL = "https://192.168.1.100:8443/ingest"; | ||
| - | |||
| - | const char* DEVICE_ID = "sparrow-01"; | ||
| - | |||
| - | // This key must match SECRET_KEY on the server | ||
| - | // NOTE: in real systems, putting secrets in firmware has risks → discussion later. | ||
| - | const char* SECRET_KEY = "LAB2_SUPER_SECRET_MAC_KEY"; | ||
| - | // =============================================== | ||
| - | |||
| - | WiFiClientSecure secureClient; | ||
| - | |||
| - | void connectToWiFi() { | ||
| - | Serial.printf("Connecting to WiFi SSID: %s\n", WIFI_SSID); | ||
| - | WiFi.mode(WIFI_STA); | ||
| - | WiFi.begin(WIFI_SSID, WIFI_PASSWORD); | ||
| - | |||
| - | uint8_t retries = 0; | ||
| - | while (WiFi.status() != WL_CONNECTED && retries < 30) { | ||
| - | delay(500); | ||
| - | Serial.print("."); | ||
| - | retries++; | ||
| - | } | ||
| - | |||
| - | if (WiFi.status() == WL_CONNECTED) { | ||
| - | Serial.println("\nWiFi connected!"); | ||
| - | Serial.print("IP address: "); | ||
| - | Serial.println(WiFi.localIP()); | ||
| - | } else { | ||
| - | Serial.println("\nFailed to connect to WiFi"); | ||
| - | } | ||
| - | } | ||
| - | |||
| - | // Very simple, non-cryptographic MAC, must match server's compute_mac() | ||
| - | uint32_t computeMac(const String& payload, const char* secret) { | ||
| - | String data = payload + secret; | ||
| - | uint32_t h = 0; | ||
| - | for (size_t i = 0; i < data.length(); i++) { | ||
| - | uint8_t b = static_cast<uint8_t>(data[i]); | ||
| - | h = (h * 31) ^ b; | ||
| - | } | ||
| - | return h; | ||
| - | } | ||
| - | |||
| - | void setup() { | ||
| - | Serial.begin(115200); | ||
| - | delay(2000); | ||
| - | |||
| - | connectToWiFi(); | ||
| - | |||
| - | // HTTPS client config: | ||
| - | secureClient.setTimeout(15000); | ||
| - | // For the lab, we disable certificate validation to keep | ||
| - | // the code simple. This gives confidentiality but not strong | ||
| - | // authenticity → good discussion point. | ||
| - | secureClient.setInsecure(); // DO NOT DO THIS IN REAL PRODUCTS | ||
| - | randomSeed(esp_random()); | ||
| - | } | ||
| - | |||
| - | void loop() { | ||
| - | if (WiFi.status() != WL_CONNECTED) { | ||
| - | Serial.println("WiFi disconnected, reconnecting..."); | ||
| - | connectToWiFi(); | ||
| - | } | ||
| - | |||
| - | if (WiFi.status() == WL_CONNECTED) { | ||
| - | // Fake sensor data, like in Lab 1 | ||
| - | float tempC = 20.0 + (random(0, 1000) / 100.0f); | ||
| - | float humidity = 40.0 + (random(0, 1000) / 50.0f); | ||
| - | int battery = random(60, 100); | ||
| - | |||
| - | // Build JSON without MAC first | ||
| - | // We'll use this version to compute MAC both here and on the server. | ||
| - | String jsonNoMac = "{"; | ||
| - | jsonNoMac += "\"battery\":" + String(battery) + ","; | ||
| - | jsonNoMac += "\"device_id\":\"" + String(DEVICE_ID) + "\","; | ||
| - | jsonNoMac += "\"humidity\":" + String(humidity, 1) + ","; | ||
| - | jsonNoMac += "\"temp_c\":" + String(tempC, 2); | ||
| - | jsonNoMac += "}"; | ||
| - | |||
| - | // Compute MAC | ||
| - | uint32_t mac = computeMac(jsonNoMac, SECRET_KEY); | ||
| - | |||
| - | // Full JSON payload with MAC | ||
| - | String payload = "{"; | ||
| - | payload += "\"battery\":" + String(battery) + ","; | ||
| - | payload += "\"device_id\":\"" + String(DEVICE_ID) + "\","; | ||
| - | payload += "\"humidity\":" + String(humidity, 1) + ","; | ||
| - | payload += "\"temp_c\":" + String(tempC, 2) + ","; | ||
| - | payload += "\"mac\":" + String(mac); | ||
| - | payload += "}"; | ||
| - | |||
| - | Serial.println("Sending HTTPS POST to server..."); | ||
| - | Serial.println("URL: " + String(SERVER_URL)); | ||
| - | Serial.println("Payload (with MAC): " + payload); | ||
| - | |||
| - | HTTPClient http; | ||
| - | if (!http.begin(secureClient, SERVER_URL)) { | ||
| - | Serial.println("Failed to start HTTP connection"); | ||
| - | } else { | ||
| - | http.addHeader("Content-Type", "application/json"); | ||
| - | |||
| - | int httpCode = http.POST(payload); | ||
| - | String response = http.getString(); | ||
| - | |||
| - | Serial.printf("HTTP response code: %d\n", httpCode); | ||
| - | Serial.println("Response body: " + response); | ||
| - | Serial.println("-----------------------------"); | ||
| - | |||
| - | http.end(); | ||
| - | } | ||
| - | } | ||
| - | |||
| - | delay(5000); | ||
| - | } | ||
| - | |||
| - | </code> | ||
| === Wireshark Sniffing === | === Wireshark Sniffing === | ||
| Line 597: | Line 387: | ||
| * try to tamper with them | * try to tamper with them | ||
| - | Next steps (for a future lab): | + | Next steps: |
| * replace ''setInsecure()'' with proper certificate pinning | * replace ''setInsecure()'' with proper certificate pinning | ||
| Line 603: | Line 393: | ||
| * think about where and how to store keys on the device securely | * think about where and how to store keys on the device securely | ||
| + | ===== Trusted Certificates ===== | ||
| + | |||
| + | At a high level, to stop MITM you need the ESP32 to trust only a specific certificate (or CA) and to check that the server it’s talking to presents that particular certificate. | ||
| + | |||
| + | That means we can't relay anymore on setInsecure() and load a CA / server cert into the firmware instead. We will the use ''WiFiClientSecure'' verify it. | ||
| + | |||
| + | ==== Generate a CA ==== | ||
| + | |||
| + | Instead of a random self-signed server cert, you create a tiny lab CA and have it sign your server cert. On the server machine delete the old ''server.crt'' and ''server.key'' and generate the following: | ||
| + | |||
| + | <code bash> | ||
| + | # 1) Create CA key + cert | ||
| + | openssl genrsa -out ca.key 2048 | ||
| + | openssl req -x509 -new -key ca.key -out ca.crt -days 365 -subj "/CN=IoT Lab CA" | ||
| + | |||
| + | # 2) Create server key + CSR | ||
| + | openssl genrsa -out server.key 2048 | ||
| + | openssl req -new -key server.key -out server.csr -subj "/CN=iot-lab.local" | ||
| + | |||
| + | # 3) Sign server cert with CA | ||
| + | openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365 | ||
| + | |||
| + | </code> | ||
| + | |||
| + | Now you have the following files: | ||
| + | * ''ca.crt'' – trust anchor you’ll embed in the ESP32 | ||
| + | * ''server.key'' + ''server.crt'' – used by Flask | ||
| + | * CN of server cert is ''iot-lab.local'' (hostname you’ll use in the URL) | ||
| + | |||
| + | Run Flask again: | ||
| + | <code bash> | ||
| + | python secure_server.py # same as before, just point it to server.crt/server.key | ||
| + | </code> | ||
| + | |||
| + | ==== Modify the Firmware to Add the Certificate ==== | ||
| + | |||
| + | Get the new modified code from [[iothings:laboratoare:2025_code:lab8_3|here]] and paste the contents of the ''ca.crt'' file into the main.cpp file. | ||
| + | |||
| + | Notice we are using a domain name instead of an IP address for the server and we are not relying anymore on ''setInsecure()'', rather we're using HTTPS with CA cert verification. | ||
| + | |||
| + | ==== Replay the MITM Attack ==== | ||
| + | |||
| + | Try to repeat your ARP poisoning + mitmproxy trick with this setup. ARP poisoning still works: packets still flow ESP32 → attacker → server, but when mitmproxy shows its own cert to the ESP32, WiFiClientSecure aborts the handshake. That is because the cert is not signed by your lab CA. Your HTTP POST never happens; your code will see a connection / TLS error. | ||
| + | In brief: | ||
| + | * With ''setInsecure()'', MITM can see JSON + MAC and divert, alter or even drop packets entirely. | ||
| + | * With ''setCACert()'' + correct hostname, MITM can still route traffic (DoS is possible), but they cannot terminate TLS in the middle; they get reduced to a dumb packet forwarder or DoS attacker. | ||