__ __ __
.-----.--.--.----.| |.--.--.--| |.-----.--| | .-----.----.-----.
| -__|_ _| __|| || | | _ || -__| _ |__| _ | _| _ |
|_____|__.__|____||__||_____|_____||_____|_____|__|_____|__| |___ |
by shaun2k2 - member of excluded-team |_____|
########################################
# Injecting signals for Fun and Profit #
########################################
Introduction
#############
More secure programming is on the rise, eliminating more generic program
exploitation vectors, such as stack-based overflows, heap overflows and symlink
bugs. Despite this, subtle vulnerabilities are often overlooked during code
audits, leaving so-called "secure" applications vulnerable to attack, but in a
less obvious manner. Secure design of signal-handlers is often not considered,
but I believe that this class of security holes deserves just as much attention
as more generic classes of bugs, such as buffer overflow bugs.
This paper intends to discuss problems faced when writing signal-handling
routines, how to exploit the problems, and presents ideas of how to avoid such
issues. A working knowledge of the C programming language and UNIX-like
operating systems would benefit the reader greatly, but is certainly not
essential.
Signal Handling: An Overview
#############################
To understand what signal handlers are, one must first know what exactly a
signal is. In brief, signals are notifications delivered to a process to alert
the given process about "important" events concerning itself. For example,
users of an application can send signals using common keyboard Ctrl
combinations, such as Ctrl-C - which will send a SIGINT signal to the given
process.
Many different signals exist, but some of the more common (or useful) ones are:
SIGINT, SIGHUP, SIGKILL, SIGABRT, SIGTERM and SIGPIPE. Many more exist,
however. Below is a complete-ish list of available signals, according to the
POSIX.1 standard, along with a basic description of their purpose.
--
"Signal Value Action Comment
-------------------------------------------------------------------------
SIGHUP 1 Term Hangup detected on controlling terminal
or death of controlling process
SIGINT 2 Term Interrupt from keyboard
SIGQUIT 3 Core Quit from keyboard
SIGILL 4 Core Illegal Instruction
SIGABRT 6 Core Abort signal from abort(3)
SIGFPE 8 Core Floating point exception
SIGKILL 9 Term Kill signal
SIGSEGV 11 Core Invalid memory reference
SIGPIPE 13 Term Broken pipe: write to pipe with no readers
SIGALRM 14 Term Timer signal from alarm(2)
SIGTERM 15 Term Termination signal
SIGUSR1 30,10,16 Term User-defined signal 1
SIGUSR2 31,12,17 Term User-defined signal 2
SIGCHLD 20,17,18 Ign Child stopped or terminated
SIGCONT 19,18,25 Continue if stopped
SIGSTOP 17,19,23 Stop Stop process
SIGTSTP 18,20,24 Stop Stop typed at tty
SIGTTIN 21,21,26 Stop tty input for background process
SIGTTOU 22,22,27 Stop tty output for background process"
--
The above signal explanations have been extracted from the signal(7) man page
documentation.
Although the above list is quite complete regarding signals accepted in the
POSIX standards, other signals are available to programmers according to "SUSv2
and SUSv3 / POSIX 1003.1-2001".
When a process receives a signal documented in the list above, the default
action is taken, stated in "Action". However, this doesn't have to be how the
application reacts when a given signal is received - how the process handles the
signal when it receives it is entirely the choice of the programmer - this is
where signal handlers come in.
It is worth noting that the signals SIGKILL and SIGSTOP cannot be handled,
ignored or blocked. One can generally assume this is because a user must have a
way of terminating a process, should the program go awfully awry.
"What are signal handlers", one might ask. The simple answer is that signal
handlers are small routines which are typically called when a pre-defined
signal, or set of signals, is delivered to the process it is running under
before the end of program execution - after execution flow has been directed to
a signal handling function, all instructions within the handler are executed in
turn. In larger applications, however, signal handling routines are often
written to complete a more complex set of tasks to ensure clean termination of
the program, such as; unlinking of tempory files, freeing of memory buffers,
appending log messages, and freeing file descriptors and/or sockets. Signal
handlers are generally defined as ordinary program functions, and are then
defined as the default handler for a certain signal usually near to the
beginning of the program.
Consider the sample program below:
--- sigint.c ---
#include <stdio.h>
#include <signal.h>
void sighndlr() {
printf("Ctrl-C caught!\n");
exit(0);
}
int main() {
signal(SIGINT, sighndlr);
while(1)
sleep(1);
/* should never reach here */
return(0);
}
--- EOF ---
'sigint.c' specifies that the function 'sighndlr' should be given control of
execution flow when a SIGINT signal is received by the program. The program
sleeps "forever", or until a SIGINT signal is received - in which case the
"Ctrl-C caught!" message is printed to the terminal - as seen below:
--- output ---
[root@localhost shaun]# gcc test.c -o test
[root@localhost shaun]# ./test
[... program sleeps ...]
Ctrl-C caught!
[root@localhost shaun]#
--- EOF ---
Generally speaking, a SIGINT signal is delivered when a user hits the Ctrl-C
combination at the keyboard, but a SIGINT signal can be generated by the kill(1)
utility.
However simple or complex the signal handler is, there are several potential
pitfalls which must be avoided during the development of the handler. Although
a signal handler may look "safe", problems may still arise, but may be
less-obvious to the unsuspecting eye. There are two main classes of problems
when dealing with signal-handler development - non-atomic process modifications,
and non-reentrant code, both of which are potentially critical to system
security.
Non-atomic Modifications
#########################
Since signals can be delivered at almost any moment, and privileges often need
to be maintained (i.e root privileges in a SUID root application) for obvious
reasons (i.e for access to raw sockets, graphical resources, etc), signal
handling routines need to be written with extra care. If they are not, and
special privileges are held by the process at the particular time of signal
delivery, things could begin to go wrong very quickly. What is meant by
'non-atomic' is that the change in the program isn't permanant - it will just be
in place temporarily. To illustrate this, we will discuss a sample vulnerable
program.
Consider the following sample program:
--- atomicvuln.c ---
#include <stdio.h>
#include <signal.h>
void sighndlr() {
printf("Ctrl-C caught!\n");
printf("UID: %d\n", getuid());
/* other cleanup code... */
}
int showuid() {
printf("UID: %d\n", getuid());
return(0);
}
int main() {
int origuid = getuid();
signal(SIGINT, sighndlr);
setuid(0);
sleep(5);
setuid(origuid);
showuid();
return(0);
}
--- EOF ---
The above program should immediately spark up any security concious programmer's
paranoia, but the insecurity isn't immediately obvious to everyone. As we can
see from above, a signal handler is declared for 'SIGINT', and the program gives
itself root privileges (so to speak). After a delay of around five seconds, the
privileges are revoked, and the program is exited with success. However, if a
SIGINT signal is received, execution is directed to the SIGINT handler,
'sighdlr()'.
Let's look at some sample outputs:
--- output ---
[root@localhost shaun]# gcc test.c -o test
[root@localhost shaun]# chmod +s test
[root@localhost shaun]# exit
exit
[shaun@localhost shaun]$ ./test
[... program sleeps 5 seconds ...]
UID: 502
[shaun@localhost shaun]$ ./test
[... CTRL-C is typed ...]
Ctrl-C caught!
UID: 0
UID: 502
[shaun@localhost shaun]$
--- EOF ---
If you hadn't spotted the insecurity in 'atomicvuln.c' yet, the above output
should make things obvious; since the signal handling routine, 'sighdlr()', was
called when root privileges were still possessed, the friendly printf()
statements kindly tell us that our privileges are root (assuming the binary is
SUID root). And just to prove our theory, if we simply allow the program to
sleep for 5 seconds without sending an interrupt, the printf() statement kindly
tells us that our UID is 502 - my actual UID - as seen above.
With this, it is easy to understand where the flaw lies; if program execution
can be interrupted between the time when superuser privileges are given, and the
time when superuser privileges are revoked, the signal handling code *will* be
ran with root privileges. Just imagine - if the signal handling routine
included potentially sensitive code, compromisation of root privileges could
occur.
Although the sample program isn't an example of privilege escalation, it at
least demonstrates how non-atomic modifications can present security issues when
signal handling is involved. And do not assume that code similar to the sample
program above isn't found in popular security critical applications in
wide-spread use - it is. An example of vulnerable code similar to that of above
which is an application in wide-spread use, see [1] in the bibliography.
Non-reentrant Code
###################
Although it may not be obvious (and it's not), some glibc functions just weren't
designed to be reentered due to receipt of a signal, thus causing potential
problems for signal handlers which use them. An example of such a function is
the 'free()' function. According to 'free()'s man page, free()
"frees the memory space pointed to by ptr, which must have been
returned by a previous call to malloc(), calloc() or realloc(). Other-
wise, or if free(ptr) has already been called before, undefined
behaviour occurs. If ptr is NULL, no operation is performed."
As the man page snippet claims, free() can only be used to release memory which
was allocated using 'malloc()', else "undefined behavior" occurs. More
specifically, or in usual cases, the heap is corrupted, if free() is called on a
memory area which has already been free()d. Because of this implementation
design, reentrant signal routines which use 'free()' can be attacked.
Consider the below sample vulnerable program:
--- reentry.c ---
#include <stdio.h>
#include <signal.h>
#include <syslog.h>
#include <string.h>
#include <stdlib.h>
void *data1, *data2;
char *logdata;
void sighdlr() {
printf("Entered sighdlr()...\n");
syslog(LOG_NOTICE,"%s\n", logdata);
free(data2);
free(data1);
sleep(10);
exit(0);
}
int main(int argc, char *argv[]) {
logdata = argv[1];
data1 = strdup(argv[2]);
data2 = malloc(340);
signal(SIGHUP, sighdlr);
signal(SIGTERM, sighdlr);
sleep(10);
/* should never reach here */
return(0);
}
--- EOF ---
The above program defines a signal handler which frees allocated heap memory,
and sleeps for around 10 seconds. However, once the signal handler has been
entered, signals are not blocked, and thus can still be freely delivered. As we
learnt above, a duplicate call of free() on an already free()d memory area will
result in "undefined behavior" - possibly corruption of the heap memory. As we
can see, user-defined data is taken, and syslog() is also called from the sig
handler function - but how does syslog() work?
'syslog()' creates a memory buffer stream, using two malloc() invokations - the
first one allocates a 'stream description structure', whilst the other creates a
buffer suitable for the actual syslog message data. This basis is essentially
used to maintain a tempory copy of the syslog message.
But why can this cause problems in context of co-usage of non-reentrant
routines? To find the answer, let's experiment a little, by attempting to
exploit the above program, which happens to be vulnerable.
--- output ---
[shaun@localhost shaun]$ ./test `perl -e 'print "a"x100'` `perl -e 'print
"b"x410'` & sleep 1 ; killall -HUP test ; sleep 1 ; killall -TERM test
[1] 2877
Entered sighdlr()...
Entered sighdlr()...
[1]+ Segmentation fault (core dumped) ./test `perl -e 'print "a"x100'`
`perl -e 'print "b"x410'`
[shaun@localhost shaun]$ gdb -c core.2877
GNU gdb 5.2.1-2mdk (Mandrake Linux)
Copyright 2002 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i586-mandrake-linux-gnu".
Core was generated by `./test
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'.
Program terminated with signal 11, Segmentation fault.
#0 0x4008e9bb in ?? ()
(gdb) info reg
eax 0x61616161 1633771873
ecx 0x40138680 1075021440
edx 0x6965fa38 1768290872
ebx 0x4013c340 1075036992
esp 0xbfffeccc 0xbfffeccc
ebp 0xbfffed0c 0xbfffed0c
esi 0x80498d8 134519000
edi 0x61616160 1633771872
eip 0x4008e9bb 0x4008e9bb
eflags 0x10206 66054
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x2b 43
gs 0x2b 43
fctrl 0x0 0
fstat 0x0 0
ftag 0x0 0
fiseg 0x0 0
fioff 0x0 0
foseg 0x0 0
fooff 0x0 0
---Type <return> to continue, or q <return> to quit---
fop 0x0 0
xmm0 {f = {0x0, 0x0, 0x0, 0x0}} {f = {0, 0, 0, 0}}
xmm1 {f = {0x0, 0x0, 0x0, 0x0}} {f = {0, 0, 0, 0}}
xmm2 {f = {0x0, 0x0, 0x0, 0x0}} {f = {0, 0, 0, 0}}
xmm3 {f = {0x0, 0x0, 0x0, 0x0}} {f = {0, 0, 0, 0}}
xmm4 {f = {0x0, 0x0, 0x0, 0x0}} {f = {0, 0, 0, 0}}
xmm5 {f = {0x0, 0x0, 0x0, 0x0}} {f = {0, 0, 0, 0}}
xmm6 {f = {0x0, 0x0, 0x0, 0x0}} {f = {0, 0, 0, 0}}
xmm7 {f = {0x0, 0x0, 0x0, 0x0}} {f = {0, 0, 0, 0}}
mxcsr 0x0 0
orig_eax 0xffffffff -1
(gdb) quit
[shaun@localhost shaun]$
--- EOF ---
Interesting. As we can see above, our large string of 'a's has found its way
into several program registers on stack - EAX and EDI. From this, we can assume
we are witnessing the "undefined behavior" we discussed earlier, when the signal
handler is reentered.
When the sample vulnerable program receives the second
signal (SIGTERM), since signals are not being ignored, the signal handler is
reentered to handle this second signal, causing something to go very wrong.
But why is this happening?
Since the second memory region (*data2) was free()d during the first entry of
the signal handler, syslog() re-uses this released memory for its own purposes -
storing its syslog message, because as the short syslog() explanation above
stated, two malloc() calls are present in most syslog() implementations, and
thus it re-uses the newly free()d memory - *data2. After the usage of the
memory once held as data2 by syslog(), a second 'free()' call is made on the
memory region, because of reentry of the signal handler function. As the
free(3) man page stated, undefined behavior *will* occur if the memory area was
already free()d, and we happen to know that this was the case. So when 'free()'
was called again on *data2, free() landed somewhere in the area containing the
'a's (hence 0x61 in hex), because syslog() had re-used the freed area to store
the syslog message, temporarily.
As the GDB output above illustrates, as long as user-input is used by 'syslog()'
(and it is in this case), we have some control over the program registers, when
this "undefined behavior" (corruption of heap in most cases) occurs. Because of
this ability, exploitation is most likely a possibility - it is left as an
exercise to the reader to play with this sample vulnerable program a little
more, and determine if the vulnerability is exploitable.
For the interested reader, 'free()' is not the only non-reentrant glibc
function. In general, it can be assumed that all glibc functions which are NOT
included within the following list are non-reentrant, and thus are not safe to
be used in signal handlers.
--
_exit(2), access(2), alarm(3), cfgetispeed(3), cfgetospeed(3),
cfsetispeed(3), cfsetospeed(3), chdir(2), chmod(2), chown(2),
close(2), creat(3), dup(2), dup2(2), execle(3), 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(3), 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),
sigpause(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)."
--
Secure Signal Handling
#######################
In general, signal handling vulnerabilities can be prevented by
--
1) Using only reentrant glibc functions within signal handlers -
This safe-guards against the possibility of "undefined behavior" or otherwise as
presented in the above example. However, this isn't *always* feasible,
especially when a programmers needs to accomplish tasks such as freeing memory.
Other counter-measures, in this case, can protect against this. See below.
2) ignoring signals during signal handling routines -
As the obvious suggests, this programming practice will indefinately prevent
handling of signals during the execution of signal handling routines, thus
preventing signal handler reentry.
Consider the following signal handler template:
--- sighdlr.c ---
void sighdlr() {
signal(SIGINT, SIG_IGN);
signal(SIGABRT, SIG_IGN);
signal(SIGHUP, SIG_IGN);
/* ...ignore other signals ... */
/* cleanup code here */
exit(0);
}
--- EOF ---
As we can see above, signals are blocked before doing anything else in the
signal handling routine. This guarantees against signal handler reentry (or
almost does).
3) Ignoring signals whilst non-atomic process modifications are in place -
This involves blocking signals, in a similar way to the above code snippet,
during the execution of code with non-atomic modifications in place, such as
code execution with superuser privileges.
Consider the following code snippet:
--- nonatomicblock.c ---
/* code exec with non-atomic process modifications starts here... */
signal(SIGINT, SIG_IGN);
signal(SIGABRT, SIG_IGN);
signal(SIGHUP, SIG_IGN);
/* block other signals if desired... */
setuid(0);
/* sensitive code here */
setuid(getuid());
/* sensitive code ends here */
signal(SIGINT, SIG_DFL);
signal(SIGABRT, SIG_DFL);
signal(SIGHUP, SIG_DFL);
/* ...code here... */
--- EOF ---
Before executing privileged code, signals are blocked. After execution of the
privileged code, privileges are dropped, and the signal action is set back to
the default action.
There are probably more ways of preventing signal vulnerabilities, but the three
above should be enough to implement semi-safe signal handlers.
Conclusion
###########
I hope this paper has at least touched upon possible problems encountered when
dealing with signals in C applications. If nothing else can be taken away from
this paper, my aim is to have outlined that secure programming practices should
always be applied when implementing signal handlers. Full stop. Remember this.
If I have missed something out, given inaccurate information, or otherwise,
please feel free to drop me a line at the email address at the top of the paper,
providing your comments are nicely phrased.
Recommended reading is presented in the Bibliography below.
Bibliography
#############
Recommended reading material is:
--
"Delivering Signals for Fun and Profit" -
http://razor.bindview.com/publish/papers/signals.txt, Michal Zalewski. Michal's
paper was a useful resource when writing this paper, and many ideas were gained
from this paper. Thanks Michal.
"Introduction To Unix Signals Programming" -
http://users.actcom.co.il/~choo/lupg/tutorials/signals/signals-programming.html,LUGPs.
"Procmail insecure signal handling vulnerability" -
http://xforce.iss.net/xforce/xfdb/6872
"Traceroute signal handling vulnerability" -
http://lwn.net/2000/1012/a/traceroute.php3
"signal(2) man page" -
http://techpubs.sgi.com/library/tpl/cgi-bin/getdoc.cgi?coll=linux&db=man&fname=/usr/share/catman/man2/signal.2.html&srch=signal
"signal(7) man page" -
http://techpubs.sgi.com/library/tpl/cgi-bin/getdoc.cgi?coll=linux&db=man&fname=/usr/share/catman/man7/signal.7.html&srch=signal
--
Greets
#######
Greets to:
--
Friends at HDC (or former HDC members), excluded.org, #hackcanada, all @ GSO,
rider (happy be-lated birthday!).
All the other great people that I have met online.
--
<shaun2k2@excluded.org>
http://www.excluded.org
# milw0rm.com [2006-04-08]