Talos Vulnerability Report

TALOS-2021-1309

Microsoft Azure Sphere Security Monitor SMSyscallPeripheralAcquire information disclosure vulnerability

September 14, 2021
CVE Number

None

SUMMARY

An information disclosure vulnerability exists in the Security Monitor SMSyscallPeripheralAcquire functionality of Microsoft Azure Sphere 21.01. A specially crafted syscall can lead to leaking heap data. An attacker can use an SMC syscall to trigger this vulnerability.

CONFIRMED VULNERABLE VERSIONS

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.01

PRODUCT URLS

Azure Sphere - 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-457 - Use of Uninitialized Variable

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.

Processes with AZURE_SPHERE_CAP_* are allowed to interact with Pluton and Security Monitor, but only via the syscalls that they are allowed to access. For instance, when a user holds the AZURE_SPHERE_CAP_PERIPHERAL_PIN_MAPPING capability, they are allowed to use the following Secmon syscalls:

{.number = SMSyscallStartRtCoreByComponentId, .caps = AZURE_SPHERE_CAP_PERIPHERAL_PIN_MAPPING, .linux_caps = 0},
{.number = SMSyscallStopRtCore, .caps = AZURE_SPHERE_CAP_PERIPHERAL_PIN_MAPPING, .linux_caps = 0},
{.number = SMSyscallSetRtCoreCommunicationBuffer, .caps = AZURE_SPHERE_CAP_PERIPHERAL_PIN_MAPPING, .linux_caps = 0},
{.number = SMSyscallPeripheralAcquire, .caps = AZURE_SPHERE_CAP_PERIPHERAL_PIN_MAPPING, .linux_caps = 0},
{.number = SMSyscallPeripheralRelease, .caps = AZURE_SPHERE_CAP_PERIPHERAL_PIN_MAPPING, .linux_caps = 0},
{.number = SMSyscallPeripheralGetAvailableDomains, .caps = AZURE_SPHERE_CAP_PERIPHERAL_PIN_MAPPING, .linux_caps = 0},
{.number = SMSyscallPeripheralLockConfig, .caps = AZURE_SPHERE_CAP_PERIPHERAL_PIN_MAPPING, .linux_caps = 0},

For our present advisory we deal with the SMSyscallPeripheralAcquire syscall, but it’s important to stress that an attacker would already needed to have elevated privileges or gained AZURE_SPHERE_CAP_PERIPHERAL_PIN_MAPPING.

To start, let us examine the parameters that SMSyscallPeripheralAcquire requires:

struct azure_sphere_syscall syscall = {};

syscall.number = SMSyscallPeripheralAcquire;
syscall.flags = 0x4945;
syscall.args[0] = inpbuf;               // inpbuf  [0]
syscall.args[1] = 0x400;                // insize  [1]
syscall.args[2] = outbuf;               // outbuf  [2]
syscall.args[3] = 0x400;                // outsize [3]

The input buffer [0] must point to valid userspace memory, and the input buffer size [1] must be greater than or equal to 0x8. The output buffer [2] and the output buffer size [3] follow the same requirements as the input buffer and input size. It’s also worth noting that insize + outsize + sizeof(azure_sphere_syscall) must be less than 0x1060, but that’s more a generic Security Monitor syscall aspect. Let us now examine our best approximation of the inpbuf and outbuf structures, starting with inpbuf:

struct PeriphAcquireInput{
    uint32_t owner_id;                      // [0]
    uint32_t desired_pin_mode;              // [1]
    uint32_t periph_pin_id[(insize-8)/4];   // [2]
}

The owner_id member is just an arbitrary identifier that aids in locking peripherals from being changed, while the much more important desired_pin_mode [1] lets Security Monitor know which mux mode to change a given pin to. Finally, the array of periph_pin_id lays out which pins exactly need to be configured. So, to summarize, if one wanted to change pin GPIO4 and GPIO5 from GPIO to PWM, one could send as an inpbuf: 0x01010101 0x00000001 0x00010004 0x00010005. The 0x00000001 says “pwm mode”, and the 0x00010004 0x00010005 says “GPIO pin 4 and GPIO pin 5”. Also worth noting, the individual pin modes differ for each pin. Now onto the outbuf structure:

struct PeriphAcquireOutput{
    uint32_t input_entries_processed;       // [0]
    uint32_t processed_entries_in_outbuf;   // [1]
    struct PeriphAcquireOutputEntry[output_entries_in_outbuf]; // size 0x10 each
}

struct PeriphAcquireOutputEntry{
    uint32_t periph_pin_id;  // [2]
    uint16_t holds_0x4;      // [3]
    uint16_t holds_???;      // [4]
    uint32_t zeroed_0;
    uint32_t zeroed_1;
 
}

At [0], the amount of input entries processed is listed, and at [1] we see the amount of processed entries that could fit in the output buffer. Following that, we see a list of output entries. We’ve only observed the first 8 bytes of each of these entries ever being filled, the first dword [2] being the given input periph_pin_id that was processed. The next hword [3] always seems to contain 0x0004, and the last eight bytes always seem to be zeroed out, but the hword at [4] is of most interest to us. To demonstrate, a sample input buffer:

[T_T] Before sorting:
0x00000000: 0x00000001 0xabcd0002 0x00010000 0x0000ff83 | ....
0x00000010: 0x0000ff06 0x0000fe89 0x0000fe0c 0x0000fd8f | ....
0x00000020: 0x0000fd12 0x0000fc95 0x0000fc18 0x0000fb9b | ....
0x00000030: 0x0000fb1e 0x0000faa1 0x0000fa24 0x0000f9a7 | ..$.
0x00000040: 0x0000f92a 0x0000f8ad 0x0000f830 0x00000001 | *.0.

With its corresponding output buffer:

[^_^] PeriphAcquire=> i:0x51 t:0x4c
0x00000000: 0x00000011 0x00000011 0x00000001 0x803e0004 | ....
0x00000010: 0x00000000 0x00000000 0x0000f830 0x803e0004 | ..0.
0x00000020: 0x00000000 0x00000000 0x0000f8ad 0x803e0004 | ....
0x00000030: 0x00000000 0x00000000 0x0000f92a 0x803e0004 | ..*.
0x00000040: 0x00000000 0x00000000 0x0000f9a7 0x00000004 | ....
0x00000050: 0x00000000 0x00000000 0x0000fa24 0x251c0004 | ..$.
0x00000060: 0x00000000 0x00000000 0x0000faa1 0x7eb30004 | ....
0x00000070: 0x00000000 0x00000000 0x0000fb1e 0x00000004 | ....
0x00000080: 0x00000000 0x00000000 0x0000fb9b 0x00000004 | ....
0x00000090: 0x00000000 0x00000000 0x0000fc18 0x96e20004 | ....
0x000000a0: 0x00000000 0x00000000 0x0000fc95 0x00000004 | ....
0x000000b0: 0x00000000 0x00000000 0x0000fd12 0x425a0004 | ....
0x000000c0: 0x00000000 0x00000000 0x0000fd8f 0x00000004 | ....
0x000000d0: 0x00000000 0x00000000 0x0000fe0c 0x251c0004 | ....
0x000000e0: 0x00000000 0x00000000 0x0000fe89 0x471b0004 | ....
0x000000f0: 0x00000000 0x00000000 0x0000ff06 0x00000004 | ....
0x00000100: 0x00000000 0x00000000 0x0000ff83 0x951e0004 | ....

The last column corresponds to the holds_0x4 and holds_??? members of the PeriphAcquireOutputEntry structure, and as shown, the holds_??? member does indeed hold an unknown value. It’s worth noting that Security Monitor lies at 0x803d0000-0x803ec4a4 in memory, which lines up with the 0x803e.... entries we see within the list. Also, based on the fact that these two bytes can change in between syscalls and reboots, we believe this to be an information leak from Security Monitor’s heap. Furthermore, we can see a lack of initialization in the allocated structure from which this info leak comes from:

alloc_and_init_0x18_LL_obj:
803d0b60  f8b5       push    {r3, r4, r5, r6, r7, lr} {var_18} {var_4} {__saved_r7} {__saved_r6} {__saved_r5} {__saved_r4}
803d0b62  0446       mov     r4, r0
803d0b64  1820       movs    r0, #0x18
803d0b66  0d46       mov     r5, r1
803d0b68  1746       mov     r7, r2
803d0b6a  1e46       mov     r6, r3
803d0b6c  07f0d9fd   bl      #allocate_bytes(size)      // [0]
803d0b70  069b       ldr     r3, [sp, #0x18] {arg5} 
803d0b72  b5f800c0   ldrh    r12, [r5]
803d0b76  1b68       ldr     r3, [r3] 
803d0b78  6d88       ldrh    r5, [r5, #2]
803d0b7a  3988       ldrh    r1, [r7]
803d0b7c  3268       ldr     r2, [r6]
803d0b7e  4361       str     r3, [r0, #0x14] {periph_0x18_thing::another_periph_upper.d}
803d0b80  6368       ldr     r3, [r4, #4]
803d0b82  a0f808c0   strh    r12, [r0, #8] {periph_0x18_thing::periph_lower}
803d0b86  4360       str     r3, [r0, #4] {periph_0x18_thing::ll_prev}
803d0b88  6368       ldr     r3, [r4, #4]
803d0b8a  4581       strh    r5, [r0, #0xa] {periph_0x18_thing::periph_upper}
803d0b8c  0261       str     r2, [r0, #0x10] {periph_0x18_thing::periph_mode_copy}
803d0b8e  8181       strh    r1, [r0, #0xc] {periph_0x18_thing::some_hword}            // [1]
803d0b90  0460       str     r4, [r0] {periph_0x18_thing::muh_ll}
803d0b92  1860       str     r0, [r3]
803d0b94  a368       ldr     r3, [r4, #8]
803d0b96  6060       str     r0, [r4, #4]
803d0b98  0133       adds    r3, #1
803d0b9a  a360       str     r3, [r4, #8]
803d0b9c  f8bd       pop     {r3, r4, r5, r6, r7, pc} {var_18} {__saved_r4} {__saved_r5} {__saved_r6} {__saved_r7} {var_4}

In this function we only care about [0], where a size 0x18 object is allocated, and [1], where we see a strh occur on object+0xc that occurs without any corresponding strh on object+0xe. We can then clearly see when pieces of this object are copied into the syscall’s output buffer that this data gets thrown into PeriphAcquireOutputEntry->holds_??? at 0x803e36ea.

803e36d8  b442       cmp     r4, r6    // while not end_of_LL, copy output entries
803e36da  00f08b82   beq     #0x803e3bf4

803e36de  53f808cb   ldr     r12, [r3], #8 {periph_0x18_thing::muh_ll}  
803e36e2  06f10807   add     r7, r6, #8
803e36e6  1036       adds    r6, #0x10
803e36e8  0fcb       ldm     r3, {r0, r1, r2, r3}
803e36ea  87e80f00   stm     r7, {r0, r1, r2, r3} {periph_acquire_outbuf_entry::pin_periph_id} {periph_acquire_outbuf_entry::hword_leak} {periph_acquire_outbuf_entry::zeroed_0x8} {periph_acquire_outbuf_entry::zeroed_0xC}
803e36ee  6346       mov     r3, r12
803e36f0  f2e7       b       #0x803e36d8

Note that while we believe this vulnerability is also within 21.04, we have only tested it on 21.01.

TIMELINE

2021-06-08 - Vendor Disclosure
2021-09-14 - Public Release

Credit

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