Spoiler-Free OverTheWire Narnia Guide :: Levels 0 – 4

Why a spoiler-free guide?

If you’re into puzzle games, I’m sure you have experienced the frustration of being stuck on that one level and ready to defenestrate yourself, but also not willing to take a peek at the solution online and deprive yourself of the pleasure of solving it yourself.

Narnia is one of the most well-known wargames out there for an introduction to binary exploitation. There is also very little guidance for beginners who might not feel comfortable asking for help in irc channels, so now that I’ve finished the entire game I’d like to write the spoiler-free guide that I wish I had when I was starting out.

Audience

This guide is written for people who are familiar with C and have some theoretical knowledge of stack overflows but who might need a little more direction in actually implementing an exploit.

If you need a refresher on stack overflows, I recommend this blog post. I’d also recommend you read up on the memory layout for C programs. As long as you are more or less solid on stack overflows and how the memory of a C program is laid out, you should be able to get through Narnia without much trouble.

Locally compiling the challenges

I personally feel more comfortable working on my machine and not stressing over wasting OverTheWire’s bandwidth while I try to work through the challenges. Here’s what I did to make sure the challenges work on my machine (Ubuntu 18.04 x86_64).

Disable ASLR

sudo bash -c 'echo 0 > /proc/sys/kernel/randomize_va_space'

Compile with these flags

gcc <in-file> -o <out-file> -w -g -Wno-format -Wno-format-security -fno-stack-protector -mp referred-stack-boundary=2 -z execstack -no-pie -Wl,-z,norelro -m32

I recommend having a handy bash function for it. Mine looks like

gcco () {
     file=$1
     gcc $1 -o ${file%.c} -w -g -Wno-format -Wno-format-security -fno-stack-protector -mp    referred-stack-boundary=2 -z execstack -no-pie -Wl,-z,norelro -m32
}

PEDA

With peda installed, you can easily check what security measures are enabled in an executable with checksec.

General Tips

SSH Tip

Typing in <level>@narnia.labs.overthewire.org -p 2226 for every single level can be tiring. I recommend setting up a shorthand for Narnia by updating ~/.ssh/config (or wherever your config file is located) to include the following:

Host narnia
    HostName narnia.labs.overthewire.org
    port 2226

You can now log into the levels with <level>@narnia!

Shell Tips

  • You can verify that your exploit succeeded by running id or whoami in your newly spawned shell (if you choose to spawn a shell).

Level 0

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

int main(){
    long val=0x41414141;
    char buf[20];

    printf("Correct val's value from 0x41414141 -> 0xdeadbeef!\n");
    printf("Here is your chance: ");
    scanf("%24s",&buf);

    printf("buf: %s\n",buf);
    printf("val: 0x%08x\n",val);

    if(val==0xdeadbeef){
        setreuid(geteuid(),geteuid());
        system("/bin/sh");
    }
    else {
        printf("WAY OFF!!!!\n");
        exit(1);
    }

    return 0;
}

General strategy

Our goal here is to somehow read the password to level 1 located at /etc/narnia_pass/narnia1 using this executable. Reading the code, we find that if val is changed to 0xdeadbeef, we’ll get a shell. And because the levels are are SUID (Set User ID, a type of Unix file flag that allows a normal user to execute the program with the privileges of the executable’s owner), we can then read the password with the privileges of user level1.

There is no direct way of changing the value of val, but with the scanf we do have control of buf. Also notice that scanf is not checking how many characters we can give it. Due to the stack growing from top to bottom (higher address to lower addresses), that means by overflowing buf, we are able to reach val and change its value. Here’s an illustration of what the stack very roughly looks like.

Illustration of the stack

Gentle nudge

Try inputing less than 20 characters and more than 20 characters to see what happens.

(python -c 'print "B" * 19') | ./narnia0 (python -c 'print "B" * 21') | ./narnia0

Tips

  • \\x42 is the typical hex representation of “B” and \\x41 is “A”.
  • Recall that x86 is little endian, so you have to reverse the order of bytes when working with addresses, data, shellcode, etc. For example, “ABCD” would be \\x44\\x43\\x42\\x41 in little endian.

Note

Even when you get the payload correct, you might run into the problem of the shell immediately exiting and not allowing you to cat the password. You can append a cat command without arguments to the end of your payload to keep the shell open. Here’s an explanation as to how that works.

Level 1

#include <stdio.h>

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

        if(getenv("EGG")==NULL){
                printf("Give me something to execute at the env-variable EGG\n");
                exit(1);
        }

        printf("Trying to execute EGG!\n");
        ret = getenv("EGG");
        ret();

        return 0;
}

General strategy

The solution seems to be straightforward, as the program itself tells us that it’s going to execute whatever we put at the environment variable EGG.

Could you make it spawn a shell with export EGG=<your code>?

Tips

  • Shellcode is a sequence of machine code instructions usually intended for executing arbitrary commands.

  • Check shellstorm for pre-made shellcodes. Our architecture is Intel x86. Always double check that the distro matches, too, otherwise the shellcode won’t work.

  • Or use msfvenom to generate your own shellcode.

  • Remember that shellcode is hexadecimal and you might need to export it like

    export EGG=$'<shellcode>'

    or

    export EGG=$(python -c 'print <shellcode>')

Which pre-made shellcode should I use?

It really depends on what you’re trying to accomplish, since shellcode is just compiled machine code of whatever (usually written in C) program you can write. But for the levels in Narnia where getting passwords to the next level depends on the SUID of the levels, shellcode generated from an “execve(‘/bin/sh’)” call will allow you to run arbitrary commands (including reading the passwords) and is a reliable one to try.

Why can’t I just put /bin/sh inside EGG?

Why can’t I use normal C code here? Why do I have to export EGG to be shellcode? Let’s turn to gdb to find the answer.

gdb-peda$ disass main
# skipping to the relevant part
   0x080484a1 <+59>:    lea    eax,[ebx-0x1233]
   0x080484a7 <+65>:    push   eax
   0x080484a8 <+66>:    call   0x8048310 <puts@plt>
   0x080484ad <+71>:    add    esp,0x4
   0x080484b0 <+74>:    lea    eax,[ebx-0x126c]
   0x080484b6 <+80>:    push   eax
   0x080484b7 <+81>:    call   0x8048300 <getenv@plt>
   0x080484bc <+86>:    add    esp,0x4
   0x080484bf <+89>:    mov    DWORD PTR [ebp-0x8],eax
   0x080484c2 <+92>:    mov    eax,DWORD PTR [ebp-0x8]
   0x080484c5 <+95>:    call   eax
   0x080484c7 <+97>:    mov    eax,0x0
   0x080484cc <+102>:   mov    ebx,DWORD PTR [ebp-0x4]
   0x080484cf <+105>:   leave
   0x080484d0 <+106>:   ret
End of assembler dump.

As we can see on line 0x080484c5 <+95> we’re calling EAX directly. A call instruction does 2 things:

  1. Push the address of the next instruction to the stack so that we can come back later.
  2. Set the EIP to the address we’re supplied with and immediately start executing from there.

EAX points to whatever we put in EGG. So our call is expecting raw machine code at EAX for execution, and not any uncompiled C code or bash command.

Level 2

#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]);
                exit(1);
        }
        strcpy(buf,argv[1]);
        printf("%s", buf);

        return 0;
}

General strategy

We have a pretty short and simple program here. The buffer is set to have 128 characters, and the strcpy call doesn’t check how long argv[1] is. Sweet! Can you take control of the EIP by overflowing argv[1]?

Try giving it 127 characters, and then 140 characters in gdb to see the difference.

When you input 140 A’s, the process segfaults at address 0x41414141 (“AAAA”) which means it’s trying to execute whatever instructions there are at 0x41414141 since EIP points to 0x41414141, but it’s not really a valid address. Can you make EIP point to an address that you control instead? And if that address contains shellcode, you’ll have a shell!

Level 3

#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";
    char ifile[32];
    char buf[32];

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

    /* open files */
    strcpy(ifile, argv[1]);
    if((ofd = open(ofile,O_RDWR)) < 0 ){
        printf("error opening %s\n", ofile);
        exit(-1);
    }
    if((ifd = open(ifile, O_RDONLY)) < 0 ){
        printf("error opening %s\n", ifile);
        exit(-1);
    }

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

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

    exit(1);
}

General Strategy

At a quick glance, we can see that this script is supposed to copy the contents from ifile, which we can supply with argv[1], to /dev/null. Remember that our goal here is to get access to the password to the next level.

Hint #1

What do we have control over here? It’s ifile. And it looks like strcpy is not bounds-checking. What can we do with that?

Hint #2

Notice how ifile is declared directly below ofile, which means on the stack it is stored before ofile. What happens if we do a buffer overflow?

Hint #3

What if we overflow the /dev/null and thus redirect our target file to a place where we have read & write access to? /tmp might be a good candidate.

Hint #4

We can refer to a file by another name by using a soft link.

Level 4

#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];

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

    if(argc>1)
        strcpy(buffer,argv[1]);

    return 0;
}

General Strategy

Hint #1

The memset line erases the environment variables. So we can’t store our shellcode in the environment variables.

Hint #2

strcpy looks like a great target of buffer overflow there without any bounds-checking. If we overflow it, we can seize control over the EIP. Now we just need to point EIP at our shellcode.

Hint #3

If we can’t store the shellcode in one of the environment variables, where else could we store it?

Hint #4

We can store it in the only place we currently have control over.

If you like my approach to these Narnia wargames, maybe you’d like my fun & interactive cybersec courses too :)