crowell.biz

11 min read Original article ↗

Running DOOM on the BMW Displaykey

By Alexei Karpenko and Jeff Crowell

First, the final result.

A group of friends recently got obsessed with the BMW i8 and we thought why not get a BMW display key fob and see what we can do with it. Perhaps we could run Doom on it. Instead of owning an i8, we would be pwning its key fob, but at least it would get us a little closer?

In any case, a key fob showed up from eBay a few days later. There is a USB port which can be used to charge the key, but nothing showed up when it was plugged into the computer. Perhaps only the power lines of the USB were connected. If that’s the case, accessing the firmware might require soldering, which is not ideal.

Let’s take the thing apart and take a look what we have.

/images/doom/IMG_3596.jpg
/images/doom/IMG_3597_2.jpg

By searching the part numbers, we found that there is a main chip, an NXP i.MX285 ARM CPU, a 64MB Spansion S25FL512S SPI Flash, 128MB RAM and also a RF portion with its own microcontroller. The other side of the circuit board has a bunch of unlabelled test points. Those are usually quite useful, but we will have to probe them or trace them to the CPU pins to understand what any of them do.

We decided to start with desoldering the NOR flash chip and dumping it. It’s a BGA chip with a pitch of 1mm.

The chip has a serial mode (SPI) as well as wider mode for faster transfers. We don’t care too much about speed for dumping, just optimizing for the number of wires that need to be soldered as it is quite tiny.

/images/doom/IMG_3610.jpg
/images/doom/IMG_3613.jpg

We used the SPI on a Raspberry Pi to dump the chip using flashrom tool.

Inspecting the dump showed a lot of interesting strings and that nothing is encrypted.

A neat little tool to use is binwalk which tries to detect different things in the binary:

$ binwalk bmw.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
524288 0x80000 uImage header, header size: 64 bytes, header CRC: 0x48E757, created: 2015-11-12 15:20:08, image size: 1145056 bytes, Data Address: 0x40008000, Entry Point: 0x40008000, data CRC: 0x510ECE89, OS: Linux, CPU: ARM, image type: OS Kernel Image, compression type: none, image name: "Linux-2.6.35.3-1129-g691c08a"
524352 0x80040 Linux kernel ARM boot executable zImage (little-endian)
541021 0x8415D gzip compressed data, maximum compression, from Unix, last modified: 2015-11-12 15:13:08
2883584 0x2C0000 Linux EXT filesystem, blocks count: 32714, image size: 33499136, rev 0.0, ext2 filesystem data, UUID=00000000-0000-0000-0000-000000000000
53215232 0x32C0000 JFFS2 filesystem, little endian
...

There is a Linux 2.6.35 kernel, ext2 root partition, and a bunch of jffs2 partitions. jffs2 partitions make sense for R/W partitions on a NOR flash chip. Since the erase block size is big, you don’t want to be doing erases very often as they wear out the bit array.

Next we mount the ext2 partition from the dump.

sudo mount -t ext2 -o loop,offset=$((0x002c0000)) bmw.bin /mnt/bmw_rootfs

Poking around the ext2 shows a pretty standard Linux busybox setup with some custom tools and assets that BMW wrote, including bmw-mkd which runs the main UI of the key fob. We can also see the /etc/passwd file with root user having a password “root”. (Some of the other keys we dumped later had a better hashed password that we didn’t manage to break — not that it was needed.) The password of “root” can be quickly broken using tools such as John The Ripper, but the other passwords couldn’t be quickly cracked on a laptop.

Of particular interest is the /etc/init.d/rcS file. rcS is the single-user run-control script, which is executed on boot by a Linux system.

Here is an excerpt of the relevant parts.

# ...
NETWORKING=no
# ...

    if [ "$NETWORKING" == "yes" ]; then
        rmmod g_file_storage
        modprobe g_ether dev_addr=62:47:eb:21:47:e1 host_addr=7e:7b:b5:01:9c:74 &> /dev/null

        if [ -d /sys/class/net/usb0 ]; then
            ip addr add 192.168.7.2/30 broadcast 192.168.7.3 dev usb0
            ip link set dev usb0 up

            rsync --daemon
            dropbear
        fi
    fi
# ...
        USB=`cat /sys/class/power_supply/usb/online`

    if [ "$USB" == "1" ]; then
        modprobe g_serial #Normal USB Cable, do probe g_serial
    fi
#...

So it looks like the USB data lines are connected after all and can be enabled. Moreover, there are two kernel modules that seem useful, g_ether and g_serial, i.e. USB gadget ethernet and serial.

The hack should be pretty straight forward. We only need to change a few bits in the script to make it setup the networking and then flash the new image to the flash chip. (We could even not bother with ext2fs and erasing the chip. It’s possible to make the script go to the if-statement just by changing some bits from 1→0 which doesn’t require an erase. However, it’s simpler to write at a block level, so we decided to just rewrite the full contents.)

We then soldered more magnet wires to the PCB with a connector so that we could re-attach the NOR flash. But it wasn’t working — probably because the length of the wires was too long which resulted in data loss. We decided to scrap this PCB and desolder the ARM CPU so that we could trace the test points to the CPU pins.

/images/doom/IMG_3707.jpg

Armed with a continuity meter and a map of the BGA array, we went through each test point and probed the BGA pads, writing down the findings.

/images/doom/IMG_3756.jpg

Here is the map of the test pads we were able to determine.

/images/doom/bmw-testpoints.jpg

So there are a bunch of super-useful signals here:

  • JTAG: in-circuit debugging
  • DUART: debug serial port
  • PSWITCH: recovery boot mode

At first we played with DUART. It showed a few U-Boot messages and then nothing after kernel started booting. Also we couldn’t trigger the U-Boot command line.

We also connected JTAG and were able to do single instruction stepping etc. But we decided to not pursue it as we wanted to find a solder-less way to hack the key fob.

Now we come to the PSWITCH. If we short it with power during boot, the chip switches to ROM Recovery mode and shows up as a USB device waiting for a boot stream. What would be nice about this approach is that no soldering is required.

NXP chips can come with a secure boot mode, called High Assurance Boot, or HAB. HAB is a mode that ensures that the system is booted to a known good state, rooted from a trusted cryptographic key. If secure boot is properly implemented, in theory, it is not possible to boot a modified version of the operating system. Quarkslab, in 2017 had reported several vulnerabilities in the i.MX series of microprocessors HAB implementation that we had intended to use ( https://blog.quarkslab.com/vulnerabilities-in-high-assurance-boot-of-nxp-imx-microprocessors.html ). However, HAB was not enabled in this particular use case.

Initially we tried building the boot stream from the original dump and this worked, but restricted us to patching things. Instead we decided to just build our own U-Boot with any functions needed to flash the firmware. It wasn’t very straight forward and took some time to figure out from the data sheets and limited set of examples in U-Boot for i.MX chip series. But finally we had a U-Boot image with USB networking that didn’t require to use DUART anymore. Our modified U-Boot is available here: https://github.com/displaykey/u-boot

It supports dumping and flashing the BMW display key firmware using TFTP.

We can now re-build the firmware image with any changes in ext2 and flash it via tftp protocol all without soldering.

Once the dropbear ssh server was enabled in the firmware through the bootscript, and a known password was set for the root account, we could finally ssh into the key. This would let us create a RAM disk in the 128MB RAM and copy Doom over after we built it.

Things that work:

  • Dumping or flashing a BMW display key, changing files in the rootfs.
  • USB ethernet, SSH
  • Framebuffer, touchscreen
  • RAM disk
  • Replacing kernel in the image
  • Adding kernel modules from different key dumps

Things that we didn’t try:

  • Hardware buttons
  • RF portion
    • the RF portion is where the per-fob keys to communicate with the car are stored.
    • a simple cloning attack of the flash would not be sufficient to steal someone’s car
      • the cryptographic key material is stored outside of the NOR flash, in the secondary “secure” chip, which we did not investigate.

Of course, it has a CPU, it has a display, it must run DOOM. Getting this to run was straightforward, because it is a (somewhat) modern Linux system, running on an architecture that is currently supported in Debian (and by qemu) so testing and building is rather simple. The display is 320x200, but framebuffer is configured in the kernel to be 200x320 (portrait vs landscape). There were a small amount of changes required to fbDOOM (removing keyboard events, hacking around hardcoded screen sizes, rotating the framebuffer, and removing audio as the display does not have a speaker), but compiling a static binary in a Debian armel chroot, or just with an arm cross compiler, was straightforward. Our code for DOOM is available here: https://github.com/displaykey/DOOM .

Along the way we purchased and used around five different physical keys. The Displaykey supports several different BMW models. 5 series, 6 series, 7 series, and i8 models are all supported on the same hardware, but with different firmwares. Interestingly, all keys, with the exception of one (which was for an i8), were flashed with the 5 series (g30) firmware. There were some differences between older and newer versions of the firmware that made exploration easier. Older versions of the firmware came with additional kernel extensions (g_serial, g_ether) which we were able to load in the rcS script to enable either serial console or a shell via SSH. Older firmwares also came with the root password set to root instead of having password disabled in the newer ones.

In digging through the flash dump, we searched for additional vulnerabilities in the stock configuration, in hopes of being able to gain code execution without modification to the flash. The key includes a binary, ftm which is a hardware testing utility. It includes features such as setting the screen to various colors, reporting status, and a few others. The utility is built without stack protection, and we discovered a stack based buffer overflow in the ftm_fill_lcm_color handler.

[0x000102fc]> pdg

// WARNING: Variable defined which should be unmapped: var_8h
// WARNING: [r2ghidra] Failed to match type __fd_mask[32] of member __fds_bits in struct type_0x7812

int dbg.ftm_fill_lcm_color(char *cmd, int *threadForceEnd)
{
    undefined uVar1;
    undefined4 uVar2;
    int32_t iVar3;
    int32_t var_94h;
    char *format;
    char buf [8];
    uint8_t valid_params [21];
    void *s2;
    int32_t i;
    int32_t j;
    int32_t k;
    int32_t bar_length;
    int32_t bar_width;
    int32_t x_margin;
    int32_t y_margin;
    int32_t ret;
    int32_t var_10h;
    int32_t status;
    int32_t var_8h;

    // int ftm_fill_lcm_color(char * cmd,int * threadForceEnd);
    sym.imp.memcpy(valid_params, COLOR_TABLE, 0x54);
    i = 0;
    j = 0;
    k = 0;
    bar_length = 200;
    bar_width = 0x28;
    x_margin = 0x10;
    y_margin = 0x3c;
    ret = dbg.init_framebuffer();
    if (ret == -1) {
        status = 1;
        dbg.send_response("init_framebuffer failed", 1);
    } else {
        sym.imp.__isoc99_sscanf(cmd, "%s %s", buf); // stack based buffer overflow HERE!
        dbg.write_log("ftm_fill_lcm_color color=%s\n", buf);
// ---- SNIP -----

While this bug is exploitable, unfortunately, it requires modifying the boot-args to enable the ftm binary from listening on serial. As long as modifications to the boot-args are required, it is simpler to just edit the contents of the flash instead of exploiting a userspace application.

Another somewhat interesting aside: the rcS references a file /etc/random-seed which if doesn’t exist, gets created by reading 512 bytes from /dev/urandom . On each of the keys, the file random-seed was identical. (sha256 039708d5ba1bc5b6eb72549bbaf353556c8f83c37711a13cadb0852dbf8b6849 ), again, this doesn’t have much use for security purpose, as this file is only referenced in rcS as far as we can tell.

As BMW has recently announced partnership with app-based keys, we expect the Display Key to slowly die out. It is seemingly unlikely that BMW will continue to ship new Display Key hardware when all of the features of the Display Key are available from the app. So for now, enjoy your Display Key, it’s a fun piece of history, a 2007-era smartphone hardware in 2020. Much like we need to save the manuals, for electronics hobbyists, sometimes it is sad that the single-use embedded systems are all being killed for a multi-purpose device.

Running DOOM

Source code with our patches: https://github.com/displaykey/DOOM

# SSH to the bmw key and create the ramdisk

mkdir /tmp/ramdisk
mount -t tmpfs -o size=20m tmpfs /tmp/ramdisk
cd /tmp/ramdisk/

# from your computer scp fbdoom and doom1.wad

scp fbdoom doom1.wad root@192.168.7.2:/tmp/ramdisk/

# run doom

cd /tmp/ramdisk/
./fbdoom -iwad doom1.wad

Kernel Replacement

# kernel extraction

#dd if=bmw2-eth.bin of=uImage.bmw2 bs=$((0x10000)) skip=$((0x8)) count=$((0x24))

# kernel replacement

dd if=bmw2-eth.bin of=memory20160323-ker.bin bs=$((0x10000)) skip=$((0x8)) seek=$((0x8)) count=$((0x24)) conv=notrunc

# bootloader replacement

dd if=bmw2-eth.bin of=memory20160323-ker-uboot.bin bs=$((0x10000)) count=$((0x8)) conv=notrunc

Source code

https://github.com/displaykey/u-boot

https://github.com/displaykey/DOOM

Thanks

Thank you to the BMW security team for clarifying things with regard to the RF setup, BMW for making such an interesting device, and reviewers for proofreading this post.