PicoCTF 2014 Write-ups

Police Records - 140 (reverse engineering)

Writeup by Gladius Maximus

Created: 2014-11-07 23:45:25

Last modified: 2014-11-11 19:26:18

Problem

A Theseus double agent has infiltrated the police force but the police won't give you access to their directory information. Thankfully you found the source code to their directory server. Write a client to interact with the server and find the duplicated badge number! The flag is the badge number.

The server is running on vuln2014.picoctf.com:21212.

Hint

You are going to have to understand structs.

Answer

Overview

Write a client in a language of choice (python for me) that interacts with the server and grabs data. You have to look at the source code for the server and reverse engineer a proper client.

Details

A struct is a way to send C primitives as a string. C primitives are like char, float, long, float, unsigned int, etc. The function struct.pack takes in a format string and data and returns the packed data as a string according to the format string. It is complimentary to struct.unpack, which does the opposite.

Accessing the server

If you look at their code,

# from directory_server.py
data = self.request.recv(1024)
...
code = struct.unpack("!i", data)[0]
if code == 0xAA:
    cookie = self.secure_send(b"WELCOME TO THE POLICE RECORDS DIRECTORY")
    access = True
else:
    raise Exception

To connect, you have to send 0xAA as an integer. Lets get all of the TCP connection code going. Make a script called directory_client.py. When executing directory_client.py, make sure to use python3. Soon we will be messing with string encodings and such.

# from directory_client.py
import struct
import socket
import json
from os import urandom

HOST = 'vuln2014.picoctf.com'
PORT = 21212
c = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
c.connect((HOST, PORT))

c.send(struct.pack('!i', 0xAA))

Now I turn my attention to the secure_send function. The server uses it to send stuff to me

# directory_server.py
def secure_send(self, msg):
    """ Sends msg back to the client securely. """

    cookie = generate_cookie()
    data = struct.pack("!B2L128s", 0xFF, cookie, len(msg), msg)
    encrypted = secure_pad(data)
    self.request.sendall(encrypted)
    return cookie
...
def secure_pad(buf):
    """ Ensure message is padded to block size. """
    key = urandom(5)
    buf = bytes([0x13, 0x33, 0x7B, 0xEE, 0xF0]) + buf
    buf = buf + urandom(16 - len(buf) % 16)
    # now we know: len(buf) % 16 == 0
    enc = xor(buf, key)
    return enc

def remove_pad(buf):
    """ Removes the secure padding from the msg. """
    if len(buf) > 0 and len(buf) % 16 == 0:
        encrypted_key = buf[:5]
        key = xor(encrypted_key, bytes([0x13, 0x33, 0x7B, 0xEE, 0xF0]))
        dec = xor(buf, key)
        return dec[5:20]

Here is how I will decode the messages. I am using remove_pad from the server's source code. Since recv pads the message with null characters, I want to cut the message to the right size msg[:lmsg].

# directory_client.py
def decode(buf):
    buf = remove_pad(buf)
    k, cookie, lmsg, msg = struct.unpack("!B2L128s", buf[:137 - len(buf)])
    return cookie, msg[:lmsg].decode('utf-8')

Now we can interpret messages from the server.

# directory_client.py
...
c.send(struct.pack('!i', 0xAA))
resp = c.recv(1024)
cookie, msg = decode(resp)
print (msg)

You should see WELCOME TO THE POLICE RECORDS DIRECTORY. Now lets look at the server's code for what we want to do next.

Accessing data on the server

# directory_server.py
decrypted = remove_pad(data)
...
magic, user_cookie, badge, cmd, entry = \
        struct.unpack("!B2LHL", decrypted)
if magic != 0xFF or user_cookie != cookie:
    self.request.sendall(b"INSECURE REQUEST")
    running = False
else:
    ...

We want to go to the else branch of the if statement. We need magic to equal 0xFF, and user_cookie to equal the previous cookie. We also need to make sure that it we are using secure_pad, since the server is using remove_pad To implement this, we need to do c.send(secure_pad(struct.pack('!B2LHL', 0xFF, cookie, _, _, _)))

We still need to fillin the blanks. To do that, we need to see how badge, cmd, and entry are used in the server's code.

# directory_server.py
if cmd == 1:
    officer = self.get_officer_data(entry)
    if officer:
        cookie = self.secure_send(officer)
    else:
        cookie = self.secure_send(b"INVALID ENTRY -- OFFICER DOES NOT EXIST")
else:
    cookie = self.secure_send(b"INVALID COMMAND")

This means we want cmd to equal 1, and entry to be 0. Badge is not used anywhere in the program, so I will set it to 0. Now we have c.send(secure_pad(struct.pack('!B2LHL', 0xFF, cookie, 0, 1, _))). One last blank to fill in.

# directory_server.py
def get_officer_data(self, entry):
    """ Retrieve binary format of officer. """

    if 0 <= entry and entry < len(self.OFFICERS):
        return json.dumps(self.OFFICERS[entry]).encode("utf-8")
    return None

Zero is always a safe value, because every with data has a zeroeth value. Let us set entry to zero.

# directory_client.py
c.send(secure_pad(struct.pack('!B2LHL', 0xFF, cookie, 0, 1, 0)))
resp = c.recv(1024)
cookie, msg = decode(resp)
data = json.loads(msg)
print (data)

Your output should look something like this.

{'NAME': 'Emmaline Jarnagin', 'BADGE': 2297648, 'ACTIVE': True, 'TITLE':
'JANITOR'}

Finding the duplicated badge from the data

Now we write python code to put it all together. This is the fun part.

entries = set()
entry = 0
for entry in itertools.count(0):
    c.send(secure_pad(struct.pack('!B2LHL', 0xFF, cookie, 0, 1, entry)))
    resp = c.recv(1024)
    cookie, msg = decode(resp)
    try:
        f = json.loads(msg)
    except ValueError:
        print ('iterated through whole database')
        break

    if f['BADGE'] in entries:
        print (entry, f)
    entries.add(f['BADGE'])

print (entry)

You can see the whole script here. The 903th entry is duplicated.

Flag

1430758