External USB fuzzing for Linux kernel
Syzkaller supports fuzzing the Linux kernel USB subsystem externally (as can be done by plugging in a programmable USB device like Facedancer). This allowed finding over 300 bugs in the Linux kernel USB stack so far.
USB fuzzing support consists of 3 parts:
- Syzkaller changes; see the Internals section for details.
- Kernel interface for USB device emulation called Raw Gadget, which is now in the mainline kernel.
- KCOV changes that allow to collect coverage from background kernel threads and interrupts, which are now in the mainline kernel.
See the OffensiveCon 2019 Coverage-Guided USB Fuzzing with Syzkaller talk (video) for some (partially outdated) details.
As USB fuzzing requires kernel side support, for non-mainline kernels you need all mainline patches that touch drivers/usb/gadget/udc/dummy_hcd.c
, drivers/usb/gadget/legacy/raw_gadget.c
and kernel/kcov.c
.
Internals
Currently, syzkaller defines 6 USB pseudo-syscalls (see syzlang descriptions and pseudo-syscalls implementation, which relies on the Raw Gadget interface linked above):
syz_usb_connect
- connects a USB device. Handles all requests to the control endpoint until aSET_CONFIGURATION
request is received.syz_usb_connect_ath9k
- connects anath9k
USB device. Compared tosyz_usb_connect
, this syscall also handles firmware download requests that happen afterSET_CONFIGURATION
for theath9k
driver.syz_usb_disconnect
- disconnects a USB device.syz_usb_control_io
- sends or receives a control message over endpoint 0.syz_usb_ep_write
- sends a message to a non-control endpoint.syz_usb_ep_read
- receives a message from a non-control endpoint.
These pseudo-syscalls targeted at a few different layers:
- USB core enumeration process is targeted by the generic
syz_usb_connect
variant. As the USB device descriptor fields for this pseudo-syscall get patched by syzkaller runtime,syz_usb_connect
also briefly targets the enumeration process of various USB drivers. - Enumeration process for class-specific drivers is targeted by
syz_usb_connect$hid
,syz_usb_connect$cdc_ecm
, etc. (the device descriptors provided to them have fixed identifying USB IDs to always match to the same USB class driver) accompanied by matchingsyz_usb_control_io$*
pseudo-syscalls. - Subsequent communication through non-control endpoints for class-specific drivers is not targeted by existing descriptions yet for any of the supported classes. But it can be triggered through generic
syz_usb_ep_write
andsyz_usb_ep_read
pseudo-syscalls. - Enumeration process for device-specific drivers is not covered by existing descriptions yet.
- Subsequent communication through non-control endpoints for device-specific drivers is partially described only for
ath9k
driver viasyz_usb_connect_ath9k
,syz_usb_ep_write$ath9k_ep1
andsyz_usb_ep_write$ath9k_ep2
pseudo-syscalls.
There are runtests for USB pseudo-syscalls. They are named starting with the vusb
prefix and can be run with:
./bin/syz-runtest -config=usb-manager.cfg -tests=vusb
Things to improve
The core support for USB fuzzing is in place, but there’s still a place for improvements:
-
Remove the device from
usb_devices
insyz_usb_disconnect
and don’t calllookup_usb_index
multiple times withinsyz_usb_connect
. Currently, this causes some reproducers to have therepeat
flag set when it’s not required. -
Add descriptions for more relevant USB classes and drivers.
-
Resolve TODOs from sys/linux/vusb.txt.
-
Implement a proper way for dynamically extracting relevant USB ids from the kernel (a related discussion).
-
Add a mode for standalone fuzzing of physical USB hosts (by using Raspberry Pi Zero, see below). This includes at least: a. making sure that current USB emulation implementation works properly on different OSes (there are some differences in protocol implementation); b. using USB requests coming from the host as a signal (like coverage) to enable “signal-driven” fuzzing, c. making UDC driver name configurable for
syz-execprog
andsyz-prog2c
. -
Generate syzkaller programs from usbmon trace that is produced by actual USB devices (this should make the fuzzer to go significantly deeper into the USB drivers code).
Setting up
-
Make sure the version of the kernel you’re using is at least 5.7. It’s recommended to backport all kernel patches that touch kcov, USB Raw Gadget, and USB Dummy UDC/HCD.
-
Configure the kernel: at the very least,
CONFIG_USB_RAW_GADGET=y
andCONFIG_USB_DUMMY_HCD=y
must be enabled.The easiest option is to use the config from the syzbot USB fuzzing instance.
-
Build the kernel.
-
Optionally update syzkaller descriptions by extracting USB IDs using the instructions below.
-
Enable
syz_usb_connect
,syz_usb_disconnect
,syz_usb_control_io
,syz_usb_ep_write
andsyz_usb_ep_read
pseudo-syscalls in the manager config. -
Set
sandbox
tonone
in the manager config. -
Pass
dummy_hcd.num=8
(or whatever number you use forprocs
) to the kernel command line in the manager config. -
Run.
Updating syzkaller USB IDs
Syzkaller uses a list of hardcoded USB IDs that are patched into syz_usb_connect
by syzkaller runtime. One of the ways to make syzkaller target only particular USB drivers is to alter that list. The instructions below describe a hackish way to generate syzkaller USB IDs for all USB drivers enabled in your .config
.
-
Apply this kernel patch.
-
Build and boot the kernel.
-
Connect a USB HID device. In case you’re using a
CONFIG_USB_RAW_GADGET=y
kernel, use the keyboard emulation program. -
Use syz-usbgen script to update syzkaller descriptions:
./bin/syz-usbgen $KERNEL_LOG ./sys/linux/init_vusb_ids.go
-
Don’t forget to revert the applied patch and rebuild the kernel before doing actual fuzzing.
Running reproducers with Raspberry Pi Zero W
It’s possible to run syzkaller USB reproducers by using a Linux board plugged into a physical USB host. These instructions describe how to set this up on a Raspberry Pi Zero W, but any other board that has a working USB UDC driver can be used as well.
-
Download
raspbian-stretch-lite.img
from here. -
Flash the image into an SD card as described here.
-
Enable UART as described here.
-
Boot the board and get a shell over UART as described here. You’ll need a USB-UART module for that. The default login credentials are
pi
andraspberry
. -
Get the board connected to the internet (plug in a USB Ethernet adapter or follow this).
-
Update:
sudo apt-get update && sudo apt-get dist-upgrade && sudo rpi-update && sudo reboot
. -
Install useful packages:
sudo apt-get install vim git
. -
Download and install Go:
curl https://dl.google.com/go/go1.14.2.linux-armv6l.tar.gz -o go.linux-armv6l.tar.gz tar -xf go.linux-armv6l.tar.gz mv go goroot mkdir gopath export GOPATH=~/gopath export GOROOT=~/goroot export PATH=~/goroot/bin:$PATH export PATH=~/gopath/bin:$PATH
-
Download syzkaller, apply the patch below and build
syz-executor
:
diff --git a/executor/common_usb_linux.h b/executor/common_usb_linux.h
index 451b2a7b..64af45c7 100644
--- a/executor/common_usb_linux.h
+++ b/executor/common_usb_linux.h
@@ -292,9 +292,7 @@ static volatile long syz_usb_connect_impl(uint64 speed, uint64 dev_len, const ch
// TODO: consider creating two dummy_udc's per proc to increace the chance of
// triggering interaction between multiple USB devices within the same program.
- char device[32];
- sprintf(&device[0], "dummy_udc.%llu", procid);
- int rv = usb_raw_init(fd, speed, "dummy_udc", &device[0]);
+ rv = usb_raw_init(fd, speed, "20980000.usb", "20980000.usb");
if (rv < 0) {
debug("syz_usb_connect: usb_raw_init failed with %d\n", rv);
return rv;
git clone https://github.com/google/syzkaller
cd syzkaller
# Put the patch above into ./syzkaller.patch
git apply ./syzkaller.patch
make executor
mkdir ~/syz-bin
cp bin/linux_arm/syz-executor ~/syz-bin/
-
Build
syz-execprog
on your host machine for arm32 withmake TARGETARCH=arm execprog
and copy to~/syz-bin
onto the SD card. You may try building syz-execprog on the Raspberry Pi itself, but that worked poorly for me due to large memory consumption during the compilation process. -
Make sure that you can now execute syzkaller programs:
cat socket.log r0 = socket$inet_tcp(0x2, 0x1, 0x0) sudo ./syz-bin/syz-execprog -executor ./syz-bin/syz-executor -threaded=0 -collide=0 -procs=1 -enable='' -debug socket.log
-
Setup the dwc2 USB gadget driver:
echo "dtoverlay=dwc2" | sudo tee -a /boot/config.txt echo "dwc2" | sudo tee -a /etc/modules sudo reboot
-
Get Linux kernel headers following this.
-
Download and build the USB Raw Gadget module following this.
-
Insert the module with
sudo insmod raw_gadget.ko
. -
Download, build, and run the keyboard emulator program:
# Get keyboard.c gcc keyboard.c -o keyboard sudo ./keyboard 20980000.usb 20980000.usb # Make sure you see the letter 'x' being entered on the host.
-
You should now be able to execute syzkaller USB programs:
$ cat usb.log r0 = syz_usb_connect(0x0, 0x24, &(0x7f00000001c0)={{0x12, 0x1, 0x0, 0x8e, 0x32, 0xf7, 0x20, 0xaf0, 0xd257, 0x4e87, 0x0, 0x0, 0x0, 0x1, [{{0x9, 0x2, 0x12, 0x1, 0x0, 0x0, 0x0, 0x0, [{{0x9, 0x4, 0xf, 0x0, 0x0, 0xff, 0xa5, 0x2c}}]}}]}}, 0x0) $ sudo ./syz-bin/syz-execprog -slowdown 3 -executor ./syz-bin/syz-executor -threaded=0 -collide=0 -procs=1 -enable='' -debug usb.log
The
slowdown
parameter is a scaling factor which can be used for increasing the syscall timeouts. -
Steps 19 through 21 are optional. You may use a UART console and a normal USB cable instead of ssh and Zero Stem.
-
Follow this to set up a Wi-Fi hotspot.
-
Follow this to enable ssh.
-
Optionally solder Zero Stem onto your Raspberry Pi Zero W.
-
You can now connect the board to an arbitrary USB port, wait for it to boot, join its Wi-Fi network, ssh onto it, and run arbitrary syzkaller USB programs.