Xiaomi AI Speaker Authenticated RCE II: How Does MICO OTA Update Work?

This three-part writeup details the journey of finding and exploiting a vulnerability in Xiaomi AI Speaker (MICO S12A) without a physical peripheral (UART). Part II explains the OTA update mechanism of Xiaomi Speaker.

Xiaomi AI Speaker Authenticated RCE II: How Does MICO OTA Update Work?

Author: Otis Chang

This three-part writeup details the journey of finding and exploiting a vulnerability in Xiaomi AI Speaker (MICO S12A) without a physical peripheral (UART). The vulnerability, CVE-2020-14096, was fixed in 1.59.6[1].


Introduction

This part explains the OTA update mechanism of Xiaomi Speaker. In the rest of the post, "Xiaomi Speaker" is abbreviated to "MICO" (its product id).

Let's first look at the MICO's communication topology (in Figure 1.). The mobile phone sends multiple commands (e.g. In Figure 2., get_player_play_status) to Xiaomi's cloud server. Then, MICO will receive MQTT messages from the cloud server. BLE is only used for setting up WiFi and playing music.

The key part is that MICO mobile app can not only call /upgrade/mico?checkUpgrade API to check updates, it can also send remote update command, /remote/ota/v2, through https (Figure 3.).

mico_topology

Figure 1. MICO communication topology

mico_ubus-1

Figure 2. Get play status API

mico_ota_v2

Figure 3. Remote update API

OTA Update Processs

In MICO's system, /usr/bin/messagingagent is responsible of processing MQTT messages. Tencent Blade Team made a great sequence diagram to explain Messageagent, as shown in Figure 3. /remote/ota/v2[2] API will trigger the Messageagent to execute /bin/ota script.

mico_messageagent

Figure 3. MICO communication sequence diagram (ref. Tencent's report[3].)

/bin/ota aims at downloading the firmware image and spawning another script /bin/flash.sh to verify and flash the firmware.

/bin/ota script (excerpt):

upgrade() {
    ...

    # start upgrade LED
    /bin/show_led 2

    predownload_cleanup "$2"

    download_upgrade "$1"
    ...

    clean_oldconfig
    set_upgrade_status "burn"

    flash.sh $OTA_FILE > /dev/null
    ...

    return 0
}

As you can see in the following /bin/flash.sh script, it will first verify the firmware image with command miso -v $file. /bin/miso's return value should be zero if the input firmware is genuine. Then, it calls the function board_system_upgrade (in /bin/boardupgrade.sh) to flash the firmware.

/bin/flash.sh script (excerpt):

#!/bin/sh
#

. /bin/boardupgrade.sh

...

# image verification...
klogger -n "Verify Image: $1..."
miso -v "$1"
if [ "$?" = "0" ]; then
	klogger "Checksum O.K."
else
	msg="Check Failed!!!"
    # print error message and exit
	hndmsg
fi

# stop services
board_prepare_upgrade
board_start_upgrade_led

# prepare to extract file
filename=`basename $1`
upgrade_prepare_dir $1
cd /tmp/system_upgrade

# start board-specific upgrading...
klogger "Begin Upgrading and Rebooting..."
board_system_upgrade $filename $2 $3

# some board may reset after system upgrade and not reach here
# clean up
cd /
rm -rf /tmp/system_upgrade

upgrade_done_set_flags $1 $2 $3

In /bin/boardupgrade.sh, you will find that

  • miso -c $file -f $segment is used to check whether $segment exists.
  • miso -r -x $file -f $segment is used to extract $segment.

Here, I list all the arguments of /bin/miso for reference:

  • -c checks whether the segment $segment exists
  • -f specifies the segment name
  • -n extracts segment data to stdout
  • -r skips the signature verification
  • -u specifies the public key manually
  • -v verifies the firmware image
  • -x extracts the segment $segment

/bin/boardupgrade.sh script (excerpt):

...
# test if $1 has $2 inside
bingo() {
	miso -c $1 -f $2 > /dev/null
	return $?
}

...

updtb() {
	local target="dtb.img"

	bingo $1 $target || return 0

	klogger "Updating dtb..."

	miso -r -x $1 -f $target
	dd if=$target of=/dev/dtb bs=128K count=1
	rm -f $target
}

...

board_system_upgrade() {
	...

	# Version file exist?
	bingo $filename "mico_version" && {
		miso -r -x $filename -f "mico_version"
		klogger "updating to `cat mico_version | grep "option ROM" | awk '{ print $3 }'`..."
	}

	updtb $filename
	upboot $filename
	upker $filename
	upfs_squash $filename

	echo "burn done"
    
    ...

	return 0
}

Conclusion

During MICO's OTA update process, it will perform a one-time firmware verification. If the firmware image is genuine, it skips the signature verification to eliminate extra costs in the extraction phase. The final post, part III, will show that how I bypass the MICO's signature verification, and craft a malicious firmware image.


  1. Xiaomi Speaker has many models and different update channels. Xiaomi does not push update to all channels. For example, model id: s12a's latest firmware is 1.34.39 (Taiwan channel). (Updated on Sep. 15 2020) ↩︎

  2. /remote/ota is deprecated. ↩︎

  3. https://media.defcon.org/DEF CON 26/DEF CON 26 presentations/DEFCON-26-Wu-HuiYu-and-Qian-Wenxiang-Breaking-Smart-Speakers-Updated.pdf ↩︎