DEF CON CTF Qualifier 2015


Access Control - Reverse Engineering

Published on May 25, 2015 by SpySheriff

post ctf

9 min READ

The following is a write up from the 2015 Def-Con CTF qualifier. This was a reverse engineering challenge with several different components.

Challenge

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)

Initial Analysis

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.

Reversing the Password

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