To have a realistic discussion of exploit countermeasures and bypass methods, we first need a realistic exploitation target. A remote target will be a server program that accepts incoming connections. In Unix, these programs are usually system daemons. A daemon is a program that runs in the background and detaches from the controlling terminal in a certain way. The term daemon was first coined by MIT hackers in the 1960s. It refers to a molecule-sorting demon from an 1867 thought experiment by a physicist named James Maxwell. In the thought experiment, Maxwell's demon is a being with the supernatural ability to effortlessly perform difficult tasks, apparently violating the second law of thermodynamics. Similarly, in Linux, system daemons tirelessly perform tasks such as providing SSH service and keeping system logs. Daemon programs typically end with a d to signify they are daemons, such as sshd or syslogd.
This new code uses a call to the daemon() function, which will spawn a new background process. This function is used by many system daemon processes in Linux, and its man page is shown below.
Code View:
DAEMON(3) Linux Programmer's Manual DAEMON(3)
NAME
daemon - run in the background
SYNOPSIS
#include
int daemon(int nochdir, int noclose);
DESCRIPTION
The daemon() function is for programs wishing to detach themselves from
the controlling terminal and run in the background as system daemons.
Unless the argument nochdir is non-zero, daemon() changes the current
working directory to the root ("/").
Unless the argument noclose is non-zero, daemon() will redirect stan
dard input, standard output and standard error to /dev/null.
RETURN VALUE
(This function forks, and if the fork() succeeds, the parent does
_exit(0), so that further errors are seen by the child only.) On suc
cess zero will be returned. If an error occurs, daemon() returns -1
and sets the global variable errno to any of the errors specified for
the library functions fork(2) and setsid(2).
System daemons run detached from a controlling terminal, so the new tinyweb daemon code writes to a log file. Without a controlling terminal, system daemons are typically controlled with signals. The new tinyweb daemon program will need to catch the terminate signal so it can exit cleanly when killed.
1. Crash Course in Signals
Signals provide a method of interprocess communication in Unix. When a process receives a signal, its flow of execution is interrupted by the operating system to call a signal handler. Signals are identified by a number, and each one has a default signal handler. For example, when CTRL-C is typed in a program's controlling terminal, an interrupt signal is sent, which has a default signal handler that exits the program. This allows the program to be interrupted, even if it is stuck in an infinite loop.
Custom signal handlers can be registered using the signal() function. In the example code below, several signal handlers are registered for certain signals, whereas the main code contains an infinite loop.
1.1. signal_example.c
Code View:
#include
#include
#include
/* Some labeled signal defines from signal.h
* #define SIGHUP 1 Hangup
* #define SIGINT 2 Interrupt (Ctrl-C)
* #define SIGQUIT 3 Quit (Ctrl-\)
* #define SIGILL 4 Illegal instruction
* #define SIGTRAP 5 Trace/breakpoint trap
* #define SIGABRT 6 Process aborted
* #define SIGBUS 7 Bus error
* #define SIGFPE 8 Floating point error
* #define SIGKILL 9 Kill
* #define SIGUSR1 10 User defined signal 1
* #define SIGSEGV 11 Segmentation fault
* #define SIGUSR2 12 User defined signal 2
* #define SIGPIPE 13 Write to pipe with no one reading
* #define SIGALRM 14 Countdown alarm set by alarm()
* #define SIGTERM 15 Termination (sent by kill command)
* #define SIGCHLD 17 Child process signal
* #define SIGCONT 18 Continue if stopped
* #define SIGSTOP 19 Stop (pause execution)
* #define SIGTSTP 20 Terminal stop [suspend] (Ctrl-Z)
* #define SIGTTIN 21 Background process trying to read stdin
* #define SIGTTOU 22 Background process trying to read stdout
*/
/* A signal handler */
void signal_handler(int signal) {
printf("Caught signal %d\t", signal);
if (signal == SIGTSTP)
printf("SIGTSTP (Ctrl-Z)");
else if (signal == SIGQUIT)
printf("SIGQUIT (Ctrl-\\)");
else if (signal == SIGUSR1)
printf("SIGUSR1");
else if (signal == SIGUSR2)
printf("SIGUSR2");
printf("\n");
}
void sigint_handler(int x) {
printf("Caught a Ctrl-C (SIGINT) in a separate handler\nExiting.\n");
exit(0);
}
int main() {
/* Registering signal handlers */
signal(SIGQUIT, signal_handler); // Set signal_handler() as the
signal(SIGTSTP, signal_handler); // signal handler for these
signal(SIGUSR1, signal_handler); // signals.
signal(SIGUSR2, signal_handler);
signal(SIGINT, sigint_handler); // Set sigint_handler() for SIGINT.
while(1) {} // Loop forever.
}
When this program is compiled and executed, signal handlers are registered, and the program enters an infinite loop. Even though the program is stuck looping, incoming signals will interrupt execution and call the registered signal handlers. In the output below, signals that can be triggered from the controlling terminal are used. The signal_handler() function, when finished, returns execution back into the interrupted loop, whereas the sigint_handler() function exits the program.
reader@hacking:~/booksrc $ gcc -o signal_example signal_example.c
reader@hacking:~/booksrc $ ./signal_example
Caught signal 20 SIGTSTP (Ctrl-Z)
Caught signal 3 SIGQUIT (Ctrl-\)
Caught a Ctrl-C (SIGINT) in a separate handler
Exiting.
reader@hacking:~/booksrc $
Specific signals can be sent to a process using the kill command. By default, the kill command sends the terminate signal (SIGTERM) to a process. With the -l command-line switch, kill lists all the possible signals. In the output below, the SIGUSR1 and SIGUSR2 signals are sent to the signal_example program being executed in another terminal.
Code View:
reader@hacking:~/booksrc $ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
9) SIG KILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT
17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU
25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH
29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN
35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4
39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12
47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14
51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10
55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6
59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
reader@hacking:~/booksrc $ ps a | grep signal_example
24491 pts/3 R+ 0:17 ./signal_example
24512 pts/1 S+ 0:00 grep signal_example
reader@hacking:~/booksrc $ kill -10 24491
reader@hacking:~/booksrc $ kill -12 24491
reader@hacking:~/booksrc $ kill -9 24491
reader@hacking:~/booksrc $
Finally, the SIGKILL signal is sent using kill -9. This signal's handler cannot be changed, so kill -9 can always be used to kill processes. In the other terminal, the running signal_example shows the signals as they are caught and the process is killed.
reader@hacking:~/booksrc $ ./signal_example
Caught signal 10 SIGUSR1
Caught signal 12 SIGUSR2
Killed
reader@hacking:~/booksrc $
Signals themselves are pretty simple; however, interprocess communication can quickly become a complex web of dependencies. Fortunately, in the new tinyweb daemon, signals are only used for clean termination, so the implementation is simple.
2. Tinyweb Daemon
This newer version of the tinyweb program is a system daemon that runs in the background without a controlling terminal. It writes its output to a log file with timestamps, and it listens for the terminate (SIGTERM) signal so it can shut down cleanly when it's killed.
These additions are fairly minor, but they provide a much more realistic exploit target. The new portions of the code are shown in bold in the listing below.
2.1. tinywebd.c
Code View:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "hacking.h"
#include "hacking-network.h"
#define PORT 80 // The port users will be connecting to
#define WEBROOT "./webroot" // The webserver's root directory
#define LOGFILE "/var/log/tinywebd.log" // Log filename
int logfd, sockfd; // Global log and socket file descriptors
void handle_connection(int, struct sockaddr_in *, int);
int get_file_size(int); // Returns the file size of open file descriptor
void timestamp(int); // Writes a timestamp to the open file descriptor
// This function is called when the process is killed.
void handle_shutdown(int signal) {
timestamp(logfd);
write(logfd, "Shutting down.\n", 16);
close(logfd);
close(sockfd);
exit(0);
}
int main(void) {
int new_sockfd, yes=1;
struct sockaddr_in host_addr, client_addr; // My address information
socklen_t sin_size;
logfd = open(LOGFILE, O_WRONLY|O_CREAT|O_APPEND, S_IRUSR|S_IWUSR);
if(logfd == -1)
fatal("opening log file");
if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1)
fatal("in socket");
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1)
fatal("setting socket option SO_REUSEADDR");
printf("Starting tiny web daemon.\n");
if(daemon(1, 0) == -1) // Fork to a background daemon process.
fatal("forking to daemon process");
signal(SIGTERM, handle_shutdown); // Call handle_shutdown when killed.
signal(SIGINT, handle_shutdown); // Call handle_shutdown when interrupted.
timestamp(logfd);
write(logfd, "Starting up.\n", 15);
host_addr.sin_family = AF_INET; // Host byte order
host_addr.sin_port = htons(PORT); // Short, network byte order
host_addr.sin_addr.s_addr = INADDR_ANY; // Automatically fill with my IP.
memset(&(host_addr.sin_zero), '\0', 8); // Zero the rest of the struct.
if (bind(sockfd, (struct sockaddr *)&host_addr, sizeof(struct sockaddr)) == -1)
fatal("binding to socket");
if (listen(sockfd, 20) == -1)
fatal("listening on socket");
while(1) { // Accept loop.
sin_size = sizeof(struct sockaddr_in);
new_sockfd = accept(sockfd, (struct sockaddr *)&client_addr, &sin_size);
if(new_sockfd == -1)
fatal("accepting connection");
handle_connection(new_sockfd, &client_addr, logfd);
}
return 0;
}
/* This function handles the connection on the passed socket from the
*.passed client address and logs to the passed FD. The connection is
*.processed as a web request and this function replies over the connected
*.socket. Finally, the passed socket is closed at the end of the function.
*/
void handle_connection(int sockfd, struct sockaddr_in *client_addr_ptr, int logfd) {
unsigned char *ptr, request[500], resource[500], log_buffer[500];
int fd, length;
length = recv_line(sockfd, request);
sprintf(log_buffer, "From %s:%d \"%s\"\t", inet_ntoa(client_addr_ptr->sin_addr),
ntohs(client_addr_ptr->sin_port), request);
ptr = strstr(request, " HTTP/"); // Search for valid-looking request.
if(ptr == NULL) { // Then this isn't valid HTTP
strcat(log_buffer, " NOT HTTP!\n");
} else {
*ptr = 0; // Terminate the buffer at the end of the URL.
ptr = NULL; // Set ptr to NULL (used to flag for an invalid request).
if(strncmp(request, "GET ", 4) == 0) // Get request
ptr = request+4; // ptr is the URL.
if(strncmp(request, "HEAD ", 5) == 0) // Head request
ptr = request+5; // ptr is the URL.
if(ptr == NULL) { // Then this is not a recognized request
strcat(log_buffer, " UNKNOWN REQUEST!\n");
} else { // Valid request, with ptr pointing to the resource name
if (ptr[strlen(ptr) - 1] == '/') // For resources ending with '/',
strcat(ptr, "index.html"); // add 'index.html' to the end.
strcpy(resource, WEBROOT); // Begin resource with web root path
strcat(resource, ptr); // and join it with resource path.
fd = open(resource, O_RDONLY, 0); // Try to open the file.
if(fd == -1) { // If file is not found
strcat(log_buffer, " 404 Not Found\n");
send_string(sockfd, "HTTP/1.0 404 NOT FOUND\r\n");
send_string(sockfd, "Server: Tiny webserver\r\n\r\n");
send_string(sockfd, "");
send_string(sockfd, "URL not found
\r\n");
} else { // Otherwise, serve up the file.
strcat(log_buffer, " 200 OK\n");
send_string(sockfd, "HTTP/1.0 200 OK\r\n");
send_string(sockfd, "Server: Tiny webserver\r\n\r\n");
if(ptr == request + 4) { // Then this is a GET request
if( (length = get_file_size(fd)) == -1)
fatal("getting resource file size");
if( (ptr = (unsigned char *) malloc(length)) == NULL)
fatal("allocating memory for reading resource");
read(fd, ptr, length); // Read the file into memory.
send(sockfd, ptr, length, 0); // Send it to socket.
free(ptr); // Free file memory.
}
close(fd); // Close the file.
} // End if block for file found/not found.
} // End if block for valid request.
} // End if block for valid HTTP.
timestamp(logfd);
length = strlen(log_buffer);
write(logfd, log_buffer, length); // Write to the log.
shutdown(sockfd, SHUT_RDWR); // Close the socket gracefully.
}
/* This function accepts an open file descriptor and returns
* the size of the associated file. Returns -1 on failure.
*/
int get_file_size(int fd) {
struct stat stat_struct;
if(fstat(fd, &stat_struct) == -1)
return -1;
return (int) stat_struct.st_size;
}
/* This function writes a timestamp string to the open file descriptor
*.passed to it.
*/
void timestamp(fd) {
time_t now;
struct tm *time_struct;
int length;
char time_buffer[40];
time(&now); // Get number of seconds since epoch.
time_struct = localtime((const time_t *)&now); // Convert to tm struct.
length = strftime(time_buffer, 40, "%m/%d/%Y %H:%M:%S> ", time_struct);
write(fd, time_buffer, length); // Write timestamp string to log.
}
This daemon program forks into the background, writes to a log file with timestamps, and cleanly exits when it is killed. The log file descriptor and connection-receiving socket are declared as globals so they can be closed cleanly by the handle_shutdown() function. This function is set up as the callback handler for the terminate and interrupt signals, which allows the program to exit gracefully when it's killed with the kill command.
The output below shows the program compiled, executed, and killed. Notice that the log file contains timestamps as well as the shutdown message when the program catches the terminate signal and calls handle_shutdown()to exit gracefully.
reader@hacking:~/booksrc $ gcc -o tinywebd tinywebd.c
reader@hacking:~/booksrc $ sudo chown root ./tinywebd
reader@hacking:~/booksrc $ sudo chmod u+s ./tinywebd
reader@hacking:~/booksrc $ ./tinywebd
Starting tiny web daemon.
reader@hacking:~/booksrc $ ./webserver_id 127.0.0.1
The web server for 127.0.0.1 is Tiny webserver
reader@hacking:~/booksrc $ ps ax | grep tinywebd
25058 ? Ss 0:00 ./tinywebd
25075 pts/3 R+ 0:00 grep tinywebd
reader@hacking:~/booksrc $ kill 25058
reader@hacking:~/booksrc $ ps ax | grep tinywebd
25121 pts/3 R+ 0:00 grep tinywebd
reader@hacking:~/booksrc $ cat /var/log/tinywebd.log
cat: /var/log/tinywebd.log: Permission denied
reader@hacking:~/booksrc $ sudo cat /var/log/tinywebd.log
07/22/2007 17:55:45> Starting up.
07/22/2007 17:57:00> From 127.0.0.1:38127 "HEAD / HTTP/1.0" 200 OK
07/22/2007 17:57:21> Shutting down.
reader@hacking:~/booksrc $
This tinywebd program serves HTTP content just like the original tinyweb program, but it behaves as a system daemon, detaching from the controlling terminal and writing to a log file. Both programs are vulnerable to the same overflow exploit; however, the exploitation is only the beginning. Using the new tinyweb daemon as a more realistic exploit target, you will learn how to avoid detection after the intrusion.