Re: Linux ELF loader vulnerabilities
From: Pavel Kankovsky (peak_at_argo.troja.mff.cuni.cz)
Date: Thu, 11 Nov 2004 20:52:27 +0100 (MET) To: firstname.lastname@example.org
On Wed, 10 Nov 2004, Paul Starzetz wrote:
> One of the Linux format loaders is the ELF (Executable and Linkable
> Format) loader. Nowadays ELF is the standard format for Linux binaries
> besides the a.out binary format, which is not used in practice anymore.
BTW: a.out loader appears to be still full of integer overflow bugs.
> 1) The Linux man pages state that a read(2) can return less than the
> requested number of bytes, even zero. It is not clear how this can
> happen while reading a disk file (in contrast to network sockets),
> however here some thoughts:
It can happen when the end of file is encountered. One might exploit this
to create an "oracle" giving quizzical answers about unused kernel memory:
You run your own malformed ELF binary that makes the kernel allocate N
bytes, read M < N bytes, and interpret all N bytes including unintialized
N-M bytes as ELF phdr entries. The fact the kernel ignores mmap() errors
could make it interesting.
On a NFS volume mounted with "intr" option, it can happen when the process
receives a signal in the middle of read(). I don't dare to say whether it
can happen in the middle of a page.
> - - most of the standard setuid binaries on a 'normal' i386 Linux
> installation have ELF headers stored below the 4096th byte, therefore
> they are probably not exploitable on i386 architecture.
I'd say that binaries with essential headers (phdr, interp) not fitting
into the first page of the executable file are extremely rare on any
platform. Afaik all standard tools put phdr right after ehdr, and interp
right after phdr. Standard ehdr size is 52 bytes (64 for 64-bit arch), one
standard phdr entry is 32 bytes (56? for 64-bit), and there is only a
handful of entries (<=10) in an ordinary phdr. Interp is quite short,
say <50 bytes. This makes <1000 bytes total.
Have you find any "naturally occuring" binary with big headers?
> 2) This bug can lead to a incorrectly mmaped binary image in the memory.
> There are various reasons why a mmap() call can fail:
> Security implications in the case of a setuid binary are quite obvious:
> we may end up with a binary without the .text or .bss section or with
> those sections shifted (in the case they are not 'fixed' sections).
ET_EXEC files (ordinary binaries) have fixed mapping. ET_DYN (ld.so or
relocatable binaries; and dynamic libraries but they are irrelevant in
this context) get MAP_FIXED after the first segment has been mapped
But there's a catch: ld.so is loaded by load_elf_interp() that stop after
the first mmap() failure. Its return value is wrong but the best thing we
can get with ET_EXEC binaries (both with and without a dynamic linker) is
an unmapped segment. A missing segment is likely to kill the program
before it can do any harm.
ET_DYN binaries may, on the other hand, be more exploitable if their
memory layout is messed up the right way. (Isn't it ironic some people
use ET_DYN binaries in order to be able to randomize process address
space and make their systems more resistant?)
> 3) This bug is similar to 2) however the code incorrectly returns the
> kernel_read status to the calling function on mmap failure which will
> assume that the program interpreter has been loaded. That means that the
> kernel will start the execution of the binary file itself instead of
> calling the program interpreter (linker) that have to finish the binary
> loading from user space.
As far as I can tell the kernel puts the result of kernel_read(), i.e. the
interpreter's phdr size (<= page size), into elf_entry and initializes the
process' instruction pointer to elf_entry. The inevitable consequence is
that the process jumps into the large black hole at the begining of its
address space (assuming standard Linux memory layout) and dies before it
can do anything harmful. Do I miss anything?
> 4) This bug leads to internal kernel file system functions beeing called
> with an argument string exceeding the maximum path size in length
> (PATH_MAX). It is not clear if this condition is exploitable.
This is funny. There used to be
elf_interpreter[elf_ppnt->p_filesz - 1] = 0;
there but it was "optimized out" between 2.2 and 2.4.
--Pavel Kankovsky aka Peak [ Boycott Microsoft--http://www.vcnet.com/bms ]
"Resistance is futile. Open your source code and prepare for assimilation."