Spoiler-Free OverTheWire Narnia Guide :: Levels 5 – 8

Level 5

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

int main(int argc, char **argv){
    int i = 1;
    char buffer[64];

    snprintf(buffer, sizeof buffer, argv[1]);
    buffer[sizeof (buffer) - 1] = 0;
    printf("Change i's value from 1 -> 500. ");

    if(i==500){
        printf("GOOD\n");
        system("/bin/sh");
    }

    printf("No way...let me give you a hint!\n");
    printf("buffer : [%s] (%d)\n", buffer, strlen(buffer));
    printf ("i = %d (%p)\n", i, &i);
    return 0;
}

General strategy

Reading the source code tells us that we don’t need to supply our own shellcode or pop a shell by ourselves. If we can change the value of i to 500, the script opens a shell for us!

What’s the gotcha? It’s the fact that we are no longer in Buffer Overflow land. See line 10, where buffer is manually terminated with 0 and line 11, instead of the usual insecure strcpy, we have a new mate onboard called snprintf.

This is the first level in Narnia where we’re exploiting a format string vulnerability.

If you need to brush up on format string vulnerabilities, I’d highly recommend this paper. I have read dozens of tutorials, blog posts, and papers on the subject, and that one is by far the most readable and comprehensive. If you’re short on time, read at least the first 4 chapters.

Hint #1

The %n specifier for a format string writes the size of the bytes already printed into the pointed location. In this case, we supply the format string to snprintf, which means we can write arbitrary data to any address we want.

Read chapters 3 and 4 for how to use %n.

Hint #2

The direct access parameter (chapter 4.3) looks useful. It allows us to specify which parameter we want %n to write to.

Hint #3

%300u or %300x creates a string that is 300 characters long. So with a payload like \xef\xbe\xad\xde%6x%1$n, you’re writing 10 to the first parameter’s location, which is 0xdeadbeef.

Level 6

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

extern char **environ;

// tired of fixing values...
// - morla
unsigned long get_sp(void) {
       __asm__("movl %esp,%eax\n\t"
               "and $0xff000000, %eax"
               );
}

int main(int argc, char *argv[]){
    char b1[8], b2[8];
    int  (*fp)(char *)=(int(*)(char *))&puts, i;

    if(argc!=3){ printf("%s b1 b2\n", argv[0]); exit(-1); }

    /* clear environ */
    for(i=0; environ[i] != NULL; i++)
        memset(environ[i], '\0', strlen(environ[i]));
    /* clear argz    */
    for(i=3; argv[i] != NULL; i++)
        memset(argv[i], '\0', strlen(argv[i]));

    strcpy(b1,argv[1]);
    strcpy(b2,argv[2]);
    //if(((unsigned long)fp & 0xff000000) == 0xff000000)
    if(((unsigned long)fp & 0xff000000) == get_sp())
        exit(-1);
    fp(b1);

    exit(1);
}

General strategy

The script clears out all the environment variables and any command line arguments after the first two, so it doesn’t look like we’ll be storing any shellcode there. But we have fp pointing to puts and executing b1, which we do control via argv[1]. This means if we can get fp pointing to a more convenient function, we’re done. Turns out, stdlib (hence system included) is already included for us, so we can easily point to system’s address!

This approach of returning to a libc function to exploit it is called a ret2libc.

If the script looks confusing, I suggest playing around with it in gdb so you can see what is actually happening. Try r AAAAAAAABBBB CCCCCCCCDDDD.

Hint #1

After trying r AAAAAAAABBBB CCCCCCCCDDDD, you’ll notice that now the program is trying to execute BBBB with DDDD as its argument. What would be a convenient function for us to replace it with?

Hint #2

Maybe system and /bin/sh as its argument?

Hint #3

p system in gdb to get system’s address.

Level 7

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

int goodfunction();
int hackedfunction();

int vuln(const char *format){
        char buffer[128];
        int (*ptrf)();

        memset(buffer, 0, sizeof(buffer));
        printf("goodfunction() = %p\n", goodfunction);
        printf("hackedfunction() = %p\n\n", hackedfunction);

        ptrf = goodfunction;
        printf("before : ptrf() = %p (%p)\n", ptrf, &ptrf);

        printf("I guess you want to come to the hackedfunction...\n");
        sleep(2);
 				ptrf = goodfunction;

        snprintf(buffer, sizeof buffer, format);

        return ptrf();
}

int main(int argc, char **argv){
        if (argc <= 1){
                fprintf(stderr, "Usage: %s <buffer>\n", argv[0]);
                exit(-1);
        }
        exit(vuln(argv[1]));
}

int goodfunction(){
        printf("Welcome to the goodfunction, but i said the Hackedfunction..\n");
        fflush(stdout);

        return 0;
}

int hackedfunction(){
        printf("Way to go!!!!");
        fflush(stdout);
        system("/bin/sh");

        return 0;
}

General Strategy

We see no strcpy or friends anywhere, but we have a snprintf where we have total control over the format string, and a hackedfunction, which conveniently opens a shell for us. So we can guess that we’re going to exploit a format string vulnerability to execute hackedfunction.

Seeing as ptrf gets pointed to goodfunction, and gets executed right after our format string vulnerability, we have the opportunity to point ptrf to a more convenient function instead.

Hint #1

hackedfunction is looking like a prime candidate for ptrf to point to.

Hint #2

To get the address of ptrf that we’re overwriting, just execute narnia7, and it will very happily print it out. Or just inspect it in gdb.

Hint #3

In level 5, we used %496x to write the number 500. The script also prints out the address of hackedfunction, which we can convert to decimal to get the target value to write with.

Hint #4

Remember the general payload format looks like <address to overwrite>%<target value or address>x%n. For an example in python, \x10\xd5\xff\xff%134514468x%n.

Level 8

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

int i;

void func(char *b){
        char *blah=b;
        char bok[20];
        //int i=0;

        memset(bok, '\0', sizeof(bok));
        for(i=0; blah[i] != '\0'; i++)
                bok[i]=blah[i];

        printf("%s\n",bok);
}

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

        if(argc > 1)
                func(argv[1]);
        else
        printf("%s argument\n", argv[0]);

        return 0;
}

General strategy

If you skipped directly to this level in this article, here is the paper again that I’d fully recommend for an comprehensive introduction to format string vulnerabilities.

It looks like we’re copying argv[1] to bok without bounds checking, so we’re dealing with a buffer overflow here. But when we try something like r $(python -c 'print "A" * 30') in gdb, eip is not overwriten to be AAAA. What’s going on here?

A very useful break point to have is the printf function in func. Set a break point there and examine the stack with x/20x $esp each time. If you can see how the stack is structured at this break point, you’ll understand why we can’t seem to overflow the buffer by more than 1 byte.

After that, it’s just a matter of putting shellcode in an environment variable and returning to it.

Hint #1 (Why can’t we overflow by more than one byte?)

Let’s examine the stack at the printf break point. The payload here is “A” * 20.

gdb-peda$ x/20wx $esp
0xffffd578:     0xffffd57c      0x41414141      0x41414141      0x41414141
0xffffd588:     0x41414141      0x41414141      0xffffd79d      0x00000000
0xffffd598:     0xffffd5a8      0x08048517      0xffffd79d      0x00000000
0xffffd5a8:     0x00000000      0xf7e05e91      0x00000002      0xffffd644
0xffffd5b8:     0xffffd650      0xffffd5d4      0x00000001      0x00000000

And let’s see what the address right after our payload is pointing to.

gdb-peda$ x/10wx 0xffffd79d
0xffffd79d:     0x41414141      0x41414141      0x41414141      0x41414141
0xffffd7ad:     0x41414141      0x5f434c00      0x3d4c4c41      0x555f6e65
0xffffd7bd:     0x54552e53      0x00382d46

That address is pointing to our payload itself. When we start to overwrite that adddress, it stops pointing to our payload, so the for loop can no longer continue copying our payload to bok. This is why it stops overflowing after just one byte.

How do we make sure we can continue overflowing the buffer and thus get control of the eip? We do that by overwriting that address by itself to preserve it. For example, we’d want "A" * 20 + "\x9d\xd7\xff\xff" here.

Hint #2 (Why does that address keep changing?)

Notice that we have two copies of the pointer address on the stack. The first one is the one we’re trying to overwrite (and thus preserve), and the second one is always the correct one.

Say we start adding the original address one byte at a time:

gdb-peda$ r $(python -c 'print "A"*20 + "\x9d"')
<skipped>
gdb-peda$ x/20wx $esp
0xffffd578:     0xffffd57c      0x41414141      0x41414141      0x41414141
0xffffd588:     0x41414141      0x41414141      0xffff4c9d      0x00000000
0xffffd598:     0xffffd5a8      0x08048517      0xffffd79c      0x00000000
0xffffd5a8:     0x00000000      0xf7e05e91      0x00000002      0xffffd644
0xffffd5b8:     0xffffd650      0xffffd5d4      0x00000001      0x00000000

Why is it that the correct address has decreased by one byte to 0xffffd79c from 0xffffd79d?

It’s because we just added one byte to our payload and the stack had to allocate one more byte for it! And remember the stack grows backwards, which means the address decreases instead of increases.

This means that for each byte that we add to our payload, we need to decrease our target address by one byte. For example:

gdb-peda$ r $(python -c 'print "A"*20 + "\x9c"')
<skipped>
gdb-peda$ x/20wx $esp
0xffffd578:     0xffffd57c      0x41414141      0x41414141      0x41414141
0xffffd588:     0x41414141      0x41414141      0xffffd79c      0x00000000
0xffffd598:     0xffffd5a8      0x08048517      0xffffd79c      0x00000000
0xffffd5a8:     0x00000000      0xf7e05e91      0x00000002      0xffffd644
0xffffd5b8:     0xffffd650      0xffffd5d4      0x00000001      0x00000000

Now we have correctly “preserved” one byte of the address!

Tip

If you find this level confusing and a lot more complex than previous ones, review the stack and the x86 calling convention again. Once you’re familiar with how the stack functions, solving this one will be a lot more intuitive.

Conclusion

Congratulations on completing Narnia! In my opinion, it is not the easiest entry into binary exploitation. If you completed every level but still don’t fully comprehend how your solution worked, I suggest trying Protostar for some gentler practice. If you’re ready for more, try Behemoth next!

I’ll cover both of them in the future, with Behemoth being the next one!

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