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_CONFIGURATIONrequest is received.syz_usb_connect_ath9k- connects anath9kUSB device. Compared tosyz_usb_connect, this syscall also handles firmware download requests that happen afterSET_CONFIGURATIONfor theath9kdriver.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_connectvariant. As the USB device descriptor fields for this pseudo-syscall get patched by syzkaller runtime,syz_usb_connectalso 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_writeandsyz_usb_ep_readpseudo-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
ath9kdriver viasyz_usb_connect_ath9k,syz_usb_ep_write$ath9k_ep1andsyz_usb_ep_write$ath9k_ep2pseudo-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_devicesinsyz_usb_disconnectand don’t calllookup_usb_indexmultiple times withinsyz_usb_connect. Currently, this causes some reproducers to have therepeatflag 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-execprogandsyz-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=yandCONFIG_USB_DUMMY_HCD=ymust 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_writeandsyz_usb_ep_readpseudo-syscalls in the manager config. -
Set
sandboxtononein 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=ykernel, 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.imgfrom 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
piandraspberry. -
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-execprogon your host machine for arm32 withmake TARGETARCH=arm execprogand copy to~/syz-binonto 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.logThe
slowdownparameter 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.