None
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:
#define PLUTON_SIGN_WITH_TENANT_ATTESTATION_KEY \
_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) {
case PLUTON_SYSCALL:
return pluton_syscall(arg);
case PLUTON_SIGN_WITH_TENANT_ATTESTATION_KEY:
return pluton_sign_with_tenant_attestation_key(arg);
default:
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
[>_>] PLUTON_SIGN_WITH_TENANT_ATTESTATION_KEY ioctl: 0xc064700a
[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
G=[...]
D=[...]
[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.