The never ending problems of local ASLR holes in Linux

SHARE

Share on facebook
Share on twitter
Share on linkedin

Introduction

Address Space Layout Randomization, or simply ASLR, is a probabilistic security defense that was released by the PaX Team in 2001 and introduced into upstream kernels in 2005 (2.6.12). As the name itself indicates, it randomly arranges the address space (thus addresses) of a running executable every time it is run, and does this by applying randomization to the base address of mappings. ASLR’s goal is to make a class of exploit techniques fail when knowledge of addresses is required in order to exploit memory corruption vulnerabilities.

Memory corruption exploits used to rely on knowing and hard coding addresses in order for an attacker to return into executable instructions (introduced or already existing) to execute arbitrary code, or corrupt critical program data. ASLR was originally created to defend against remote attackers, because if an attacker even needs addresses, remote attacks give an attacker the least amount of a priori information.

A Little Bit About History

For local attackers, /proc/[pid]/ has always been problematic and a major source for generic information leaks in order to bypass ASLR for setuid binaries or root processes in general. In 2009, Tavis Ormandy and Julien Tinnes, from the Google Security Team at the time, gave a lightning talk at CanSecWest on Linux ASLR Curiosities (https://www.cr0.org/paper/to-jt-linux-alsr-leak.pdf), where they demonstrate that /proc/[pid]/stat and /proc/[pid]/wchan leaked information such as the process’ instruction pointer and stack pointer, which could be abused to reconstruct the address space layout of a process that they couldn’t ptrace() attach to the PID. Subsequently, such issues were supposedly fixed (https://lkml.org/lkml/2009/5/4/322).

On 3rd April 2019, 10 years later, an exploit was published (https://www.openwall.com/lists/oss-security/2019/04/03/4/1) that worked for Linux kernels below version 4.8 that used, again, /proc/pid/stat in order to obtain the previously mentioned (by Tavis and Julien) instruction pointer and stack pointer of setuid binaries. Because install_exec_creds() was called too late in load_elf_binary() in fs/binfmt_elf.c, this meant that an executable was mapped into its address space before its credentials were set and attackers could now pass the ptrace_may_access() check introduced as a fix to Tavis’ and Julien’s attack. This race condition could be run by attackers if they read() /proc/[pid]/stat before install_exec_creds() was called. This vulnerability is identified by CVE-2019-11190.

On 25th April 2019, a few days after CVE-2019-11190, a Security Engineer of SUSE Linux also posted on Openwall’s oss-security list a previously known and “fixed” issue that affected kernels < 3.18 (https://www.openwall.com/lists/oss-security/2019/04/25/4). This issue was about a generic local ASLR bypass for arbitrary processes, due to the permission check on the /proc/[pid]/maps pseudo file being done at read() time instead of at open() time. As per the man pages of proc(5), this pseudo-file contains the currently mapped memory regions and their access permissions.

$ cat /proc/self/maps
00400000-0040c000 r-xp 00000000 08:04 3670122 /bin/cat
0060b000-0060c000 r--p 0000b000 08:04 3670122 /bin/cat
0060c000-0060d000 rw-p 0000c000 08:04 3670122 /bin/cat
02496000-024b7000 rw-p 00000000 00:00 0 [heap]
7f508bd4b000-7f508beec000 r-xp 00000000 08:04 7605352 /lib/x86_64-linux-gnu/libc.so
7f508beec000-7f508c0ec000 ---p 001a1000 08:04 7605352 /lib/x86_64-linux-gnu/libc.so
7f508c0ec000-7f508c0f0000 r--p 001a1000 08:04 7605352 /lib/x86_64-linux-gnu/libc.so
7f508c0f0000-7f508c0f2000 rw-p 001a5000 08:04 7605352 /lib/x86_64-linux-gnu/libc.so
7f508c0f2000-7f508c0f6000 rw-p 00000000 00:00 0
7f508c0f6000-7f508c117000 r-xp 00000000 08:04 7605349 /lib/x86_64-linux-gnu/ld.so
7f508c164000-7f508c2ed000 r--p 00000000 08:04 800126 /usr/lib/locale/locale-archive
7f508c2ed000-7f508c2f0000 rw-p 00000000 00:00 0
7f508c2f2000-7f508c316000 rw-p 00000000 00:00 0
7f508c316000-7f508c317000 r--p 00020000 08:04 7605349 /lib/x86_64-linux-gnu/ld.so
7f508c317000-7f508c318000 rw-p 00021000 08:04 7605349 /lib/x86_64-linux-gnu/ld.so
7f508c318000-7f508c319000 rw-p 00000000 00:00 0
7ffcf3496000-7ffcf34b7000 rw-p 00000000 00:00 0 [stack]
7ffcf351b000-7ffcf351e000 r--p 00000000 00:00 0 [vvar]
7ffcf351e000-7ffcf351f000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

Since version 2.6.22, the Linux kernel does not allow reading the /proc/[pid]/maps pseudo file if you cannot ptrace() attach to the PID, meaning that an unprivileged user cannot read the maps pseudo file of, say, a process running as root (it would render ASLR useless otherwise).

$ su &
[1] 2661
$ cat /proc/2661/maps
cat: /proc/2661/maps: Permission denied

As described in the oss-sec post, the permission check being done at read() time is problematic because it allows an unprivileged user to open a valid file descriptor to the maps file and pass it to privileged (e.g., setuid root) programs which can, depending on the program, somehow leak the contents of the file to the unprivileged user (privileged processes have the required permissions to read() the pseudo file).

In order to fix this vulnerability, the permission check was simply moved to open() time instead of at read() time, as seen by the following commit:
https://github.com/torvalds/linux/commit/29a40ace841cba9b661711f042d1821cdc4ad47c

The fun

The problem with this “fix”, and what the SUSE Security Engineer forgot to mention, is that there are other /proc/[pid]/ pseudo files that are able to leak the currently mapped memory addresses, i.e., where the permission check is still done at read() time. One of these pseudo files is /proc/[pid]/stat (again!), a long-known source of such information leaks.

$ su &
[1] 2767
$ ls -l /proc/2767/stat
-r--r--r-- 1 root root 0 Feb 4 16:50 /proc/2767/stat

[1]+ Stopped su
$ cat /proc/2767/stat
2767 (su) T 2766 2767 2766 34817 2773 1077936128 266 0 1 0 0 0 0 0 20 0 1 0 181759 58273792 810 18446744073709551615 1 1 0 0 0 0 524288 6 0 0 0 0 17 1 0 0 6 0 0 0 0 0 0 0 0 0 0

The situation here is a little different from the previous case. Unprivileged users are able to read() the /proc/[pid]/stat of processes they can’t ptrace() attach the pid to, however, the interesting fields (addresses) are replaced with 0. From fs/proc/array.c in Linux 5.5:

static int do_task_stat(struct seq_file *m, struct pid_namespace *ns,
struct pid *pid, struct task_struct *task, int whole)
{

[...]

int permitted;

[...]

permitted = ptrace_may_access(task, PTRACE_MODE_READ_FSCREDS | PTRACE_MODE_NOAUDIT);

[...]

seq_put_decimal_ull(m, " ", mm ? (permitted ? mm->start_code : 1) : 0);
seq_put_decimal_ull(m, " ", mm ? (permitted ? mm->end_code : 1) : 0);
seq_put_decimal_ull(m, " ", (permitted && mm) ? mm->start_stack : 0);

[...]
}

The way this is exploited is exactly the same as the one described in the oss-sec post by the SUSE Security Engineer, one just simply needs to read /proc/[pid]/stat instead. Other setuid binaries which are able to somehow leak the addresses are procmail (appends to /var/spool/mail/$USER what it read as stdin), which is setuid root on Debian/Ubuntu, and spice-client-glib-usb-acl-helper (outputs to stdout what it read via stdin), also setuid root.

Here’s an example by using procmail:

$ su &
[1] 3122
$ cut -d' ' -f51 /proc/3122/stat
0

[1]+ Stopped su
$ procmail < /proc/3122/stat
$ tail -2 /var/spool/mail/user | cut -d' ' -f51
140726221803504

$ printf '0x%x\n' 140726221803504
0x7ffd60760ff0

# cat /proc/3122/maps
[...]
7ffd60740000-7ffd60761000 rw-p 00000000 00:00 0 [stack]

After coordinated disclosure with Zero Day Initiative (ZDI), Linux kernel developers replied that they have located an optional configuration remediation already implemented and will not do further patching/remediation at this time. The configuration mentioned is the hidepid mount(8) parameter.

Unfortunately, this does NOT remediate the issue.

Again, from man proc(5):

hidepid=n (since Linux 3.3)
This option controls who can access the information in
/proc/[pid] directories. The argument, n, is one of the fol‐
lowing values:

0 Everybody may access all /proc/[pid] directories. This is
the traditional behavior, and the default if this mount
option is not specified.

1 Users may not access files and subdirectories inside any
/proc/[pid] directories but their own (the /proc/[pid]
directories themselves remain visible). Sensitive files
such as /proc/[pid]/cmdline and /proc/[pid]/status are now
protected against other users. This makes it impossible
to learn whether any user is running a specific program
(so long as the program doesn't otherwise reveal itself by
its behavior).

2 As for mode 1, but in addition the /proc/[pid] directories
belonging to other users become invisible. This means
that /proc/[pid] entries can no longer be used to discover
the PIDs on the system. This doesn't hide the fact that a
process with a specific PID value exists (it can be
learned by other means, for example, by "kill -0 $PID"),
but it hides a process's UID and GID, which could other‐
wise be learned by employing stat(2) on a /proc/[pid]
directory. This greatly complicates an attacker's task of
gathering information about running processes (e.g., dis‐
covering whether some daemon is running with elevated
privileges, whether another user is running some sensitive
program, whether other users are running any program at
all, and so on).

Using the hidepid=2 mount option, an attacker can still exploit this by using open() on /proc/[pid]/stat (or /proc/[pid]/syscall, or /proc/[pid]/auxv) of their own process which, later, does an execve() of the target setuid binary the attacker wants to leak addresses from. Because the file descriptor was opened prior to the setuid execve() (an attacker can access its own pseudo files), the hidepid mount option plays no role in mitigating the attack. The attacker can now leak the addresses using the same technique of passing the fd to a privileged processes to somehow leak the file’s contents.

Exploiting

We have released a fully working proof of concept in our Github, named ASLREKT: https://github.com/blazeinfosec/aslrekt

Here’s the PoC in action, now using spice-client-glib-usb-acl-helper.

$ ./aslrekt
***** ASLREKT *****
Password:
[+] /bin/su .text is at 0x564219868000
[+] /bin/su heap is at 0x56421b657000
[+] /bin/su stack is at 0x7ffe78d76000

# cat /proc/$(pidof su)/maps
564219868000-564219871000 r-xp 00000000 08:04 3674996 /bin/su
[...]
56421b657000-56421b678000 rw-p 00000000 00:00 0 [heap]
[...]
7ffe78d76000-7ffe78d97000 rw-p 00000000 00:00 0 [stack]

Conclusion

Since its introduction in upstream kernels, it is safe to say that locally, ASLR has been broken for all this time and will continue on to be. Not just because of /proc/[pid]/, but also because of the lack of bruteforce detection mechanisms, i.e., multiple crash detection in a short amount of time. There also seems to be a lack of understanding from the Linux kernel developers of the underlying problems with /proc/[pid]/ pseudo files and the threat these may cause, since the issues weren’t properly fixed by patching and a “workaround” that does NOT work around the problem was suggested as a mitigation. On the other hand, grsecurity provides a configuration option that, when enabled, prevents information leakage via /proc/[pid]/ files (CONFIG_GRKERNSEC_PROC_MEMMAP).

Until next time!

About the author

Blaze Labs

Blaze Labs

RELATED POSTS

Ready to take your security
to the next level?

We are! Let’s discuss how we can work together to create strong defenses against real-life cyber threats.

Stay informed, stay secure

Subscribe to our monthly newsletter

Get notified about new articles, industry insights and cybersecurity news