Why a spoiler-free guide?
If you’re into puzzle games, I’m sure you have experienced the frustration of being stuck on that one level and ready to defenestrate yourself, but also not willing to take a peek at the solution online and deprive yourself of the pleasure of solving it yourself.
Narnia is one of the most well-known wargames out there for an introduction to binary exploitation. There is also very little guidance for beginners who might not feel comfortable asking for help in irc channels, so now that I’ve finished the entire game I’d like to write the spoiler-free guide that I wish I had when I was starting out.
Audience
This guide is written for people who are familiar with C and have some theoretical knowledge of stack overflows but who might need a little more direction in actually implementing an exploit.
If you need a refresher on stack overflows, I recommend this blog post. I’d also recommend you read up on the memory layout for C programs. As long as you are more or less solid on stack overflows and how the memory of a C program is laid out, you should be able to get through Narnia without much trouble.
Locally compiling the challenges
I personally feel more comfortable working on my machine and not stressing over wasting OverTheWire’s bandwidth while I try to work through the challenges. Here’s what I did to make sure the challenges work on my machine (Ubuntu 18.04 x86_64).
Disable ASLR
sudo bash -c 'echo 0 > /proc/sys/kernel/randomize_va_space'
Compile with these flags
gcc <in-file> -o <out-file> -w -g -Wno-format -Wno-format-security -fno-stack-protector -mp referred-stack-boundary=2 -z execstack -no-pie -Wl,-z,norelro -m32
I recommend having a handy bash function for it. Mine looks like
gcco () {
file=$1
gcc $1 -o ${file%.c} -w -g -Wno-format -Wno-format-security -fno-stack-protector -mp referred-stack-boundary=2 -z execstack -no-pie -Wl,-z,norelro -m32
}
PEDA
With peda installed, you can easily check what security measures are enabled in
an executable with checksec
.
General Tips
SSH Tip
Typing in <level>@narnia.labs.overthewire.org -p 2226
for every single level
can be tiring. I recommend setting up a shorthand for Narnia by updating
~/.ssh/config
(or wherever your config file is located) to include the
following:
Host narnia
HostName narnia.labs.overthewire.org
port 2226
You can now log into the levels with <level>@narnia
!
Shell Tips
- You can verify that your exploit succeeded by running
id
orwhoami
in your newly spawned shell (if you choose to spawn a shell).
Level 0
#include <stdio.h>
#include <stdlib.h>
int main(){
long val=0x41414141;
char buf[20];
("Correct val's value from 0x41414141 -> 0xdeadbeef!\n");
printf("Here is your chance: ");
printf("%24s",&buf);
scanf
("buf: %s\n",buf);
printf("val: 0x%08x\n",val);
printf
if(val==0xdeadbeef){
(geteuid(),geteuid());
setreuid("/bin/sh");
system}
else {
("WAY OFF!!!!\n");
printf(1);
exit}
return 0;
}
General strategy
Our goal here is to somehow read the password to level 1 located at
/etc/narnia_pass/narnia1
using this executable. Reading the code, we find that
if val
is changed to 0xdeadbeef
, we’ll get a shell. And because the levels
are are SUID (Set User ID, a type of Unix file flag that allows a normal user to
execute the program with the privileges of the executable’s owner), we can then
read the password with the privileges of user level1
.
There is no direct way of changing the value of val
, but with the scanf
we
do have control of buf
. Also notice that scanf
is not checking how many
characters we can give it. Due to the stack growing from top to bottom (higher
address to lower addresses), that means by overflowing buf
, we are able
to reach val
and change its value. Here’s an illustration of what the stack
very roughly looks like.
Gentle nudge
Try inputing less than 20 characters and more than 20 characters to see what happens.
(python -c 'print "B" * 19') | ./narnia0
(python -c 'print "B" * 21') | ./narnia0
Tips
\\x42
is the typical hex representation of “B” and\\x41
is “A”.- Recall that x86 is little endian, so you have to reverse the order of bytes
when working with addresses, data, shellcode, etc. For example, “ABCD” would be
\\x44\\x43\\x42\\x41
in little endian.
Note
Even when you get the payload correct, you might run into the problem of the
shell immediately exiting and not allowing you to cat
the password. You can
append a cat
command without arguments to the end of your payload to keep the
shell open. Here’s an explanation as to how that works.
Level 1
#include <stdio.h>
int main(){
int (*ret)();
if(getenv("EGG")==NULL){
("Give me something to execute at the env-variable EGG\n");
printf(1);
exit}
("Trying to execute EGG!\n");
printf= getenv("EGG");
ret ();
ret
return 0;
}
General strategy
The solution seems to be straightforward, as the program itself tells us that
it’s going to execute whatever we put at the environment variable EGG
.
Could you make it spawn a shell with export EGG=<your code>
?
Tips
Shellcode is a sequence of machine code instructions usually intended for executing arbitrary commands.
Check shellstorm for pre-made shellcodes. Our architecture is Intel x86. Always double check that the distro matches, too, otherwise the shellcode won’t work.
Or use
msfvenom
to generate your own shellcode.Remember that shellcode is hexadecimal and you might need to export it like
export EGG=$'<shellcode>'
or
export EGG=$(python -c 'print <shellcode>')
Which pre-made shellcode should I use?
It really depends on what you’re trying to accomplish, since shellcode is just compiled machine code of whatever (usually written in C) program you can write. But for the levels in Narnia where getting passwords to the next level depends on the SUID of the levels, shellcode generated from an “execve(‘/bin/sh’)” call will allow you to run arbitrary commands (including reading the passwords) and is a reliable one to try.
Why can’t I just put /bin/sh
inside EGG
?
Why can’t I use normal C code here? Why do I have to export EGG
to be
shellcode? Let’s turn to gdb
to find the answer.
gdb-peda$ disass main
# skipping to the relevant part
0x080484a1 <+59>: lea eax,[ebx-0x1233]
0x080484a7 <+65>: push eax
0x080484a8 <+66>: call 0x8048310 <puts@plt>
0x080484ad <+71>: add esp,0x4
0x080484b0 <+74>: lea eax,[ebx-0x126c]
0x080484b6 <+80>: push eax
0x080484b7 <+81>: call 0x8048300 <getenv@plt>
0x080484bc <+86>: add esp,0x4
0x080484bf <+89>: mov DWORD PTR [ebp-0x8],eax
0x080484c2 <+92>: mov eax,DWORD PTR [ebp-0x8]
0x080484c5 <+95>: call eax
0x080484c7 <+97>: mov eax,0x0
0x080484cc <+102>: mov ebx,DWORD PTR [ebp-0x4]
0x080484cf <+105>: leave
0x080484d0 <+106>: ret
End of assembler dump.
As we can see on line 0x080484c5 <+95>
we’re calling EAX directly. A call
instruction does 2 things:
- Push the address of the next instruction to the stack so that we can come back later.
- Set the EIP to the address we’re supplied with and immediately start executing from there.
EAX points to whatever we put in EGG
. So our call
is expecting raw machine code at EAX for
execution, and not any uncompiled C code or bash command.
Level 2
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char * argv[]){
char buf[128];
if(argc == 1){
("Usage: %s argument\n", argv[0]);
printf(1);
exit}
(buf,argv[1]);
strcpy("%s", buf);
printf
return 0;
}
General strategy
We have a pretty short and simple program here. The buffer is set to have 128
characters, and the strcpy
call doesn’t check how long argv[1]
is. Sweet!
Can you take control of the EIP by overflowing argv[1]
?
Try giving it 127 characters, and then 140 characters in gdb to see the difference.
When you input 140 A’s, the process segfaults at address 0x41414141
(“AAAA”)
which means it’s trying to execute whatever instructions there are at
0x41414141
since EIP points to 0x41414141
, but it’s not really a valid
address. Can you make EIP point to an address that you control instead? And if
that address contains shellcode, you’ll have a shell!
Level 3
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv){
int ifd, ofd;
char ofile[16] = "/dev/null";
char ifile[32];
char buf[32];
if(argc != 2){
("usage, %s file, will send contents of file 2 /dev/null\n",argv[0]);
printf(-1);
exit}
/* open files */
(ifile, argv[1]);
strcpyif((ofd = open(ofile,O_RDWR)) < 0 ){
("error opening %s\n", ofile);
printf(-1);
exit}
if((ifd = open(ifile, O_RDONLY)) < 0 ){
("error opening %s\n", ifile);
printf(-1);
exit}
/* copy from file1 to file2 */
(ifd, buf, sizeof(buf)-1);
read(ofd,buf, sizeof(buf)-1);
write("copied contents of %s to a safer place... (%s)\n",ifile,ofile);
printf
/* close 'em */
(ifd);
close(ofd);
close
(1);
exit}
General Strategy
At a quick glance, we can see that this script is supposed to copy the contents from ifile
, which we can supply with argv[1]
, to /dev/null
. Remember that our goal here is to get access to the password to the next level.
Hint #1
What do we have control over here? It’s ifile
. And it looks like
strcpy
is not bounds-checking. What can we do with that?
Hint #2
Notice how ifile
is declared directly below ofile
, which means on
the stack it is stored before ofile
. What happens if we do a buffer
overflow?
Hint #3
What if we overflow the /dev/null
and thus redirect our target file
to a place where we have read & write access to? /tmp
might be a good
candidate.
Hint #4
We can refer to a file by another name by using a soft link.
Level 4
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
extern char **environ;
int main(int argc,char **argv){
int i;
char buffer[256];
for(i = 0; environ[i] != NULL; i++)
(environ[i], '\0', strlen(environ[i]));
memset
if(argc>1)
(buffer,argv[1]);
strcpy
return 0;
}
General Strategy
Hint #1
The memset
line erases the environment variables. So we can’t store our shellcode in the environment variables.
Hint #2
strcpy
looks like a great target of buffer overflow there without any bounds-checking. If we overflow it, we can seize control over the EIP. Now we just need to point EIP at our shellcode.
Hint #3
If we can’t store the shellcode in one of the environment variables, where else could we store it?
Hint #4
We can store it in the only place we currently have control over.
If you like my approach to these Narnia wargames, maybe you’d like my fun & interactive cybersec courses too :)