Challenge completed

This post completes the challenge I took on a while back. I you have not read the first post you should go back and do that before continuing, because the rest would not make much sense.

As I found myself with both some time on my hands and a need to focus on “something completely different” I picked up the thread I threw on the stack a few months ago. I needed to crack the final task in the FRA challenge which was to be able to decrypt the high score list that had been encrypted with RC4.

So I read some more on RC4 weaknesses and I knew the same key was used every time, but I still felt that the task was insanely hard compared to the others if I was to actually calculate the keystream based on the encrypted messages. Sure, I probably had some known plain text, like ANSI escape codes and the player name ‘Kalle’ along with his score. But it just felt wrong.

So I went back to look at the original client code for clues…and there it was, staring at me. Probably laughing too since I should have seen this way back when I originally started out on this task.

Here is a snippet of the code with the clue:

class ClientView:
    def __init__(self, s):
        self.sock=s

        try:
            f=open('crypto.txt', 'r')
            self.xor_key=f.readline()
            self.longxor_key=f.readline().strip()
            self.rc4_key=f.readline().strip()
            f.close()
        except:
            # If keys are missing
            self.xor_key=raw_input('Enter xor key: ')
            self.longxor_key=raw_input('Enter long xor key: ')

            # generate rc4 key
            self.rc4_key=''.join(random.sample(string.uppercase, 7))
            print "Keys are stored in crypto.txt"
            f=open('crypto.txt', 'w')
            self.xor_key=self.xor_key[0]
            print >>f, self.xor_key
            print >>f, self.longxor_key
            print >>f, self.rc4_key
            f.close()

Do you see it? In the exception clause there is code for when the crypto.txt is missing. And if we look closely the RC4 key will only be generated 7 characters long, in uppercase letters! *face palm* Why didn’t I see it before?

self.rc4_key=”.join(random.sample(string.uppercase, 7))

So I went ahead and created a brute force script that will try all combinations until it succeeds. After all, I had md5 hashes for the plain text of all messages. So I could easily check if each attempt was correct or not.

For a 7 characters long password that only consist of uppercase letters there are 8,031,810,176 possible combinations. So I would have to either write a brute force script that used several threads or just write a quick one that I just left running over night. I went with the latter.

#!/usr/bin/python

import json
from Crypto.Cipher import ARC4
from hashlib import md5
import string
import itertools
import time

KEY_LENGTH = 7
checked = 0
found = False


def check(key, data, correct_md5):
    global checked
    global found
    decrypted = ARC4.new(key).decrypt(data)
    checked += 1
    if md5(decrypted).hexdigest() == correct_md5:
        print('Key found: %s' % key)
        found = True

    if checked % 401590508 == 0:
        print('Brute force is at attempt %d trying key %s' % (checked, key))


def worker(base):
    global found
    rc4_message = ''
    correct_md5 = ''
    f = open('packets2.dump', 'r')
    data = f.read()
    f.close()
    for d in ''.join(data).split('\n'):
        try:
            if not len(d):
                continue
            j = json.loads(d)
            if j['encoding'] == 'rc4':
                rc4_message = j['buf'].decode('base64')
                correct_md5 = j['md5']
        except:
            continue

    for i in itertools.product(string.ascii_uppercase, repeat=KEY_LENGTH-len(base)):
        check(''.join(base + i), rc4_message, correct_md5)
        if found:
            exit()


if __name__ == "__main__":
    worker(tuple())

What the script does is simply take the last message encrypted with RC4 and then go into a loop testing all possible keys for the RC4 keystream that encrypted the message, from AAAAAAA, AAAAAAB, AAAAAAC and so on up to ZZZZZZZ. If the correct key is found it will print it out and stop execution of the script. At about each 5% of running it will print out the current progress. I started the script and went to bed wondering if the clue would turn out to be a dead end or not.

When I woke up the next morning and finally came around to check the results I found myself staring at this:

Wow! I had to test this key immediately! So I opened up my old player.py and filled in the key for RC4 and ran the script.
Yes…there it is…the all time high…

So there, finally I can leave that one to history and go crazy on the next FRA challenge! I learned a lot from this one, and I hope the next one will teach me things as well.

Security Incident Management should feed into Risk Management

The problem many security organizations meet is when providing numbers that are based on guess work, and not real numbers, makes senior management less interested in investing time discussing the matter of setting aside resources to address something that could, potentially, be a real risk for the organization.

A reason for this could be that in many organisations the Risk Management and the Incident Management processes are disconnected. This means that risk assessments cannot move from a qualitative (guessing) to a quantitative process using real numbers.

A quantitative assessment makes it easier to compare the cost of implementing a control to mitigate the risk versus the estimated cost of accepting the risk. However, it requires that the organization have some historical statistics to make informed decisions on.

This can also make the process faster, especially since management should have defined the organization’s risk appetite, that is to say how much money is the organization willing to accept loosing before it will invest in mitigating controls. If a certain risk cost less than what is accepted or if addressing it does not save the organization a lot of money (mind that many mitigating controls rarely is a one-time cost/investment and require continuous maintenance and come with a post in the operational expenses, that particular risk does not have to be raised to the immediate attention of senior management.

To start this journey you must invest resources in your Incident Management process and allow it to mature enough to enable quantitative risk assessments. The incident management process must include steps for assessing the cost of each incident. Then ensure that the process is well defined, approved, communicated, and audited to ensure that it is followed every time.

 

Challenge accepted!

A while back someone tipped me off about challenges posted by the Swedish FRA that they use for recruiting, and for fun as you will see in this post. FRA, the National Defence Radio Establishment, is the Swedish national authority for Signals Intelligence. So I checked it out and immediately got excited.

Check them out here: http://challenge.fra.se

The first one that caught my eye was the one labeled “Underrättelseanalytiker inom IT-säkerhetsområdet”, ruffly translated it means “IT Security Intelligence Analyst”, which was the first one that involved cryptography. So I downloaded the zip-file (pcap-challenge20016-1.zip).

Once I had extracted the file I was presented with two files;

  • challenge.pcap
  • readme.txt

The readme.txt contain the objectives written in Swedish, so here is a translation:

Are you the right person for us?

We understand that everyone does not have the time to solve all tasks but we
want you to solve at least one in order to apply for a job here.

1.      What color is the ball?

2.      Draw the game map

3.      Draw the game map in color

4.      What does the high score look like?

E-mail your solution to uuuy25@fra.se

Good luck!

OK, I was not expecting that given I was presented a pcap file which normally is a file containing captured traffic on a network, but I got even more excited to look into the packet capture. So I fired up Wireshark, which is a program to capture traffic but also to look at and analyze captured traffic.

Reconnaissance

The first thing I noticed was that the captured traffic was local on a single system (the same machine). “127.0.0.1” is always the localhost and I saw that both the source and the destination was “127.0.0.1” . This most likely meant that the server and the client software was running on the same machine as the software capturing the traffic.

The time of the capture is mid-lunch on the 21st of January 2016. I guess the creator of the challenge took a desk lunch.

The first six packets are two TCP-handshakes which are then followed by an HTTP GET. HTTP GET is a request usually made by web browsers when they want to download something from a web server. In this case we can see that it is likely a Google Chrome browser requesting the index file of “http://localhost:8000” where the “:8000″ means that instead of the standard port 80 for HTTP, the browser should query port 8000.

I continued to look at the packets after the GET request, which is the response to the HTTP GET request I saw earlier:

I was looking at HTML code that presents the contents of a directory on the host, which was two files;

  • client.py
  • crypto.txt

“.py” indicates a script written in my favorite language Python, so now I knew this was going to be a fun exercise.

I continued browsing the packets to see if there was a download of the files, and I did find a GET request for the Python script shortly followed by the actual script.

For the Python people, here is the full client.py script, as-is:

#!/usr/bin/python
# tested on Ubuntu

import json, socket
import tty, termios, sys, select, time
import socket, md5
from Crypto.Cipher import ARC4 # apt-get install python-pycryptopp
import random, string

class ClientView:
    def __init__(self, s):
        self.sock=s

        try:
            f=open('crypto.txt', 'r')
            self.xor_key=f.readline()
            self.longxor_key=f.readline().strip()
            self.rc4_key=f.readline().strip()
            f.close()
        except:
            # If keys are missing
            self.xor_key=raw_input('Enter xor key: ')
            self.longxor_key=raw_input('Enter long xor key: ')

            # generate rc4 key
            self.rc4_key=''.join(random.sample(string.uppercase, 7))
            print "Keys are stored in crypto.txt"
            f=open('crypto.txt', 'w')
            self.xor_key=self.xor_key[0]
            print >> f, self.xor_key
            print >> f, self.longxor_key
            print >> f, self.rc4_key
            f.close()

        def render(self):
            data=[]
            try:
                while 1:
                    data+=[s.recv(8192)]
                    except socket.timeout:
                        pass

                    if len(data):
                        for d in ''.join(data).split('\n'):
                            try:
                                if not len(d):
                                    continue

                                j=json.loads(d)

                                if j['encoding']=='longxor':
                                    decoded=''.join([chr(ord(j['buf'][i]) ^ ord(self.longxor_key[i % len(self.longxor_key)])) for i in range(len(j['buf']))])
                                elif j['encoding']=='xor':
                                    decoded=''.join([chr(ord(i) ^ ord(self.xor_key[0])) for i in j['buf']])
                                elif j['encoding']=='rc4':
                                    C=ARC4.new(self.rc4_key)
                                    decoded=C.decrypt(j['buf'].decode('base64'))
                                else:
                                    decoded=j['buf']

                                # Received data must be decoded correctly
                                if j['md5']==md5.md5(decoded).hexdigest():
                                    print decoded
                            except:
                                print "ERROR", len(data), len(d)

        def send_packet(self, com, arg):
            buf={"command": com, "arg": arg}
            self.sock.sendall(json.dumps(buf)+'\n')

        def set_player_name(self, buf):
            self.send_packet("set_player_name", buf)

        def player_input(self, buf):
            if 'LEFT'==buf:
                self.send_packet("input", "LEFT")
            if 'RIGHT'==buf:
                self.send_packet("input", "RIGHT")
            if 'FIRE'==buf:
                self.send_packet("input", "FIRE")

HOST='localhost'
PORT=1234
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
s.settimeout(0.0001)

S=ClientView(s)

name=raw_input('What is your name? ')

S.set_player_name(name[:10])

old_settings = termios.tcgetattr(sys.stdin)

try:
    tty.setcbreak(sys.stdin.fileno())
    while 1:
        # get updates from server
        S.render()

        # nonblocking input
        if select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []):
            c=sys.stdin.read(1)

            if c=='\033':
                break # player pressed esc
            if c=='z': # left
                S.player_input('LEFT')
            if c=='x': # right
                S.player_input('RIGHT')
            if c==' ': # space
                S.player_input('FIRE')

            sys.stdin.flush()
        time.sleep(0.01)

finally:
    termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)

s.sendall('Bye')
s.close()

So now I had something we could call a client, that look a lot like a game given the references in the comments (in-line comments in Python starts with a ‘#’) like “# player pressed esc”, “FIRE”, etc.

Basically, it starts by asking the player to enter his or her name. Then the server will send data to the client, which can be encoded with either “xor_key”, “long_xor_key”, “rc4_key”, or no encoding.

Given that this was a challenge in network protocols and crypto I did not expect to find any trace of the crypto.txt in the packet capture. But I looked anyway. Doing so I stumbled upon a packet with payload that looked like JSON.

JSON is a way of structuring data and in this case it contains four parameters with different values;

  • “function” set to “clear”
  • “md5” set to “84fc620ddf5d285f4d15bfe440da1100”
  • “buf” set to “\u001b[2J”
  • “encoding” set to “none”

Seeing this I noted two things; MD5 is a hashing function, or one-way encryption algorithm designed to output unique strings called “hashes”, or “checksums”, or “digests” for its input. And the value of “buf” sure looks like an ANSI escape code. ANSI escape codes are used to control the formatting, color, and other options in terminals, like this:

Entering print u”\u001b[2J” cleared the console and I had a confirmed link between the function “clear” and the value of “buf” which is an ANSI escape code to clear the terminal. To verify the logic I calculated the MD5 checksum of the “buf” value, and it matched the “md5” value perfectly.

After the “clear” packet there is another packet with JSON. This time the “function” has the value of “layout”, then there is the MD5 hash and the encoding is set to “none”. But “buf” contain a lot more data, still ANSI escape codes.

{"function": "layout", "md5": "98997c968f32a8e562b6a07e15961505", "buf": "\u001b[0;0H\u001b[42m                                                                                         \u001b[1;0H \u001b[1;66H \u001b[1;90H \u001b[2;0H \u001b[2;66H \u001b[2;90H \u001b[3;0H \u001b[3;66H \u001b[3;90H \u001b[4;0H \u001b[4;66H \u001b[4;90H \u001b[5;0H \u001b[5;66H \u001b[5;90H \u001b[6;0H \u001b[6;66H \u001b[6;90H \u001b[7;0H \u001b[7;66H \u001b[7;90H \u001b[8;0H \u001b[8;66H \u001b[8;90H \u001b[9;0H \u001b[9;66H \u001b[9;90H \u001b[10;0H \u001b[10;66H \u001b[10;90H \u001b[11;0H \u001b[11;66H \u001b[11;90H \u001b[12;0H \u001b[12;66H \u001b[12;90H \u001b[13;0H \u001b[13;66H \u001b[13;90H \u001b[14;0H \u001b[14;66H \u001b[14;90H \u001b[15;0H \u001b[15;66H \u001b[15;90H \u001b[16;0H \u001b[16;66H \u001b[16;90H \u001b[17;0H \u001b[17;66H \u001b[17;90H \u001b[18;0H \u001b[18;66H \u001b[18;90H \u001b[19;0H \u001b[19;66H \u001b[19;90H \u001b[20;0H \u001b[20;66H \u001b[20;90H \u001b[21;0H \u001b[21;66H \u001b[21;90H \u001b[22;0H \u001b[22;66H \u001b[22;90H \u001b[23;0H \u001b[23;66H \u001b[23;90H \u001b[24;0H \u001b[24;66H \u001b[24;90H \u001b[25;0H \u001b[25;66H \u001b[25;90H \u001b[26;0H \u001b[26;66H \u001b[26;90H \u001b[27;0H \u001b[27;66H \u001b[27;90H \u001b[28;0H \u001b[28;66H \u001b[28;90H \u001b[29;0H \u001b[29;66H \u001b[29;90H \u001b[30;0H \u001b[30;66H \u001b[30;90H \u001b[31;0H \u001b[31;66H \u001b[31;90H \u001b[32;0H \u001b[32;66H \u001b[32;90H \u001b[33;0H \u001b[33;66H \u001b[33;90H \u001b[34;0H \u001b[34;66H \u001b[34;90H \u001b[35;0H \u001b[35;66H \u001b[35;90H \u001b[36;0H \u001b[36;66H \u001b[36;90H \u001b[37;0H \u001b[37;66H \u001b[37;90H \u001b[38;0H                                                                                         \u001b[40m", "encoding": "none"}

The next step was obvious, print it in a console!

Now this really started to look like the games I knew from way back when I was a kid messing around with my Commodore 64. So I went back to look more closely at the code in client.py and doing so I thought that I could easily write my own version that take a file as input and parses each JSON message and print it out in the console. In theory, I should be able to see the game being played. First, I went through the rest of the packet capture and found other “functions” like “menu”, “highscore”, “score”, “ball”, “player”, “mono_map”, “color_map”. The “mono_map” and the “color_map” data was encrypted using XOR and “highscore” was encrypted with RC4 which is also using XOR, but with a continuous key stream instead of a shorter, repeating key.

XOR, or eXclusive OR, is a logical operator that look at two bits (a bit is a one or a zero, where 8 of them sums up to a byte) and if they are equal it output zero, but if they are not equal it output one, like this:

INPUT

OUTPUT

A

B

A XOR B

0

0

0

0

1

1

1

0

1

1

1

0

In addition to all these “functions” I found “commands”, also in JSON format, that seemed to be things sent to the server;

{"command": "set_player_name", "arg": "Kalle"}

{"command": "input", "arg": "RIGHT"}

{"command": "input", "arg": "LEFT"}

I decided to extract all the JSON messages which is very easy to do in Wireshark. Right-click on any packet and choose “Follow” -> “TCP stream”. This will open a window with all the payload nice and clean without any other details about the packets. Doing this I saw that there were two TCP streams with “game” data. I saved both in case I needed the other one later. I could already see that the “highscore” data differed when I was comparing the streams.

Recon analysis

Now I had a lot of information about this challenge and could draw some conclusions;

  • It was the network traffic containing the download of a python script that looked like a game client, followed by the conversation between the game client and the game server.
  • The conversation was in JSON, which I had extracted. Two TCP streams (or game rounds?).
  • There were three kinds of encoding/encryption all using XOR. One with a long key and presumably another with a smaller key, and one using RC4 which is a streaming key that is as long as the input. This made sense looking back at the tasks:

The first task is to find out the color of the ball and that had no encryption, leading me to assume that the first task was made to test basic skills in analyzing network traffic and identifying the payload as JSON and ANSI escape codes.

The second task was to draw the mono_map which was XOR’ed with the smaller key. So when I looked at the client.py code again for the xor_key I noticed this:

elif j['encoding'] == 'xor':
    decoded=''.join([chr(ord(i) ^ ord(self.xor_key[0])) for i in j['buf']])

The [0] means that whatever the xor_key consist of I should only look at the first character! Easy.

The third task was to draw the color_map which was XOR’ed with a longer key, and finally tell what the high score looked like which was encrypted with RC4 stream cipher.

The difficulty was undeniably increased for every task.

  • It should be possible to write a “player” to animate the game that was played, the hard part is to break the keys…

Writing the player

Stealing with pride I altered the client.py to read from a file instead and just print out the ANSI escape codes, if the MD5-hash was correct for each “function”:

#!/usr/bin/python

from __future__ import print_function import json

import tty, termios, sys, select, time
from hashlib import md5
from Crypto.Cipher import ARC4

old_settings = termios.tcgetattr(sys.stdin)
tty.setcbreak(sys.stdin.fileno())

xor_key = ''
long_xor_key = ''
rc4_key = ''

f = open('packets1.dump', 'r')
data = f.read()

for d in ''.join(data).split('\n'):
    try:
        if not len(d):
            continue

        j = json.loads(d)

        if j['function'] != 'clear':
            time.sleep(0.01)

        if j['encoding'] == 'longxor' and len(long_xor_key) > 0:
            decoded = ''.join([chr(ord(j['buf'][i]) ^ ord(long_xor_key[i % len(long_xor_key)])) for i in range(len(j['buf']))])
        elif j['encoding'] == 'xor' and len(xor_key) > 0:
            decoded = ''.join([chr(ord(i) ^ ord(xor_key)) for i in j['buf']])
        elif j['encoding'] == 'rc4' and len(rc4_key) > 0:
            C = ARC4.new(rc4_key)
            decoded = C.decrypt(j['buf'].decode('base64'))
        else:
            decoded = j['buf']

        if j['md5'] == md5(decoded).hexdigest():
            print(decoded)

        if select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []):
            c = sys.stdin.read(1)

            if c == '\033':
                break
    except:
        continue

termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)

Testing the player

There was plenty of data that had encoding set to “none”, so I was now very eager to test the player and confirm my suspicion about the game thing…and voilà!

I had solved the first task. The color of the ball was blue (ETERNAL BLUE?) and I was witnessing the good old Arkanoid game. I could see the ball bouncing against something that was not visible, but the score was increasing so that was making sense in terms of how the game works. I was ready to find the xor_key.

Writing breakXor.py

Using the same code, but stripping of all of the unnecessary things and adding a “brute force” part was simple enough since I already knew that the xor_key was only one character. In Python that is a simple task by iterating the key in an array called string.printable that contains all printable characters A-Z, a-z, 0-9 and all special characters and white spaces. So after each XOR I compared the MD5 hash with the expected, and if I had a match I knew I had the correct key.

#!/usr/bin/python

from __future__ import print_function
import string
import json
from hashlib import md5

f = open('packets1.dump', 'r')
data = f.read()

keyFound = False

for d in ''.join(data).split('\n'):
   if keyFound:
       break

   try:
       if not len(d):
           continue

       j = json.loads(d)

       if j['encoding'] == 'xor':
           for char in string.printable:
               decoded = ''.join([chr(ord(i) ^ ord(char)) for i in j['buf']])
               if j['md5'] == md5(decoded).hexdigest():
                   print('XOR key: %s' % char)
                   keyFound = True
                   break
       else:
           continue
   except:
       continue

It was simple enough. Running the script took no time, and the result was:

So I inserted the super secret character “A” into my player; xor_key = ‘A’ and ran the player again:

I had now successfully completed task number two: “Draw the game map”.

The next task proved to be a bit more difficult. I had no information about long_xor_key. How long was it? Does it contain non-printable characters? I also knew very little about the plain text that would be the result after XOR’ing the data with the long_xor_key except that I was dead sure it had to be ANSI escape codes.

Breaking the long_xor_key

It then hit me that the ANSI escape codes in the color_map should have many similarities with the ANSI escape codes in the mono_map, and I had the plain text of that. The cool thing with XOR is that if you take a plain text and XOR it with a key, then XOR the output of that first operation with the plain text you end up with the key, like so (the XOR operation is represented by ^):

TEXT ^ KEY = CODE
TEXT ^ CODE = KEY

So if I wrote a script that take the plain text of the mono_map and XOR that with the encoded version of the color_map I should get some hints about what the long_xor_key could be. So I decided to create something that did just that, then look at the result for repeating strings of a certain length and store them along with the number of times it repeated. (Probably not how one should do this, but hey, I am just having fun!) Then I would “move” the mono_map one step left and repeat the procedure to see if the same repetitions occur elsewhere, and if so add the number of repetitions to the previous result. So “sliding” the mono_map plain text over the color_map a number of times should give me a better idea about the long_xor_key than just doing it once. I also wanted to sort the list of potential keys by how many times it was found and start with the ones with most repetitions first to hopefully speed up the process. I also wanted to be able to limit the key length, starting out with a minimum length of 8 characters and a maximum length of 16 characters. I also wanted to disqualify any key that did not repeat at least 64 times during the process. In addition I decided to slide 16 times (the same as the longest acceptable key length). The code ended up like this (I never claimed to be a skilled programmer, I just want to be able to write things that solve my problems):

#!/usr/bin/python
from __future__ import print_function
import json
import string
from hashlib import md5
import operator

chars = string.digits + string.lowercase + string.uppercase # Assuming there are no non-printable characters in the key and for now excluding whitespaces
MAX_SLIDING_WINDOW = 16 # We want to use an offset to find the key
MIN_LENGTH = 8
MAX_LENGTH = 16
MIN_REPEAT = 64

xor_key = 'A' # We already know the XOR key and we need it to produce the known plaintext
gotXor = False # To know when we have data encoded with the short XOR key
gotLongXor = False # To know when we have data encoded with the long XOR key

running = True # We only want to work on one set, not the entire file, this is to keep track

xor = '' # Placeholder for data with short XOR key
longxor = '' # Placeholder for data with long XOR key
md5longXor = '' # Placeholder for the MD5 hash of the longxor data to check if we got the correct key
result = '' # Placeholder for the result of XOR of known plaintext and XORed data

f = open('packets1.dump', 'r') # File containing JSON from the packet capture
data = f.read() # Load file content

for d in ''.join(data).split('\n'):
   if not running:
       break

   try:
       if not len(d):
           continue

       j = json.loads(d)

       if j['encoding'] == 'xor' and not gotXor:
           xor = ''.join([chr(ord(i) ^ ord(xor_key)) for i in j['buf']])
           gotXor = True

       if j['encoding'] == 'longxor' and not gotLongXor:
           longxor = j['buf']
           md5longXor = j['md5']
           gotLongXor = True

       if gotXor and gotLongXor:
           running = False
           candidates = {}

           for slidingWindow in range(MAX_SLIDING_WINDOW):
               for i in range(0, len(xor)):
                   char = ord(xor[i]) ^ ord(longxor[i + slidingWindow])
                   if chr(char) in chars:
                       result += chr(char)
           for cursor in range(len(result)):
               for window in range(MIN_LENGTH, MAX_LENGTH):
                   if (cursor + window < len(result)): strBuffer = result[cursor:cursor + window] if len(strBuffer) >= MIN_LENGTH:
                           if strBuffer in candidates:
                               candidates[strBuffer] += 1
                           else:
                               candidates[strBuffer] = 1
           shortList = {}
           for key in candidates:
               if candidates[key] >= MIN_REPEAT:
                   shortList[key] = candidates[key]

           rShortList = sorted(shortList, key=operator.itemgetter(1), reverse=True)

           print('Testing the most likely keys...')
           for key in rShortList:
               print('Testing key: %s' % key)
               decoded = ''.join([chr(ord(longxor[i]) ^ ord(key[i % len(key)])) for i in range(len(longxor))])

               print('Checking MD5...', end='')
               if md5longXor == md5(decoded).hexdigest():
                   print('success!\n\nLong XOR key found: %s' % key)
                   break
               else:
                   print('not a match.')
   except:
       continue

Finally, I ran the script which completed much faster than I had expected. I was probably lucky, but when I saw the result I smiled…

I quickly went back to my player and insert the value for long_xor_key and ran the player again:

I had completed the third task; “Draw the game map in color”, and I was quite happy with my achievements so far. But there was a gaping, black, empty square taunting me…the area where the RC4 encrypted high score is supposed to be.

The final task

I knew this would be much harder. Breaking the xor_key was easy, the long_xor_keywas a bit harder (and I might had been lucky). But the rc4_key should be something else in terms of complexity. After some thinking I came to my senses and realized I was never going to find out the exact key that was used to produce the random noise of bits that was actually used to encrypt the high score. I am still scratching my head so there will (hopefully) be a part two to this article. But I do have a way forward…I just need to find the time. What I have so far is:

  • I have several messages encoded with RC4 (EM1, EM2, EM3, etc).
  • RC4 produce pseudo-random bits based on a key which can be referred to as a keystream (KS).
  • The same key is used each time! (based on the Python code in the client.py file).

Therefore;

Key -> RC4 algorithm -> KS

M1 ^ KS = EM1

M2 ^ KS = EM2

etc...

When I think back of the properties of XOR I understand that XOR’ing M1E and M2E would cancel out the KS. This means that:

EM1 ^ EM2 = (M1 ^ KS) ^ (M2 ^ KS)

and:

(M1 ^ KS) ^ (M2 ^ KS) = M1 ^ M2

giving:

EM1 ^ EM2 = M1 ^ M2

So, XOR’ing two RC4 encrypted messages (that was encrypted using the same key) should get me the same product as XOR’ing the original messages. The key will be gone!

I still want the KS, at least as long as the longest message, this way I can decode the high score on the fly, but I will rid my player of the RC4 key and just use an even longer XOR key like in the previous tasks. But how do I get the key stream? I could use the above to get more plain text of the high score, then attack an encrypted message to get the key stream. I know the player name is “Kalle” and his final score. I am pretty sure there is ANSI escape codes. So I have things to play with…I just need time.

I will give the last task a go, but if I fail I will put it on ice for a while and move to another challenge. It is fun and I learn a lot.

Hat’s of to FRA for creating these challenges!