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:~$
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?
After we return to the main function, We can quickly follow the flow of the program to the mbstowcs function.
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:
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:
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:
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.
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.
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
(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
Great post!
ReplyDeleteI 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!
thanks sooo much for the tutorial keep it up !!
ReplyDelete