CLD-405 Details

Other IDs this deficiency may be known by:

CVE ID CVE-2018-1124 (nvd) (mitre) (debian) (archlinux) (red hat) (suse) (ubuntu)
Other ID(s)

Basic Information:

Affected Package(s) procps-ng
Deficiency Type SECURITY
Date Created 2018-05-17 13:20:46
Date Last Modified 2018-05-17 17:44:46

Version Specific Information:

Cucumber 1.0 i686fixed in procps-ng-3.3.11-i686-2
Cucumber 1.0 x86_64fixed in procps-ng-3.3.11-x86_64-2 and procps-ng-lib_i686-3.3.11-lib_i686-2

Cucumber 1.1 i686 fixed in procps-ng-3.3.11-i686-2
Cucumber 1.1 x86_64 fixed in procps-ng-3.3.11-x86_64-2 and procps-ng-lib_i686-3.3.11-lib_i686-2

Details:

=================================== Overview ===================================

  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.

================================ Initial Report ================================

From http://www.openwall.com/lists/oss-security/2018/05/17/1:

5. Local Privilege Escalation in libprocps (CVE-2018-1124)
========================================================================

========================================================================
5.1. Vulnerability
========================================================================

libprocps's file2strvec() function parses a process's /proc/PID/cmdline
(or /proc/PID/environ), and creates an in-memory copy of this process's
argv[] (command-line argument strings, and pointers to these strings).
file2strvec() is called when either PROC_FILLCOM or PROC_FILLARG, but
not PROC_EDITCMDLCVT, is passed to openproc() or readproctab() (or
PROC_FILLENV but not PROC_EDITENVRCVT).

file2strvec() is vulnerable to three integer overflows (of "tot", "c",
and "tot + c + align"):

 660 static char** file2strvec(const char* directory, const char* what) {
 661     char buf[2048];     /* read buf bytes at a time */
 662     char *p, *rbuf = 0, *endbuf, **q, **ret;
 663     int fd, tot = 0, n, c, end_of_file = 0;
 664     int align;
 ...
 670     /* read whole file into a memory buffer, allocating as we go */
 671     while ((n = read(fd, buf, sizeof buf - 1)) >= 0) {
 ...
 686         rbuf = xrealloc(rbuf, tot + n);         /* allocate more memory */
 687         memcpy(rbuf + tot, buf, n);             /* copy buffer into it */
 688         tot += n;                               /* increment total byte ctr */
 ...
 697     endbuf = rbuf + tot;                        /* count space for pointers */
 698     align = (sizeof(char*)-1) - ((tot + sizeof(char*)-1) & (sizeof(char*)-1));
 699     for (c = 0, p = rbuf; p < endbuf; p++) {
 700         if (!*p || *p == '\n')
 701             c += sizeof(char*);
 ...
 705     c += sizeof(char*);                         /* one extra for NULL term */
 706 
 707     rbuf = xrealloc(rbuf, tot + c + align);     /* make room for ptrs AT END */

To the best of our knowledge, the integer overflows of "c" and "tot + c
+ align" are not exploitable beyond a denial of service: they result in
an mmap-based buffer overflow of rbuf, but with pointers only (pointers
to our command-line argument strings, and a NULL terminator). Similarly,
we were unable to exploit the integer overflow of "tot" on 32-bit.

On 64-bit, however, the integer overflow of "tot" results in a memcpy()
of arbitrary bytes (our command-line arguments) to an offset of roughly
-2GB below rbuf. Surprisingly, the "xrealloc(rbuf, tot + n)" before the
memcpy() does not exit() when "tot" becomes negative, because xrealloc()
incorrectly uses an "unsigned int size" argument instead of a size_t
(CVE-2018-1126):

 66 void *xrealloc(void *oldp, unsigned int size) {

========================================================================
5.2. Exploitation
========================================================================

To exploit the integer overflow of "tot" on 64-bit, we are faced with
several difficulties:

- We must defeat NX, ASLR, PIE, full RELRO, SSP (Stack-Smashing
  Protector), and FORTIFY.

- Our exploit must be one-shot, or as close to one-shot as possible: we
  may use brute-force if the target procps-ng utility is executed by a
  script, but we have only one chance to exploit this vulnerability if
  the target utility is executed manually by an administrator.

- We have no control over the target utility's command-line arguments,
  environment variables, or resource limits (it is executed by another
  user, administrator, or script), and we have no direct channel for an
  information leak (we have no access to the target utility's output,
  for example).

- We were unable to exploit the integer overflow of "tot" when rbuf is
  mmap()ed (but we were also unable to prove that it is unexploitable);
  when the integer "tot" overflows, rbuf is an mmap()ed chunk (its size
  is roughly 2GB), and because Linux's mmap() is a top-down allocator,
  we believe that:

  . rbuf must be allocated in a hole of the mmap-space (to survive the
    memcpy() at a negative offset below rbuf);

  . it is impossible to make such a large hole (in procps-ng, calls to
    the malloc functions are extremely rare).

Despite these difficulties, we developed proof-of-concept exploits
against the procps-ng utility "w" on Ubuntu 16.04 (a one-shot exploit
against a partial RELRO, non-PIE w), Debian 9 and Fedora 27 (a nearly
one-shot exploit against a full RELRO, PIE w): if we first force "w" to
malloc()ate n_mmaps_max = 64K mmap()ed chunks (whose size is larger than
mmap_threshold = 128KB), then malloc() will not call mmap() anymore, but
will call brk() instead, even for chunks larger than mmap_threshold. The
2GB rbuf (after the integer overflow of tot) will therefore be allocated
on the heap by brk(), and because brk() is a bottom-up allocator, we can
easily arrange for the memcpy() at rbuf - 2GB to overwrite the beginning
of the heap:

- if w is not a PIE, we overwrite libprocps's internal PROCTAB structure
  and its function pointers;

- if w is a PIE, we overwrite the glibc's internal *gettext() structures
  and transform this memory corruption into a format-string exploit.

To force 64K allocations of 128KB (8GB) in w, we need 64K distinct PIDs
(each /proc/PID/cmdline allocates 128KB in file2strvec()): consequently,
/proc/sys/kernel/pid_max must be greater than 64K (it is 32K by default,
even on 64-bit). This is not an unusual setting: large servers (database
servers, container and storage platforms) commonly increase the value of
pid_max (up to 4M on 64-bit). Besides pid_max, other settings may limit
our ability to spawn 64K processes: /proc/sys/kernel/threads-max,
RLIMIT_NPROC, and systemd-logind's UserTasksMax. Unlike pid_max,
however, these limits are not insuperable obstacles:

- they may be naturally greater than 64K, depending on the total number
  of RAM pages (for /proc/sys/kernel/threads-max and RLIMIT_NPROC) or
  the value of pid_max (for UserTasksMax);

- they may not apply to the attacker's user account (for example,
  systemd-logind may not at all manage this specific user account);

- in any case, we do not need to spawn 64K concurrent processes: if we
  use /proc/PID/cmdline as a FUSE-backed synchronization tool, we need
  only a few concurrent processes.

========================================================================
5.3. Exploitation details
========================================================================

Our proof-of-concept exploit spawns five different types of processes
("main", "mmap", "dist", "wrap", and "srpt"):

- a long-lived "main" process, which spawns and coordinates the other
  processes;

- 64K long-lived "mmap" processes, which guarantee that the ~2GB rbufs
  of our "dist" and "wrap" processes are allocated by brk() in the heap
  of our future "w" target; the "mmap" processes occupy the lowest PIDs
  available, to avoid interference from other processes with the heap
  layout of w;

- a long-lived "dist" ("distance") process, whose /proc/PID/cmdline is
  carefully constructed to cover the exact distance between our target
  structure (at the beginning of w's heap) and the rbuf of our "wrap"
  process (at the end of w's heap);

- a long-lived "wrap" ("integer wrap") process, which overflows the
  integer "tot" and overwrites our target structure at the beginning of
  w's heap (with the memcpy() at rbuf - 2GB);

- short-lived "srpt" ("simulate readproctab") processes, which measure
  the exact distance between our target structure (at the beginning of
  w's heap) and the rbuf of our "wrap" process (at the end of w's heap);
  because this distance depends on an accurate list of processes running
  on the system, our exploit regularly spawns "srpt" processes until the
  distance stabilizes (it is particularly unstable after a reboot).

We use a few noteworthy tricks in this exploit:

- we do not fork() but clone() the "mmap" processes (we use the flags
  CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM | CLONE_SIGHAND, but
  not CLONE_THREAD, because each process must have its own /proc/PID
  entry): this is much faster, and significantly reduces the memory
  consumption of our exploit (the target "w" process itself already
  consumes over 12GB = 64K*128KB + 2GB + 2GB -- the rbufs for the
  "mmap", "dist", and "wrap" processes);

- we analyze the ~2GB command-line argument strings of our "dist" and
  "wrap" processes, to detect repeated patterns and replace them with
  our equivalent file-backed mmap()s (this further reduces the memory
  consumption of the exploit); moreover, we replace the argv[] pointers
  of these processes with PROT_NONE mmap()s (hundreds of megabytes that
  are never accessed);

- we initially simulated readproctab() with our own exploit code, but
  eventually switched to a small LD_PRELOAD library that instruments the
  real "w" utility and provides more accurate measurements.

There is much room for improvement in this proof-of-concept exploit: for
example, it depends on the exact distance between our target structure
(at the beginning of w's heap) and the rbuf of our "wrap" process (at
the end of w's heap), but this distance is hard to measure inside a
container, because processes running outside the container are not
visible inside the container (brute-force may be a solution if the
target utility is executed by a script, but not if it is executed
manually by an administrator; better solutions may exist).

========================================================================
5.4. Non-PIE exploitation
========================================================================

In this section, we describe our simplest proof-of-concept exploit,
against the non-PIE "w" on Ubuntu 16.04: we overflow the integer "tot"
in file2strvec(), we overwrite the PROCTAB structure and its function
pointers, and we jump into the executable segment of w. However, w is
very small and contains no useful gadgets, syscall instructions, or
library calls. Instead, we use a technique pioneered by Nergal in
http://phrack.org/issues/58/4.html ("5 - The dynamic linker's
dl-resolve() function"):

We jump to the very beginning of w's PLT (Procedure Linkage Table),
which calls _dl_runtime_resolve() and _dl_fixup() with a "reloc_arg"
that we control (it is read from the stack) and that indexes our own
fake Elf64_Rela structure (in w's heap), which in turn indexes a fake
Elf64_Sym structure, which in turn indexes a string that we control and
that allows us to call any library function, by name (even if it does
not appear in w's PLT). The obvious choice here is the "system"
function:

- the RDI register (the first argument of the function pointer that we
  overwrote, and hence the command argument of system()) points to the
  PROCTAB structure, whose contents we control;

- we do not need to worry about the privilege dropping of /bin/sh,
  because w is not a set-user-ID executable.

Finally, we must solve two practical problems to use this dynamic-linker
technique against w:

- our fake ELF structures are located in the heap, but indexed from the
  executable, and a random gap separates the heap from the executable:
  we therefore allocate four large areas in the heap (large enough to
  defeat the randomization of the heap), one for each of our fake
  structures (Elf64_Rela, Elf64_Sym, "system", and ndx for symbol
  versioning);

- malloc guarantees a 16-byte alignment, but Elf64_Rela and Elf64_Sym
  are 24-byte structures: luckily, the last 8 bytes of these structures
  are unused, and we therefore truncate our fake structures to 16 bytes.

For example, on Ubuntu 16.04.4, we overwrite the PROCTAB structure with
the following ROP chain:

  procfs  taskdir  tdu  df   finder   reader  tfinder
|--------|--------|----+---|--------|--------|--------|------|--------|--------|
| id>>/tmp/w.$$        |000|0x4020bb|0x4029db|0x401100| .... |relocarg|0x402a50|
|--------|--------|----+---|--------|--------|--------|------|--------|--------|
                                                    0xffb8 bytes

- the first gadget that we execute, 0x4020bb, pivots the stack pointer
  to RDI (which points to the very beginning of the PROCTAB structure):
  "push rdi; ...; pop rsp; pop r13; pop r14; pop r15; pop rbp; ret;"

- the second gadget that we execute, 0x4029db, increases the stack
  pointer by 0xffb8 bytes (it would otherwise crash into the beginning
  of the heap, because the stack grows down): "ret 0xffb8;"

- the third gadget that we execute, 0x401100, calls
  _dl_runtime_resolve() and _dl_fixup() with our own "relocarg" (this
  effectively calls system() with the command located at RDI,
  "id>>/tmp/w.$$"):

  401100:       ff 35 02 2f 20 00       pushq  0x202f02(%rip)
  401106:       ff 25 04 2f 20 00       jmpq   *0x202f04(%rip)

- the fourth gadget that we execute, 0x402a50, makes a clean exit:

  402a50:       bf 01 00 00 00          mov    $0x1,%edi
  402a55:       e8 36 e7 ff ff          callq  401190 <_exit@plt>

$ ./w-exploit-Non-PIE
positive_tot 2147482113
distance_tot 2147482112
distance 12024752
...
distance 12024752
off 279917264
ver_beg  2e26ce0 ver_end  5426ce0
rel_beg 15f19fb0 rel_end 18519fb0
str_beg 2900d280 str_end 2b60d280
sym_beg 3c100570 sym_end 3e700570
reloc_arg 16957128
nentries 5
POSITIVE_TOT 2147482113
DISTANCE_TO_PT 1
negwrite_off 2147485183
nentries 1
ready

Then, if an administrator executes w:

# cat /tmp/w.*
cat: '/tmp/w.*': No such file or directory

# w

# cat /tmp/w.*
uid=0(root) gid=0(root) groups=0(root)

========================================================================
5.5. PIE exploitation
========================================================================

In this section, we describe our proof-of-concept exploit against the
PIE "w" on Debian 9 and Fedora 27. The first technique that we tried, a
partial overwrite of a function pointer in the PROCTAB structure, does
not work:

- we are limited to a 2-byte overwrite, or else we lose the "one-shot"
  quality of our exploit (we must brute-force the random bits that we
  overwrite);

- the original function pointer refers to a piece of code in libprocps
  that offers a very limited choice of gadgets;

- file2strvec() ends our command-line argument strings (which overwrite
  the function pointer) with a null byte, and further reduces the number
  of available gadgets.

Our second, working technique is derived from halfdog's fascinating
https://www.halfdog.net/Security/2017/LibcRealpathBufferUnderflow/ and
transforms libprocps's integer overflow and memory corruption into a
format-string exploit:

- we overwrite the dirname pointer to "/usr/share/locale" (a member of
  the struct binding malloc()ated at the very beginning of w's heap by
  bindtextdomain()) with a pointer to "/tmp" -- we do not need to worry
  about ASLR, because we arrange for file2strvec() to overwrite dirname
  with a pointer to our command-line argument strings; alternatively, we
  could overwrite the "procps-ng" string (malloc()ated at the beginning
  of w's heap by textdomain()), but this would also overwrite the chunk
  header of the struct PROCTAB, and would cause a crash in closeproc();

- we thereby control the translation strings returned by the *gettext()
  functions and the _() macro (the overwritten dirname pointer is used
  to construct the names of the translation files ".mo") and therefore
  control two format-strings in w's main():

591                 printf(_("%-*s TTY      "), userlen, _("USER"));
...
595                         printf(_("  LOGIN@   IDLE   JCPU   PCPU WHAT\n"));

- we exploit the first format-string to create a pointer to a saved RIP
  on the stack, and we write this pointer to the stack itself;

- we use this pointer, and the second format-string, to overwrite the
  saved RIP with the address of a useful libc gadget (we return into
  popen() on Debian 9, and wordexp() on Fedora 27).

However, unlike halfdog, we cannot defeat ASLR by simply dumping the
contents of the stack with a format-string, because we have not access
to the output of "w" (it is executed by another user, administrator, or
script). Instead, we implement Chris Evans's "read-add-write" primitive
https://scarybeastsecurity.blogspot.com/2016/11/0day-exploit-advancing-exploitation.html
("Trick #6: co-opting an addition primitive") with format-strings only.

With the first format-string:

- we "read" the LSW (Least Significant Word, 32 bits) of a stack pointer
  that is located on the stack itself and hence accessible through the
  format-string arguments -- for example, the argv pointer;

- we "add" a distribution-specific constant to this LSW, to make it
  point to a saved RIP on the stack -- for example, the saved RIP pushed
  onto the stack by the call to printf_positional() in vfprintf();

- we "write" this modified LSW to the LSW of another stack pointer that
  is also located on the stack itself and hence accessible through the
  format-string arguments -- for example, the argv[0] pointer.

With the second format-string:

- we "read" the LSW of a libc pointer that is located on the stack and
  hence accessible through the format-string arguments -- for example,
  the pointer to __libc_start_main();

- we "add" a distribution-specific constant to this LSW, to make it
  point to a useful libc gadget -- for example, popen() or wordexp();

- we "write" this modified LSW to the LSW of a saved RIP on the stack:
  we use the pointer (to the saved RIP) created on the stack by the
  first format-string.

To implement the "read-add-write" primitive:

- we "read" the LSW of a pointer (we load it into vfprintf's internal
  character counter) through a variable-width specifier such as "%*R$x",
  where R is the position (among the format-string arguments on the
  stack) of the to-be-read pointer;

- we "add" a constant A to this LSW through a constant-width specifier
  such as "%Ax";

- we "write" this modified LSW to the LSW of another pointer through a
  specifier such as "%W$n", where W is the position (among the format-
  string arguments on the stack) of a pointer to the to-be-overwritten
  pointer (for example, in our first format-string we overwrite the LSW
  of the argv[0] pointer through the argv pointer, and in our second
  format-string we overwrite the LSW of a saved RIP through the
  overwritten argv[0] pointer); in summary:

  . if we want to "add" a constant to the LSW that we "read", we use a
    simple format-string such as "%*R$x%Ax%W$n", where A is equal to the
    constant that we want to add;

  . if we want to "subtract" a constant from the LSW that we "read", we
    use a format-string such as "%*R$x%W$n%Ax%W$hn", where A is equal to
    65536 minus the constant that we want to subtract (the smaller the
    constant, the higher the probability of success).

This generic technique defeats NX, ASLR, PIE, SSP, and FORTIFY, but it
suffers from three major drawbacks:

- it requires two different format-strings, because it must reset
  vfprintf's internal character counter between the two "read-add-write"
  primitives;

- its probability of success is 1/4 (not a one-shot, but not a
  brute-force either), because the probability of success of each
  "read-add-write" primitive is 1/2 (the randomized LSW that is "read"
  as an "int width" must be positive), and the stack is randomized
  independently of the libc;

- it outputs 2*1GB on average (2*2GB at most): this may be acceptable if
  the target utility is executed by a script or daemon, but not if it is
  executed manually by an administrator (terminal escape sequences may
  be used to overcome this drawback, but we did not explore this
  possibility yet).

It is also possible to implement distribution-specific variants of this
generic technique: for example, we developed a Debian-specific version
of our "w" exploit that requires only one format-string, has an 11/12
probability of success (nearly one-shot), and outputs only a few
kilobytes. This is left as an exercise for the interested reader.


================================= Our Analysis =================================

Fixed in patch 0074-proc-readproc.c-Fix-bugs-and-overflows-in-file2strve.patch
from https://www.qualys.com/2018/05/17/procps-ng-audit-report-patches.tar.gz

================================= Our Solution =================================

We have applied the aforementioned patch and rebuilt. We had to modify it
slightly to get it work on procps-ng 3.3.11. Our modified patch can be found
at:
https://mirror.cucumberlinux.com/cucumber/cucumber-1.1/source/base/procps-ng/patches/00030_CVE-2018-1124_0074-proc-readproc.c-Fix-bugs-and-overflows-in-file2strve.patch