ESP32 hidden HCI vendor commands, technical details and use cases

The Bluetooth standard defines a two-component architecture:
- The Host. This software manages high-level communications without dealing with low-level packet details. It orchestrates the controller’s functions.
- The Controller. This is specialized hardware responsible for implementing the radio frequency physical layer. It runs firmware that handles low-level communications, which require real-time precision.
De maIn simple terms, the host is software running on a device (typically the main processor in an IoT device), while the controller is the chip responsible for sending packets, maintaining connections, and managing low-level operations.
Communication between the host and the controller happens through the Host Controller Interface (HCI) protocol. This protocol is OS-independent and can work over USB or UART/Serial.
Particularly, the ESP32 chip can operate in two different configurations:
- Host and Controller on the Same Chip. The host application runs on the ESP32 itself, communicating with the controller via a virtual HCI interface.
- Controller-Only Mode. The host application runs on an external chip or computer, using the ESP32 solely as a Bluetooth controller via UART HCI.
This article focuses on the second configuration, where the ESP32 acts only as a Bluetooth controller, controlled by another chip or computer via HCI commands to explore what can be achieved with this chip locally.
HCI protocol mainly consists of two types of packets:
- Commands. Sent by the host to the controller.
- Events. Responses from the controller to the host.
Communications are initiated by the host by sending a command, which may receive one or multiple responses in the form of events from the controller.
When analyzing the structure of HCI packets, they begin with a 1-byte header that indicates the packet type:
Tipo HCI
Value | Meaning |
---|---|
0X01 | Command |
0X04 | Event |
For commands, the header is followed by an opcode (2 bytes) and a length field (1 byte):
HCI Command Header
Opcode | Lenght |
---|---|
2 bytes | 1 byte |
The opcode identifies the specific command type and indicates the action the host wants to perform. It is a composite value that can be divided into two parts: The OGF (Opcode Group Field) which is 6 bits in length and identifies the command group and the OCF (Opcode Command Field) which is 10 bits and Specifies the exact command within the group.
In other words, the opcode consists of two sections: one that categorizes the command and another that defines its specific function.
The Bluetooth standard defines a specific OGF (Opcode Group Field) for vendor-specific commands, which is OGF 0x3F. All commands within this group are implemented by the manufacturer and follow a proprietary format, which may or may not be publicly documented.
Through the reverse engineering of the ESP32 ROM binaries published in the chip’s SDK, 29 undocumented vendor-specific commands have been identified. These commands provide functionalities beyond standard HCI commands, offering greater control over the Bluetooth controller. Some of them even allow access to internal ESP32 resources, such as RAM and flash memory. This raises security concerns, as these commands grant access to resources that are not covered by the standard HCI security model.
Also, through reverse engineering, the parameters, their format, and the return event format for these commands have also been identified.
Opcode | Command |
---|---|
0xFC01 | Read memory |
0xFC02 | Write memory |
0xFC03 | Delete NVDS parameter |
0xFC05 | Get flash ID |
0xFC06 | Erase flash |
0xFC07 | Write flash |
0xFC08 | Read flash |
0xFC09 | Read NVDS parameter |
0xFC0A | Write NVDS parameter |
0xFC0B | Enable/disable coexistence |
0xFC0E | Send LMP packet |
0xFC10 | Read kernel stats |
0xFC11 | Platform reset |
0xFC12 | Read memory info |
0xFC30 | Register read |
0xFC31 | Register write |
0xFC32 | Set MAC address |
0xFC35 | Set CRC initial value |
0xFC36 | LLCP msgs discard |
0xFC37 | Reset RX count |
0xFC38 | Reset TX count |
0xFC39 | RF register read (Not implemented) |
0xFC3A | RF register write (Not implemented) |
0xFC3B | Set TX password |
0xFC40 | Set LE parameters |
0xFC41 | Write LE default values |
0xFC42 | LLCP pass through enable |
0xFC43 | Send LLCP packet |
0xFC44 | LMP msgs discard |
The following section examines some of the identified commands that facilitate Bluetooth attacks against third-party devices or compromise the security of the ESP32 chip when there is the capability to execute code on the host.
Set Mac Address command
Command | OCF | Command parameters | Return parameters |
---|---|---|---|
HCI_ESP32_CMD_SET_MAC | 0x32 | Bd_Addr | Status |
Description:
Changes the MAC address of the Bluetooth controller.
Command Parameters:
Bd_Addr Size: 6 Octets
Value | Parameter description |
---|---|
0xXXXXXXXXXXXX | MAC Address of the device |
Return Parameters:
Status Size: 1 Octet
Value | Parameter description |
---|---|
0x00 | HCI_Esp32_Set_Mac_Address command succeeded. |
0x01 to 0xFF | HCI_Esp32_Set_Mac_Address command failed. See [Vol 1] Part F, Controller Error Codes, for error codes and descriptions |
This command allows modifying the MAC address that the controller presents and uses to interact with other devices.
This is a particularly interesting feature because, in Bluetooth, devices are identified by their MAC address or data derived from it, and most Bluetooth devices do not allow this value to be changed.
From a security perspective, MAC address spoofing can be used to perform various attacks, including:
- Bypassing tracking mechanisms. Changing the MAC address continuously can disrupt device tracking and identification systems. This is done by sending advertising packets with random MAC addresses, injecting false data, and improving privacy.
- Forcing devices to connect unknowingly. Spoofing a known MAC address and starting an advertising process can trick devices into initiating a connection. Some Bluetooth devices, like smartphones, do not broadcast their presence continuously but listen for known peripherals (e.g., headphones) and connect automatically when they appear.
- Re-pairing attacks. Spoofing a known MAC address (e.g., from a smartphone) and attempting to connect to paired devices (e.g., headphones) that are in “non-pairable” mode can lead to forced key renewals. If the device supports key renegotiation, an attacker can invalidate the old encryption key and establish a new one, sometimes even when the target device is not in pairing mode. Devices without physical security confirmation (like buttons) may accept this process automatically.
- Denial of Service (DoS). Spoofing a MAC address, connecting to a device, and keeping the connection open indefinitely prevents the legitimate user from connecting. This denial of service can become semi-permanent if the target device allows re-pairing, an attacker can renew the existing key and invalidate the original encryption key, forcing the user to re-pair before each device use.
- Brute-force attack facilitation. By repeatedly forcing pairing processes as explained above, attackers can create opportunities for brute-force attacks against Bluetooth pairing mechanisms. Many Bluetooth pairing processes rely on low-complexity numeric confirmations, making them susceptible to cryptographic attacks that recover legitimate connection keys.
Read Memory command
Command | OCF | Command parameters | Return parameters |
---|---|---|---|
HCI_ESP32_CMD_READ_MEM | 0x01 | Start_Address Access_Size Length | Status Length Data |
Description:
This command allows reading memory from the Bluetooth controller. The command parameters specify the starting memory address, the size of the units to read (8, 16, or 32 bits), and the number of units to read.
The Access_Size parameter is limited to the values 8, 16, or 32.
Command Parameters:
Start_Address – Size: 4 Octets
Value | Parameter description |
---|---|
0xXXXXXXXX | Memory address from where to start reading |
Access_Size – Size: 1 Octet
Value | Parameter description |
---|---|
0xXX | Access size value, possible values are: 8, 16, 32. |
Length: – Size: 1 Octet
Value | Parameter description |
---|---|
0xXX | Number of elements of Access_Size to be read |
Return parameters:
Status – Size: 1 Octet
Value | Parameter description |
---|---|
0x00 | HCI_Esp32_Read_Memory command succeeded. |
0x01 to 0xFF | HCI_Esp32_Read_Memory command failed. See [Vol 1] Part F, Controller Error Codes, for error codes and descriptions |
Length – Size: 1 Octet
Value | Parameter description |
---|---|
0xXX | Number of elements returned |
Data – Length x Access_Size Octets
Value | Parameter description |
---|---|
0xXX | Read data |
From a security perspective, this command is particularly interesting for extracting any memory value from the ESP32, with the following use cases:
- Exfiltration of secrets from ESP32 memory. This includes WiFi credentials (if the chip is also used as a WiFi adapter), encryption keys for other services, and Bluetooth pairing keys stored in the controller. Normally, unprivileged access in a host device cannot retrieve pairing keys, as they are stored securely. However, by sending HCI packets to the Bluetooth controller and initiating a connection to a specific device, the controller requests the pairing key from the host kernel. During this process, the key can be extracted from memory using the read command.
- Debugging mechanism. Helps identify free memory regions and can be used for debugging purposes.
Write Memory command
Command | OCF | Command parameters | Return parameters |
---|---|---|---|
HCI_ESP32_CMD_WRITE_MEM | 0x02 | Start_Address Access_Size Length Data | Status |
Description:
This command allows writing data to arbitrary memory addresses on the ESP32. The Start_Address parameter specifies the memory address where the writing begins. Access_Size defines the size (in bits) of each unit to be written, with possible values of 8, 16, or 32. Length indicates the total number of units to write. Finally, Data is a buffer containing the data to be written, with a total size of Access_Size × Length.
Command Parameters:
Start_Address – Size: 4 Octets
Value | Parameter description |
---|---|
0xXXXXXXXX | Start memory address to write to |
Access_Size – Size: 1 Octet
Value | Parameter description |
---|---|
0xXX | Access size value, possible values are: 8, 16, 32 |
Length – Size: 1 Octet
Value | Parameter description |
---|---|
0xXX | Length of the Data field |
Data – Length x Access_Size Octets
Value | Parameter description |
---|---|
0xXX | Read Data |
Return parameters:
Status – Size: 1 Octet
Value | Parameter description |
---|---|
0x00 | HCI_Esp32_Read_Memory command succeeded. |
0x01 to 0xFF | HCI_Esp32_Write_Memory command failed. See [Vol 1] Part F, Controller Error Codes, for error codes and descriptions |
From a security standpoint, this command is the most important among those mentioned.
The following security risks have been identified when using this local HCI command:
- Code execution on the ESP32. The simplest method is to overwrite part of a function, such as the r_hci_cmd_received callback in the firmware, which is executed during the processing of incoming HCI commands. Once this callback is modified, sending an HCI command triggers the injected code, allowing the attacker to gain full control of the ESP32.
- Bypassing “Secure Boot”, Espressif’s protection against unsigned code execution. Since the write operation is performed directly in the ESP32’s RAM, and it occurs after the firmware’s security validation mechanisms have been applied, these protections do not prevent modifications made once the firmware is loaded into RAM.
- Reading encrypted flash memory. With code execution capabilities on the device, an attacker can read flash memory using internal API functions such as esp_flash_read() to read flash memory contents in an encrypted form or esp_flash_read_encrypted() to read the contents already decrypted. By combining these functions with the ability to generate HCI events via code, an attacker can dump the entire decrypted firmware. This effectively bypasses Espressif’s flash encryption mechanisms, allowing reverse engineering of the firmware contents.
- Authentication bypass via key injection. By leveraging the ESP32’s memory write capability, an attacker can modify the r_llc_ltk_req_send function in the firmware, replacing it with a version that checks the MAC address of incoming connections. This modification allows the ESP32 to skip the pairing process for a specific MAC address. Instead of requesting the pairing key from the host, the function directly triggers an HCI LE Long Term Key Request Reply event using r_hci_le_ltk_req_reply_cmd_handler.
As a result, when a connection request comes from the targeted MAC, the ESP32 automatically uses a hardcoded encryption key. This effectively enables authenticated and encrypted connections to the modified ESP32 without requiring pairing or user interaction, creating a stealthy backdoor. - Backdoor installation. The reception of Bluetooth LE packets is handled by the llc_llcp_recv_handler function in the firmware. This function verifies the validity of incoming packets from the RF module and processes them accordingly. By modifying this function, an attacker can implement custom processing for non-standard packets that would normally be discarded as invalid. The custom processing routine can remotely execute code on the ESP32 without authentication, granting full control of the chip without physical access, as long as the device remains powered on. After this modification, the attacker can send remote instructions to the ESP32 to, among other things, capture or retransmit Bluetooth pairing keys, extract WiFi credentials or intercept other sensitive data exchanged over communication channels.
- Persistent backdoor installation. By leveraging the code execution techniques described earlier, an attacker can use internal flash memory writing functions (esp_flash_write()) to store malicious code directly in the firmware, ensuring persistence across device reboots. The simplest method is modifying a function that runs during device startup, such as app_main.
This method allows an attacker to maintain control of the ESP32 without physical access, enabling continuous data exfiltration or deploying new attacks to extract information from other devices.
This persistence is possible when the ESP32 does not implement Secure Boot or uses Secure Boot v1. In the case of Secure Boot v2, Espressif has introduced enhanced protections. This version requires a private cryptographic key from the firmware developer, which is not stored on the chip, making persistence attacks significantly more difficult.
In conclusion
The undocumented HCI commands found in the ESP32 offer significant potential not only for Bluetooth attacks implementation but also from the perspective of the chip’s cybersecurity.
Usage ranges from the versatility of identity spoofing attacks using the MAC address modification command to the execution of advanced attacks by altering the controller’s behavior.
It is also possible, using the memory write command, to bypass protections against unsigned code execution and circumvent flash encryption mechanisms to extract secrets from the chip firmware.
This same commands could allow the installation of backdoors by injecting predefined encryption keys into the device to enable connections from specific devices or by modifying the code to enable remote code execution via over-the-air commands.
Espressif has stated in its blog that the functions reported by Tarlogic do not pose a remotely exploitable security risk via Bluetooth commands or wireless signals. Additionally, Espressif has expressed its intention to disable these undocumented debugging commands in future production devices to enhance chip security.
Update
10/03/25: View article Hacking Bluetooth the Easy way with ESP32 HCI Commands and hidden features