TL;DR
This Philips Hue Bridge firmware emulation write-up covers the first phase of the project: moving from encrypted firmware update files to a working MIPS QEMU lab.
The useful milestone was not a dramatic final exploit. It was a stable, repeatable environment.
We parsed the Hue FW2 update format, decrypted encrypted firmware sections, reconstructed enough of the OpenWrt-based filesystem to boot it, and got the important bridge services running under QEMU on a MIPS big-endian Malta machine.
That gave us a lab that could be started, broken, inspected, reset, and documented without constantly touching the physical device.
This article deliberately stops at successful emulation. Later research can build on this foundation for service reversing and vulnerability analysis.
Key takeaways
- Philips Hue Bridge firmware can be unpacked by parsing the FW2 container and decrypting selected encrypted sections.
- A booting kernel is not enough: appliance userspace needs writable paths, runtime directories, service state, and hardware assumptions handled.
- A small
mini_initentry point made the QEMU lab deterministic and easier to debug. - Forwarded localhost ports made SSH, HTTP, HTTPS, and HomeKit-facing services easy to reach from the host.
- The result was a practical firmware emulation lab for repeatable reverse engineering.
Contents
- Why emulate the bridge?
- Step 1: Understanding the FW2 update package
- Step 2: Extracting enough filesystem to boot
- Step 3: Choosing the QEMU machine
- Step 4: Making the services useful
- Step 5: Preparing for Ghidra
- Emulation evidence
Why emulate the bridge?
Embedded research becomes much easier when the target can be reset quickly.
A physical Philips Hue Bridge is useful, but it is not always a convenient daily analysis target. You have to deal with device state, networking, hardware quirks, storage wear, serial access, firmware recovery, and the risk of turning a working unit into a paperweight.
For this project, the goal was to build a safer research loop:
- unpack and understand the firmware update format
- recover the filesystem and service layout
- boot a representative Linux userspace under QEMU
- expose the same key network services on localhost
- make service startup and logging repeatable
- keep the lab deterministic enough for Ghidra-backed reverse engineering later
The target device was a Philips Hue Bridge BSB002 running a custom OpenWrt-based Linux distribution on MIPS32 big-endian hardware.
The services of interest included:
- nginx for HTTP and HTTPS routing
- ipbridge for the main Hue API and bridge logic
- clipd for CLIPv2 API handling
- hk_hap for Apple HomeKit Accessory Protocol support
- mosquitto for MQTT-related messaging
- Dropbear SSH in the lab image for controlled orchestration
The important outcome was reaching a point where these components could be studied in a controlled emulated environment. In practical terms, that meant turning Philips Hue Bridge firmware emulation into a repeatable engineering workflow rather than a one-off boot experiment.
Step 1: Understanding the FW2 update package
The first practical problem was getting a trustworthy view of the firmware.
The local research had two complementary sources:
- Filesystem and flash material from a lab bridge.
- Firmware update package analysis.
The update package work focused on the Hue FW2 container. The format starts with a small FW2 header:
Header: 0x22 bytes
+0..+5 magic, for example BSB002
+6 format byte
+7 file or section count
+8..+11 payload size, big-endian u32
+12..+33 builder string, 22 bytes
That is followed by one section header per embedded file:
+0..+3 payload size, big-endian u32
+4..+5 section type, big-endian u16
+6..+9 options, big-endian u32
+10..+25 version string, 16 bytes
Then the section payloads follow directly.
For encrypted image sections, the payload structure is:
[ 16-byte IV ][ AES-CBC ciphertext with PKCS#7 padding ]
The decryptor supports selecting sections by index or type. The research notes identify image-like section types such as:
0x201
0x202
0x203
0x204
0x103
The AES key is loaded from local key material recovered during the lab work. The key itself is not something to publish. The useful part for the write-up is the process: the firmware container can be parsed, encrypted sections can be identified, and the image contents can be recovered in a repeatable way without embedding secrets into the tooling.
The core decryptor looked like this. It keeps the recovered key material outside the script, parses the FW2 section table, then decrypts the selected encrypted payload with AES-CBC and PKCS#7 unpadding.
#!/usr/bin/env python3
from dataclasses import dataclass
from pathlib import Path
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
@dataclass
class Section:
index: int
payload_offset: int
payload_size: int
section_type: int
options: int
version: str
def parse_fw2(data: bytes) -> list[Section]:
if len(data) < 0x22:
raise ValueError("FW2 file too small")
if data[0:3] != b"BSB":
raise ValueError(f"unexpected FW2 magic: {data[0:6]!r}")
file_count = data[7]
pos = 0x22
sections = []
for idx in range(file_count):
hdr = data[pos:pos + 0x1a]
size = int.from_bytes(hdr[0:4], "big")
section_type = int.from_bytes(hdr[4:6], "big")
options = int.from_bytes(hdr[6:10], "big")
version = hdr[10:26].split(b"\x00", 1)[0].decode("ascii", "ignore")
payload_offset = pos + 0x1a
sections.append(Section(idx, payload_offset, size, section_type, options, version))
pos = payload_offset + size
return sections
def decrypt_aes_cbc_with_pkcs7(iv: bytes, ciphertext: bytes, key: bytes) -> bytes:
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
decryptor = cipher.decryptor()
padded = decryptor.update(ciphertext) + decryptor.finalize()
unpadder = padding.PKCS7(128).unpadder()
return unpadder.update(padded) + unpadder.finalize()
def decrypt_section(fw2_path: str, key_path: str, section_type=0x201) -> bytes:
data = Path(fw2_path).read_bytes()
key = Path(key_path).read_bytes()
section = next(s for s in parse_fw2(data) if s.section_type == section_type)
payload = data[section.payload_offset:section.payload_offset + section.payload_size]
iv, ciphertext = payload[:16], payload[16:]
return decrypt_aes_cbc_with_pkcs7(iv, ciphertext, key)
# Example lab usage, with key material kept local and unpublished:
# Path("section_201.dec").write_bytes(decrypt_section("bridge.fw2", "enc.k", 0x201))
Step 2: Extracting enough filesystem to boot
Once the firmware sections were accessible, the next job was reconstructing a usable filesystem view.
This was not just “extract an archive and run it”. The bridge software expects a specific embedded environment. Parts of the runtime assume OpenWrt conventions, persistent data paths, device nodes, service supervision, writable storage, network interfaces, and hardware-specific behaviours that do not exist by default inside QEMU.
The emulation work therefore needed a patched root filesystem and a separate writable data disk.
Several practical issues had to be solved before the environment became stable:
- OpenWrt/procd reboot loops when the expected writable overlay was missing
- missing musl loader and library symlinks
- missing BusyBox applets
- missing service directories in the first extracted filesystem view
- nginx shared memory zone sizing problems
- mosquitto compatibility issues around the emulated userspace
- application assumptions about persistent paths under
/home - hardware expectations around GPIO and Zigbee devices
- networking setup, initially through bridge/TAP experiments and later through user-mode port forwarding
The exact set of patches is lab-specific, but the pattern is familiar in firmware emulation: booting the kernel is only the beginning. The real work is persuading the userspace to believe it is still on its appliance.
Step 3: Choosing the QEMU machine
The working lab uses qemu-system-mips with a Malta machine and a 24Kf CPU.
The launch shape is:
qemu-system-mips \
-L ./qemu-bios \
-M malta \
-cpu 24Kf \
-m 256 \
-nographic \
-kernel ./archives/linux/vmlinux \
-append "root=/dev/sda rw console=ttyS0,115200 init=/sbin/mini_init" \
-drive file=rootfs_patched.ext4,format=raw,if=ide,index=0 \
-drive file=data.ext4,format=raw,if=ide,index=1 \
-netdev user,id=net0,hostfwd=tcp:127.0.0.1:22222-:22,hostfwd=tcp:127.0.0.1:18080-:8080,hostfwd=tcp:127.0.0.1:18443-:443,hostfwd=tcp:127.0.0.1:18081-:80 \
-device pcnet,netdev=net0
The important forwarded ports are:
127.0.0.1:22222 -> guest SSH on 22
127.0.0.1:18080 -> guest service on 8080
127.0.0.1:18443 -> guest HTTPS on 443
127.0.0.1:18081 -> guest HTTP on 80
The mini_init entry point is used to make startup predictable. In a normal embedded boot, procd and a collection of init scripts expect a very specific device environment. In the lab, a small controlled init path is easier to reason about and easier to debug.
This is one of the biggest quality-of-life improvements in the workflow. Instead of trying to reproduce every hardware detail up front, the lab can start the pieces needed for analysis and leave hardware-specific gaps stubbed, patched, or isolated.
The simplified init path was intentionally small and boring. That made it much easier to tell whether a failure was a real service problem or just missing appliance scaffolding.
# mini_init service sequence used in the QEMU lab
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t tmpfs tmpfs /tmp
mount -t devtmpfs devtmpfs /dev
mount -t devpts devpts /dev/pts
mount -t ext4 /dev/sdb /data
mkdir -p /var /tmp/run /tmp/lock /tmp/log /tmp/platform
mkdir -p /home/homekit/var /tmp/homekit /tmp/clipv2_cache
ifconfig lo 127.0.0.1 up
ifconfig eth0 up
udhcpc -i eth0 -n -q
/usr/sbin/dropbear -F -R &
/sbin/ubusd &
/usr/sbin/mdnsd &
/usr/sbin/mosquitto &
echo 17 > /var/platform/hk_hap-resetreason
/usr/bin/ipbridge -p /home/ipbridge/var -z /dev/null -h /home -v 1973038060 -e c42996fffe60cf23 -n HueBridge2K15 -c HueBridge2K15 -k 3c205100b5f3bdaaefae0cd828b72487 &
LD_PRELOAD="/usr/lib/libsetlbf.so /usr/lib/ipbridge_preload_noldep.so" /usr/bin/hk_hap &
For HomeKit-focused testing, a separate helper reset the HAP state and brought hk_hap up in a repeatable way.
#!/bin/sh
# /sbin/start_hap.sh - used after QEMU boot for repeatable HAP testing
killall -9 hk_hap 2>/dev/null
sleep 1
rm -f /home/homekit/var/hk_keystorage 2>/dev/null
echo 17 > /var/platform/hk_hap-resetreason 2>/dev/null
rm -f /tmp/core.* /tmp/pwned /tmp/hk_hap.log 2>/dev/null
/usr/bin/hk_hap > /tmp/hk_hap.log 2>&1 &
HAP_PID=$!
sleep 1
# Patch ZigbeeBridgeInitState=2 and BridgeMigrationStatusFlag |= 0x30.
# These runtime flags normally depend on Zigbee bridge state.
printf '\x00\x00\x00\x02' | dd of=/proc/$HAP_PID/mem bs=1 seek=$((0x00cfa620)) conv=notrunc 2>/dev/null
printf '\x30' | dd of=/proc/$HAP_PID/mem bs=1 seek=$((0x00cfa7d8)) conv=notrunc 2>/dev/null
sleep 8
netstat -tlnp 2>/dev/null | grep 8080 && echo "OK: hk_hap listening"
Step 4: Making the services useful
A booting kernel is not enough. A useful firmware lab needs target services to run in a way that can be reached and observed from the host.
The successful emulation milestone was being able to start and interact with the key Hue bridge services in the guest environment, especially the HTTP-facing components and HomeKit-related service surface.
The service work included:
- preparing runtime directories expected by the bridge applications
- ensuring the dynamic loader and musl-linked binaries could start cleanly
- fixing missing filesystem paths
- exposing service ports through QEMU host forwarding
- keeping SSH available for lab control and log collection
- restarting individual services without rebooting the whole emulated machine
Once this was stable, the bridge lab became practical: services could be started, requests could be sent from the host, logs could be collected, and the filesystem could be snapshotted between experiments.
That is the point where firmware analysis turns into a proper research platform.
Step 5: Preparing for Ghidra without jumping ahead
Although this post stops at emulation, the emulation work was done with reverse engineering in mind.
The bridge binaries are not friendly desktop binaries. The HomeKit-related daemon, for example, is a stripped MIPS big-endian executable. The useful workflow was to combine:
- Ghidra project analysis
- exported decompiler output
- function renaming
- comments and plate notes
- runtime logs from the emulated guest
- manual validation against service behaviour
The important distinction is that tooling did not replace manual reverse engineering. It made the repetitive parts faster: naming functions, preserving context, comparing decompiled code, and keeping notes aligned with runtime behaviour.
For this first article, the takeaway is simpler: stable emulation makes this kind of reverse engineering workflow far more effective. Instead of analysing dead firmware in isolation, you can compare decompiler output against runtime behaviour in a lab you control.
What “success” looked like
The successful milestone for this phase was:
- encrypted firmware sections could be parsed and decrypted locally
- the recovered filesystem could be converted into a bootable lab image
- QEMU could boot the patched MIPS environment reliably
- a writable data disk could persist runtime state
- key bridge services could start in the guest
- host port forwarding exposed the useful service ports on localhost
- SSH access allowed controlled lab orchestration
- the setup was stable enough to support repeatable Ghidra-backed reverse engineering
At that point, the bridge was no longer just a physical device on the desk. It was a repeatable software lab.
That was the foundation needed before doing deeper service analysis.
Lessons learned
A few practical lessons stand out.
First, firmware decryption is only one part of the problem. It gets you access to the content, but it does not automatically give you a working system.
Second, appliance firmware carries a lot of hardware assumptions. GPIO, serial devices, writable overlays, service directories, init behaviour, and application state can all block progress before the interesting services start.
Third, deterministic startup matters. A small controlled init path and predictable QEMU command line made the lab much easier to debug than trying to perfectly reproduce the full boot environment from day one.
Finally, stopping at emulation is a valid milestone. In embedded research, getting to a reliable booting and service-running lab is often the difference between guessing at firmware and being able to investigate it properly.
Closing note
This article intentionally stops at successful emulation.
For now, the result is the foundation: an encrypted firmware package turned into a working MIPS QEMU lab for a Philips Hue Bridge.
Emulation evidence
The emulator was started locally and verified over forwarded ports. The screenshot below shows the QEMU lab booted, SSH reachable on localhost port 22222, and key bridge services running inside the MIPS guest.
