COMMAND
kernel
SYSTEMS AFFECTED
Most unices
PROBLEM
Razor Team (BindView) is proud to announce their new security
paper, "Delivering signals for fun and profit". This paper is an
attempt to discuss security aspects of very common signal handler
coding practices, describing theoretical background and
demonstrating actual attack scenarios against live code in Unix
environment. The paper is available at:
http://razor.bindview.com/publish/papers/signals.txt
According to a popular belief, writing signal handlers has little
or nothing to do with secure programming, as long as handler code
itself looks good. At the same time, there have been discussions
on functions that shall be invoked from handlers, and functions
that shall never, ever be used there. Most Unix systems provide
a standarized set of signal-safe library calls. Few systems have
extensive documentation of signal-safe calls - that includes
OpenBSD, Solaris, etc.:
http://www.openbsd.org/cgi-bin/man.cgi?query=sigaction:
"The following functions are either reentrant or not
interruptible by sig- nals and are async-signal safe.
Therefore applications may invoke them, without restriction,
from signal-catching functions:
_exit(2), access(2), alarm(3), cfgetispeed(3), cfgetospeed(3),
cfsetispeed(3), cfsetospeed(3), chdir(2), chmod(2), chown(2),
close(2), creat(2), dup(2), dup2(2), execle(2), execve(2),
fcntl(2), fork(2), fpathconf(2), fstat(2), fsync(2),
getegid(2), geteuid(2), getgid(2), getgroups(2), getpgrp(2),
getpid(2), getppid(2), getuid(2), kill(2), link(2), lseek(2),
mkdir(2), mkfifo(2), open(2), pathconf(2), pause(2), pipe(2),
raise(3), read(2), rename(2), rmdir(2), setgid(2), setpgid(2),
setsid(2), setuid(2), sigaction(2), sigaddset(3),
sigdelset(3), sigemptyset(3), sigfillset(3), sigismember(3),
signal(3), sigpending(2), sigprocmask(2), sigsuspend(2),
sleep(3), stat(2), sysconf(3), tcdrain(3), tcflow(3),
tcflush(3), tcgetattr(3), tcgetpgrp(3), tcsendbreak(3),
tcsetattr(3), tcsetpgrp(3), time(3), times(3), umask(2),
uname(3), unlink(2), utime(3), wait(2), waitpid(2), write(2),
sigpause(3), sigset(3).
All functions not in the above list are considered to be
unsafe with respect to signals. That is to say, the behaviour
of such functions when called from a signal handler is
undefined. In general though, signal handlers should do
little more than set a flag; most other actions are not safe."
It is suggested to take special care when performing any
non-atomic operations while signal delivery is not blocked, and/or
not to rely on internal program state in signal handler.
Generally, signal handlers should do not much more than setting a
flag, whenever it is acceptable.
Unfortunately, there were no known, practical security
considerations of such bad coding practices. And while signal
can be delivered _anywhere_ during the userspace execution of
given program, most of programmers never take enough care to avoid
potential implications caused by this fact. Approximately 80 to
90% of signal handlers we have examined were written in insecure
manner.
This paper is an attempt to demonstrate and analyze actual risks
caused by this kind of coding practices, and to discuss threat
scenarios that can be used by an attacker in order to escalate
local privileges, or, sometimes, gain remote access to a machine.
This class of vulnerabilities affects numerous complex setuid
programs (Sendmail, screen, pppd, etc.) and several network
daemons (ftpd, httpd and so on).
Thanks to Theo de Raadt for bringing this problem to Zalewski's
attention; to Przemyslaw Frasunek for remote attack possibilities
discussion; Dvorak, Chris Evans and Pekka Savola for outstanding
contribution to heap corruption attacks field; Gregory Neil
Shapiro and Solar Designer for their comments on the issues
discussed below. Additional thanks to Mark Loveless, Dave Mann,
Matt Power and other RAZOR team members for their support and
reviews.
1) Impact: handler re-entry (Sendmail case)
===========================================
Before we discuss more generalized attack scenarios, we would like
to explain signal handler races starting with very simple and
clean example. We would try to exploit non-atomic signal handler.
The following code generalizes, in simplified way, very common bad
coding practice (which is present, for example, in setuid root
Sendmail program up to 8.11.3 and 8.12.0.Beta7):
/*********************************************************
* This is a generic verbose signal handler - does some *
* logging and cleanup, probably calling other routines. *
*********************************************************/
void sighndlr(int dummy) {
syslog(LOG_NOTICE,user_dependent_data);
// *** Initial cleanup code, calling the following somewhere:
free(global_ptr2);
free(global_ptr1);
// *** 1 *** >> Additional clean-up code - unlink tmp files, etc <<
exit(0);
}
/**************************************************
* This is a signal handler declaration somewhere *
* at the beginning of main code. *
**************************************************/
signal(SIGHUP,sighndlr);
signal(SIGTERM,sighndlr);
// *** Other initialization routines, and global pointer
// *** assignment somewhere in the code (we assume that
// *** nnn is partially user-dependent, yyy does not have to be):
global_ptr1=malloc(nnn);
global_ptr2=malloc(yyy);
// *** 2 *** >> further processing, allocated memory <<
// *** 2 *** >> is filled with any data, etc... <<
This code seems to be pretty immune to any kind of security
compromises. But this is just an illusion. By delivering one of
the signals handled by sighndlr() function somewhere in the middle
of main code execution (marked as '*** 2 ***' in above example)
code execution would reach handler function. Let's assume we
delivered SIGHUP. Syslog message is written, two pointers are
freed, and some more clean-up is done before exiting (*** 1 ***).
Now, by quickly delivering another signal - SIGTERM (note that
already delivered signal is masked and would be not delivered, so
you cannot deliver SIGHUP, but there is absolutely nothing against
delivering SIGTERM) - attacker might cause sighndlr() function
re-entry. This is a very common condition - 'shared' handlers are
declared for SIGQUIT, SIGTERM, SIGINT, and so on.
Now, for the purpose of this demonstration, we would like to
target heap structures by exploiting free() and syslog() behavior.
It is very important to understand how [v]syslog() implementation
works. We would focus on Linux glibc code - this function creates
a temporary copy of the logged message in so-called memory-buffer
stream, which is dynamically allocated using two malloc() calls -
the first one allocates general stream description structure, and
the other one creates actual buffer, which would contain logged
message.
Please refer the following URL for vsyslog() function sources:
http://src.openresources.com/debian/src/libs/HTML/S/glibc_2.0.7t.orig%20glibc-2.0.7t.orig%20misc%20syslog.c.html#101
Stream management functions (open_memstream, etc.) can be found at
http://src.openresources.com/debian/src/libs/HTML/S/glibc_2.0.7t.orig%20glibc-2.0.7t.orig%20libio%20memstream.c.html#63
In order for this particular attack to be successful, two
conditions have to be met:
* syslog() data must be user-dependent (like in Sendmail log
messages describing transferred mail traffic),
* second of these two global memory blocks must be aligned the
way that would be re-used in second open_memstream()
malloc() call.
The second buffer (global_ptr2) would be free()d during the first
sighndlr() call, so if these conditions are met, the second
syslog() call would re-use this memory and overwrite this area,
including heap-management structures, with user-dependent syslog()
buffer.
Of course, this situation is not limited to two global buffers -
generally, we need one out of any number of free()d buffers to be
aligned that way. Additional possibilities are related to
interrupting free() chain by precise SIGTERM delivery and/or
influencing buffer sizes / heap data order by using different
input data patterns.
If so, the attacker can cause second free() pass to be called
with a pointer to user-dependent data (syslog buffer), this leads
to instant root compromise - see excellent article by Chris Evans
(based on observations by Pekka Savola):
http://lwn.net/2000/1012/a/traceroute.php3
Practical discussion and exploit code for the vulnerability
discussed in above article can be found there:
http://security-archive.merton.ox.ac.uk/bugtraq-200010/0084.html
Below is a sample 'vulnerable program' code:
#include <signal.h>
#include <syslog.h>
#include <string.h>
#include <stdlib.h>
void *global1, *global2;
char *what;
void sh(int dummy) {
syslog(LOG_NOTICE,"%s\n",what);
free(global2);
free(global1);
sleep(10);
exit(0);
}
int main(int argc,char* argv[]) {
what=argv[1];
global1=strdup(argv[2]);
global2=malloc(340);
signal(SIGHUP,sh);
signal(SIGTERM,sh);
sleep(10);
exit(0);
}
You can exploit it, forcing free() to be called on a memory region
filled with 0x41414141 (you can see this value in the registers at
the time of crash -- the bytes represented as 41 in hex are set by
the 'A' input characters in the variable $LOG below). Sample
command lines for a Bash shell are:
$ gcc vuln.c -o vuln
$ PAD=`perl -e '{print "x"x410}'`
$ LOG=`perl -e '{print "A"x100}'`
$ ./vuln $LOG $PAD & sleep 1; killall -HUP vuln; sleep 1; killall -TERM vuln
The result should be a segmentation fault followed by nice core
dump (for Linux glibc 2.1.9x and 2.0.7).
(gdb) back
#0 chunk_free (ar_ptr=0x4013dce0, p=0x80499a0) at malloc.c:3069
#1 0x4009b334 in __libc_free (mem=0x80499a8) at malloc.c:3043
#2 0x80485b8 in sh ()
#4 0x400d5971 in __libc_nanosleep () from /lib/libc.so.6
#5 0x400d5801 in __sleep (seconds=10) at ../sysdeps/unix/sysv/linux/sleep.c:85
#6 0x80485d6 in sh ()
So, as you can see, failure was caused when signal handler was
re-entered. __libf_free function was called with a parameter of
0x080499a8, which points somewhere in the middle of our AAAs:
(gdb) x/s 0x80499a8
0x80499a8: 'A' <repeats 94 times>, "\n"
You can find 0x41414141 in the registers, as well, showing this
data is being processed. For more analysis, please refer to the
paper mentioned above.
Obviously, that is just an example of this attack. Whenever
signal handler execution is non-atomic, attacks of this kind are
possible by re-entering the handler when it is in the middle of
performing non-reentrant operations. Heap damage is the most
obvious vector of attack, in this case, but not the only one.
2) Impact: signal in the middle (screen case)
=============================================
The attack described above usually requires specific conditions
to be met, and takes advantage of non-atomic signal handler
execution, which can be easily avoided by using additional flags
or blocking signal delivery.
But, as signal can be delivered at any moment (unless explictly
blocked), this is obvious that it is possible to perform an attack
without re-entering the handler itself. It is enough to deliver a
signal in a 'not appropriate' moment. There are two attack
schemes:
A) re-entering libc functions:
Every function that is not listed as reentry-safe is a
potential source of vulnerabilities. Indeed, numerous library
functions are operating on global variables, and/or modify
global state in non-atomic way. Once again, heap-management
routines are probably the best example. By delivering a signal
when malloc(), free() or any other libcall of this kind is
being called, all subsequent calls to the heap management
routines made from signal handler would have unpredictable
effect, as heap state is completely unpredictable for the
programmer.
Other good examples are functions working on static/global
variables and buffers like certain implementations of strtok(),
inet_ntoa(), gethostbyname() and so on. In all cases, results
will be unpredictable.
B) interrupting non-atomic modifications:
This is basically the same problem, but outside library
functions. For example, the following code:
dropped_privileges = 1;
setuid(getuid());
is, technically speaking, using safe library functions only. But,
at the same time, it is possible to interrupt execution between
substitution and setuid() call, causing signal handler to be
executed with dropped_privileges flag set, but superuser
privileges not dropped.
This, very often, might be a source of serious problems.
First of all, we would like to come back to Sendmail example, to
demonstrate potential consequences of re-entering libc. Note that
signal handler is NOT re-entered - signal is delivered only once:
#0 0x401705bc in chunk_free (ar_ptr=0x40212ce0, p=0x810f900) at malloc.c:3117 #1 0x4016fd12 in chunk_alloc (ar_ptr=0x40212ce0, nb=8200) at malloc.c:2601
#2 0x4016f7e6 in __libc_malloc (bytes=8192) at malloc.c:2703
#3 0x40168a27 in open_memstream (bufloc=0xbfff97bc, sizeloc=0xbfff97c0) at memstream.c:112
#4 0x401cf4fa in vsyslog (pri=6, fmt=0x80a5e03 "%s: %s", ap=0xbfff99ac) at syslog.c:142
#5 0x401cf447 in syslog (pri=6, fmt=0x80a5e03 "%s: %s") at syslog.c:102
#6 0x8055f64 in sm_syslog ()
#7 0x806793c in logsender ()
#8 0x8063902 in dropenvelope ()
#9 0x804e717 in finis ()
#10 0x804e9d8 in intsig () <---- ** SIGINT **
#11 <signal handler called>
#12 chunk_alloc (ar_ptr=0x40212ce0, nb=4104) at malloc.c:2968
#13 0x4016f7e6 in __libc_malloc (bytes=4097) at malloc.c:2703
Heap corruption is caused by interruped malloc() call and, later,
by calling malloc() once again from vsyslog() function invoked
from handler.
There are two another examples of very interesting stack
corruption caused by re-entering heap management routines in
Sendmail daemon - in both cases, signal was delivered only once:
A)
#0 0x401705bc in chunk_free (ar_ptr=0xdbdbdbdb, p=0x810b8e8) at malloc.c:3117
#1 0xdbdbdbdb in ?? ()
B)
/.../
#9 0x79f68510 in ?? ()
Cannot access memory at address 0xc483c689
We'd like to leave this one as an exercise for a reader - try to
figure out why this happens and why this problem can be
exploitable. For now, we would like to come back to our second
scenario, interrupting non-atomic code to show that targeting heap
is not the only possibility.
Some programs are temporarily returning to superuser UID in
cleanup routines, e.g., in order to unlink specific files. Very
often, by entering the handler at given moment, is possible to
perform all the cleanup file access operations with superuser
privileges.
Here's an example of such coding, that can be found mainly in
interactive setuid software:
#include <signal.h>
#include <string.h>
#include <stdlib.h>
void sh(int dummy) {
printf("Running with uid=%d euid=%d\n",getuid(),geteuid());
}
int main(int argc,char* argv[]) {
seteuid(getuid());
setreuid(0,getuid());
signal(SIGTERM,sh);
sleep(5);
// this is a temporarily privileged code:
seteuid(0);
unlink("tmpfile");
sleep(5);
seteuid(getuid());
exit(0);
}
---- EOF ----
# gcc vuln.c -o vuln; chmod 4755 vuln
# su user
$ ./vuln & sleep 3; killall -TERM vuln; sleep 3; killall -TERM vuln
Running with uid=500 euid=500
Running with uid=500 euid=0
Such a coding practice can be found, par example, in 'screen'
utility developed by Oliver Laumann. One of the most obvious
locations is CoreDump handler [screen.c]:
static sigret_t
CoreDump SIGDEFARG
{
/.../
setgid(getgid());
setuid(getuid());
unlink("core");
/.../
SIGSEGV can be delivered in the middle of user-initiated screen
detach routine, for example. To better understand what and why is
going on, here's an strace output for detach (Ctrl+A, D) command:
23534 geteuid() = 0
23534 geteuid() = 0
23534 getuid() = 500
23534 setreuid(0, 500) = 0 *** HERE IT HAPPENS ***
23534 getegid() = 500
23534 chmod("/home/lcamtuf/.screen/23534.tty5.nimue", 0600) = 0
23534 utime("/home/lcamtuf/.screen/23534.tty5.nimue", NULL) = 0
23534 geteuid() = 500
23534 getuid() = 0
Marked line sets uid to zero. If SIGSEGV is delivered somewhere
near this point, CoreDump() handler would run with superuser
privileges, due to initial setuid(getuid()).
3) Remote exploitation of signal delivery (WU-FTPD case)
========================================================
This is a very interesting issue, directly related to re-entering
libc functions and/or interrupting non-atomic code. Many complex
daemons, like ftp, some http/proxy services, MTAs, etc., have
SIGURG handlers declared - very often these handlers are pretty
verbose, calling syslog(), or freeing some resources allocated
for specific connection. The trick is that SIGURG, obviously,
can be delivered over the network, using TCP/IP OOB message.
Thus, it is possible to perform attacks using network layer
without any priviledges.
Below is a SIGURG handler routine, which, with small modifications
is shared both by BSD ftpd and WU-FTPD daemons:
static VOIDRET myoob FUNCTION((input), int input)
{
/.../
if (getline(cp, 7, stdin) == NULL) {
reply(221, "You could at least say goodbye.");
dologout(0);
}
/.../
}
As you can see in certain conditions, dologout() function is
called. This routine looks this way:
dologout(int status)
{
/.../
if (logged_in) {
delay_signaling(); /* we can't allow any signals while euid==0: kinch */
(void) seteuid((uid_t) 0);
wu_logwtmp(ttyline, "", "");
}
if (logging)
syslog(LOG_INFO, "FTP session closed");
/.../
}
As you can see, the authors took an additional precaution not to
allow signal delivery in the "logged_in" case. Unfortunately,
syslog() is a perfect example of a libc function that should NOT
be called during signal handling, regardless of whether
"logged_in" or any other special condition happens to be in
effect.
As mentioned before, heap management functions such as malloc()
are called within syslog(), and these functions are not atomic.
The OOB message might arrive when the heap is in virtually any
possible state. Playing with uids / privileges / internal state
is an option, as well.
In most cases this is a non-issue for local attacks, as the
attacker might control the execution environment (e.g., the load
average, the number of local files that the daemon needs to
access, etc.) and try a virtually infinite number of times by
invoking the same program over and over again, increasing the
possibility of delivering signal at given point. For remote
attacks, this is a major issue, but as long as the attack itself
won't cause service to stop responding, thousands of attempts
might be performed.
SOLUTION
This is a very complex and difficult task. There are at least
three aspects of this:
* Using reentrant-safe libcalls in signal handlers only. This
would require major rewrites of numerous programs. Another
half-solution is to implement a wrapper around every
insecure libcall used, having special global flag checked to
avoid re-entry,
* Blocking signal delivery during all non-atomic operations
and/or constructing signal handlers in the way that would
not rely on internal program state (e.g. unconditional
setting of specific flag and nothing else),
* Blocking signal delivery in signal handlers.