<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Donato Maglie</title>
    <description>The latest articles on DEV Community by Donato Maglie (@donato_maglie).</description>
    <link>https://dev.to/donato_maglie</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4099462%2Fea3a6ce9-d4c3-4195-82bb-0d4a890e8c10.png</url>
      <title>DEV Community: Donato Maglie</title>
      <link>https://dev.to/donato_maglie</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/donato_maglie"/>
    <language>en</language>
    <item>
      <title>Title: Building an ESP32 Multi-Tool - Part 3: Packet Sniffing, Deauth Detection, and Web Portal</title>
      <dc:creator>Donato Maglie</dc:creator>
      <pubDate>Fri, 18 Sep 2026 22:18:04 +0000</pubDate>
      <link>https://dev.to/donato_maglie/title-building-an-esp32-multi-tool-part-3-packet-sniffing-deauth-detection-and-web-portal-545p</link>
      <guid>https://dev.to/donato_maglie/title-building-an-esp32-multi-tool-part-3-packet-sniffing-deauth-detection-and-web-portal-545p</guid>
      <description>&lt;p&gt;In the previous articles of this series, I covered how I turned an ESP32-S3 into a BadUSB and built the FreeRTOS and LVGL architecture to run everything. Now it's time to actually use the radio. The ESP32 is famous for its Wi-Fi capabilities, and leaving it as just an offline hardware tool felt like a waste. &lt;/p&gt;

&lt;p&gt;For network analysis, having a portable device that can monitor 802.11 traffic is a game changer. The ESP-IDF APIs are pretty flexible. Instead of just connecting to a router like a normal IoT device, we can set up a SoftAP or put the silicon into promiscuous mode to sniff raw frames right out of the air.&lt;/p&gt;

&lt;p&gt;This post breaks down how I built the network modules for my multi-tool. I'll walk through the local Web Portal I use to upload payloads wirelessly to the LittleFS filesystem, the network scanner, and the packet sniffer that saves .pcap files directly to memory. I also added a "Deauth Radar" to catch Wi-Fi deauthentication attacks in real-time. &lt;/p&gt;

&lt;p&gt;Handling all these network events without crashing the limited RAM of the microcontroller required some specific architectural choices, which I'll share below.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Bridge: SoftAP and Web Portal
&lt;/h3&gt;

&lt;p&gt;In part one, I showed the BadUSB executing DuckyScript payloads. The problem was that updating those scripts meant plugging the device into a PC over serial every single time. I needed a wireless way to transfer files.&lt;/p&gt;

&lt;p&gt;I configured the ESP32 to run in WIFI_MODE_AP to broadcast its own network. I used the esp_http_server component from ESP-IDF to serve a retro-themed HTML interface. The server has a few endpoints, like /upload for normal files and /upload_payload which routes DuckyScript files straight into a specific LittleFS folder.&lt;/p&gt;

&lt;p&gt;Handling uploads on an embedded device is tricky. If you try to buffer a 2MB file in RAM on an ESP32, you're going to get an Out-Of-Memory crash immediately. &lt;/p&gt;

&lt;p&gt;To fix this, the upload handler works as a stream. First, it checks esp_littlefs_info to make sure there's actually enough flash storage left. If there is, it reads the HTTP request in small chunks using httpd_req_recv and flushes each chunk directly to the flash memory with fwrite. &lt;/p&gt;

&lt;p&gt;Here is the streaming logic I wrote in tool_rete.c:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Safety check: verify available storage on LittleFS&lt;/span&gt;
&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;esp_littlefs_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"storage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;used&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;free_space&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;content_len&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;free_space&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;httpd_resp_send_err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPD_500_INTERNAL_SERVER_ERROR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Insufficient disk space!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ESP_FAIL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;FILE&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"w"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;content_len&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Stream processing: read from socket and write directly to flash&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;to_read&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;read_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpd_req_recv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_read&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;read_bytes&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;read_bytes&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;HTTPD_SOCK_ERR_TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Non-blocking retry&lt;/span&gt;
        &lt;span class="n"&gt;fclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Clean up partial file on error&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ESP_FAIL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;fwrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;fclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;httpd_resp_sendstr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"File successfully saved to LittleFS!&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ESP_OK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the memory footprint completely flat during the transfer, so the rest of the OS keeps running smoothly.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Network Discovery: The Wi-Fi Scanner
&lt;/h3&gt;

&lt;p&gt;Before playing around with network diagnostics, the tool needs to know what APs are around. I built a simple network discovery module that turns the ESP32 into a Wi-Fi scanner. &lt;/p&gt;

&lt;p&gt;It uses esp_wifi_scan_start() and esp_wifi_scan_get_ap_records(). When you trigger the scan from the UI, it drops into Station mode (WIFI_MODE_STA) and scans the 2.4GHz channels. &lt;/p&gt;

&lt;p&gt;One issue I ran into was that doing a blocking Wi-Fi scan on the main thread freezes the LVGL interface entirely. ESP-IDF supports async scans, but I found it cleaner to just run a synchronous scan inside a separate, low-priority FreeRTOS task. This way, the UI stays responsive, and when the scan is done, I lock the LVGL mutex and update the screen.&lt;/p&gt;

&lt;p&gt;After the scan finishes, I grab the array of wifi_ap_record_t structures. I pull the BSSID, SSID, channel, and RSSI, and use that data to build a scrollable list of buttons on the screen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Configure the scan parameters&lt;/span&gt;
&lt;span class="n"&gt;wifi_scan_config_t&lt;/span&gt; &lt;span class="n"&gt;scan_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ssid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bssid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;show_hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Start a blocking scan in the background task&lt;/span&gt;
&lt;span class="n"&gt;esp_wifi_scan_start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;scan_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Retrieve the number of discovered Access Points&lt;/span&gt;
&lt;span class="kt"&gt;uint16_t&lt;/span&gt; &lt;span class="n"&gt;ap_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;esp_wifi_scan_get_ap_num&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ap_count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Allocate memory and fetch the records&lt;/span&gt;
&lt;span class="n"&gt;wifi_ap_record_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ap_records&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;malloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wifi_ap_record_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ap_count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;esp_wifi_scan_get_ap_records&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ap_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ap_records&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Iterate through records to extract intelligence&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;ap_count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SSID: %s, BSSID: %02x:%02x:%02x:%02x:%02x:%02x, CH: %d, RSSI: %d&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;ap_records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;ssid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;ap_records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;bssid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ap_records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;bssid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ap_records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;bssid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
           &lt;span class="n"&gt;ap_records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;bssid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ap_records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;bssid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ap_records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;bssid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
           &lt;span class="n"&gt;ap_records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;ap_records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;rssi&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// The extracted data is then used to populate the LVGL scrollable list&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ap_records&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives me the exact channels and MAC addresses I need to configure the sniffer and radar later.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Entering the Matrix: Promiscuous Mode and the PCAP Sniffer
&lt;/h3&gt;

&lt;p&gt;Scanning is fine, but I wanted actual packet sniffing. Normally the Wi-Fi chip ignores frames that aren't meant for it. If you enable promiscuous mode, the hardware forwards every 802.11 frame it hears on the current channel to your code.&lt;/p&gt;

&lt;p&gt;Setting it up is easy enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;esp_wifi_set_promiscuous&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;esp_wifi_set_promiscuous_rx_cb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;wifi_promiscuous_cb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real problem is the I/O bottleneck. The wifi_promiscuous_cb callback is an interrupt that fires thousands of times a second on a busy network. Writing to LittleFS is slow and blocking. If you call fwrite inside that Wi-Fi callback, you will instantly trigger a Watchdog Timeout and crash the board.&lt;/p&gt;

&lt;p&gt;I solved this by setting up a basic Producer-Consumer pipeline with a FreeRTOS Queue.&lt;/p&gt;

&lt;p&gt;The Wi-Fi callback acts as the producer. It grabs the payload, adds a timestamp using esp_timer_get_time(), and pushes the struct into a queue using xQueueSendFromISR. If the queue is full because traffic is too heavy, it just drops the packet to keep the radio alive.&lt;/p&gt;

&lt;p&gt;Then I have a separate, lower-priority task (sniffer_write_task) acting as the consumer. It waits on xQueueReceive. When a packet pops out, it handles the slow fwrite to the flash memory without blocking the Wi-Fi interrupts.&lt;/p&gt;

&lt;p&gt;To make this data useful, I had to save it as a valid .pcap file so I could open it in Wireshark later. When the sniffer starts, it writes a standard PCAP global header with the magic number 0xa1b2c3d4. Then, for each packet, it writes a packet header containing the length and microsecond timestamp, followed by the raw 802.11 frame.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The Consumer Task&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;sniffer_write_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;sniffer_packet_t&lt;/span&gt; &lt;span class="n"&gt;pkt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Wait for a packet from the Wi-Fi callback queue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xQueueReceive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sniffer_queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;pkt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;portMAX_DELAY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;pdTRUE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pcap_file&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;sniffer_running&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;pcap_packet_header_t&lt;/span&gt; &lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ts_sec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pkt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ts_sec&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ts_usec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pkt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ts_usec&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;incl_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pkt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orig_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pkt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

                &lt;span class="c1"&gt;// Write the PCAP packet header&lt;/span&gt;
                &lt;span class="n"&gt;fwrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;hdr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pcap_packet_header_t&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pcap_file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="c1"&gt;// Write the raw 802.11 frame payload&lt;/span&gt;
                &lt;span class="n"&gt;fwrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pkt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pkt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pcap_file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;fflush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pcap_file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This decoupling lets the ESP32 survive traffic spikes and gives me clean files I can actually analyze on my PC.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The Deauth Radar: Real-Time Attack Detection
&lt;/h3&gt;

&lt;p&gt;Saving packets for later is cool, but I also wanted a real-time warning if a deauthentication attack was happening around me.&lt;/p&gt;

&lt;p&gt;For this "Deauth Radar", I kept the ESP32 in promiscuous mode but applied a hardware filter: WIFI_PROMIS_FILTER_MASK_MGMT. This tells the Wi-Fi silicon to ignore data frames and only pass management frames to my callback, which saves a massive amount of CPU cycles.&lt;/p&gt;

&lt;p&gt;Inside the callback, I look at the first byte of the MAC header. If it's 0xC0 (Deauth) or 0xA0 (Disassoc), I pull the source, destination, and BSSID MAC addresses to figure out who is attacking who.&lt;/p&gt;

&lt;p&gt;Since attacks can happen anywhere, I made a small FreeRTOS task that hops the Wi-Fi channel every 250 milliseconds to sweep the whole spectrum.&lt;/p&gt;

&lt;p&gt;I also had to be careful with RAM. Tracking every deauth packet would eat the heap immediately. Instead, I keep a small array of structs in RAM tracking up to 10 APs and 20 targets each. &lt;/p&gt;

&lt;p&gt;I also had to account for false positives, because regular deauth frames happen naturally when devices roam or disconnect. My logic only flags a device as "Under Attack" if it gets hit with a fast burst of more than 20 deauth frames.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Inside the promiscuous callback&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;fc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pkt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="c1"&gt;// Check for Deauth (0xC0) or Disassoc (0xA0)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fc&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mh"&gt;0xC0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;fc&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mh"&gt;0xA0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;addr1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pkt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Destination&lt;/span&gt;
        &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;addr2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pkt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Source&lt;/span&gt;
        &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;addr3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pkt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// BSSID&lt;/span&gt;

        &lt;span class="c1"&gt;// Determine the victim based on the packet direction&lt;/span&gt;
        &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;victim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memcmp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;addr2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;addr3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;addr1&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;addr2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Log the target in RAM to track the packet count&lt;/span&gt;
        &lt;span class="n"&gt;radar_add_to_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;addr3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;victim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_radar_channel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once that threshold is hit, it sets a flag. The LVGL UI timer picks up the flag, throws a warning popup on the screen, and writes the attack details to a text file for later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrapping up
&lt;/h3&gt;

&lt;p&gt;By using the ESP-IDF Wi-Fi APIs and managing memory with FreeRTOS queues and streams, I managed to turn a standard ESP32 into a pretty decent network analysis tool. It now has a web portal for payloads, a Wi-Fi scanner, a PCAP sniffer, and a real-time deauth radar.&lt;/p&gt;

&lt;p&gt;Next time, I'll switch gears from security to the UI and show how I built a Game Boy-style interface and put some playable retro games on this thing. &lt;/p&gt;

</description>
      <category>esp32</category>
      <category>embedded</category>
      <category>cybersecurity</category>
      <category>programming</category>
    </item>
    <item>
      <title>Exploiting "Trust-by-Default": How I transformed an ESP32-S3 into a BadUSB</title>
      <dc:creator>Donato Maglie</dc:creator>
      <pubDate>Tue, 08 Sep 2026 18:25:34 +0000</pubDate>
      <link>https://dev.to/donato_maglie/exploiting-trust-by-default-how-i-transformed-an-esp32-s3-into-a-badusb-1c57</link>
      <guid>https://dev.to/donato_maglie/exploiting-trust-by-default-how-i-transformed-an-esp32-s3-into-a-badusb-1c57</guid>
      <description>&lt;p&gt;The first tool I decided to arm my Swiss army knife with was a BadUSB emulator. First and foremost, this extremely powerful tool (provided you have physical access to the target machine) relies on an inherent vulnerability of the USB protocol (born in 1996), originally designed to make connecting peripherals as simple and invisible as possible.&lt;/p&gt;

&lt;p&gt;Practically, when we plug a USB device into a computer, a rigorous process called &lt;em&gt;Enumeration&lt;/em&gt; begins. This is divided into several phases: the device is physically connected, its communication speed is identified, and immediately after, the device sends its Descriptors. These descriptors are a set of vital information that defines how the host machine will communicate with the device; subsequently, the device is reset, an address is assigned to it, the interfaces are loaded, and finally, the drivers.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(If you want to delve deeper into the enumeration phase, I recommend &lt;a href="https://www.totalphase.com/blog/2020/08/what-is-enumeration-why-usb-descriptors-important/" rel="noopener noreferrer"&gt;this article by TotalPhase&lt;/a&gt;).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Having defined this, it's easy to guess that if we alter the descriptors via software, we can disguise an ordinary flash drive (or in our case, an M5Stick) making it appear as a keyboard or a mouse.&lt;/p&gt;

&lt;p&gt;And this is where the "Trust-by-Default" vulnerability comes into play: all operating systems blindly trust an HID (Human Interface Device) like a keyboard, as it is an essential input tool. Generic drivers are already pre-installed in the kernel (zero incompatibilities or warning popups) and, moreover, antiviruses have no say in the matter, since they are not designed to scan hardware inputs sent via electrical impulses.&lt;/p&gt;

&lt;h3&gt;
  
  
  The architectural choice: Dynamic Parser vs Hardcoding
&lt;/h3&gt;

&lt;p&gt;Initially, for "simplicity", I thought about hardcoding the payloads directly in the C source code. But thinking about it carefully, the use cases in pentesting are infinite: the end-user must have the ability to modify scripts on the fly, without having to recompile or flash the ESP32 firmware every time.&lt;/p&gt;

&lt;p&gt;For this reason, I decided to implement a dynamic Parser for DuckyScript. This allows the user to upload their own plain text scripts (&lt;code&gt;.txt&lt;/code&gt;) onto the device's LittleFS memory and have the microcontroller interpret them on the fly.&lt;/p&gt;

&lt;p&gt;For those unfamiliar with it, DuckyScript is a macro language created by Hak5 that abstracts interaction with the victim machine. It's simple and easily readable.&lt;br&gt;
To better explain what I mean by "abstract": if I were to write a payload directly in low-level C or Bash, I would have to carefully calculate every single delay between one command and the next to give the PC time to open windows, and I would have to manually map the hexadecimal codes (HID Keycodes) of every single key to "press". DuckyScript, combined with my parser, automates all of this.&lt;/p&gt;

&lt;p&gt;Originally, to use Rubber Ducky scripts, they had to be transformed from simple text files into binary files; this conversion was performed by a PC tool called Ducky Encoder, which converted them into &lt;code&gt;inject.bin&lt;/code&gt;. This tool was used because many older devices lacked the computational power to analyze text during runtime, a problem that the ESP32-S3 has now overcome.&lt;/p&gt;

&lt;p&gt;So I decided to integrate a native parser into my code: this way, a user can simply upload a file from their phone (via the dedicated internal HTML page) or from a computer and execute it immediately, without having to use an external tool for the conversion. Practically, this is done using standard C functions to separate the actual command from the argument at runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;strtok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;strtok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strcmp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"DELAY"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;delay_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;atoi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay_ms&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
        &lt;span class="n"&gt;vTaskDelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdMS_TO_TICKS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay_ms&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; 
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  RAM Security: O(1) Space Complexity
&lt;/h3&gt;

&lt;p&gt;But we all know that C doesn't joke around when it comes to memory allocation, so I tried to make the script reading phase as secure as possible. The underlying problem is that if a user decided to use a very long script (for example, uploading an entire binary file encoded in text), the M5Stick S3 would risk running out of the memory designated for the SRAM, resulting in a Kernel Panic followed by a crash and a device reboot.&lt;/p&gt;

&lt;p&gt;To bypass this hardware limit, I thought of allocating a buffer that reads the uploaded file in small, controlled blocks with the help of the &lt;code&gt;fgets()&lt;/code&gt; function, which is memory-safe by nature. This mathematically prevents a Buffer Overflow from occurring: even if the user uploads a malicious or malformed file with a single line 10,000 characters long, &lt;code&gt;fgets&lt;/code&gt; will still only read the first 255 characters per cycle, safeguarding the microcontroller's stack.&lt;/p&gt;

&lt;p&gt;If we look at the reading process from a space-complexity perspective, you can see that the way I decided to implement this mechanic has a space complexity of O(1); whereas the classic implementation would have had a complexity of O(N), as it would have loaded the entire file into RAM simultaneously.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// RAM consumption locked at only 256 bytes&lt;/span&gt;
&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// The file is strictly read and processed line by line&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fgets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;trim_trailing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strlen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// ... start parsing for this specific line ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Beyond abstraction: USB Protocol and Polling Rate
&lt;/h3&gt;

&lt;p&gt;Despite all the numerous advantages that come from using the ESP32-S3, one "limit" remains: not being able to use libraries that allow a high level of abstraction, such as Arduino's &lt;code&gt;Keyboard.h&lt;/code&gt;. This is a problem because, in the USB standard, a keyboard must regularly build and send 8-byte raw packets (divided into modifier mask, protocol byte, and key array) known as HID Reports.&lt;/p&gt;

&lt;p&gt;Sending these reports falls under the polling operation, which is a mechanism by which the Host (the PC) cyclically queries the USB device to check if there have been any state changes. Specifically, I handle these cyclic requests with &lt;code&gt;tud_hid_ready()&lt;/code&gt;, which checks if the USB Endpoint is free and if the PC has confirmed the reception of the previous packet. Through the &lt;code&gt;while (!tud_hid_ready()) vTaskDelay(...)&lt;/code&gt; block, I prevent the ESP32 from overwriting its own buffer before the packets have actually traveled across the cable. Without this blocking check, the keystrokes would be "dropped" (lost), and a DuckyScript command like &lt;code&gt;STRING powershell&lt;/code&gt; would be read by the victim PC as an incomprehensible &lt;code&gt;pwsel&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  State-Based Logic and the Auto-Repeat Trap
&lt;/h3&gt;

&lt;p&gt;Added to this strict temporal "dance" of polling is one last huge engineering pitfall related to the nature of 8-byte reports. The most common cognitive error people run into is thinking that USB sends "print commands" (e.g., "type the letter R"). In reality, the USB architecture imposes a State-Based logic: the keyboard only communicates its electromechanical state to the PC at that precise moment.&lt;/p&gt;

&lt;p&gt;When we send a report with a specific key, we are not telling it to print it, but we are communicating: "Right now, the user is physically pressing this key".&lt;/p&gt;

&lt;p&gt;The operating system's driver receives this state. If the microcontroller were to immediately move to the next command without doing anything else, the PC would think the user's finger was glued to the key, triggering the operating system's Auto-Repeat (which would endlessly open dozens of windows or print RRRRRRRR...). To solve this problem, my firmware is forced to close the cycle by always sending a subsequent "zeroed" HID Report (passing the &lt;code&gt;NULL&lt;/code&gt; array to TinyUSB). This tricks the PC, communicating that the key's physical contacts have finally been released (Key Release), allowing us to safely move on to the next letter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Developing a low-level BadUSB emulator using &lt;code&gt;esp_tinyusb&lt;/code&gt; is an immensely more educational challenge compared to using "pre-built" libraries. It forces you to study the anatomy of USB packets, understand how operating systems handle polling, and deal with the physical limits of a microcontroller's memory.&lt;/p&gt;

&lt;p&gt;Now our digital "Swiss Army Knife" has its first real weapon, capable of uploading and launching payloads on the fly in total safety.&lt;/p&gt;

&lt;p&gt;But a multi-tool isn't just a keyboard. What if we used the ESP32's antenna to intercept the invisible data floating in the air? In the next article, I'll show you how I configured the network interface in promiscuous mode to turn the ESP32 into a Wi-Fi Sniffer, capable of capturing raw packets and saving them directly to the file system in &lt;code&gt;.pcap&lt;/code&gt; format, ready to be analyzed on Wireshark.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>esp32</category>
      <category>c</category>
      <category>embedded</category>
    </item>
    <item>
      <title>From Arduino to ESP-IDF: The architecture behind my digital "Swiss Army Knife"</title>
      <dc:creator>Donato Maglie</dc:creator>
      <pubDate>Mon, 31 Aug 2026 20:52:11 +0000</pubDate>
      <link>https://dev.to/donato_maglie/from-arduino-to-esp-idf-the-architecture-behind-my-digital-swiss-army-knife-38o8</link>
      <guid>https://dev.to/donato_maglie/from-arduino-to-esp-idf-the-architecture-behind-my-digital-swiss-army-knife-38o8</guid>
      <description>&lt;h2&gt;
  
  
  1. Why build another multi-tool?
&lt;/h2&gt;

&lt;p&gt;How many of you have often found yourselves wanting to buy a Flipper Zero? I thought about it many times, but there were always problems holding me back: stock is often limited, the price tag is quite high, and above all, you miss out on the thrill of building such a powerful tool literally from scratch.&lt;/p&gt;

&lt;p&gt;From these observations, my project was born: designing and developing a low-level "Swiss Army Knife".&lt;/p&gt;

&lt;p&gt;It all started a few months ago. I was thinking about buying an M5Stick S3 after watching some videos online where people spoke very highly of it, especially for one major detail: unlike the Flipper, it has Wi-Fi and Bluetooth modules already built-in.&lt;/p&gt;

&lt;p&gt;Digging deeper, I quickly realized the advantages of the ESP32-S3 over the classic Arduino. The key features that convinced me were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dual-core processor: It opens the door to serious features, like managing firmware tasks separately.&lt;/li&gt;
&lt;li&gt;More RAM: It allows integrating very complex external libraries (like heavy graphical interfaces) without killing performance.&lt;/li&gt;
&lt;li&gt;Native USB HID: It allows emulating peripherals like keyboards or mice natively and quickly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, the hardware was decided. But why build a multi-tool?&lt;/p&gt;

&lt;p&gt;The main reason is to explore and understand the technical background of as many tools as possible. Lately, I feel there is a tendency to overlook the ingenuity of the mechanisms operating right in front of our eyes. We prefer having a ready-made tool, usable perhaps without even knowing the basics of computer science. I wanted to go in the opposite direction and understand exactly how these things work at the code level.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Fluid Graphics and Multitasking: How not to blow up an ESP32
&lt;/h2&gt;

&lt;p&gt;A major problem when rendering a graphical interface on a microcontroller is that the CPU has to calculate and send every single pixel. Since this is a time-consuming operation, the entire device gets blocked until the whole interface is completely redrawn. In a multi-tool, if the ESP32 is stuck drawing buttons, it cannot sniff packets or execute attacks.&lt;/p&gt;

&lt;p&gt;To solve this problem and make the device truly responsive, I decided to use DMA (Direct Memory Access). DMA is a dedicated hardware component that allows peripherals (in this case the SPI display) to read data directly from RAM without involving the CPU, generating a single interrupt only when the transfer is complete.&lt;/p&gt;

&lt;p&gt;I initialized the SPI bus asking ESP-IDF to automatically assign a DMA channel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;spi_bus_initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SPI2_HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;buscfg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SPI_DMA_CH_AUTO&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The RAM problem and Chunk Rendering
&lt;/h3&gt;

&lt;p&gt;However, implementing DMA without the right precautions wouldn't have been enough. The device's screen resolution is 135x240 at 16 bits, which requires a total of 64.8 KB to be rendered. This leads to two major problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Wasting so many resources just for the interface is not sustainable for a microcontroller.&lt;/li&gt;
&lt;li&gt;It is virtually impossible to find 64.8 KB of contiguous memory inside the RAM once the system is running.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To bypass this issue, I decided to divide the screen into horizontal "slices" of 20 lines each (about 5.4 KB total). Using the advanced ESP-IDF APIs, I allocated this buffer in a memory area compatible with DMA:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;lv_color_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;buf1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;heap_caps_malloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LCD_H_RES&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lv_color_t&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;MALLOC_CAP_DMA&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, we optimize RAM usage while still maintaining super fluid rendering.&lt;/p&gt;

&lt;p&gt;At this point, a question arises: how does LVGL (the library I use to manage the interface) know when the DMA has finished sending the bits for those 20 lines? Simply, with a Hardware Interrupt system that defines this flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LVGL fills the buffer and calls the driver.&lt;/li&gt;
&lt;li&gt;The driver passes the buffer to the DMA controller.&lt;/li&gt;
&lt;li&gt;When the transmission process ends, the interrupt is triggered.&lt;/li&gt;
&lt;li&gt;The interrupt immediately calls a callback (&lt;code&gt;notify_lvgl_flush_ready&lt;/code&gt;) which orders LVGL to calculate the next slice of the interface.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;notify_lvgl_flush_ready&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;esp_lcd_panel_io_handle_t&lt;/span&gt; &lt;span class="n"&gt;panel_io&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;esp_lcd_panel_io_event_data_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;edata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;user_ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;lv_disp_drv_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;disp_driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lv_disp_drv_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;user_ctx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;lv_disp_flush_ready&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;disp_driver&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Semaphores for Safety (Thread-Safety)
&lt;/h3&gt;

&lt;p&gt;Since FreeRTOS is a system based on multi-threading, it is important to implement proper task management. A fundamental detail to know is that LVGL is not thread-safe by design. This means that the library has no internal mechanisms to protect itself: if two different tasks try to access or modify the graphical interface at the exact same moment, the system will crash.&lt;/p&gt;

&lt;p&gt;To prevent these race-conditions, which can lead to data inconsistency and deadlocks, I implemented a Semaphore (Mutex) system: every time an external task (other than the graphical one) wants to modify a GUI element, it must first "take" the semaphore.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;lvgl_mux&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;xSemaphoreCreateMutex&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the graphical task is already working, the network task (for example, the WiFi sniffer) waits in queue for a few milliseconds until the Mutex is released. This guarantees that LVGL's memory is read or written by only one thread at a time, making the firmware solid and crash-proof.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Data Security and Crash Prevention
&lt;/h2&gt;

&lt;p&gt;Having a responsive interface and fast loading times is not enough: we also must try to make the firmware as robust as possible against hardware failures.&lt;/p&gt;

&lt;p&gt;This justifies the choice of the file system: LittleFS. Unlike the classic FAT32 or SPIFFS (which would corrupt the disk leading to the loss of all data in case of sudden shutdown), LittleFS was specifically designed for battery-powered microcontrollers and can maintain its integrity even if the power is suddenly cut off.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;esp_vfs_littlefs_conf_t&lt;/span&gt; &lt;span class="n"&gt;conf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/memoria"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;partition_label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"storage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format_if_mount_failed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dont_mount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="n"&gt;esp_err_t&lt;/span&gt; &lt;span class="n"&gt;ret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;esp_vfs_littlefs_register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A great engineering practice is using the &lt;code&gt;.format_if_mount_failed = true&lt;/code&gt; parameter, which allows handling the very first boot of the firmware on an empty memory by simply formatting it entirely on its own, without the user having to do anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to read Giant Payloads without running out of RAM
&lt;/h3&gt;

&lt;p&gt;The second major risk for stability is reading files. Many microcontrollers have very limited RAM that fills up in an instant if you try to open a file of several Megabytes all at once.&lt;/p&gt;

&lt;p&gt;To prevent the BadUSB tool from going into Out of Memory when executing complex payloads, I decided to implement a "line-by-line" reading. Think of it a bit like an e-reader: it doesn't load the whole book into memory, but only shows the small part of text you are reading, allowing you to scroll through the pages interacting with the interface.&lt;/p&gt;

&lt;p&gt;My parser dynamically loads only 256 bytes at a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;esegui_duckyscript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;FILE&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"r"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="c1"&gt;// RAM consumption is capped at 256 bytes, regardless &lt;/span&gt;
    &lt;span class="c1"&gt;// of the size of the file loaded by the user!&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fgets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;trim_trailing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strlen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;strtok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// parsing and execution of the single command...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;fclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This is how I structured my project to guarantee its stability, responsiveness, and scalability, especially considering the large number of tools I am going to implement.&lt;/p&gt;

&lt;p&gt;In the next episode, we will get to the heart of the action: I will show you how I used the USB stack to make the computer believe that the ESP32 is a keyboard, in order to execute DuckyScript scripts (BadUSB).&lt;/p&gt;

&lt;p&gt;In the meantime, which pentesting tool would you like to see added to the project? Let me know in the comments!&lt;/p&gt;

</description>
      <category>esp32</category>
      <category>cpp</category>
      <category>cybersecurity</category>
      <category>programming</category>
    </item>
    <item>
      <title>Hello World!</title>
      <dc:creator>Donato Maglie</dc:creator>
      <pubDate>Fri, 28 Aug 2026 21:04:00 +0000</pubDate>
      <link>https://dev.to/donato_maglie/hello-world-4ik5</link>
      <guid>https://dev.to/donato_maglie/hello-world-4ik5</guid>
      <description>&lt;p&gt;Hello everyone! 👋 Happy to be joining the DEV community.&lt;/p&gt;

&lt;p&gt;I’m a Computer Engineering student based in Italy. My main focus is Cybersecurity, but I strongly believe you have to know how to build a system before you can secure (or break) it.&lt;/p&gt;

&lt;p&gt;Lately, I’ve been jumping between two very different worlds:&lt;/p&gt;

&lt;p&gt;Embedded C: writing firmware, managing file systems, and building custom OLED menus for the M5Stick S3.&lt;/p&gt;

&lt;p&gt;Frontend: building web apps using Next.js and React.&lt;/p&gt;

&lt;p&gt;My workflow is a bit of a hybrid. I like to focus on the system architecture, memory management, and edge cases, while using AI tools to do the heavy lifting of writing the actual code. Then, I review everything strictly to make sure it doesn't break.&lt;/p&gt;

&lt;p&gt;I’m here to build in public, share my projects, and learn from this awesome community.&lt;/p&gt;

&lt;p&gt;What are you all currently hacking on? See you around!&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>cybersecurity</category>
      <category>nextjs</category>
      <category>react</category>
    </item>
  </channel>
</rss>
