Talos Vulnerability Report

TALOS-2020-1138

Microsoft Azure Sphere Normal World application /proc/thread-self/mem unsigned code execution vulnerability

August 24, 2020
CVE Number

CVE-2020-16987

Summary

A code execution vulnerability exists in the normal world’s signed code execution functionality of Microsoft Azure Sphere 20.07. A specially crafted shellcode can cause a process’ non-writable memory to be written. An attacker can execute a shellcode that modifies the program at runtime via /proc/thread-self/mem to trigger this vulnerability.

Tested Versions

Microsoft Azure Sphere 20.07

Product URLs

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

CVSSv3 Score

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

CWE

CWE-284 - Improper Access Control

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.

For the purposes of this writeup, we focus upon the Azure Sphere Normal World’s innate memory protection: memory that has ever been marked as writable cannot be marked as executable, likewise memory that has been marked executable cannot be marked as writable. This is also discussed in one of Azure Sphere’s presentations.
To illustrate:

[o.o]> call (int *)malloc(0x1000)
$3 = (int *) 0xbeeff010

[~.~]> !addr $3
0xbeeff010('$3') => 0xbeeff000 0xbef03000 0x4000 0x0 rw-p [heap]

[o.o]> call (int)mprotect($3, 0x1000, 0x5)
$13 = -1

Likewise, if we do something similar with mmap and mprotect, the same situation occurs:

unsigned char *addr = mmap(0x0, 0x1000,
               PROT_WRITE,
               MAP_ANONYMOUS | MAP_PRIVATE, -1,0);
Log_Debug("[^_^] mmap(WRITE) addr => 0x%lx\n",addr);

ret = mprotect(addr,0x1000,PROT_EXEC|PROT_READ);
Log_Debug("[?.?] mprotect(PROT_EXEC|PROT_READ); %d\n",ret);

ret = mprotect(addr,0x1000,PROT_READ);
Log_Debug("[?.?] mprotect(PROT_READ): %d\n",ret);

We are left with the following output:

[^_^] mmap(WRITE) addr => 0xbeefc000
[?.?] mprotect(PROT_EXEC|PROT_READ); -1
[?.?] mprotect(PROT_READ): 0

This is a feature included into the Azure Sphere Linux kernel, so regardless of the method of mapping, the results end up the same. Thus, being able to write to and then execute memory inside a given process is actually a non-trivial endevour.
It’s also worth noting that one cannot write to flash memory in order to store shellcode, due to the only flash memory available (/mnt/config) being heavily restricted. We also cannot write to the application’s filesystem that gets mounted in order to run, since the asxipfs filesystem (a fork of cramfs) is strictly read-only.

A quick note: for the purposes of the Azure Sphere Security Research Challenge, the attack surface provided is essentially: “A given application has been compromised, what could be done from there?”.

A previous issue, TALOS-2020-1093, dealt with the writeability of /proc/self/mem, which could lead to the .text section of a given binary to be overwritten in memory and then subsequently executed. As a result of that vulnerability, the following patch was put in place in version 20.07 to fix the issue:

diff --git a/fs/proc/base.c b/fs/proc/base.c
index ebea9501afb8..987d7f107e73 100644
--- a/fs/proc/base.c
+++ b/fs/proc/base.c
@@ -3027,7 +3027,7 @@ static const struct pid_entry tgid_base_stuff[] = {
 #ifdef CONFIG_NUMA
    REG("numa_maps",  S_IRUGO, proc_pid_numa_maps_operations),
 #endif
-   REG("mem",        S_IRUSR|S_IWUSR, proc_mem_operations),
+   REG("mem",        S_IRUSR, proc_mem_operations),

Thus, if we look at /proc/self/mem, we can see:

/ # ls -l /proc/$$/mem
-r--------    1 root     root             0 Mar 29  1935 /proc/84/mem

But /proc/$$/mem is not the only place we can see these proc_mem_operations from above, in grepping for proc_mem_operations in fs/proc of the kernel source, we also see this:

base.c:static const struct file_operations proc_mem_operations = {
base.c: REG("mem",        S_IRUSR, proc_mem_operations),
base.c: REG("mem",       S_IRUSR|S_IWUSR, proc_mem_operations),

And if we go to look in base.c where this second reference to the proc_mem_operations are at, we find the following structure:

/*
 * Tasks
 */
static const struct pid_entry tid_base_stuff[] = {
    DIR("fd",        S_IRUSR|S_IXUSR, proc_fd_inode_operations, proc_fd_operations),
    DIR("fdinfo",    S_IRUSR|S_IXUSR, proc_fdinfo_inode_operations, proc_fdinfo_operations),
    ...
    REG("cmdline",   S_IRUGO, proc_pid_cmdline_ops),
    ...
    REG("maps",      S_IRUGO, proc_pid_maps_operations),
    ...
    REG("mem",       S_IRUSR|S_IWUSR, proc_mem_operations),      // [1]

This tid_base_stuff struct is created for each thread-id in the thread group that a given pid belongs to. By default, there will always be one thread entry in this /proc/$$/task directory that matches the pid. As we can see at [1] the “mem” path for the tasks still sets write permissions. In fact, looking at the task directory we can see the following:

/ # ls -l /proc/$$/mem
-r--------    1 root     root             0 Mar 29  1935 /proc/84/mem
/ # ls -l /proc/$$/task/$$/mem
-rw-------    1 root     root             0 Apr  2  1935 /proc/84/task/84/mem

In order to write our arbitrary shellcode to memory with this file, the pseudo-code this would be as such:

int fd = open("/proc/thread-self/mem", O_WRONLY);
lseek(fd, func, 0);                        // alignment
write(fd, shellcode, shellcode_len);       // overwrite the function "func"

This sequence of commands overwrites the function pointed by func with an arbitrary shellcode, and could be used by an attacker to run unsigned code, after compromising an application.
Finally note that since the scope of this issue is within an already compromised application, the pseudo-code above would have to be implemented via ROP gadgets.

Timeline

2020-08-10 - Vendor Disclosure

2020-08-24 - Public Release

Credit

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