None
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.
The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.
Microsoft Azure Sphere 21.06
Azure Sphere - https://azure.microsoft.com/en-us/services/azure-sphere/
4.4 - CVSS:3.0/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:N
CWE-196 - Unsigned to Signed Conversion Error
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.
2021-07-09 - Vendor Disclosure
2021-11-09 - Vendor Patch
2021-11-09 - Public Release
Discovered by Lilith >_> and Claudio Bozzato of Cisco Talos.