Talos Vulnerability Report


Microsoft Azure Sphere Pluton SIGN_WITH_TENANT_ATTESTATION_KEY memory corruption vulnerability

September 23, 2020
CVE Number



A memory corruption vulnerability exists in the Pluton SIGN_WITH_TENANT_ATTESTATION_KEY functionality of Microsoft Azure Sphere 20.07. A sequence of specially crafted ioctl calls can cause memory corruption in Pluton. An attacker can issue an ioctl from the Normal World 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 20.07


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


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


CWE-119 - Improper Restriction of Operations within the Bounds of a Memory Buffer


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 /dev/pluton kernel driver facilitates communication between the normal Linux kernel running on Cortex A7 and the Pluton subsystem on the Cortex M4, which is accessible by any user on the system. It implements very few functions (open, read, poll, close, and ioctl), but provides a large amount of functionality through its two ioctl:

    _IOWR('p', 0x0A, struct azure_sphere_ecdsa_signature)
#define PLUTON_SYSCALL _IOWR('p', 0x20, struct azure_sphere_syscall)

long pluton_ioctl(struct file *filp, unsigned int cmd, unsigned long arg_)
    void __user *arg = (void __user *)arg_;

    switch (cmd) {
        return pluton_syscall(arg);
        return pluton_sign_with_tenant_attestation_key(arg);
        return -EINVAL;

Out of these pluton ioctls, the PLUTON_SYSCALL ioctl has a lot more functionality under the hood, however both ioctls are essentially wrappers for the code path we list below:

struct azure_sphere_ecdsa_signature {                          // [1]
    size_t size;
    uint8_t R[48];
    uint8_t S[48];

struct azure_sphere_digest {                                   // [2]
    uint32_t type;
    size_t size;
    uint8_t data[64];

int pluton_sign_with_tenant_attestation_key(void __user *arg) {
    u32 ret = 0;
    struct azure_sphere_syscall args = {0};
    struct azure_sphere_task_cred *tsec;
    struct azure_sphere_tenant_id tenant_id;
    struct azure_sphere_digest digest;
    struct azure_sphere_ecdsa_signature signature;

    // no runtime permission check

    ret = copy_from_user(&digest, arg, sizeof(digest));        // [3]
    if (unlikely(ret)) {
        return ret;

    // copy out the tenant id
    tsec = current->cred->security;
    memcpy(&tenant_id, tsec->daa_tenant_id, sizeof(tenant_id));

    args.number = PlutonSyscallSignWithTenantKey;              // [4]
    args.flags = MakeFlagsForArg(0, Input | Reference)         // [5]
        | MakeFlagsForArg(1, Input)
        | MakeFlagsForArg(2, Input | Reference)
        | MakeFlagsForArg(3, Input)
        | MakeFlagsForArg(4, Output | Reference)
        | MakeFlagsForArg(5, Input);
    args.args[0] = (uintptr_t)&tenant_id;                      // [6]
    args.args[1] = sizeof(tenant_id);
    args.args[2] = (uintptr_t)&digest;
    args.args[3] = sizeof(digest);
    args.args[4] = (uintptr_t)&signature;
    args.args[5] = sizeof(signature);

    ret = azure_sphere_pluton_execute_syscall(&args, false);   // [7]

    // no data sent back on err
    if (!ret) {
        ret = copy_to_user(arg, &signature, sizeof(signature)); 

    return ret;

Breaking this down, [1] and [2] are structures we’ll refer to later, and at [3] we copy the ioctl user data into a azure_sphere_digest object [2]. It’s worth noting that while the ioctl definition says we need a azure_sphere_ecdsa_signature, the input is treated like an azure_sphere_digest object while the output is treated like an azure_sphere_ecdsa_signature. Regardless, at [4] we can see the assignment of a syscall number to the azure_sphere_syscall object along with some argument flags [5] and arguments [6]. The actual details of this are more important for the PLUTON_SYSCALL ioctl since the user making the request has to assign all of it correctly, but for pluton_sign_with_tenant_attestation_key we need to only provide an azure_sphere_digest object.
Continuing on in azure_sphere_pluton_execute_syscall[7]:

int azure_sphere_pluton_execute_syscall(struct azure_sphere_syscall *syscall, bool from_user)
    return azure_sphere_execute_syscall(syscall, from_user, false);

int azure_sphere_sm_execute_syscall(struct azure_sphere_syscall *syscall, bool from_user)
    return azure_sphere_execute_syscall(syscall, from_user, true);

As a quick side note, both the pluton and security-monitor ioctls end up hitting azure_sphere_execute_syscall, with the difference being that the pluton ioctls are under more scrutiny, but we’ll see this later. Continuing on into azure_sphere_execute_syscall:

int azure_sphere_execute_syscall(struct azure_sphere_syscall *syscall, bool from_user, bool security_monitor)
// [...]

    // invoke the call
    if (security_monitor) {
        arm_smccc_smc(SECURITY_MONITOR_FUNCTION(SECURITY_MONITOR_API_SYNC, translated_args.number), translated_args.args[0],
            translated_args.args[1], translated_args.args[2], translated_args.args[3], translated_args.args[4],
            translated_args.args[5], 0, &res);
        result = res.a0;
    } else {
        result = pluton_remote_api_send((uintptr_t)params.coherent_memory_addr);   // [1]

Admittedly there’s a lot of code being skipped in this function, but for our purposes we don’t really care since we know the SIGN_WITH_TENANT_KEY request is happening since it’s returning as expected.
Back on track, at [1] the pluton-specific code path happens, and the processed azure_sphere_digest object is sent over the linux mbox api to the pluton processor. Since we do not have debugging capabilities on the Pluton M4, we have to settle for heuristics and describing some interesting behavior. To summarize, if one sends an azure_sphere_digest object with a length of 0x1de1, the device panics and reboots:

[^_^] Fuzzer sock created...
[>_>] pluton: 4
[>_>] PLUTON_SYSCALL ioctl: 0xc0207020
[z.z] Waiting for fuzzer connection...

digest object...
0x00 0x00 0x00 0x00 
0xe0 0x1d 0x00 0x00 
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 [...]

[^_^] (0) SIGN Ioctl (Ret: 0)
  => azure_sphere_ecdsa_signature(size:0x20)
0x84 0x81 0xc8 0x6d 0x41 0xfa 0x57 0x07
0x67 0x58 0xf3 0x06 0x75 0x5a 0x7e 0x7a

0xe2 0x61 0x9c 0xbc 0xbb 0x85 0x27 0x41
0xf3 0x3e 0xbc 0xd6 0x06 0xaf 0x80 0x1e

digest object...
0x00 0x00 0x00 0x00 
0xe0 0x1d 0x00 0x00 
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 [...] 

[^_^] (1) SIGN Ioctl (Ret: 0)
  => azure_sphere_ecdsa_signature(size:0x20)
0x0f 0x10 0xfb 0x43 0xd1 0x8e 0x6d 0xff
0x9e 0xd3 0x45 0xf9 0x0d 0x32 0x33 0x82

0x1d 0x60 0x94 0xca 0x1c 0xa9 0x33 0xde
0x4a 0x80 0x99 0x9b 0x01 0x55 0x42 0x27

Ncat: connection reset by peer

If we watch the UART logs during this process:

!!! PANIC: C: 2807831058 L: 1765716560 A: 8�[1BL] BOOT: 70e00000/00000004/01010000
[PLUTON] Logging initialized
[PLUTON] Booting HLOS core

Investigating further from the C: 2807831058 L: 1765716560, searching for those constants within the Pluton binary itself we get to here:

0010c160  019a       ldr     r2, [sp, #4] {var_b4}
0010c162  1e49       ldr     r1, [pc, #0x78]  {data_10c1dc}  {0x693eb250} // 2807831058
0010c164  1e48       ldr     r0, [pc, #0x78]  {data_10c1e0}  {0xa75c1a12} // 1765716560
0010c166  c4e7       b       #0x10c0f2
0010c0f2  fcf7f5fd   bl      #do_panic
{ Does not return }

This sets us up to find the crash location rather quickly, as it conveniently lies within the Pluton syscall table:

0010dc64  struct ioctl_entry data_10dc64 =
0010dc64  {
0010dc64      uint32_t syscall_num = 0x1c
0010dc68      uint8_t argument_flags[4] =
0010dc68      {
0010dc68          [0x0] = 0x46
0010dc69          [0x1] = 0x46
0010dc6a          [0x2] = 0x4a
0010dc6b          [0x3] = 0x0
0010dc6c      }
0010dc6c      uint32_t* func_handler_ = SIGN_WITH_TENANT_ATTESTATION_KEY
0010dc70      void* func_handler_2 = sub_10966a
0010dc74  }

Looking inside SIGN_WITH_TENANT_ATTESTATION_KEY, we see a suspect memcpy():

0010c106  2e46       mov     r6, r5  {0x2f0201c0} // [1]

0010c10c  7a68       ldr     r2, [r7, #4] // digest->size
0010c10e  07f10801   add     r1, r7, #8   // digest->data
0010c112  3046       mov     r0, r6       // destination
0010c114  fcf73bf9   bl      #memcpy

At [1] we see the destination we are copying to (presumably a shared memory buffer between Pluton and the crypto hardware), but the important thing is that r7 directly references our azure_sphere_digest struct, which if we recall looks like such:

    struct azure_sphere_digest {  // [2]
        uint32_t type;
        size_t size;
        uint8_t data[64];

As such, Pluton is copying a buffer that we control, of length that we control, without any length checks. Heuristically, 0x1de1 seems to be the crashing length. Because of this out-of-bounds write, an attacker could, with careful control of the size and contents of the buffer, manage to corrupt memory in Pluton, and, depending on the way memory is mapped in Pluton’s processor, potentially execute arbitrary code.
However, due to time constraints and lack of debugging equipment, we regretfully were not able to investigate more before submitting this issue, we thought it enough to prove memory corruption.


2020-08-06 - Vendor Disclosure
2020-10-06 - Public Release


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