DEF CON 2015 Quals - access control (1pt) writeup
The challenge description was: It's all about who you know and what you want. access_control_server_f380fcad6e9b2cdb3c73c651824222dc.quals.shallweplayaga.me:17069
A binary called client_197010ce28dffd35bf00ffc56e3aeb9f was provided.
Let's connect to the server and see what it's really about (since the server is offline now I'm going to try and replicate what was happening):
mrt:~/ctf/defcon/reverse/nc access_control_server_f380fcad6e9b2cdb3c73c651824222dc.quals.shallweplayaga.me 17069
connection ID: &7"R%{7+e*QZAu
*** Welcome to the ACME data retrieval service ***
what version is your client?
Ok we definitely need more information, the server is asking us for a client version and we are not yet aware of that, let's have a look at the binary:
mrt:~/ctf/defcon/reverse/access_control$ file client_197010ce28dffd35bf00ffc56e3aeb9f
client_197010ce28dffd35bf00ffc56e3aeb9f: ELF 32-bit LSB executable, Intel 80386,
version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24,
BuildID[sha1]=d50f26cfcf4c2be1ac779d789d046a70054fdf83, stripped
mrt:~/ctf/defcon/reverse/access_control$ strings client_197010ce28dffd35bf00ffc56e3aeb9f
/lib/ld-linux.so.2
__gmon_start__
libc.so.6
...
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?
We have the version now, and what seems to be a username and a couple commands, let's see:
*** Welcome to the ACME data retrieval service ***
what version is your client?
version 3.11.54
hello...who is this?
grumpy
enter user password
And this is as far as we can go without looking at the binary provided. What the binary does is actually running as a client and connect to the server as the user grumpy, give a proper password, list all users and finally try to retrieve the key. Since grumpy is not an administrator the key access is denied.
We have more than enough to actually know exactly what to do, let's first sniff the traffic to analyze what is exactly going on:
mrt:~/ctf/defcon/reverse/access_control$ tshark -i eth0 -f "port 17069" -w /tmp/pcap.cap
And in another session running the client:
mrt:~/ctf/defcon/reverse/access_control$ ./client_197010ce28dffd35bf00ffc56e3aeb9f 52.17.77.77
And here is the output:
connection ID: &7"R%{7+e*QZAu
*** Welcome to the ACME data retrieval service ***
what version is your client?
version 3.11.54
hello...who is this?grumpy
enter user password
E P6G
hello grumpy, what would you like to do?
list users
grumpy
mrvito
gynophage
selir
jymbolia
sirgoon
duchess
deadwood
hello grumpy, what would you like to do?
print key
the key is not accessible from this account. your administrator has been notified.
Now we have a password, the password depends of the connection ID since it's never the same but this means we can log as anyone in the list if we know how to generate a proper password for a specific user and connection ID. The password was always 5 characters long.
Let's have a look at the client:
mrt:~/ctf/defcon/reverse/access_control$ objdump -M intel -d client_197010ce28dffd35bf00ffc56e3aeb9f > dump
mrt:~/ctf/defcon/reverse/access_control$ less dump
8048eab: 55 push ebp
8048eac: 89 e5 mov ebp,esp
8048eae: 53 push ebx
8048eaf: 83 ec 44 sub esp,0x44
8048eb2: 8b 45 08 mov eax,DWORD PTR [ebp+0x8]
8048eb5: 89 45 d4 mov DWORD PTR [ebp-0x2c],eax
8048eb8: 8b 45 0c mov eax,DWORD PTR [ebp+0xc]
8048ebb: 89 45 d0 mov DWORD PTR [ebp-0x30],eax
8048ebe: 65 a1 14 00 00 00 mov eax,gs:0x14
8048ec4: 89 45 f4 mov DWORD PTR [ebp-0xc],eax
8048ec7: 31 c0 xor eax,eax
8048ec9: 8b 0d 80 bc 04 08 mov ecx,DWORD PTR ds:0x804bc80
8048ecf: ba 56 55 55 55 mov edx,0x55555556
8048ed4: 89 c8 mov eax,ecx
8048ed6: f7 ea imul edx
8048ed8: 89 c8 mov eax,ecx
8048eda: c1 f8 1f sar eax,0x1f
8048edd: 89 d3 mov ebx,edx
8048edf: 29 c3 sub ebx,eax
8048ee1: 89 d8 mov eax,ebx
8048ee3: 89 45 e8 mov DWORD PTR [ebp-0x18],eax
8048ee6: 8b 55 e8 mov edx,DWORD PTR [ebp-0x18]
8048ee9: 89 d0 mov eax,edx
8048eeb: 01 c0 add eax,eax
8048eed: 01 d0 add eax,edx
8048eef: 89 ca mov edx,ecx
8048ef1: 29 c2 sub edx,eax
8048ef3: 89 d0 mov eax,edx
8048ef5: 89 45 e8 mov DWORD PTR [ebp-0x18],eax
8048ef8: a1 4c b0 04 08 mov eax,ds:0x804b04c ; 0x00000001
8048efd: 01 45 e8 add DWORD PTR [ebp-0x18],eax
8048f00: ba 70 bc 04 08 mov edx,0x804bc70
8048f05: 8b 45 e8 mov eax,DWORD PTR [ebp-0x18]
8048f08: 01 d0 add eax,edx
8048f0a: c7 44 24 08 05 00 00 mov DWORD PTR [esp+0x8],0x5
8048f11: 00
8048f12: 89 44 24 04 mov DWORD PTR [esp+0x4],eax
8048f16: 8d 45 ef lea eax,[ebp-0x11]
8048f19: 89 04 24 mov DWORD PTR [esp],eax
8048f1c: e8 7f f6 ff ff call 80485a0 <strncpy@plt>
8048f21: c7 45 e4 00 00 00 00 mov DWORD PTR [ebp-0x1c],0x0
8048f28: eb 20 jmp 8048f4a <send@plt+0x94a>
This is the function called to generate the password, we are going to split it in two parts since what is going on until the strncpy is a bit confusing. The pseudo code looks like this for this first part:
result = strncpy(dest, &::dest[dword_804b04c] + dword_804bc80 % 3, 5u);
dword_804b04c initial value is 1 and dword_804bc80 is set during the request for client version:
8048900: 0f b6 05 77 bc 04 08 movzx eax,BYTE PTR ds:0x804bc77 ; 0x804bc74 + 3
8048907: 0f be c0 movsx eax,al
804890a: a3 80 bc 04 08 mov ds:0x804bc80,eax
This is copying 5 bytes from the connection ID starting at the offset specified by dword_804bc80.
result = strncpy(id_chunk, id_chunk[1]+offset%3, 5);
Then come the next part:
8048f21: c7 45 e4 00 00 00 00 mov DWORD PTR [ebp-0x1c],0x0 ; our counter set to 0
8048f28: eb 20 jmp 8048f4a <send@plt+0x94a>
8048f2a: 8b 45 e4 mov eax,DWORD PTR [ebp-0x1c] ; index in eax
8048f2d: 03 45 d0 add eax,DWORD PTR [ebp-0x30] ; + pass address
8048f30: 8d 55 ef lea edx,[ebp-0x11] ; our id_chunk address
8048f33: 03 55 e4 add edx,DWORD PTR [ebp-0x1c] ; move to index
8048f36: 0f b6 0a movzx ecx,BYTE PTR [edx] ; load ID char into ecx
8048f39: 8b 55 e4 mov edx,DWORD PTR [ebp-0x1c] ; index into edx
8048f3c: 03 55 d4 add edx,DWORD PTR [ebp-0x2c] ; + username address
8048f3f: 0f b6 12 movzx edx,BYTE PTR [edx] ; load user char into edx
8048f42: 31 ca xor edx,ecx ; edx ^ ecx
8048f44: 88 10 mov BYTE PTR [eax],dl ; store char in pass char
8048f46: 83 45 e4 01 add DWORD PTR [ebp-0x1c],0x1 ; increase counter
8048f4a: 83 7d e4 04 cmp DWORD PTR [ebp-0x1c],0x4 ; if <= 4 loop
8048f4e: 7e da jle 8048f2a <send@plt+0x92a>
...
The password is generated with the five bytes of the ID and the five first bytes of the username using xor operation for each letter. After that the generated password is sent to a function just making sure it stays in a range of valid characters:
8048f67: 55 push ebp
8048f68: 89 e5 mov ebp,esp
8048f6a: 83 ec 10 sub esp,0x10
8048f6d: c7 45 fc 00 00 00 00 mov DWORD PTR [ebp-0x4],0x0 ; set counter to 0
8048f74: eb 5a jmp 8048fd0 <send@plt+0x9d0>
8048f76: 8b 45 fc mov eax,DWORD PTR [ebp-0x4] ; eax = counter
8048f79: 03 45 08 add eax,DWORD PTR [ebp+0x8] ; + address of pass
8048f7c: 0f b6 00 movzx eax,BYTE PTR [eax] ; load character in eax
8048f7f: 3c 1f cmp al,0x1f ; if character is > 0x1f jump
8048f81: 7f 14 jg 8048f97 <send@plt+0x997>
8048f83: 8b 45 fc mov eax,DWORD PTR [ebp-0x4] ; counter
8048f86: 03 45 08 add eax,DWORD PTR [ebp+0x8] ; + address of pass
8048f89: 8b 55 fc mov edx,DWORD PTR [ebp-0x4] ; counter
8048f8c: 03 55 08 add edx,DWORD PTR [ebp+0x8] ; + address of pass
8048f8f: 0f b6 12 movzx edx,BYTE PTR [edx] ; load pass[counter] in edx
8048f92: 83 c2 20 add edx,0x20 ; add 0x20
8048f95: 88 10 mov BYTE PTR [eax],dl ; save char
8048f97: 8b 45 fc mov eax,DWORD PTR [ebp-0x4] ; counter
8048f9a: 03 45 08 add eax,DWORD PTR [ebp+0x8] ; + address of pass
8048f9d: 0f b6 00 movzx eax,BYTE PTR [eax] ; load char in eax
8048fa0: 3c 7f cmp al,0x7f ; if char != 0x7f jump
8048fa2: 75 28 jne 8048fcc <send@plt+0x9cc>
8048fa4: 8b 45 fc mov eax,DWORD PTR [ebp-0x4] ; counter
8048fa7: 03 45 08 add eax,DWORD PTR [ebp+0x8] ; + address of pass
8048faa: 8b 55 fc mov edx,DWORD PTR [ebp-0x4] ; counter
8048fad: 03 55 08 add edx,DWORD PTR [ebp+0x8] ; + address of pass
8048fb0: 0f b6 12 movzx edx,BYTE PTR [edx] ; load char in edx
8048fb3: 83 ea 7e sub edx,0x7e ; char - 0x7e
8048fb6: 88 10 mov BYTE PTR [eax],dl ; save character
8048fb8: 8b 45 fc mov eax,DWORD PTR [ebp-0x4] ; counter
8048fbb: 03 45 08 add eax,DWORD PTR [ebp+0x8] ; + address of pass
8048fbe: 8b 55 fc mov edx,DWORD PTR [ebp-0x4] ; counter
8048fc1: 03 55 08 add edx,DWORD PTR [ebp+0x8] ; + address of pass
8048fc4: 0f b6 12 movzx edx,BYTE PTR [edx] ; load char in edx
8048fc7: 83 c2 20 add edx,0x20 ; char + 0x20
8048fca: 88 10 mov BYTE PTR [eax],dl ; save char
8048fcc: 83 45 fc 01 add DWORD PTR [ebp-0x4],0x1 ; increment counter
8048fd0: 83 7d fc 04 cmp DWORD PTR [ebp-0x4],0x4 ; if <= 4 loop
8048fd4: 7e a0 jle 8048f76 <send@plt+0x976>
8048fd6: c9 leave
8048fd7: c3 ret
...
For each character in the password if pass[i] is smaller or equal to 0x1f add 0x20 and if pass[i] == 0x7f substract 0x5e (-0x7e + 0x20) to that char.
To make sure we had the password generation function correctly we just had to check it against the ID and password we captured earlier.
This is the script I ended up making:
#!/usr/bin/env python
import socket
def genpass(user, id):
result = ''
for i in range(0, 5):
char = ord(id[i]) ^ ord(user[i])
if char <= 0x1F:
char += 0x20
if char == 0x7F:
char -= 0x5E
result += chr(char)
return result
def getkey(USER):
HOST = '52.17.77.77'
PORT = 17069
ID = ''
VERSION = 'version 3.11.54'
PASS = ''
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
while 1:
data = s.recv(512)
chunks = data.split('\n')
for chunk in chunks:
print chunk
if "connection ID:" in chunk:
ID = chunk.split(': ')[1]
print "[+] ID:", ID
elif "wrong password, fat fingers" in chunk:
s.close()
exit(0)
elif "the key is not accessible" in chunk:
s.close()
return False
elif "you are not worthy" in chunk:
s.close()
return False
elif "the key is:" in chunk:
s.close()
return True
elif "your client" in chunk:
s.send(VERSION + '\n')
elif "who is this?" in chunk:
s.send(USER + '\n')
elif "enter user password" in chunk:
PASS = genpass(USER, ID[1+ord(ID[7])%3:])
print "[+] USER: %s\n[+] PASS: %s" % (USER, PASS)
s.send(PASS + '\n')
elif "you like to do?" in chunk:
s.send('print key\n')
elif "challenge:" in chunk:
CHALLENGE = chunk.split(': ')[1]
ANSWER = genpass(CHALLENGE, ID[7+ord(ID[7])%3:])
print "[+] CHALLENGE: %s\n[+] ANSWER: %s" % (CHALLENGE, ANSWER)
s.send(ANSWER + '\n')
'''
# try all users until we find who is the admin
USERS = ['mrvito', 'gynophage', 'selir', 'jymbolia', 'sirgoon', 'duchess', 'deadwood']
while 1:
print
if (getkey(USERS.pop()) == True) or (len(USERS) == 0):
exit(0)
'''
# duchess is the admin
getkey('duchess')
The while loop was to let the script find out which user was an administrator, after finding out it was 'duchess' I made the request with that username directly only to find out there is another step to retrieve the key. The server would send a challenge with a string (5 bytes) and expect you to answer properly. Fortunately the client binary shows that part as well and it's using the same functions we described earlier with a tiny difference:
8048c0c: c7 05 4c b0 04 08 07 mov DWORD PTR ds:0x804b04c,0x7
8048c0c which is part of the offset calculated to retrieve 5 bytes from the ID is set to 7 while it was set to 1 for the password generation process. Instead of our username we now need to generate a new password with the challeng string and the new ID chunk. With that in mind we have our final script running and this was the output:
mrt:~/ctf/defcon/reverse/access_control$ ./client.py
connection ID: MNDCJ%4eN7&7A#
[+] ID: MNDCJ%4eN7&7A#
*** Welcome to the ACME data retrieval service ***
what version is your client?
hello...who is this?
enter user password
[+] USER: duchess
[+] PASS: '?F\
hello duchess, what would you like to do?
challenge: P0-_C
[+] CHALLENGE: P0-_C
[+] ANSWER: g6:>`
answer?
the key is: The only easy day was yesterday. 44564
We got our flag:
The only easy day was yesterday. 44564