The following is a write up from the 2015 Def-Con CTF qualifier. This was a reverse engineering challenge with several different components.
It's all about who you know and what you want.
access_control_server_f380fcad6e9b2cdb3c73c651824222dc.quals.shallweplayaga.me:17069
Download Client (link: http://downloads.notmalware.ru/client_197010ce28dffd35bf00ffc56e3aeb9f)
First, we connected to the server to see what we get.
$ nc access_control_server_f380fcad6e9b2cdb3c73c651824222dc.quals.shallweplayaga.me 17069
connection ID: }Q0jm5/+.^q`DY
*** Welcome to the ACME data retrieval service ***
what version is your client?
The “connection ID” string changes with every connection. So the file we downloaded is probably a client that interacts with the server. We’ll need to study the client file to make any progress.
$ file client_197010ce28dffd35bf00ffc56e3aeb9f
client_197010ce28dffd35bf00ffc56e3aeb9f: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=d50f26cfcf4c2be1ac779d789d046a70054fdf83, stripped
It’s a standard 32-bit Linux executable so let’s run it.
$ ./client_197010ce28dffd35bf00ffc56e3aeb9f
need IP
We ran nslookup on the server hostname and passed it as an argument.
$ ./client_197010ce28dffd35bf00ffc56e3aeb9f 54.84.39.118
Socket created
Enter message :
I tried typing in a couple test messages but everything responded with “Nope…” and exited. It looks like I need the right message to continue.
$ strings client_197010ce28dffd35bf00ffc56e3aeb9f
...
need IP
Could not create socket
Socket created
connect failed. Error
Enter message :
hack the world
nope...%s
what version is your client?
version 3.11.54
hello...who is this?
grumpy
grumpy
enter user password
hello %s, what would you like to do?
list users
deadwood
print key
the key is:
challenge:
answer?
recv failed
<< %s
connection ID:
connection ID:
challenge:
Send failed
...
Running strings gave a lot of useful hints. The string immediately after “Enter message :” was “hack the world” so I entered that and the client kept going. Progress! Here’s what I saw on the console:
<< connection ID: nlY#(:X`_/T]V^
*** Welcome to the ACME data retrieval service ***
what version is your client?
<< hello...who is this?
<<
<< enter user password
<<
<< hello grumpy, what would you like to do?
<< contact your company admin for help
<<
hello grumpy, what would you like to do?
<< grumpy
<<
mrvito
gynophage
selir
jymbolia
sirgoon
duchess
deadwood
hello grumpy, what would you like to do?
<< the key is not accessible from this account. your administrator has been notified.
Now we’re starting to see what we saw from the server. It looks like the client doesn’t print out what it sends to the server so I ran tcpdump to capture the traffic. It sends “version 3.11.54” as the version, “grumpy” as the user, a random 5 character string as the password, then “list users” which prints the list of users, and lastly “print key” which produces the “key is not accessible from this account” message.
So now we understand how to attack this challenge. One of the users in the list has access to the key and we need to find it. The problem is that when we ran the client multiple times, the password was always different. We can deduce that the password is based on the “connection ID” that we get, so now we’ll need to figure out how the password is calculated.
Note: The easier way to solve this challenge probably would have been to simply patch the program to replace “grumpy” with the other users, but we didn’t think of this at the time and reversed the (simple) password algorithm. Read on if you’re curious, but otherwise this challenge is solved.
As noted in the above section, the password sent was different every time so we’ll need to figure out how this works to login as different users.
We opened up the binary in IDA and breezed through the disassembled code near 0x08048977 – the only place in code where the string “enter user password” is referenced. Right after this, there is a call to 0x08048EAB followed by 0x08048F67.
The first function took the connection ID and the string “grumpy” as its arguments. There was some mumbo jumbo followed by a small loop before it returned. I looked at the loop first.
.text:08048F2A loc_8048F2A:
.text:08048F2A mov eax, [ebp+var_1C]
.text:08048F2D add eax, [ebp+var_30]
.text:08048F30 lea edx, [ebp+dest]
.text:08048F33 add edx, [ebp+var_1C]
.text:08048F36 movzx ecx, byte ptr [edx]
.text:08048F39 mov edx, [ebp+var_1C]
.text:08048F3C add edx, [ebp+var_2C]
.text:08048F3F movzx edx, byte ptr [edx]
.text:08048F42 xor edx, ecx
.text:08048F44 mov [eax], dl
.text:08048F46 add [ebp+var_1C], 1
.text:08048F4A
.text:08048F4A loc_8048F4A:
.text:08048F4A cmp [ebp+var_1C], 4
.text:08048F4E jle short loc_8048F2A
It didn’t take long to figure out that this was xor-ing 5 bytes. I went into GDB and set a breakpoint here to figure out that it was xor-ing 5 bytes from the connection ID and the user name.
The second function is also straight forward which looks at those 5 bytes from above and adds/subtracts values under certain conditions. This probably converted everything into printable ASCII.
.text:08048F76 loc_8048F76:
.text:08048F76 mov eax, [ebp+var_4]
.text:08048F79 add eax, [ebp+arg_0]
.text:08048F7C movzx eax, byte ptr [eax]
.text:08048F7F cmp al, 1Fh
.text:08048F81 jg short loc_8048F97
.text:08048F83 mov eax, [ebp+var_4]
.text:08048F86 add eax, [ebp+arg_0]
.text:08048F89 mov edx, [ebp+var_4]
.text:08048F8C add edx, [ebp+arg_0]
.text:08048F8F movzx edx, byte ptr [edx]
.text:08048F92 add edx, 20h
.text:08048F95 mov [eax], dl
.text:08048F97
.text:08048F97 loc_8048F97:
.text:08048F97 mov eax, [ebp+var_4]
.text:08048F9A add eax, [ebp+arg_0]
.text:08048F9D movzx eax, byte ptr [eax]
.text:08048FA0 cmp al, 7Fh
.text:08048FA2 jnz short loc_8048FCC
.text:08048FA4 mov eax, [ebp+var_4]
.text:08048FA7 add eax, [ebp+arg_0]
.text:08048FAA mov edx, [ebp+var_4]
.text:08048FAD add edx, [ebp+arg_0]
.text:08048FB0 movzx edx, byte ptr [edx]
.text:08048FB3 sub edx, 7Eh
.text:08048FB6 mov [eax], dl
.text:08048FB8 mov eax, [ebp+var_4]
.text:08048FBB add eax, [ebp+arg_0]
.text:08048FBE mov edx, [ebp+var_4]
.text:08048FC1 add edx, [ebp+arg_0]
.text:08048FC4 movzx edx, byte ptr [edx]
.text:08048FC7 add edx, 20h
.text:08048FCA mov [eax], dl
.text:08048FCC
.text:08048FCC loc_8048FCC:
.text:08048FCC add [ebp+var_4], 1
.text:08048FD0
.text:08048FD0 loc_8048FD0:
.text:08048FD0 cmp [ebp+var_4], 4
.text:08048FD4 jle short loc_8048F76
These two calls make up the password function, which I implemented in Python (code below). At first I used conn_id[2] to conn_id[6] as the 5 bytes because that’s what I saw when I first ran the debugger. But after failing to successfully login as different users, I noticed that it used a different set of 5 bytes each time. So I added an “offset” parameter to the function as the starting byte.
Luckily, failing to login only kicks you back to the initial login prompt instead of killing the connection. This means that the connection ID didn’t change. So instead of reversing the offset calculation, I just tried each offset until it logs in successfully.
After trying each user name, it was duchess which had access to the “print key” command.
hello duchess, what would you like to do?
$ print key
challenge: HV==L
answer?
Ugh, not so simple. Fortunately the client tells us how to solve this too.
.text:08048BE5 mov dword ptr [esp], offset aAnswer? ; "answer?"
.text:08048BEC call sub_8048CFA
.text:08048BF1 test eax, eax
.text:08048BF3 jz loc_8048CB9
.text:08048BF9 lea eax, [esp+80h]
.text:08048C00 mov dword ptr [eax], 0
.text:08048C06 mov word ptr [eax+4], 0
.text:08048C0C mov dword_804B04C, 7
.text:08048C16 mov byte ptr ds:word_804B470+1, 0
.text:08048C1D lea eax, [esp+80h]
.text:08048C24 mov [esp+4], eax
.text:08048C28 mov dword ptr [esp], offset dword_804B46C
.text:08048C2F call sub_8048EAB
.text:08048C34 mov dword_804B04C, 1
.text:08048C3E lea eax, [esp+80h]
.text:08048C45 mov [esp], eax
.text:08048C48 call sub_8048F67
Notice the calls to 0x08048EAB and 0x08048F67 – the exact same calls for the password. So we just reused the same password function we created earlier. Fortunately failing the challenge kicks you back to the login so we were able to guess the offset for this part too.
#/usr/bin/env python2
from pwn import *
def pw(username, conn_id, offset):
result = []
for i in range(5):
result.append(ord(username[i]) ^ ord(conn_id[offset+i]))
for i in range(5):
c = result[i]
if c < ord(' '):
c += 0x20
if c == 0x7f:
c -= 0x7e
if c < 0:
c = int(c) & 0xff
c += 0x20
result[i] = c
result = ''.join([chr(x) for x in result])
return result
def main():
HOST = 'access_control_server_f380fcad6e9b2cdb3c73c651824222dc.quals.shallweplayaga.me'
PORT = 17069
USER = 'duchess'
r = remote(HOST, PORT)
r.recvuntil('ID: ')
conn_id = r.recvuntil('\n')
r.recvuntil('client?\n')
r.sendline('version 3.11.54')
r.recvuntil('who is this?\n')
pw_offset = 0
chal_offset = 0
while True:
while True:
r.sendline(USER)
r.recvuntil('user password\n')
r.sendline(pw(USER, conn_id, pw_offset))
if r.recvline()[-4:] == 'do?\n':
break
r.recvuntil('?\n')
pw_offset += 1
r.sendline('print key')
r.recvuntil('challenge: ')
chal = r.recvuntil('\n')[:-1]
r.recvline()
r.sendline(pw(chal, conn_id, chal_offset))
resp = r.recvline()
if not resp.endswith('not worthy\n'):
break
r.recvuntil('who is this?\n')
chal_offset += 1
r.interactive()
if __name__ == '__main__':
main()
Running the code eventually yielded the correct answer and the key.
the key is: The only easy day was yesterday. 44564