Talos Vulnerability Report

TALOS-2021-1339

Microsoft Azure Sphere Kernel GPIO_SET_PIN_CONFIG_IOCTL information disclosure vulnerability

November 9, 2021
CVE Number

CVE-2021-41374

Summary

An information disclosure vulnerability exists in the GPIO_SET_PIN_CONFIG_IOCTL functionality of Microsoft Azure Sphere 21.06. A specially crafted ioctl can lead to kernel memory leak. An attacker can issue an ioctl to trigger this vulnerability.

Tested Versions

Microsoft Azure Sphere 21.06

Product URLs

https://azure.microsoft.com/en-us/services/azure-sphere/

CVSSv3 Score

6.7 - CVSS:3.0/AV:L/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:H

CWE

CWE-129 - Improper Validation of Array Index

Details

Microsoft’s Azure Sphere is a platform for the development of internet-of-things applications. It features a custom SoC that consists of a set of cores that run both high-level and real-time applications, enforces security and manages encryption (among other functions). The high-level applications execute on a custom Linux-based OS, with several modifications to make it smaller and more secure, specifically for IoT applications.

The Azure Sphere platform provides multiple different configuration modes for all of the dedicated pins that it exposes, including a standard GPIO mode that can be assigned to any of the pins. Normally, in order to gain access to the GPIO pins, an application must add an entry to its app_manifest.json as such:

"Gpio": [ "$MT3620_RDB_HEADER1_PIN6_GPIO", "$MT3620_RDB_LED1_RED", "$MT3620_RDB_BUTTON_A" ],

The above configuration would cause GPIO pins 1, 8, and 12 to become accessible to the Linux userland via the /dev/gpiochip0 char device’s ioctls. We truncate the gpio_ioctl function below to list out the utilized ioctls:

static long gpio_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
  if (cmd == GPIO_GET_CHIPINFO_IOCTL) {
    } else if (cmd == GPIO_GET_LINEHANDLE_IOCTL) {
    } else if (cmd == GPIO_GET_LINEEVENT_IOCTL) {
    } else if (cmd == GPIO_SET_PIN_CONFIG_IOCTL) {
    } else if (cmd == GPIO_SET_PIN_ACCESS_CONTROL_ENABLED) {
    } else if (cmd == GPIO_SET_PIN_ACCESS_CONTROL_USER) {
    } else if (cmd == GPIO_GET_PIN_ACCESS_CONTROL_USER) {
    } else if (cmd == GPIO_GET_LINEINFO_IOCTL || cmd == GPIO_GET_LINEINFO_WATCH_IOCTL) {
    } else if (cmd == GPIO_V2_GET_LINEINFO_IOCTL || cmd == GPIO_V2_GET_LINEINFO_WATCH_IOCTL) {
    } else if (cmd == GPIO_V2_GET_LINE_IOCTL) {
    } else if (cmd == GPIO_GET_LINEINFO_UNWATCH_IOCTL) {
 }

Of interest for the current advisory is the GPIO_SET_PIN_CONFIG_IOCTL ioctl, which runs the following code directly inside gpio_ioctl:

struct gpiopin_request {
    __u32 linecount;
    __u32 lineoffsets[GPIOHANDLES_MAX];
    __u32 config_type;
    __u32 config_arg;
};

static long gpio_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
    struct gpio_chardev_data *cdev = file->private_data;
    struct gpio_device *gdev = cdev->gdev;
    void __user *ip = (void __user *)arg;
    
    ...

    } else if (cmd == GPIO_SET_PIN_CONFIG_IOCTL) {
        struct gpiopin_request pinrequest;
        int ret, i;
        u32 config;

        if (copy_from_user(&pinrequest, ip, sizeof(pinrequest))) {
            return -EFAULT;
        }
        if ((pinrequest.linecount == 0) || (pinrequest.linecount > GPIOHANDLES_MAX)) {              // [1]
            return -EINVAL; 
        }

        if ((pinconf_to_config_param(pinrequest.config_type) != PIN_CONFIG_DRIVE_STRENGTH)
            && (pinconf_to_config_param(pinrequest.config_type) != PIN_CONFIG_DRIVE_STRENGTH_UA)) {
            return -EINVAL;
        }

        for (i = 0; i < pinrequest.linecount; i++) { 
            #ifdef CONFIG_GPIOLIB_PIN_ACCESS_CONTROL
            struct gpio_desc *desc;
            if (gdev->chip->enable_pin_access_control) {
                desc = &gdev->descs[pinrequest.lineoffsets[i]];                                     // [2]
                if (!capable(CAP_SYS_ADMIN) && !uid_eq(desc->allowed_user, current->cred->uid)) {   // [3]
                    return -EACCES;
                }
            }
            #endif

            config = pinrequest.config_type + (pinrequest.config_arg << 8);
            ret = gpiochip_generic_config(gdev->chip, pinrequest.lineoffsets[i], config);           // [4]
            if (ret) {
                return ret;
            }
        }

        return 0;
    }

To cut to the chase, since it’s a rather simple bug, while there is a check on the amount of lines/pins that we’re requesting to set at [1], there’s no checks on the individual values of the lineoffsets that we pass in [2]. Thus, when we start dereferencing array indexes at [2], while the i variable will be valid (0x1-0x40), our controlled value of pinrequest.lineoffsets[i] is never checked, thus resulting in an equivalent line of desc = &gdev->descs[<value_we_control>], letting us cast a struct gpio_desc anywhere in memory.
While this primitive initially seemed really powerful, the struct gpio_desc desc object isn’t really used for much, isn’t written to, and has no function pointers, it’s only read from at [3]. Readers might also notice that we also control the second and third argument of gpiochip_generic_config at [4], however we did not find anything useful in there, since that function will error out during gpio pin ranges checks.

Thus we’re left with the situation of “How can we make this bug useful?” when we only really have three lines of code that we affect.

if (!capable(CAP_SYS_ADMIN) && !uid_eq(desc->allowed_user, current->cred->uid)) { // [1]
   return -EACCES;
}

The check at [1] can be used as an oracle to see whether or not a given address is equal to our UID (32bit check). If it isn’t, we get EACCES from our ioctl, otherwise a different error. With this, we’ve come up with two scenarios in which this vulnerability can actually be utilized by an attacker.
First, it’s possible to leak the address of the gdev->descs array by having a repeating set of dwords set to your UID at a known address, and then incrementing the infoleak’s address until it finds a consecutive set of your UID. Alternatively, it’s also possible to decrement the lineoffset (so using negative values) until the device will read out-of-bounds and reboot. This would tell us that we reached just below 0xc0000000, which is the start of the lowmem section. Note that while the gdev->descs array is at a fixed spot in kernel memory and doesn’t change between reboots, it does change between Azure Sphere versions and is really difficult to find without this bug.
The second situation is a lot more useful for attackers: leaking your process’ struct cred structure address.

[o.o]> ptype struct cred
type = struct cred {
    atomic_t usage;
    kuid_t uid;
    kgid_t gid;
    kuid_t suid;
    kgid_t sgid;
    kuid_t euid;
    kgid_t egid;
    kuid_t fsuid;
    kgid_t fsgid;
    unsigned int securebits;
    kernel_cap_t cap_inheritable;
    kernel_cap_t cap_permitted;
    kernel_cap_t cap_effective;
    kernel_cap_t cap_bset;
    kernel_cap_t cap_ambient;
    void *security;
    user_struct *user;
    user_namespace *user_ns;
    group_info *group_info;
    union {
        int non_rcu;
        callback_head rcu;
    };
}

Since the cred structure is in the heap and contains your process’ uid, gid, suid, sgid, euid, egid, fsuid, and fsgid, all of which are the same, it’s enough to use the oracle above to look for 32 contiguous bytes (8 fields) that will give us an error different from EACCESS. Since we know the address of the gdev->descs structure, this will tell us the exact offset of our cred structure. Note that the address to this structure will change across reboots, unlike the gdev->descs one.

Note that, the desc->allowed_user dereference at [1] will not allow to directly read sequentially using incrementing values of lineoffsets. This is because the struct gpio_desc that is the type for the gdev->descs array, is 20 bytes long and only the last field is accessed:

struct gpio_desc {
    struct gpio_device  *gdev;
    unsigned long       flags;
    const char          *label;
    const char          *name;
#ifdef CONFIG_GPIOLIB_PIN_ACCESS_CONTROL
    kuid_t              allowed_user;
#endif
};

Because the gdev->descs objects are 20 bytes in size, and also because we’re only reading four bytes from the allowed_user field, the read operation is as follows:

gdev->descs + 20 * offset + 16

In order to read any dword sequentially at an arbitrary address, we can exploit integer wraparounds and iterate over all possible dword addresses, for example, assuming that gdev->descs + 16 is at 0xc0000000:

>>> for i in range(10):
...     offset = 0xccccccd * i
...     print(hex((0xc0000000 + offset * 20) & 0xffffffff))
...
0xc0000000
0xc0000004
0xc0000008
0xc000000c
0xc0000010
0xc0000014
0xc0000018
0xc000001c
0xc0000020
0xc0000024

While iterating this way in an ioctl call loop, we just need to keep track of the offset value to figure out the exact cred structure address, once the ioctl returns an error different from EACCESS for 8 consecutive times.

Timeline

2021-07-08 - Vendor Disclosure
2021-11-09 - Vendor Patch
2021-11-09 - Public Release

Credit

Discovered by Claudio Bozzato and Lilith >_> of Cisco Talos.