Thursday, 26 June 2014

MWR Hackfu Challenge 2014

Every year MWR release their infamous Hackfu challenge in the build-up to the actual Hackfu event. I had a crack at this years challenge and managed to get just over half way. In this post I'll discuss some of the solutions.

For MWR Hackfu 2013 solutions check the excellent post here.


Pre-Challenge-Challenge

The challenge started off with a zip file containing a code book, some encoded text and an AES encrypted file. Decoding the text would give you the password to decrypt the file.

The whole challenge was based around the one time pad CT-37 (http://users.telenet.be/d.rijmenants/en/otp.htm). You were given a set of 1000+ possible pads and had to decode the encoded text with each pad. Searching through the 1000+ decoded texts you would find the answer.

The pads were in a bit of an awkward format:

So the first thing to do was format the pads into something more user friendly:
with open("book.txt") as f:
    content = f.readlines()

for x in xrange(0, len(content)-1, 17):
    col1 = ""
    col2 = ""
    for y in range(x+4,x+14):
        col1 = col1 + str(content[y].split("|")[1].replace(" ", ""))
        col2 = col2 + str(content[y].split("|")[2].replace(" ", "")) 
    print col1
    print col2

Next came the decoding - I read in the pads, modulus with the encoded text then convert the result from numbers back to letters. I didn't quite finish the logic for number decoding so the final result had letters decoded but numbers encoded, meaning some manual conversion was needed.
ct37 = {"1":"A", "2":"E", "3":"I", "4":"N","5":"O","6":"T","70":"B","71":"C","72":"D","73":"F","74":"G","75":"H","76":"J","77":"K","78":"L","79":"M","80":"P","81":"Q","82":"R","83":"S","84":"U","85":"V","86":"W","87":"X","88":"Y","89":"Z","90":"FIG","91":".","92":":","93":"'","94":"bla","95":"+","96":"-","97":"=","98":"REQ","99":" "}

#MWR TEXT
text = "769605051216509986104949466790121237625886055201851226360699645529130149291137238279392786680278378964378759191773333762068904750697824787177658393352150777878727078"

with open("book1.txt") as f:
    content = f.readlines()

for k in range(1200):
    
    book = str(content[k])

    ctext = list(text.replace(" ", ""))
    pad = list(book.replace(" ", ""))

    print "********************BOOK " + str(k) + "********************"
    #print "Text: " + str(ctext)
    #print "Book: " + str(pad)
    
    x = []
    for i in range(0,len(ctext)):
        x.append(str((int(ctext[i]) + int(pad[i])) % 10))

    i = 0
    y = []
    code = 0
    double = 0 
    
    for i in range(0,len(x)):
        if(code>0):
            code += 1
        if(double>0):
            double += 1

        if(code==0 and double==0 and int(x[i])==0 and i<len(x)-3):
            code=1
            y.append(x[i] + x[i+1] + x[i+2] + x[i+3])
        elif(code==0 and double==0 and int(x[i])>6 and i<len(x)-1):
            double=1
            y.append(x[i] + x[i+1])
        elif(code==0 and double==0 and int(x[i])<7):
            y.append(x[i])

        if(code>3):
            code = 0 
        if(double>1):
            double = 0

    for j in y:
        if j in ct37:
            print ct37[j],
        else:
            print j,
    print "."

The final message:
PREVIOUS COMMUNICATIONS COMPROMISED. NEW INSTRUCTIONS SENT. PASSWORD IS BOGEY-23+FOX22.59


The Container

With the answer from the pre-challenge I could now decode the AES encrypted file to access the rest of the challenges:
openssl enc -base64 -d -aes-256-cbc -in container.zip.aes -out container.zip
The zip file contained four challenges, I managed to complete 1, 3 and half of 4.
  • 01 - Binary Reversing
  • 02 - Steganography
  • 03 - Encryption
  • 04 - Password Recovery

01 - Binary Reversing

This challenge involved analysing a binary and discovering a secret password. You could enter a password guess and the program would tell you if it was right or wrong.


Assuming that brute force would be too time consuming I set to work analysing the binary with IDA and EDB debugger. The first part of the program could be followed with relative ease and you could observe the program iterating over the characters of the input password one by one. I discovered that when a valid character was found a counter variable was incremented. When the counter reached 19 the below condition would be met and the success message given:


To find valid characters the most obvious approach was to check the code for where the counter was incremented, I had hoped there would be some simple comparisons that revealed the characters... Unfortunately things weren't that simple and the bottom part of the program that actually analysed the characters was too complex to follow.

So at this point I knew that a counter was incremented on correct password characters but I couldn't find out how/why using static analysis - what about dynamic analysis?

If there was some way to iterate over password characters and read the counter's memory value (at esp+78h+var_50) as the program executed, I would know when a valid character was hit as the counter would increment. Never wanting to reinvent the wheel I hit Google and came across an interesting GDB fuzzer here. With a few tweaks it was ready to go.

(The more sexy solution would have been to patch the binary to spit out the counter value, unfortunately my assembly skills are somewhat limited)

To execute a python script in GDB:
gdb -q -x myscript.py 

The fuzzing script:
# -*- coding: utf-8 -*-
import gdb
sys.path.append(os.getcwd())

guess = ['"','z','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','a','"']

def break_handler (event):
         print "Break hit!"
         gdb.execute('x/x $esp+40')

gdb.execute('set pagination off')
gdb.execute('set verbose off')
gdb.execute('set confirm off')
gdb.execute('file check-passwd')
gdb.execute('break *0x8049603')
gdb.events.stop.connect(break_handler)

i=1
for i in range(13,len(guess)-7):
    for j in range(21,700):
        if j==34 or j==92 or j==96: j+=1
        val = unichr(j).encode('utf8')
        print j
        guess[i] = str(val)
        print "Guess: " + ''.join(guess)
        gdb.execute('set args ' + ''.join(guess))
        gdb.execute('run')

gdb.execute('quit')

As an example, when trying "u" as the 18th character you can see the counter incremented indicating this was a valid character. But when trying "t" or "v" the counter stayed at zero.


By analysing the script output I was able to construct the complete password.
novus orD0_saclorum


02 - Steganography

In this challenge you had to reverse the stego techniques used to hide data within an image. You were given a before image and an after image, by diff-ing the images you could see a pattern of changed bytes. Without knowing much about the jpeg format or compression I passed on this challenge, apparently the solution had to do with the least significant bit of the luminescence blocks.


03 - Encryption

This was one of those fiendishly simple yet annoyingly difficult challenges. You were basically told to decode this:
+------++++-+-+-++++++----+++--++--+-----+--+++--++-----+++--+++-+-+-+
-++++-----+-+-+++---+-+++++--+++-----+++--+++-+++++----+---++-+-++-+++
++--+--+++-+++++----+-+--+-+-++-+-++-+++++--+--++--++---+++------++--+
--+++++---+-+-----+-++---+-++-+-+-+++--++-++++-+-+-++-+-++--+---++-+-+
-+++++--++++--+-+++-+----++--+-+++-++----+++++-++-+-+-+-+++-++--+-+-++
+-----+--++-+-+++--+-++---+--++-+++-++--+-++-+----++--+++-++--+-++-+-+
++--+++----++++---++-++---+--++-++-++--++-+--+----+-++++-+-+++--+--+-+
+-+-+-+--++----+--++--++-+-+-+-+++--+++--++---+--++-+-++-+-+--+-+-+++-
--++-++---+--++-+++-++---+--++++-----+-+-++--+-+-+--+-+-+--+-++---+--+

I initially tried some binary and ascii conversion as well as chunking as the 630 character block evenly split into chunks of 3, 5, 6, 7, 9 and 10 but couldn't spot anything. With a dot/dash like pattern I tried some Morse code analysis too but with spaces missing and no kind of deliminator the number of word combinations was too great. So what was the answer?

After a hint from MWR I revisited the binary conversion/chunking and realised I had missed the obvious. By converting the -/+ to 0's and 1's then splitting the data into chunks of 5 bits, you ended up with each chunk corresponding to a number in the range 1-26, hello alphabet!

etext = "100000011110101011111100001110011001000001001110011000001110011101010101111000001010111000101111100111000001110011101111100001000110101101111100100111011111000010100101011010110111110010011001100011100000011001001111100010100000101100010110101011100110111101010110101100100011010101111100111100101110100001100101110110000111110110101010111011001010111000001001101011100101100010011011101100101101000011001110110010110101110011100001111000110110001001101101100110100100001011110101110010010110101010011000010011001101010101110011100110001001101011010100101011100011011000100110111011000100111100000101011001010100101010010110001001"

for n in range(5,8):
    print "CHUNK SIZE: " + str(n)
    arr = []
    for i in xrange(0, len(etext), n):
        arr.append(int(str(etext[i:i+n]),2))
    print arr

Of course no challenge is that easy, simple A=1, B=2 etc. substitution didn't work and neither did Caeser cipher variants. To find the answer frequency analysis was needed. I initially tried to do the substitution manually but the number of combinations made life difficult so in the end I used the excellent cryptogram solver here: http://quipqiup.com

This gave me a rough answer:
operations otter heads approved s proceeds with spackages deliverfs to stargets bones ind i go sumesaryings codesmen of wettine em twaft a famiemnin

With a little hangman style guess work I cleaned it up to get the final answer:
operation otterhead approved proceed with package delivery to target zone indigo use arming code senoywettineestwaytayasiesnin


04 - Password Recovery

The final challenge involved decrypting a set of messages from a Mexican wrestling forum that had been hacked, you were given the password hashes and messages from the forum. First-up messages had to be base64 decoded, then translated from Spanish to English. The two target users could be seen sending AES encrypted messages back and forth.

My first plan of attack was to target password reuse. I guessed the target users may have used the same password for the forum as they did when encrypting their private conversation. In the story text MWR had given us a user and a password (NaClgoofd), this suggested a weak or badly designed salt scheme was in use. After much toying with hashcat I figured out that the site had used the id and username of the user as the salt when hashing passwords e.g. md5(id+username+password).

To crack the hashes I added the salt prefix using a hashcat mask:
cudaHashcat64.exe -m 0 -a 7 hash.txt ^0^0^1^a^d^m^i^n^i^s^t^r^a^d^o^r rockyou.txt

With the salt scheme I was able to crack the hashes of the two target users and the admin:
administrador:Redfish99
LaSombra:2fast4u
ElEnterrador:password

Unfortunately the private messages weren't just encrypted with these passwords. There was actually a more funky encryption routine at work. MWR released the following hint:
secret = SaltMachine.generate(msg.ref_info,application.secrets.privateboard)
key = md5.md5(secret).digest()

The private messages were actually encrypted with a key that was an md5 of msgid + threadid + userid + admin password. I've not used pycrypto much in the past so even with the solution in hand it took me a while (and a hint or two) to finally get the answer:
import hashlib
import base64
from Crypto.Cipher import AES

def decrypt(key,input):
    ciphertext = base64.b64decode(input)
    iv = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
    cipher = AES.new(hashlib.md5(key).digest(), AES.MODE_CBC, iv)
    return cipher.decrypt(ciphertext)

print decrypt("003575029004Redfish99","WMJJBOOhGCkQrUVE0T03paykzQA5HZAdBVf5LaPcBpDZUIO9Y4M9lo2oU7ra9Gji7F3Qpph7jgvtpXIJDIgs+Q==")

print decrypt("003578029003Redfish99","KM4C5iPGzm9d01TwsLTqfA7Pas20qGgsiolhKMly9PT0qdIIjX8+mh2wrXqR4fdga0Aw+AF9g2YMGOoMQDeb0VmWWT06FtdZxv2CFIUAa1A9+rlkWyiMa4zewrYyKJDy")

print decrypt("003581029004Redfish99","2MyEvMWqWyfWAC/z6gypxvpvgCLIwt7ZHw64Yy3KBeqS9+QOk0bVzXQeI9MJo0Hm")

Decrypting the text gave the final solution:
ElEnterrador: Do we have progress? You committed to having it ready by now...
LaSombra: Calmate... Package is already in escrow. Pay my guy, and you'll get your little present.
ElEnterrador: Payment made. Reference: 18954-XW-8893432-AQP7


Final Thoughts

Hackfu certainly isn't for the faint-hearted. The challenges require technical know-how, the ability to think outside the box and dedication (unlike a bug bounty there was no prize money!). Despite not completing every challenge I still found Hackfu a great learning experience and it was an amazing feeling finally reaching answers after grinding away at them for so long.

If you have any questions about the challenges (or if you have a solution to challenge #2!) just drop me a comment below.

Big thanks to MWR for creating HackFu 2014, can't wait to see what next year brings!

Pwndizzle out.