Black Box Probing: How We Reverse-Engineered Xiaomi’s MJA1 Secure Chip With Zero Documentation + Video

Listen to this Post

Featured Image

Introduction:

In the world of hardware security, the absence of public documentation is often the first line of defense for proprietary silicon. Xiaomi’s MJA1 secure chip — embedded in recent cameras like the BW300, C500 Pro, and C700 — promises hardware-level protection for sensitive data and device communications, with each chip carrying its own unique private key and certificate. But when no datasheet exists and no prior research has been published, how does a security researcher evaluate whether that promise holds up? This article chronicles Quarkslab researcher Mengsi Wu’s black-box security analysis of the MJA1 chip, covering hardware identification, I2C sniffing, flash dumping, firmware reverse engineering, and command-space brute-forcing — a masterclass in practical hardware reverse engineering.

Learning Objectives:

  • Understand the methodology of black-box hardware security analysis when no documentation is available
  • Learn how to identify communication interfaces (I2C, UART, SPI) through logic analyzer probing and signal analysis
  • Master firmware extraction techniques using both universal programmers and Raspberry Pi-based SPI NAND dumping
  • Gain hands-on experience reverse-engineering host-side binaries to recover proprietary command protocols
  • Explore command fuzzing and brute-forcing techniques to uncover undocumented chip functionality
  • Assess secure element trust boundaries and identify attack surfaces for future side-channel research

1. Target Identification and Hardware Reconnaissance

The first challenge in any black-box analysis is finding a suitable target. The researcher needed a device that integrated the MJA1 chip, cost under €50–100, and allowed firmware upgrade interception in case flash dumping failed. The solution? Purchasing two devices for the price of one — a Xiaomi Base Station and an Outdoor Camera BW300 — both of which contain the MJA1 chip and can interact with each other, enabling cross-validation of findings.

Upon opening both devices and inspecting their PCBs, the MJA1 chip was located on each board — both using the same MJA1 C06CW model. The chip comes in a DFN 2×3-8 package (Dual Flat No-lead, 2×3 mm, 8 pins). With a pin pitch of just 0.5 mm, probing requires precision equipment.

Step-by-Step: Hardware Identification

  1. Visual inspection: Locate the secure chip on the PCB using magnification. Look for markings indicating “MJA1” or similar identifiers.
  2. Package identification: Note the package type (DFN, QFN, etc.) and pin count. This informs your probing strategy.
  3. Pinout mapping: With the device powered on, use a logic analyzer with PCBite probes to capture signals on each pin.
  4. Signal analysis: Identify active pins — those showing data exchange rather than constant high (VCC) or low (GND).
  5. Protocol deduction: Based on pin count and signal characteristics, determine the communication protocol:

– UART: 2 pins (TX, RX), asynchronous, bidirectional
– SPI: 4+ pins (CLK, MOSI, MISO, CS), synchronous
– I2C: 2 pins (SCL, SDA), synchronous, shared bus

In the MJA1 case, only two pins showed active data exchange, and zooming in on the captures revealed the typical signature of an I2C bus with a regular clock line and data line. The logic analyzer’s built-in I2C decoder confirmed the chip’s address as 0x2A.

2. Sniffing Communications and Capturing the Transport Layer

With the interface identified as I2C, the next step was to capture actual transactions between the main SoC and the MJA1 chip. Using a PCBite setup combined with a logic analyzer, the researcher captured a representative transaction:

Write [bash] to address the chip, then a sequence of data bytes:
0x05, 0x00, 0x03, 0x00, 0x02, 0x00, 0x08, 0x58, 0xEF

Each byte was acknowledged by the slave. This exact byte sequence — `0x05 0x00 0x03 0x00 0x02 0x00 0x08 0x58 0xEF` — was later identified as a READ command (command ID 0x05).

Sniffing gave us the transport layer, but the actual command structure on top of I2C remained unknown. To understand it, we needed to extract and analyze the firmware of the host device that talks to the chip.

Linux I2C Tools for Similar Analysis:

 Install i2c-tools
sudo apt-get install i2c-tools

Detect I2C buses
i2cdetect -l

Scan for devices on bus 0
i2cdetect -y 0

Read from a specific device address
i2cget -y 0 0x2A 0x00

Dump all registers
i2cdump -y 0 0x2A

Windows Equivalent (using FTDI-based adapters):

  • Use FTDI FT260 or Total Phase Aardvark adapters with provided GUI tools
  • Python `pyftdi` library for programmatic I2C communication
  1. Firmware Extraction: From Universal Programmers to Raspberry Pi

Two devices meant two different flash chips. The Xiaomi Base Station used an MD25Q128 flash memory, supported by the TNM5000 universal programmer — making the dump straightforward. The Xiaomi Outdoor Camera BW300, however, was equipped with a Winbond 25N01KVZEIR — a 1 Gbit SPI NAND flash memory in a WSON 8×6 mm package that the TNM5000 did not support.

Step-by-Step: Dumping SPI NAND Flash with Raspberry Pi

For unsupported SPI NAND flash, a Raspberry Pi can speak SPI directly to the chip. Here’s the wiring and code used:

Wiring: Winbond 25N01KVZEIR to Raspberry Pi 3 B+

| Winbond Flash Pin | Raspberry Pi Pin |

|-||

| /CS | GPIO 7 (SPI0 CE1) |

| DO | GPIO 9 (SPI0 MISO) |

| /WP | 3.3V |

| GND | GND |

| DI | GPIO 10 (SPI0 MOSI) |

| CLK | GPIO 11 (SPI0 SCLK) |

| /HOLD | 3.3V |

| VCC | 3.3V |

Python Script to Read JEDEC ID and Dump Flash:

import spidev
import time

=== Configuration ===
MAX_PAGE_NUMBER = 0x10000
PAGE_SIZE = 2048  Size of data area (without spare/OOB)
SPI_BUS = 0
SPI_DEVICE = 1

=== Setup SPI ===
spi = spidev.SpiDev()
spi.open(SPI_BUS, SPI_DEVICE)
spi.max_speed_hz = 10_000_000
spi.mode = 0b00

=== Read JEDEC ID ===
response = spi.xfer2([0x9F, 0, 0, 0, 0, 0, 0, 0])
print("JEDEC ID:", [hex(x) for x in response])
 Expected output: JEDEC ID: ['0x0', '0x0', '0xef', '0xae', '0x21', '0x0', '0x0', '0x0']

=== Helper: Wait until NAND is ready ===
def wait_ready():
while True:
spi.xfer2([0x0F, 0xC0])  0x0F: Read status, 0xC0: Status register
status = spi.readbytes(1)[bash]
if (status & 0x01) == 0:  Bit 0 == 0 means "Ready"
break
time.sleep(0.001)

=== Load page into cache (command 0x13) ===
def load_page_to_cache(page_number):
addr = page_number.to_bytes(3, 'big')  24-bit address
spi.xfer2([bash] + list(addr))
wait_ready()

=== Read data from cache (command 0x03) ===
def read_cache_data():
data = []
for i in range(0, PAGE_SIZE, 4):
addr = i.to_bytes(2, 'big')
data += spi.xfer2([bash] + list(addr) + [0x00, 0x00, 0x00, 0x00, 0x00])[4:]
return data

=== Dump all pages ===
with open("dump.bin", "wb") as f:
for page in range(MAX_PAGE_NUMBER):
print(f"Reading page {page}...")
load_page_to_cache(page)
page_data = read_cache_data()
f.write(bytes(page_data))

spi.close()

4. Firmware Analysis and Reverse Engineering miio_client

With both firmwares extracted, the researcher used binwalk, unblob, and SquashFS tools to extract the file systems. As expected, no code specific to the secure chip itself was found — the chip runs its own firmware, which is not exposed to the host.

The goal was to find the host-side code that talks to the chip over I2C. By running `strings` on the extracted binaries, many references to the prefix mjac (MJA Chip?) were found inside miio_client:

$ strings mi_ot/miio_client
...
mjac_reset
mjac_get_did
mjac_get_certificate_pem
mjac_crc16_ccitt
mjac_i2c
...

`miio_client` is a MIPS 32-bit binary, dynamically linked against uClibc, typical for an embedded Linux device:

$ file mi_ot/miio_client
mi_ot/miio_client: ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), 
dynamically linked, interpreter /lib/ld-uClibc.so.0, no section header

The `no section header` part means the binary was stripped, making reverse engineering harder. However, function names were preserved in the symbol table. Key functions identified:

  • mjac_init: initializes the I2C interface
  • mjac_cmd_build_: builds different supported commands
  • mjac_resp_parse_data: parses responses from the chip
  • mjac_crc16_ccitt: computes the CRC16 used for frame integrity

Identified Commands from miio_client:

| ID | Command |

|-||

| 0x00 | Echo |

| 0x02 | Generate random |

| 0x05 | Read |

| 0x0D | Hibernate |

| 0x11 | Generate key |

| 0x14 | Query |

| 0x16 | Generate signature |

| 0x17 | Verify signature |

| 0x18 | Establish key |

These commands fall into three categories:

  • Utility: Echo, Generate random, Hibernate
  • Data access: Read, Query
  • Cryptographic operations: Generate key (ephemeral ECC keypair for ECDH), Establish key (shared secret derivation), Generate signature, Verify signature

5. Recovering the CRC and Rebuilding Command Protocol

Even with function names preserved, reconstructing the exact format of each command was not trivial. Each `mjac_cmd_build_` function packs arguments into a structure of magic offsets, computes a CRC over a specific range, and returns a length that varies.

Take `mjac_cmd_build_establish_key` as an example:

int mjac_cmd_build_establish_key(__mjac_cmd_establish_key _cmd, uint _cmd_len, 
void _src, int _src_len) {
ushort crc;
int ret;
if (_src == (void )0x0) { return 0; }
if (_src_len == 0x45) {
if (_cmd == (__mjac_cmd_establish_key )0x0) { ret = 0; }
else {
ret = 0;
if (0x48 < _cmd_len) {
_cmd->field0_0x0 = 0x18; // Command ID
_cmd->field1_0x1 = 0xff; // Constant
memcpy(_cmd->___key, _src, 0x45);
crc = mjac_crc16_ccitt(&_cmd->field0_0x0, 0x47);
_cmd->_crc16_lo = (char)crc;
_cmd->_crc16_hi = (char)(crc >> 8);
ret = 0x49;
}
}
return ret;
}
return 0;
}

This reveals the command format:

  • Byte 0: Command ID (0x18 for Establish key)
  • Byte 1: Constant (0xff)
  • Bytes 2–0x46: Payload (0x45 bytes)
  • Bytes 0x47–0x48: CRC16 (little-endian)
  • Total frame length: 0x49 bytes

CRC-16/X-25 Implementation:

The function name suggested a standard CRC-16/CCITT variant, but there are many. By reading the decompiled function, the researcher identified:
– Polynomial: 0x8408 (bit-reversed form of 0x1021)
– Initial value: 0xFFFF
– Reflected input and output
– Final XOR: bitwise inversion

This corresponds to CRC-16/X-25 (also known as CRC-16/IBM-SDLC).

static const uint16_t crc16_table[bash] = {
0x0000, 0x1189, 0x2312, 0x329B, 0x4624, 0x57AD, 0x6536, 0x74BF,
0x8C48, 0x9DC1, 0xAF5A, 0xBED3, 0xCA6C, 0xDBE5, 0xE97E, 0xF8F7,
/ ... 240 more entries ... /
0x7BC7, 0x6A4E, 0x58D5, 0x495C, 0x3DE3, 0x2C6A, 0x1EF1, 0x0F78,
};

uint16_t crc16(const uint8_t data, int len) {
uint16_t crc = 0xFFFF;
for (uint32_t i = 0; i < len; i++) {
crc = crc16_table[(crc ^ data[bash]) & 0xFF] ^ (crc >> 8);
}
return ~crc;
}

6. Active Testing and Command Brute-Forcing

With the protocol reconstructed, the researcher needed to actively send commands. A third device — the Xiaomi Camera C301 — was used, on which root access was available.

C Program for I2C Communication:

int fd = open("/dev/i2c-0", O_RDWR);
ioctl(fd, I2C_SLAVE, 0x2A); // Set slave address
write(fd, cmd, cmd_len); // Send command
read(fd, resp, resp_len); // Read response

A CLI was built for interactive testing:

$ ./mjac_send read 3 2 8  READ zone=3, offset=2, length=8
$ ./mjac_send query  Query chip info
$ ./mjac_send custom 130000...  Send arbitrary frame for fuzzing

Critical Limitation: After each command sent, the chip became unresponsive and required a full device reboot. This did not happen during passive sniffing, suggesting the issue is specific to active interaction. Despite extensive tweaking — adjusting I2C transaction timing, splitting read/write phases, and comparing against legitimate `miio_client` exchanges — the root cause remained unidentified.

Brute-Forcing Undocumented Commands:

With the active testing setup in place, the researcher iterated over all possible command IDs (0x00 to 0xFF), sending each with a valid CRC. A Python controller automated the full cycle:

 Pseudo-code for automation
for cmd_id in range(0x100):
ssh.run(f"./mjac_send custom {cmd_id:02x}0000...")
response = ssh.capture()
if response != "Unsupported command (0x04)":
log(f"Found interesting command: 0x{cmd_id:02x}")
if chip_unresponsive:
ssh.run("reboot")
wait_for_ssh()

This revealed two additional commands not present in miio_client:

  • 0x06 → Update: Writes data to a zone (see below)
  • 0x13 → Unknown: Always returns an undocumented error code 0x0F

Command 0x13 is particularly intriguing — it does not return “Unsupported command” but rather a custom error code, suggesting the command exists and is partially handled, but requires specific preconditions or arguments.

  1. Security Assessment: What the MJA1 Chip Gets Right

Read Command (0x05) Structure:

| Parameter | Description |

|–|-|

| Index | Data zone: 0=Device certificate, 1=Manufacturer certificate, 2=Root certificate, 3=Product data, 4=User data |
| Offset | Starting position within the selected zone |
| Length | Number of bytes to read (capped at 512 bytes) |

Critical Security Finding: All three certificates available via the Read command are public certificates. The corresponding private keys are never exposed over I2C. The private key material stays inside the chip and is only used internally by cryptographic commands (Generate signature, Establish key). This is exactly the trust boundary a secure element is meant to enforce.

Update Command (0x06) — Write Protection:

The Update command has the same structure as Read but writes data to a selected zone. After extensive testing:

  • Only the user data zone (Index=4) can actually be modified
  • All other zones (certificates and product data) are read-only
  • Data length is limited by I2C buffer size
  • Offset is bounded by the size of the user data zone

The chip enforces strong write protection on the zones that matter most: device, manufacturer, and root certificates, as well as product data, are all immutable from the host side.

Command Response Status Codes:

| Code | Meaning |

|||

| 0x00 | OK |

| 0x01 | Invalid CRC |

| 0x02 | Invalid arguments |

| 0x04 | Unsupported command |

| 0x06 | Length too large |

Example Exchange — Read Command:

Request: READ (Index=3, Offset=2, Length=8, CRC=0x58EF)
Response: [ 00 | 000A | 00 00 00 00 41 FF E5 6F | 71C8 ]
Decoded: Status: OK (0x00); Length: 0x000A; Product ID: 0x41FFE56F; CRC: 0x71C8

What Undercode Say:

  • Black-box analysis is feasible even with zero documentation — systematic probing, sniffing, dumping, and reverse engineering can recover proprietary protocols from the ground up, as demonstrated by the MJA1 analysis.
  • Secure elements are only as strong as their physical attack resistance — while the MJA1’s protocol-level security appears robust (private keys never leave the chip, write protection is enforced), the real test lies in physical attacks: fault injection, side-channel analysis, and chip isolation.
  • The undocumented 0x13 command is a red flag — a command that exists, returns a custom error, but isn’t referenced in host firmware suggests either a debug/backdoor interface or an incomplete implementation. Targeted fuzzing could reveal dangerous functionality.
  • Active testing limitations can severely hamper research — the chip’s unresponsiveness after each active command forced a reboot cycle, making comprehensive fuzzing impractical. This could be a deliberate anti-tampering mechanism or a hardware bug.
  • Public GitHub repositories can accelerate research — finding the handshow-firmware repository with MJA1 wrapper source code confirmed findings and filled gaps, highlighting the value of open-source intelligence in hardware reverse engineering.
  • The protocol is only the front door — the most interesting attack vectors are physical: desoldering the chip, voltage glitching, EM pulse injection, and power/EM side-channel analysis. These could potentially bypass the zone access controls and extract private keys.
  • Xiaomi’s implementation follows secure element best practices — certificates are public, private keys never leave the chip, and sensitive zones are read-only. No obvious flaws were found at the protocol level.
  • The “financial-grade protection” claim is partially validated — the chip does enforce cryptographic boundaries correctly from the host interface perspective, though physical attacks remain untested.
  • Cross-validation across multiple devices is essential — using two devices for comparison and a third for active testing provided redundancy and confidence in findings.
  • This research is a starting point, not an endpoint — the next phase involves physical isolation, fault injection, and side-channel analysis to truly assess the chip’s security.

Prediction:

  • +1 As IoT devices increasingly adopt dedicated secure elements, black-box security analysis will become a critical skill for vulnerability researchers. The MJA1 analysis provides a replicable methodology that can be applied to other proprietary chips.
  • +1 The discovery of undocumented commands (0x06 and 0x13) in a production chip highlights the importance of command-space fuzzing. Expect more researchers to adopt similar brute-forcing techniques against secure elements.
  • -1 The unresponsiveness after active commands is concerning — if this is an intentional anti-tampering mechanism, it’s effective but also suggests the chip may have undocumented watchdog or fault detection logic that could be triggered during physical attacks.
  • -1 Command 0x13 returning a custom error (0x0F) rather than “Unsupported command” suggests either a debug interface, a partially implemented feature, or a backdoor. Targeted fuzzing of this command could reveal privileged functionality.
  • +1 The public release of the CRC-16/X-25 implementation and command protocol will enable the broader security community to audit other Xiaomi devices using the MJA1 chip, potentially uncovering vulnerabilities across the ecosystem.
  • -1 Physical attacks — fault injection, side-channel analysis, and chip decapsulation — remain untested. Given that ECC operations are performed on-chip (Generate key, Establish key, Generate signature), power and EM side-channel attacks could potentially recover private keys.
  • +1 The researcher’s methodology — from hardware probing to firmware reverse engineering to active command fuzzing — sets a new standard for black-box secure element analysis. This will influence how hardware security is taught and practiced.
  • -1 The fact that no public documentation exists for the MJA1 chip is a security anti-pattern. Proprietary security through obscurity hinders independent auditing and may mask vulnerabilities that would be caught in open, peer-reviewed designs.
  • +1 The availability of root access on the Xiaomi Camera C301 (thanks to Alexandre Chazal) demonstrates the value of the modding community in enabling security research. Collaboration between researchers and the community accelerates vulnerability discovery.
  • -1 If the 0x13 command is indeed a backdoor or debug interface, it could affect millions of Xiaomi cameras and IoT devices using the MJA1 chip. The research community should prioritize fuzzing this command and investigating its behavior.

▶️ Related Video (82% Match):

🎯Let’s Practice For Free:

🎓 Live Courses & Certifications:

Join Undercode Academy for Verified Certifications

🚀 Request a Custom Project:

Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands

IT/Security Reporter URL:

Reported By: Aleborges Cybersecurity – Hackers Feeds
Extra Hub: Undercode MoN
Basic Verification: Pass ✅

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow UndercodeTesting & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin | 🦋BlueSky