CVE-2020-16987
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.
Microsoft Azure Sphere 20.07
https://azure.microsoft.com/en-us/services/azure-sphere/
6.2 - CVSS:3.0/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N
CWE-284 - Improper Access Control
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.
2020-08-10 - Vendor Disclosure
2020-08-24 - Public Release
Discovered by Lilith >_>, Claudio Bozzato and Dave McDaniel of Cisco Talos.