Sunday, October 21, 2012

Smash the Stack IO Level 1 Writeup

Introduction

One of the best ways to either learn new exploitation techniques or practice ones you already understand is through events called Wargames, otherwise known as "Capture the Flags" (CTFs).  There are two common types of CTFs: a typical "Offensive/Defensive" strategy, in which teams are simultaneously attacking each other's networks in attempt to capture their flag, and a "Jeopardy", or "Offense Only", type in which all teams are trying to solve problems to obtain the same flag.

In addition to this, CTFs can be further classified as either 'ongoing', in which participation is not limited to a small time frame, or 'Event Based', in which participants have a limited time (usually a few days) to attempt to capture as many flags as possible. As an example, the recent CSAW CTF (for which there are writeups on this blog) is considered a Jeopardy-style Event CTF because participation was limited to a weekend.

Now, with the introduction out of the way (see the end of the post for misc. CTF resources), the following is a writeup for level 1 of the ongoing Jeopardy-style CTF called Smash the Stack - IO. I have tried to make the writeup comprehensive for those that may have never participated in a CTF, or do not have much experience reversing binaries.

Connecting to the Server

As mentioned on the CTF's homepage, we connect to level1 using an SSH client (ie PuTTY for Windows) and connecting to io.smashthestack.org:2224 using the credentials 'level1:level1'.

 >>> !! notice !! <<< i refreshed the early levels and changed their passwords.  
 Sept 24 2012. (levels 1 -> 11 are affected).  
 Server refused our key  
 level1@io.smashthestack.org's password:  
  ______  _____  
 /\__ _\ /\ __`\    Levels are in /levels  
 \/_/\ \/ \ \ \/\ \   Passes are in ~/.pass  
   \ \ \ \ \ \ \ \   Readmes in /home/level1  
   \_\ \__\ \ \_\ \  
   /\_____\\ \_____\  Server admin: bla (bla@smashthestack.org)  
   \/_____/ \/_____/  
     1. No DoS, local or otherwise  
     2. Do not try to connect to remote systems from this box  
     3. Quotas, watch resources usage, max 2 connections per IP  
                 (29 levels)  
 level1@io:~$  

Analyzing Level 1:

As is the case when reversing any binary, it's important to know what it actually does. With this being the case, let's run level1 and see what it expects. As mentioned in the README (you did read the README, didn't you? :), the levels are located in /levels.

 level1@io:~$ cd /levels/  
 level1@io:/levels$  
 level1@io:/levels$ ./level01  
 You need to supply a password.  
 Usage: ./level01 [password]  
 level1@io:/levels$  
 level1@io:/levels$ ./level01 test_password  
 Fail.  

It appears as though the binary simply takes one argument as input, and checks to see if it's a correct password. If it is, we can assume that we will be able to retrieve the flag. As we can see in the MOTD, the levels were 'refreshed' and their passwords were 'changed' on Sept 24, 2012. Previously, level 1 was as simply as running the 'strings' command on the binary, and the password could be found ('omgpassword').

However, since that old password no longer works, let's fire up GDB try to find a way to retrieve the password for this updated version of the program. The first thing we want to after loading the binary in gdb is to disassemble the main function and get an idea of the main 'flow' of execution.

 level1@io:/levels$ gdb -q level01  
 Reading symbols from /levels/level01...(no debugging symbols found)...done.  
 (gdb) set disassembly-flavor intel  
 (gdb)  
 (gdb) disassemble main  
 Dump of assembler code for function main:  
 0x08048596 <main+0>:  push  ebp  
 0x08048597 <main+1>:  mov  ebp,esp  
 0x08048599 <main+3>:  sub  esp,0x18  
 0x0804859c <main+6>:  and  esp,0xfffffff0  
 0x0804859f <main+9>:  mov  eax,0x0  
 0x080485a4 <main+14>:  sub  esp,eax  
 0x080485a6 <main+16>:  cmp  DWORD PTR [ebp+0x8],0x2  
 0x080485aa <main+20>:  je   0x80485ca <main+52>  
 0x080485ac <main+22>:  mov  eax,DWORD PTR [ebp+0xc]  
 0x080485af <main+25>:  mov  eax,DWORD PTR [eax]  
 0x080485b1 <main+27>:  mov  DWORD PTR [esp+0x4],eax  
 0x080485b5 <main+31>:  mov  DWORD PTR [esp],0x8048760  
 0x080485bc <main+38>:  call  0x80483b8 <printf@plt>  
 0x080485c1 <main+43>:  mov  DWORD PTR [ebp-0x4],0x0  
 0x080485c8 <main+50>:  jmp  0x8048618 <main+130>  
 0x080485ca <main+52>:  call  0x804852d <pass>  
 0x080485cf <main+57>:  mov  DWORD PTR [esp+0x8],0x64  
 0x080485d7 <main+65>:  mov  eax,DWORD PTR [ebp+0xc]  
 0x080485da <main+68>:  add  eax,0x4  
 0x080485dd <main+71>:  mov  eax,DWORD PTR [eax]  
 0x080485df <main+73>:  mov  DWORD PTR [esp+0x4],eax  
 0x080485e3 <main+77>:  mov  DWORD PTR [esp],0x80491a0  
 ---Type <return> to continue, or q <return> to quit---  
 0x080485ea <main+84>:  call  0x80483a8 <mbstowcs@plt>  
 0x080485ef <main+89>:  mov  DWORD PTR [esp+0x4],0x8049140  
 0x080485f7 <main+97>:  mov  DWORD PTR [esp],0x80491a0  
 0x080485fe <main+104>: call  0x80483d8 <wcscmp@plt>  
 0x08048603 <main+109>: test  eax,eax  
 0x08048605 <main+111>: jne  0x804860c <main+118>  
 0x08048607 <main+113>: call  0x80484b4 <win>  
 0x0804860c <main+118>: mov  DWORD PTR [esp],0x8048795  
 0x08048613 <main+125>: call  0x80483e8 <puts@plt>  
 0x08048618 <main+130>: mov  eax,DWORD PTR [ebp-0x4]  
 0x0804861b <main+133>: leave  
 0x0804861c <main+134>: ret  
 End of assembler dump.  

For the sake of comprehension, let's take a look at the code piece by piece in an attempt to get an idea of what the program is doing, starting immediately after the function prologue.

 0x080485a6 <main+16>:  cmp  DWORD PTR [ebp+0x8],0x2  
 0x080485aa <main+20>:  je   0x80485ca <main+52>  
 0x080485ac <main+22>:  mov  eax,DWORD PTR [ebp+0xc]  
 0x080485af <main+25>:  mov  eax,DWORD PTR [eax]  
 0x080485b1 <main+27>:  mov  DWORD PTR [esp+0x4],eax  
 0x080485b5 <main+31>:  mov  DWORD PTR [esp],0x8048760  
 0x080485bc <main+38>:  call  0x80483b8 <printf@plt>  
 0x080485c1 <main+43>:  mov  DWORD PTR [ebp-0x4],0x0  
 0x080485c8 <main+50>:  jmp  0x8048618 <main+130>  
 0x080485ca <main+52>:  call  0x804852d <pass>  

We can first notice a 'compare' instruction that checks to see if the value located at the address where ebp+0x8 is pointing is 2. We remember that we have just entered the main function, and so values at an offset of ebp + n refer to arguments pushed onto the stack when main was called. We then can see the following signature of a standard main function to determine what ebp + 8 refers to:

 int main(int argc, char *argv[])  

We know that when a function is called, its arguments are pushed onto the stack in reverse order, followed by the return address. Then, when execution starts in the new function, the old value of ebp is pushed onto the stack, and the value of esp (which is now put into ebp) is used as a location reference. Therefore, we can see that char * argv[] is located at (ebp + 12), argc is located at (ebp + 8), and eip is located at (ebp + 4).

Using this knowledge, we can deduce that the compare instruction does the following:

  • Checks to see if the user provided one argument (as the other argument is reserved for the name of the executing program)
  • If so:
    • Jump to the instruction at 0x80485ca
  • Else:
    • Call printf with some string (in this case the 'usage' string), and jump to the instruction at 0x8048618 (the end of the function), and exits.
We now consider the case when we provided a password, so the appropriate number of arguments have been set. We see that we jump to a 'call' instruction, which calls the 'pass' subroutine. Let's disassemble this routine and see what it does:

 (gdb) disassemble pass  
 Dump of assembler code for function pass:  
 0x0804852d <pass+0>:  push  ebp  
 0x0804852e <pass+1>:  mov  ebp,esp  
 0x08048530 <pass+3>:  sub  esp,0x4  
 0x08048533 <pass+6>:  mov  DWORD PTR [ebp-0x4],0x8049140  
 0x0804853a <pass+13>:  mov  DWORD PTR ds:0x8049140,0x53  
 0x08048544 <pass+23>:  mov  DWORD PTR ds:0x8049144,0x65  
 0x0804854e <pass+33>:  mov  DWORD PTR ds:0x8049148,0x63  
 0x08048558 <pass+43>:  mov  DWORD PTR ds:0x804914c,0x72  
 0x08048562 <pass+53>:  mov  DWORD PTR ds:0x8049150,0x65  
 0x0804856c <pass+63>:  mov  DWORD PTR ds:0x8049154,0x74  
 0x08048576 <pass+73>:  mov  DWORD PTR ds:0x8049158,0x50  
 0x08048580 <pass+83>:  mov  DWORD PTR ds:0x804915c,0x57  
 0x0804858a <pass+93>:  mov  DWORD PTR ds:0x8049160,0x0  
 0x08048594 <pass+103>: leave  
 0x08048595 <pass+104>: ret  
 End of assembler dump.  

We can see that this function loads an address into the value pointed to by ebp-0x4. Seeing the negative offset of ebp, we can deduce that this address is being loaded into the value pointed to by a local pointer variable. Then, we can see bytes being loaded into these addresses. We can see that each of these bytes is being loaded in a 4 byte segment of memory. This will be important later, however, consider the name of the function, and the values being loaded. What are the sequence of bytes being loaded into memory?

 0x080485cf <main+57>:  mov  DWORD PTR [esp+0x8],0x64  
 0x080485d7 <main+65>:  mov  eax,DWORD PTR [ebp+0xc]  
 0x080485da <main+68>:  add  eax,0x4  
 0x080485dd <main+71>:  mov  eax,DWORD PTR [eax]  
 0x080485df <main+73>:  mov  DWORD PTR [esp+0x4],eax  
 0x080485e3 <main+77>:  mov  DWORD PTR [esp],0x80491a0  
 0x080485ea <main+84>:  call  0x80483a8 <mbstowcs@plt>  

After we return to the main function, We can quickly follow the flow of the program to the mbstowcs function.

 size_t mbstowcs ( wchar_t * dest, const char * src, size_t max );  

As we can see, this function takes three arguments: a pointer to an array of wchar_t elements long enough to store a wide string of max wide characters, a pointer to a string (source), and the max number of wchar_t characters to write to the destination. Again, when a function is called, the arguments are pushed in reverse order. Therefore, we can see that 64 is 'pushed' into esp+8, The value pointed to by ebp+12 (see above - this is a pointer to our argument array) + 4 (to get access to the 'second argument' which is our provided password) is pushed into esp+4, and a pointer to a destination array is pushed into esp. This has the same effect as pushing the arguments in reverse order. So we can see that this code is used to convert our provided argument to a wide string.

Moving on, we can see the following code:

 0x080485ef <main+89>:  mov  DWORD PTR [esp+0x4],0x8049140  
 0x080485f7 <main+97>:  mov  DWORD PTR [esp],0x80491a0  
 0x080485fe <main+104>: call  0x80483d8 <wcscmp@plt>  
 0x08048603 <main+109>: test  eax,eax  
 0x08048605 <main+111>: jne  0x804860c <main+118>  
 0x08048607 <main+113>: call  0x80484b4 <win>  
 0x0804860c <main+118>: mov  DWORD PTR [esp],0x8048795  
 0x08048613 <main+125>: call  0x80483e8 <puts@plt>  

This code loads two arguments - our wide-string converted password, and the password created in the 'pass' function, and calls the wcscmp function, which is used to compare two wide-character strings. Depending on the comparison, it will either call the 'win' function, or jump to 0x0804860c. Let's quickly take a brief look at what the 'win' function does:

 (gdb) disassemble win  
 Dump of assembler code for function win:  
 0x080484b4 <win+0>:   push  ebp  
 0x080484b5 <win+1>:   mov  ebp,esp  
 0x080484b7 <win+3>:   push  esi  
 0x080484b8 <win+4>:   push  ebx  
 0x080484b9 <win+5>:   sub  esp,0x20  
 0x080484bc <win+8>:   mov  DWORD PTR [ebp-0x18],0x8048700  
 0x080484c3 <win+15>:  mov  DWORD PTR [ebp-0x14],0x8048708  
 0x080484ca <win+22>:  mov  DWORD PTR [ebp-0x10],0x0  
 0x080484d1 <win+29>:  mov  DWORD PTR [esp],0x804870b  
 0x080484d8 <win+36>:  call  0x80483e8 <puts@plt>  
 0x080484dd <win+41>:  mov  DWORD PTR [esp],0x8048720  
 0x080484e4 <win+48>:  call  0x80483e8 <puts@plt>  
 0x080484e9 <win+53>:  call  0x80483f8 <geteuid@plt>  
 0x080484ee <win+58>:  mov  esi,eax  
 0x080484f0 <win+60>:  call  0x80483f8 <geteuid@plt>  
 0x080484f5 <win+65>:  mov  ebx,eax  
 0x080484f7 <win+67>:  call  0x80483f8 <geteuid@plt>  
 0x080484fc <win+72>:  mov  DWORD PTR [esp+0x8],esi  
 0x08048500 <win+76>:  mov  DWORD PTR [esp+0x4],ebx  
 0x08048504 <win+80>:  mov  DWORD PTR [esp],eax  
 0x08048507 <win+83>:  call  0x8048398 <setresuid@plt>  
 0x0804850c <win+88>:  mov  DWORD PTR [esp+0x8],0x0  
 ---Type <return> to continue, or q <return> to quit---  
 0x08048514 <win+96>:  lea  eax,[ebp-0x18]  
 0x08048517 <win+99>:  mov  DWORD PTR [esp+0x4],eax  
 0x0804851b <win+103>:  mov  eax,DWORD PTR [ebp-0x18]  
 0x0804851e <win+106>:  mov  DWORD PTR [esp],eax  
 0x08048521 <win+109>:  call  0x80483c8 <execve@plt>  
 0x08048526 <win+114>:  add  esp,0x20  
 0x08048529 <win+117>:  pop  ebx  
 0x0804852a <win+118>:  pop  esi  
 0x0804852b <win+119>:  pop  ebp  
 0x0804852c <win+120>:  ret  
 End of assembler dump.  

So, this function is used to print a message, set our EUID (Effective User ID) to that of level2 (since this is a SUID binary), and start a shell for us with level2 permissions. We can see that we want the 'win' function to execute, so our provided password must match the one generated in the 'pass' function. We can use our debugger to see what password was generated in the 'pass' function in one of two ways:

  • Go back and see what byte values are being stored in the 'pass' function
  • See what argument is being loaded into the wcscmp function
For the sake of completeness, we will do both. The following byte values were loaded in the pass function (we can use an ASCII table to view the values of the bytes):
  • 0x53 - S
  • 0x65 - e
  • 0x63 - c
  • 0x72 - r
  • 0x65 - e
  • 0x74 - t
  • 0x50 - P
  • 0x57 - W
  • 0x00 - Null terminator
Therefore, using this method, we can see that the password that will spawn a shell is 'SecretPW'. Let's verify that using the other method. There is no way by default to print the value of a wide-character string, so we simply put a break point before the wcscmp function and view the memory located at the argument.

 (gdb) break *0x080485fe  
 Breakpoint 1 at 0x80485fe  
 (gdb) run AAAAAA  
 Starting program: /levels/level01 AAAAAA  
 Breakpoint 1, 0x080485fe in main ()  
 (gdb) x/32xc 0x8049140  
 0x8049140 <pw>: 83 'S' 0 '\000'    0 '\000'    0 '\000'    101 'e'0 '\000' 0 '\000'    0 '\000'  
 0x8049148 <pw+8>:    99 'c' 0 '\000'    0 '\000'    0 '\000'    114 'r' 0 '\000'    0 '\000'    0 '\000'  
 0x8049150 <pw+16>:   101 'e' 0 '\000'    0 '\000'    0 '\000'    116 't' 0 '\000'    0 '\000'    0 '\000'  
 0x8049158 <pw+24>:   80 'P' 0 '\000'    0 '\000'    0 '\000'    87 'W'  0 '\000'    0 '\000'    0 '\000'  

By remembering that each byte takes up a 4 byte space, we can see the value 'SecretPW' loaded into memory. Let's see what happens when we execute the program using 'SecretPW' as the password.

 level1@io:/levels$ ./level01 SecretPW  
 Win!  
 You will find the ssh password for level2 in /home/level2/.pass  
 sh-4.1$ cat /home/level2/.pass  
 tLmf7msJTJHEpw  

Just like that, a shell is spawned with level2 permissions, and we can view the '.pass' file, which gives us the SSH password to log in as level2. I hope this write-up helps those getting started with CTF's, binary reversing, or exploitation in general. Leave me a comment if you have any questions!

-Jordan

2 comments:

  1. Great post!
    I really learned a lot.

    Actually, I came to another way to guess the password.
    Believe it or not, the old "cat level01" did the trick.

    Between al the symbols displayed is the password in plain text.

    Thanks for the tutorial!

    ReplyDelete
  2. thanks sooo much for the tutorial keep it up !!

    ReplyDelete