Talos Vulnerability Report

TALOS-2021-1340

Microsoft Azure Sphere Kernel GPIO_GET_PIN_ACCESS_CONTROL_USER information disclosure vulnerability

November 9, 2021
CVE Number

CVE-2021-41375

Summary

An information disclosure vulnerability exists in the GPIO_GET_PIN_ACCESS_CONTROL_USER functionality of Microsoft Azure Sphere 21.06. A specially crafted ioctl can lead to a 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

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

CWE

CWE-196 - Unsigned to Signed Conversion Error

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_GET_PIN_ACCESS_CONTROL_USER ioctl, which, as one might guess from the title, calls gpio_get_access_control(gdev, ip);, with gdev as the gpio chip device, and ip as our ioctl’s argument. Also worth noting is that this specific ioctl was added by Azure Sphere and is not in the normal Linux kernel. To proceed, gpio_get_access_control():

 int gpio_get_access_control(struct gpio_device *gdev, void __user *ip) {

    struct gpioaccess_status status;
    int i = 0;
    int offset = 0; 

    if (!capable(CAP_SYS_ADMIN))  // [1]
        return -EACCES;

We must first note the CAP_SYS_ADMIN check at [1], meaning this vulnerability essentially requires high privileged access in the first place. This level of privilege is normally root-equivalent in Linux systems, but due to the architecture of the Azure Sphere (e.g. it’s not possible to insert modules at runtime, and there being higher privileges than root (i.e. AZURE_SPHERE_CAPABILITIES)), there is a defacto security boundary that must still be traversed by attackers even if that capability is owned, which makes this a legitimate vulnerability in Azure’s context. Continuing on with this in mind:

#define GPIOHANDLES_MAX 64

struct gpioaccess_status {
   __u32 linecount;
   __u32 lineoffsets[GPIOHANDLES_MAX];
   __u32 uid[GPIOHANDLES_MAX];
 };

 int gpio_get_access_control(struct gpio_device *gdev, void __user *ip) {
    struct gpioaccess_status status;
    int i = 0;
    int offset = 0; 

    if (!capable(CAP_SYS_ADMIN)) 
        return -EACCES;

    if (copy_from_user(&status, ip, sizeof(status)))
        return -EFAULT;

    if (status.linecount >= gdev->ngpio || status.linecount >= GPIOHANDLES_MAX) // [1]
        return -EINVAL;

    for(i=0; i<status.linecount; ++i) {
        offset = status.lineoffsets[i];
        if (offset >= gdev->ngpio) {   // [2]
            return -EINVAL;
        }
        status.uid[i] = from_kuid(current_user_ns(), gdev->descs[offset].allowed_user); // [3]
     }

     if (copy_to_user(ip, &status, sizeof(status))) // [4]
         return -EFAULT;

    return 0;
}

The general idea of the function is to see which Linux UIDs can access which pins by passing in a structure with a list of pins (gpioaccess_status.lineoffsets). The requested amount of pins is checked against the total amount of pins at [1] to prevent overflows, and then for each pin, another check is done at [2] to make sure that the pin number is not bigger than the total amount of pins as well. Assuming we’re requesting a valid pin number, the pin’s assigned UID is grabbed at [3], and if all the pins are valid, this UID information is copied back to userland at [4].

A quick detour before we come back to the gpio_get_access_control function, here’s what the struct gpio_device gdev looks like:

struct gpio_device {
    int                 id;
    struct device       dev;
    struct cdev         chrdev;
    struct device       *mockdev;
    struct module       *owner;
    struct gpio_chip    *chip;
    struct gpio_desc    *descs;
    int                 base;
    u16                 ngpio;      // [1]
    const char          *label;
    void                *data;
    struct list_head    list;
    struct blocking_notifier_head notifier;

#ifdef CONFIG_PINCTRL
    /*
     * If CONFIG_PINCTRL is enabled, then gpio controllers can optionally
     * describe the actual pin range which they serve in an SoC. This
     * information would be used by pinctrl subsystem to configure
     * corresponding pins for gpio usage.
     */
    struct list_head pin_ranges;
#endif
};

Squirrel away into your mind the fact that the ngpio field is a uint16_t [1] and let’s go back:

 int gpio_get_access_control(struct gpio_device *gdev, void __user *ip) {
    struct gpioaccess_status status;
    int i = 0;
    int offset = 0; // [1]

    if (!capable(CAP_SYS_ADMIN)) 
        return -EACCES;

    if (copy_from_user(&status, ip, sizeof(status)))
        return -EFAULT;

    if (status.linecount >= gdev->ngpio || status.linecount >= GPIOHANDLES_MAX)
        return -EINVAL;

    for(i=0; i<status.linecount; ++i) {
        offset = status.lineoffsets[i];
        if (offset >= gdev->ngpio) {   // [2]
            return -EINVAL;
        }
        status.uid[i] = from_kuid(current_user_ns(), gdev->descs[offset].allowed_user); // [3]
     }

     if (copy_to_user(ip, &status, sizeof(status))) 
         return -EFAULT;

    return 0;
}

Note that the offset variable at [1] is a signed integer (32 bit). As such, and also because we remember that gdev->ngpio is a uint16_t, we can verbosely say that the check at [2] is comparing a int32_t with a uint16_t. For such comparison, the uint16_t is promoted to int, as stated in C11 6.3.1.1 rules:

If an int can represent all values of the original type (as restricted by the width, for a bit-field), the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions. … The integer promotions preserve value including sign.

This makes the check at [2] signed. We can verify this also by examining the disassembly:

// in `gpio_get_access_control`
c026d264  57f8043f   ldr     r3, [r7, #4]!
c026d268  b5f8f021   ldrh    r2, [r5, #0x1f0]
c026d26c  9a42       cmp     r2, r3       // if offset >= gdev->ngpio
c026d26e  eedd       ble     #0xc026d24e  // [1]

As shown by [1], the resulting instruction for the comparison is ble, which is a signed comparison. Thus, if we pass a pin offset that’s greater than 0x7FFFFFFF, it’s a negative number, which passes the if offset >= gdev->ngpio check. To proceed:

    for(i=0; i<status.linecount; ++i) {
        offset = status.lineoffsets[i];
        if (offset >= gdev->ngpio) {   // [1]
            return -EINVAL;
        }
        status.uid[i] = from_kuid(current_user_ns(), gdev->descs[offset].allowed_user); // [2]
     }

     if (copy_to_user(ip, &status, sizeof(status)))  // [3]
         return -EFAULT;

    return 0;
}

At [1] our signedness issue allows us to bypass this check, causing a read to occur at [2] with an offset that’s absurdly large.

Let us look at struct gpio_desc that is the type for the gdev->descs array:

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 (into status.uid[i]), at first glance it might be assumed that we can only read every 20th dword, but this is not necessarily correct. In practice, the read operation at [2] 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, we can generate offsets for sequential reads as follows:

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

This results in an attacker being able to leak arbitrary kernel memory via one ioctl call.

Timeline

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

Credit

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