Friday, 10 November 2017

CTF Writeup - Flare-On 2017 - 11: covfefe.exe


  • Name - covfefe.exe
  • Category - Reverse Engineering
  • Points - 1
  • Binary - Download here

The penultimate Flare-On challenge was, in hindsight, my absolute favourite. From the outside, the binary is just a basic command prompt that asks for a password:

Command Prompt
C:\>covfefe.exe Welcome to the magic box. Enter the password: random_stuff You did not guess the password. Consider brute-force guessing using a script, and letting it run for a few trillion years. C:\>


Looking at it under IDA, the binary's code is deceptively simple and doesn't do much. We only have 3 functions with the most important being sub_401000:

bool __cdecl sub_401000(int a1, int a2, int a3, int a4)
{
    bool result; // al@2

    *(_DWORD *)(a1 + 4 * a3) -= *(_DWORD *)(a1 + 4 * a2);
    if ( a4 )
        result = *(_DWORD *)(a1 + 4 * a3) <= 0;
    else
        result = 0;
    return result;
}

The function accepts a start position, a1, and 2 offsets, a2 and a3, and returns True if a1[a2] - a1[a3] is less than or equal to 0, else it returns False. The entire logic of this program is based solely on this instruction. We're hence dealing with a SUBLEQ VM.

For those of you who would like to see an elegant way of solving this challenge, please find another write-up. Here I'll be going through a very basic and manual way of how I solved it. The SUBLEQ VM is too complicated to go through instruction by instruction so, my technique was to put RW breakpoints on the memory region that holds our input and follow any transformations that happen to it. If parts of the input are copied to another memory region, we'll put RW breakpoints on those as well and monitor them.

So, let's get started by supplying the program with 'random' as input. After a few breakpoints we see that our input has been stored in location 0x40313C:


Add a new RW breakpoint to cover the 0x18-byte memory region starting from 0x40313C. After hitting the new breakpoint a few times we find that the first 2 chars of our input were copied to locations 0x406A68 and 0x406A6C respectively:


Put a RW breakpoint on these 2 new locations. Continuing with the execution we encounter the first operation on the first character:


The value 0x72, which is hex for 'r', the first character of our input, has been changed to 0x6AE. In other words, it has been multiplied by 0xF. The resultant value is then squared 7 times, which gives us 0x35700:


The focus now shifts to the 2nd char, which is 'a' in our case. This is copied to location 0x405FC4. At the same time, the value 0x7F is copied to location 0x405FC0. Both these values can be seen in the following screenshot:


By putting a breakpoint on each of these memory regions containing the 2 values we can see them change with each iteration. This part wasn't that straight forward to deduce using my method. After a while the values stop changing and we get the following scenario:


The original values, 0x7F and 0x61, which resided in locations 0x405FC4 and 0x405FC0 respectively, have been modified to show 0xFFFFFFFF. At the same time though, location 0x405FDC contains the value 0x20. This is a simple counter which tells the SUBLEQ VM to stop operating on these 2 values when it reaches 0x20. More importantly though is location 0x405FD0 which contains the value 0x1E. So which operation on values 0x7F and 0x61 gives 0x1E? Well, it's XOR of course.

The SUBLEQ VM now copies the resultant to where the original value was:


So at this point we have values 0x35700 and 0x1E. A few breakpoints later, we can see that the values have been OR'd and the result replaces the first value:


To be honest the operation could have been interpreted as an XOR as it produces the same result. In fact, it doesn't matter if we use OR or XOR, the challenge is still possible with either. Continuing, we encounter this situation:


The value 0x35E8A overwrites the second value. Then, our result, which resides at location 0x406A68, and this new value are subtracted from each other. If we continue execution we notice that the whole process is restarted with the next 2 characters of our input. This strongly suggests that the value 0x35E8A is what our result should have been.

The strategy to completing the challenge is now straight forward: collect all the comparison results and for each, reverse the algorithm to obtain 2 characters of the input each time. First, let us reiterate what the algorithm is for each 2 characters of our input:
  1. Multiply the first char by 0xF and square the value 7 times
  2. XOR the 2nd character by 0x7F
  3. OR the 2 results together
  4. Compare the final result to a fixed value
The following script bruteforces each 2 characters from the collected values by reversing the algorithm above:

import string, sys

sols = [0x35E8A,0x2DF13,0x2F58E,0x2C89E,0x3391B,0x2C88D,0x2F59B,0x36D9C,0x36616,0x340A0,0x2D79B,0x2C89E,0x2DF0C,0x36D8D,0x2EE0A,0x331FF]

def try_sol(c1, c2, sol):
    c1 *= 0xF
    for x in range(7):
        c1 += c1
    c2 ^= 0x7F
    c12 = c1 | c2
    if c12 == sol:
        return True
    return False

for sol in sols:
    for c1 in string.printable + '\x00':
        for c2 in string.printable + '\x00':
            if try_sol(ord(c1),ord(c2), sol):
                sys.stdout.write(c1)
                sys.stdout.write(c2)

Running the script:

Command Prompt
C:\>python covfefe_sol.py subleq_and_reductio_ad_absurdum C:\>

CTF Writeup - Flare-On 2017 - 10: shell.php


  • Name - shell.php
  • Category - Reverse Engineering
  • Points - 1
  • Binary - Download here

The next challenge on the list was a PHP one. This is by far the one I liked the least as it requires a lot of manual work and guessing. The following shows the contents of shell.php:

<?php
    $o__o_ = base64_decode('QAYLHhIJ ... [snip] ... kCEh8NSh4PEA4eGFsaGXoo');
    $o_o = isset($_POST['o_o']) ? $_POST['o_o'] : "";
    $o_o = md5($o_o) . substr(MD5(strrev($o_o)) , 0, strlen($o_o));
    
    for ($o___o = 0; $o___o < 2268; $o___o++)
    {
        $o__o_[$o___o] = chr((ord($o__o_[$o___o]) ^ ord($o_o[$o___o])) % 256);
        $o_o.= $o__o_[$o___o];
    }
    
    if (MD5($o__o_) == '43a141570e0c926e0e3673216a4dd73d')
    {
        if (isset($_POST['o_o'])) @setcookie('o_o', $_POST['o_o']);
        $o___o = create_function('', $o__o_);
        unset($o_o, $o__o_);
        $o___o();
    }
    else
    {
        echo '<form method="post" action="shell.php"><input type="text" name="o_o" value=""/><input type="submit" value=">"/></form>';
    }
?>

The script is easy enough to understand. It basically does the following:
  • Base64 decodes a massive string (line 2)
  • Accepts a password via a POST request (line 3)
  • Concatenates it's MD5 and the substring of the MD5 of the reverse string (line 4)
  • Uses the result as the key to XOR the massive string (lines 6 - 10)
  • Creates a new function from the decrypted text (line 15)
  • Calls the function (line 17)
The aim here is to find the key and decrypt the Base64 string. There are a few constraints and possibilities here that we need to note 1) the XOR key is made up of only [0-9][a-f] characters due to it being the result of an MD5, 2) the key is of length between 33 and 64, and 3) the decrypted string has to be PHP code, i.e. made out of printable characters.

Expanding on constraint #3, notice that the key is repeated which means that a character of the key is a potential solution if it produces a printable character each time it is used. As an example let us assume that the key length is 64 and that we want to verify if character '7' is a potential candidate to being the first character of the key. Then, '7' XOR'd with the first byte of the encrypted text has to produce a printable character; the result XOR'd with the 65th byte of the encrypted text has to produce a printable character as well, and so on so forth until we exhaust the encrypted text. If it produces printable characters everywhere then '7' is a potential candidate, else not.

The following script will determine all possible characters for key lengths between 33 and 64:

import base64
import string

encoded = "QAYLHhIJ ... [snip] ... kCEh8NSh4PEA4eGFsaGXoo"
decoded = base64.b64decode(encoded)

possible_chars = "0123456789abcdef"
   
for key_len in xrange(33, 65):

    print "-- Key Length: " + str (key_len) + " --"

    for key_pos in xrange(0, key_len):
        
        potential_sol = []
    
        for possible_char in possible_chars:
            temp_possible_char = possible_char
            possible = True
            for pos in range(key_pos, len(decoded), key_len):
                temp_possible_char = chr((ord(decoded[pos]) ^ ord(temp_possible_char)))
                if temp_possible_char not in string.printable:
                    possible = False
                    break 
                    
            if possible:
                potential_sol.append(possible_char)
        
        if potential_sol:
            print key_pos, ":", potential_sol

Running the script we get:

Command Prompt
C:\>python possible_solutions.py -- Key Length: 33 -- -- Key Length: 34 -- -- Key Length: 35 -- -- Key Length: 36 -- -- Key Length: 37 -- -- Key Length: 38 -- -- Key Length: 39 -- -- Key Length: 40 -- -- Key Length: 41 -- -- Key Length: 42 -- 9 : ['4', '6', '7'] -- Key Length: 43 -- -- Key Length: 44 -- -- Key Length: 45 -- -- Key Length: 46 -- -- Key Length: 47 -- -- Key Length: 48 -- -- Key Length: 49 -- -- Key Length: 50 -- -- Key Length: 51 -- 31 : ['e'] -- Key Length: 52 -- -- Key Length: 53 -- -- Key Length: 54 -- -- Key Length: 55 -- 49 : ['c', 'f'] -- Key Length: 56 -- 39 : ['c'] -- Key Length: 57 -- -- Key Length: 58 -- -- Key Length: 59 -- 16 : ['a', 'd', 'f'] -- Key Length: 60 -- -- Key Length: 61 -- -- Key Length: 62 -- -- Key Length: 63 -- -- Key Length: 64 -- 0 : ['c', 'd', 'e'] 1 : ['a', 'b', 'c', 'd', 'e'] 2 : ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] 3 : ['8', '9'] 4 : ['2', '3', '4', '5', '6'] 5 : ['0', '1', '2', '3', '4', '6', '7', '8'] 6 : ['b', 'c', 'd', 'f'] 7 : ['8', '9'] 8 : ['0', '2', '4', '5'] 9 : ['a', 'b', 'f'] 10 : ['0', '2', '3', '4', '5', '6', '7', '8', '9'] 11 : ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] 12 : ['b', 'c', 'd'] 13 : ['8', '9'] 14 : ['0', '2', '3', '5'] 15 : ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] 16 : ['a', 'b', 'c', 'd', 'e'] 17 : ['a', 'b', 'c', 'd', 'e', 'f'] 18 : ['b', 'c', 'd', 'f'] 19 : ['2', '3', '4', '5', '7'] 20 : ['2', '3', '7'] 21 : ['5', '6', '7'] 22 : ['0', '1', '2', '4', '5', '6', '7', '8', '9'] 23 : ['0', '1', '5', '6', '7'] 24 : ['8', '9'] 25 : ['a', 'b', 'c', 'd', 'e', 'f'] 26 : ['b', 'c', 'd', 'e'] 27 : ['8', '9'] 28 : ['d', 'e'] 29 : ['8', '9'] 30 : ['0', '1', '2', '3', '4', '5', '6', '7', '9'] 31 : ['a', 'b', 'c', 'd', 'e', 'f'] 32 : ['0', '1', '2', '3', '5', '6', '7', '8', '9'] 33 : ['0', '1', '2', '3', '4', '5', '6', '7', '9'] 34 : ['0', '1', '3', '6', '7'] 35 : ['b', 'c', 'd', 'e'] 36 : ['d', 'e', 'f'] 37 : ['0', '1', '4', '6', '7'] 38 : ['2', '3', '4', '5'] 39 : ['0', '1', '4', '6', '7'] 40 : ['1', '2', '3', '4', '5', '6', '7', '8', '9'] 41 : ['0', '3', '4', '7', '8', '9'] 42 : ['a', 'e'] 43 : ['a', 'f'] 44 : ['b', 'c'] 45 : ['0', '1', '3', '6', '7'] 46 : ['8', '9'] 47 : ['1', '2', '3', '5', '6', '7', '8', '9'] 48 : ['d', 'e'] 49 : ['0', '1', '7'] 50 : ['0', '1', '5', '6', '7'] 51 : ['0', '1', '2', '3', '4', '5', '6', '7', '9'] 52 : ['a', 'b', 'c', 'f'] 53 : ['a', 'b', 'c', 'd', 'e', 'f'] 54 : ['a', 'b', 'c', 'd', 'f'] 55 : ['a', 'b', 'c', 'd', 'e', 'f'] 56 : ['0', '1', '2', '3', '5', '6', '7', '8', '9'] 57 : ['0', '1', '3', '6', '7'] 58 : ['8', '9'] 59 : ['a', 'e', 'f'] 60 : ['0', '1', '2', '3', '6', '7'] 61 : ['a', 'b', 'c', 'e', 'f'] 62 : ['a', 'e', 'f'] 63 : ['b', 'c', 'd'] C:\>


Here we've hit 2 birds with 1 stone. It is obvious from the script that the the key length is 64 as it has a potential solution for each position of the key. We also have a list of potential solutions that we can use to start decrypting the text.

So how are going to further reduce the potential solutions? Well, it's a semi-manual task; and it's at this point that the challenge becomes a bit mundane. Let's try and find out the first 4 characters of the key with this script:

import base64
import itertools

encoded = "QAYLHhIJ ... [snip] ... kCEh8NSh4PEA4eGFsaGXoo"

decoded = base64.b64decode(encoded)

pos_key = [ ['c', 'd', 'e'], ['a', 'b', 'c', 'd', 'e'], ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], ['8', '9']]  
  
for x in itertools.product(*pos_key):
    key1 = ''.join(x)
    key = key1 + ('_' * (64 - len(key1)))
    decrypted = [None] * 2268

    for x in range(2268):
        decrypted[x] = chr((ord(decoded[x]) ^ ord(key[x])) % 256)
        key = key + decrypted[x]

    print "Key :" + key1 
    print ''.join(decrypted)[:len(key1)] , '|', ''.join(decrypted)[64*10:(64*10)+len(key1)], '|', ''.join(decrypted)[64*31:(64*31)+len(key1)] , '|', ''.join(decrypted)[64*32:(64*32)+len(key1)]
    print

The script covers the first 4 characters of the 64-char key. Notice that on line 8 we put the possible solutions for the first 4 chars obtained from the previous result. For each permutation of these characters (line 10), we decrypt sections of the text (line 16), and print some parts (line 20) which we will later manually verify if they make sense or not.

Running the above script:

Command Prompt
C:\>python stage1_decrypter.py [ ... snip ... ] Key :db68 $d=& | $kex | ost# | e="s Key :db69 $d=' | $key | ost" | e="r Key :db78 $d<& | $kdx | osu# | e=#s [ ... snip ... ] C:\>


Note the string '$key' which indicates that we're on the right track with 'db69'. Using this labour-intensive, nerve-wracking method we finally get 'db6952b84a49b934acb436418ad9d93d237df05769afc796d067bccb379f2cac and a 2nd PHP script as the decrypted text:

$d='';
$key = "";

if (isset($_POST['o_o']))
    $key = $_POST['o_o'];

if (isset($_POST['hint']))
    $d = "www.p01.org";

if (isset($_POST['t'])) {
    if ($_POST['t'] == 'c') {
        $d = base64_decode('SDcGHg1feVUIEhsbDxFhIBIYFQY+VwMWTyAcOhEYAw4VLVBaXRsKADMXTWxrSH4ZS1IiAgA3GxYUQVMvBFdVTysRMQAaQUxZYTlsTg0MECZSGgVcNn9AAwobXgcxHQRBAxMcWwodHV5EfxQfAAYrMlsCQlJBAAAAAAAAAAAAAAAAAFZhf3ldEQY6FBIbGw8RYlAxGEE5PkAOGwoWVHgCQ1BGVBdRCAAGQVQ2Fk4RX0gsVxQbHxdKMU8ABBU9MUADABkCGHdQFQ4TXDEfW0VDCkk0XiNcRjJxaDocSFgdck9CTgpPDx9bIjQKUW1NWwhERnVeSxhEDVs0LBlIR0VlBjtbBV4fcBtIEU8dMVoDACc3ORNPI08SGDZXA1pbSlZzGU5XVV1jGxURHQoEK0x+a11bPVsCC1FufmNdGxUMGGE=');
        
        $key = preg_replace('/(.)../', '$1', $key);
    }

    if ($_POST['t'] == 's') {
        $d = base64_decode('VBArMg1HYn1XGAwaAw1GDCsACwkeDgABUkAcESszBEdifVdNSENPJRkrNwgcGldMHFVfSEgwOjETEE9aRlJoZFMKFzsmQRALSilMEQsXHEUrPg9ZDRAoAwkBHVVIfzkNGAgaBAhUU00AAAAAAAAAAAAAAAAASkZSVV0KDAUCHBFQHA0MFjEVHB0BCgBNTAJVX3hkAkQiFh8ESw0AG0M5MBNRGkpdWV4bVEEVdGJGRR9XGBgcAgpVCDAsCA0GGAVWBAwcBxQqKwRCGxgbVkJFR11IdHcbRFxOUkNNV0RAVXIKSgxCWk1aVkdGQVI8dxRTVl5CR0JLVAQdOStbXkRfXlxOFEULUCp2SFJIUlVGQlUtRhExMQQLJyMmIFgDTUQtYmZIRUAECB4MHhtWRHA9Dh0WSWZmWUEHHBUzYQ==');

        $key = preg_replace('/.(.)./', '$1', $key);
    }

    if ($_POST['t'] == 'w') {

        $d = base64_decode('DycdGg1hYjl8FURaAVZxPhgNOQpdMxVIRwNKc0YDCCsDVn5sJxJMHmJJOgArB1olFA0JHQN+TlcpOgFBKUEAA1M+RVUVDjsWEy8PQUEMV3IsSgJxCFY0IkJAGVY3HV9DbQsRaU1eSxl6IR0SEykOX2gnEAwZGHJHRU0OUn4hFUUADlw8UhRPNwpaJwlZE14Df1IRDi1HS30JFlZAHnRAEQ4tR0p9CRZXQB50LFkHNgNfEgROWkVLZV1bGHVbHyJMSRFZCQtGRU0bQAFpSEtBHxsLVEdaeEEUfCd2akdKYAFaJXBdT3BeHBRFV3IdXCV1PhsUXFUBBR5hXFwwdxsab1kECFoaM0FET2pEd2owBXpAC2ZAS11sMhVmJREWVlFyDV4ldFIdcUMBWlBbcl5CSGFTUCEPW08eEyYNSgJhYjl8Tk9BCUpvDxsAODBeLwUfE08AAAAAAAAAAAAAAAAAEXFkfV1wB0ctDRM=');

        $key = preg_replace('/..(.)/', '$1', $key);
    }

    while(strlen($key) < strlen($d))
        $key = $key.$key;
    $d = $d ^ $key;
}

if (strlen($d))
    echo $d;
else
    echo '<form action="shell.php" method="post"><input type="hidden" name="o_o" value="'.$key.'"><input type="radio" name="t" value="c"> Raytraced Checkboard<br> <input type="radio" name="t" value="s"> p01 256b Starfield<br> <input type="radio" name="t" value="w"> Wolfensteiny<br><input type="submit" value="Show"/></form>';

Alas, another 3 decryption exercises. This time the key is split into 3 parts by taking every other 3rd char, and each one decrypts a different string. At this point I was completely lost as to how I'm going to decrypt these texts or obtain the key so I made a leap of faith which, luckily for me, turned out to be correct. I assumed that the decryption key was the key to the challenge, i.e. the final email which, just like in all the challenges, ends with '@flare-on.com'.

If we make this assumption then the 3 partial keys in question should end with '@a-.m', 'froc' and 'leno'. The plan now is to try each of these partial keys with each of the encrypted text and hopefully get something that makes sense in one of the combinations. The following script tries '@a-.m' against the 3rd ciphertext at each position and prints out the resultant decrypted text if it is printable:

import base64
import string

cb = "DycdGg1hYjl8FURaAVZxPhgNOQpdMxVIRwNKc0YDCCsDVn5sJxJMHmJJOgArB1olFA0JHQN+TlcpOgFBKUEAA1M+RVUVDjsWEy8PQUEMV3IsSgJxCFY0IkJAGVY3HV9DbQsRaU1eSxl6IR0SEykOX2gnEAwZGHJHRU0OUn4hFUUADlw8UhRPNwpaJwlZE14Df1IRDi1HS30JFlZAHnRAEQ4tR0p9CRZXQB50LFkHNgNfEgROWkVLZV1bGHVbHyJMSRFZCQtGRU0bQAFpSEtBHxsLVEdaeEEUfCd2akdKYAFaJXBdT3BeHBRFV3IdXCV1PhsUXFUBBR5hXFwwdxsab1kECFoaM0FET2pEd2owBXpAC2ZAS11sMhVmJREWVlFyDV4ldFIdcUMBWlBbcl5CSGFTUCEPW08eEyYNSgJhYjl8Tk9BCUpvDxsAODBeLwUfE08AAAAAAAAAAAAAAAAAEXFkfV1wB0ctDRM="

c = base64.b64decode(cb)
key = "@a-.m"

for x in range(len(c)-len(key)):
    temp = ''.join(chr(ord(x) ^ ord(y)) for x,y in zip(key,c[x:]))
    printable = True
    for y in temp:
        if y not in string.printable:
            printable = False
            break
            
    if printable:
        print x, temp

Running the script:

Command Prompt
C:\>python xor_cracker.py 0 OF04` 1 g|7# 3 ZlLLT 6 "XQ;) 8 <titl 9 U%w/; 14 1_5#T 17 MX's^ 21 stein [ ... snip ... ] 318 "XQ`" 320 </bod 324 I+B!v [ ... snip ... ] C:\>


We start noticing some HTML code at positions 8 and 320. At position 21 we also get 'stein' which is part of 'Wolfensteiny' from the PHP script. Using this information we can find the rest of the partial key which turns out to be '3Oiwa_o3@a-.m'. Using the same method we get the other 2 partial keys: 't_rsaat_4froc' and 'hx__ayowkleno'

Combining the 3 partial keys we get: th3_xOr_is_waaaay_too_w34k@flare-on.com

Sunday, 5 November 2017

CTF Writeup - Flare-On 2017 - 08: flair.apk


  • Name - flair.apk
  • Category - Reverse Engineering
  • Points - 1
  • Binary - Download here

Next up is an Android challenge. To install the APK file I've created a Marshmallow Android x86 emulated phone using 'Visual Studio Emulator for Android' and installed it using adb:


To debug the APK file I decided to use JEB Android Debugger. This debugger is amazing and really easy to use as you'll soon find out in this blog post. It also allows you to debug applications which are NOT marked as debuggable such as flair.apk.

Load the APK in JEB and view the decoded manifest:
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.flare_on.flair" platformBuildVersionCode="25" platformBuildVersionName="7.1.1" xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="25" />
    <meta-data android:name="android.support.VERSION" android:value="25.3.1" />
    <application android:allowBackup="true" android:icon="@mipmap/flair_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme">
        <activity android:name="com.flare_on.flair.Chotchkies" android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:exported="false" android:label="@string/michael_title" android:name="com.flare_on.flair.Michael" android:screenOrientation="portrait" />
        <activity android:exported="false" android:label="@string/brian_title" android:name="com.flare_on.flair.Brian" android:screenOrientation="portrait" />
        <activity android:exported="false" android:label="@string/milton_title" android:name="com.flare_on.flair.Milton" android:screenOrientation="portrait" />
        <activity android:exported="false" android:label="@string/printer_title" android:name="com.flare_on.flair.Printer" android:screenOrientation="portrait" />
        <meta-data android:name="vdf" android:value="cov" />
    </application>
</manifest>

The manifest shows the main class that is called when the application is launched: 'com.flare_on.flair.Chotchkies'. This class reveals that we have 4 hurdles to overcome before we are presented with the key:
private void getFlair(int arg3) {
    Intent v0 = null;
    switch(arg3) {
        case 0: {
            v0 = new Intent(((Context)this), Michael.class);
            break;
        }
        case 1: {
            v0 = new Intent(((Context)this), Brian.class);
            break;
        }
        case 2: {
            v0 = new Intent(((Context)this), Milton.class);
            break;
        }
        case 3: {
            v0 = new Intent(((Context)this), Printer.class);
            break;
        }
    }
    this.startActivityForResult(v0, arg3);
}

So let's get started.

Michael




The Michael activity contains a checkPassword() function which validates the input:

private boolean checkPassword(String arg7) {
    boolean v0;
    int v5 = 9;
    int v4 = 7;
    int v3 = 5;
    if((arg7.isEmpty()) || arg7.length() != 12) {
        v0 = false;
    }
    else {
        v0 = true;
        if(!arg7.startsWith("M")) {
            v0 = false;
        }

        if(arg7.indexOf(89) != 1) {
            v0 = false;
        }

        if(!arg7.substring(2, v3).equals("PRS")) {
            v0 = false;
        }

        if(arg7.codePointAt(v3) != 72 || arg7.codePointAt(6) != 69) {
            v0 = false;
        }

        if(arg7.charAt(v4) != arg7.charAt(8) || arg7.substring(v4, v5).hashCode() != 3040) {
            v0 = false;
        }

        if(arg7.indexOf("FT") != v5) {
            v0 = false;
        }

        if(arg7.lastIndexOf(87) == arg7.length() - 1) {
            return v0;
        }

        v0 = false;
    }

    return v0;
}

The only string that will make it through all the if statements is 'MYPRSHE__FTW'

Brian




The important functions in the Brian class are the following:


[... snip ...]

public void onClick(View arg5) {
    switch(arg5.getId()) {
        case 2131427424: {
            String v2 = this.q.getText().toString();
            if(this.teraljdknh(v2, this.asdjfnhaxshcvhuw(this.findViewById(2131427422), this.findViewById(2131427423)))) {
                Util.flairSuccess(((Activity)this), v2);
            }
            else {
                Util.flairSadness(((Activity)this), this.tEr);
                ++this.tEr;
            }

            break;
        }
    }
}

[... snip ...]

private boolean teraljdknh(String arg2, String arg3) {
    return arg2.equals(arg3);
}

If this.teraljdknh() returns True, then we go to Util.flairSuccess() otherwise we branch to Util.flairSadness(). Since teraljdknh() is just a string equals function, we can put a breakpoint when it is called and peek at the parameters:


As soon as we hit the breakpoint, JEB shows us the local parameters and their values. The ones we're interested in are v2, which contains our input, and v3, which contains the expected input, as these are passed to teraljdknh(). The default type for local variables is 'int' so make sure to change it to 'string' to view the result.

The expected input for this puzzle is therefore 'hashtag_covfefe_Fajitas!'

Milton




The first things to notice are 1) The 5-star rating system and 2) the button to advance to the next level is disabled. Let's look at the Milton class. The following in an excerpt of the relevant sections:


[ ... snip ... ]

public void onRatingChanged(RatingBar arg6, float arg7, boolean arg8) {
    if(arg8) {
        if((((double)arg7)) == 4) {
            Milton.this.hild = Milton.this.hild + Stapler.vutfs("JP+98sTB4Zt6q8g=", 56, "State");
            Milton.this.hild = Milton.this.hild + Stapler.vutfs("rh6HkuflHmw5Rw==", 96, "Chile");
            Milton.this.hild = Milton.this.hild + Stapler.vutfs("+BNtTP/6", 118, "eagle");
            Milton.this.hild = Milton.this.hild + Stapler.vutfs("oLLoI7X/jIp2+w==", 33, "wind");
            Milton.this.hild = Milton.this.hild + Stapler.vutfs("w/MCnPD68xfjSCE=", 148, "river");
            Milton.this.r.setEnabled(true);
        }

        arg6.setEnabled(false);
    }
}

[ ... snip ... ]

private boolean breop(String arg5) {
    boolean v2 = false;
    if(!Milton.trsbd(((Context)this))) {
        byte[] v1 = Stapler.neapucx(arg5);
        try {
            v2 = Arrays.equals(v1, this.nbsadf());
        }
        catch(NoSuchAlgorithmException v0) {
            v0.printStackTrace();
        }
        catch(UnsupportedEncodingException v0_1) {
            v0_1.printStackTrace();
        }
    }

    return v2;
}

[ ... snip ... ]

public void onClick(View arg3) {
    switch(arg3.getId()) {
        case 2131427439: {
            String v0 = this.pexu.getText().toString();
            if(this.breop(v0)) {
                Util.flairSuccess(((Activity)this), v0);
            }
            else {
                this.vbdrt();
                Util.flairSadness(((Activity)this), this.rtgb);
                ++this.rtgb;
            }

            break;
        }
    }
}

[ ... snip ... ]


The onRatingChanged() function is called when the rating on the 5-star scale has been selected. If the rating is 4 (line 5), the button is enabled (line 11). Now we can continue as usual.

In the onClick() function, if this.breop() returns True, then we go to Util.flairSuccess() otherwise we branch to Util.flairSadness(). To ensure that breop() returns True, Array.equals() has to return True as well.

Let's put a breakpoint on Array.equals(), input '123456' and hit the button:


The function Array.equals() is comparing v1 and v3. I had to manually change the type to [B to get them to display an array of bytes. The value of v1 in the extra column is [0x12, 0x34, 0x56] which is our input so v3 must hold the required input.

Disregarding the sign of the byte we get: '10AEA594831E0B42B956C578EF9A6D44EE39938D'

Printer




This is the last hurdle to overcome and the most complicated one. Nothing that JEB cannot handle though! The important function here is cgHbC(); if it returns True we get to Util.flairSuccess():

private boolean cgHbC(String arg15) {
    try {
        if(Printer.ksdc(this)) {
            boolean v9 = false;
            return v9;
        }

        Object v2 = this.wJPBw(Stapler.iemm("Gv@H"));
        Class v4 = Class.forName(Stapler.iemm(",e}e8yGS!8Dev)-e@"));
        int v5 = v4.getMethod(Stapler.iemm("vSBH"), null).invoke(v2, null).intValue();
        byte[] v6 = new byte[v5];
        Method v7 = v4.getMethod(Stapler.iemm("LHG"), Object.class);
        short v0;
        for(v0 = 0; v0 < v5; v0 = ((short)(v0 + 1))) {
            v6[v0] = Byte.class.cast(v7.invoke(v2, Short.valueOf(v0))).byteValue();
        }

        return Class.forName(Stapler.iemm(",e}e8yGS!81PPe(v")).getMethod(Stapler.iemm("H?ye!v"), byte[].class, byte[].class).invoke(null, Stapler.neapucx(arg15), Stapler.poserw(v6)).booleanValue();
    }
    catch(Exception v1) {
        v1.printStackTrace();
        return false;
    }
}

The function is highly obfuscated. Notice though that arg15, which is our input, is only used on line 18. If we put a breakpoint at the end of function Stapler.poserw() to try and make sense of this line of code we get:

return Class.forName("java.util.Arrays").getMethod("equals"), byte[].class, byte[].class).invoke(null, Stapler.neapucx(arg15), Stapler.poserw(v6)).booleanValue();

So this is a simple Array comparison between our input, arg15, and Stapler.poserw(v6).

Putting a breakpoint at the end of Stapler.poserw(v6), changing the type of v1 to [B and collecting the hex values from the Extra column as we did before we get '5F1BE3C9B081C40DDFC4A0238156008EE71E24A4'.

Piecing It Together


At this point we have the 4 correct inputs:
  1. MYPRSHE__FTW
  2. hashtag_covfefe_Fajitas!
  3. 10AEA594831E0B42B956C578EF9A6D44EE39938D
  4. 5F1BE3C9B081C40DDFC4A0238156008EE71E24A4
Inputting these we get:

Saturday, 4 November 2017

CTF Writeup - Flare-On 2017 - 09: remorse.ino.hex


  • Name - remorse.ino.hex
  • Category - Reverse Engineering
  • Points - 1
  • Binary - Download here

The 9th Flare-On challenge was an Arduino one. When I saw the contents of remorse.ino.hex I was confused as to what it was. It didn't look remotely like any other RE challenge I've seen before:

:100000000C9462000C948A000C948A000C948A0070
:100010000C948A000C948A000C948A000C948A0038
:100020000C948A000C948A000C948A000C948A0028
:100030000C948A000C948A000C948A000C948A0018

[ ... snip ... ]


After some research I found that this I was dealing with an Intel hex file for an Arduino UNO, i.e. an ATmega328P microcontroller. From here I've tried several methods to run the file such as using Atmel Studio or simavr but I couldn't find ways of interacting with the running program. I was even contemplating running it on a real Arduino. At the end I decided to load it in IDA and try my luck using static analysis.

To load it properly in IDA, open the file, select ATMEL AVR and then select ATmega323_L. IDA nicely annotates some of the registers and pins for us too. After some time looking at the functions, sub_536() in particular caught my eye. The following are 2 screenshots of the entire function:


Notice the comparison with the character '@' in small red basic block. This hinted that we were probably dealing with an email address, and hence the key. Before we can understand what is happened, we need to learn a few AVR instructions. The following are the necessary ones to understand what is happening in the green and yellow basic blocks:
  • LD - Load Indirect from Data Space to Register
  • LDI - Load Immediate
  • ST - Store Indirect From Register to Data Space
  • STD - Store Indirect From Register to Data Space
  • EOR - Exclusive OR
  • SUBI - Subtract Immediate
  • CPI - Compare with Immediate

Nothing too fancy! The instructions are very simple. Using our newly acquired knowledge we can see that in the green basic block, 0x17 bytes are being loaded starting from the Y register. In the yellow basic block, for each of the previous bytes, it XOR's it with an unknown value taken from register Z+ and adds the counter value to it, which is held in register r18. At each iteration, register r18 is incremented and compared with value 0x17.

As the only unknown value here is Z+, and we're dealing with an 8-bit processor, we can easily bruteforce the solution. The following is a python program that does this:

encrypted = [0xb5,0xb5,0x86,0xb4,0xf4,0xb3,0xf1,0xb0,0xb0,0xf1,0xed,0x80,0xbb,0x8f,0xbf,0x8d,0xc6,0x85,0x87,0xc0,0x94,0x81,0x8c]

for y in range (255):
    answer = ''
    for x in range(len(encrypted)):
        answer += chr(((encrypted[x] ^ y) + x) % 256)
        
    if answer[10] == '@':
        print answer
        break

Running the script:

Command Prompt
C:\>python solution.py no_r3m0rs3@flare-on.com C:\>

Saturday, 28 October 2017

CTF Writeup - Flare-On 2017 - 07: zsud.exe


  • Name - zsud.exe
  • Category - Reverse Engineering
  • Points - 1
  • Binary - Download here

The 7th Flare-On challenge is an x86 single-user dungeon game:


The game allows you to move around, interact with objects, pick them up, equip them, drop them and even talk to a guy called Kevin.

Loading the binary in IDA and looking at the strings tab we notice a few interesting ones:
  • M:\\whiskey_tango_flareon.dll
  • flareon.dll
  • !This program cannot be run in DOS mode.
  • .text
  • .rsrc
  • .reloc

All of these strings point towards an embedded DLL that starts at location 0x148AB0:


Following the xrefs for this memory region we are directed to function sub_F7170() from which we can identify the size of the embedded DLL:


Extract 0x1200 bytes starting from 0x148AB0, dump them into a file, rename to flareon.dll and open it using dnSpy:

using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Management.Automation;
using System.Security.Cryptography;
using System.Text;

namespace flareon
{
    public class four
    {
        private static string Decrypt2(byte[] cipherText, string key)
        {
            byte[] bytes = Encoding.UTF8.GetBytes(key);
            byte[] array = new byte[16];
            byte[] iV = array;
            string result = null;
            using (RijndaelManaged rijndaelManaged = new RijndaelManaged())
            {
                rijndaelManaged.Key = bytes;
                rijndaelManaged.IV = iV;
                ICryptoTransform transform = rijndaelManaged.CreateDecryptor(rijndaelManaged.Key, rijndaelManaged.IV);
                using (MemoryStream memoryStream = new MemoryStream(cipherText))
                {
                    using (CryptoStream cryptoStream = new CryptoStream(memoryStream, transform, CryptoStreamMode.Read))
                    {
                        using (StreamReader streamReader = new StreamReader(cryptoStream))
                        {
                            result = streamReader.ReadToEnd();
                        }
                    }
                }
            }
            return result;
        }

        public static int Smth(string arg)
        {
            using (PowerShell powerShell = PowerShell.Create())
            {
                try
                {
                    byte[] cipherText = Convert.FromBase64String(arg);
                    string script = four.Decrypt2(cipherText, "soooooo_sorry_zis_is_not_ze_flag");
                    powerShell.AddScript(script);
                    Collection collection = powerShell.Invoke();
                    foreach (PSObject current in collection)
                    {
                        Console.WriteLine(current);
                    }
                }
                catch (Exception var_5_70)
                {
                    Console.WriteLine("Exception received");
                }
            }
            return 0;
        }
    }
}

The DLL accepts a Base64 string, decodes it, AES decrypts it using the key 'soooooo_sorry_zis_is_not_ze_flag' and runs the resultant PowerShell script. Let's try and extract the PS1 script from the binary.

At the beginning of function sub_12523D0(), a region in memory is Base64'd and then passed to flareon.dll after this has been loaded into memory. The following image shows the resultant base64 string pointed at by EAX right after it has been encoded:


Extract the base64 string and create a C# program, based on the .NET flareon.dll, to recover the PS1 script:


[ ... snip ... ]

public static int Smth(string arg)
{
    byte[] cipherText = Convert.FromBase64String(arg);
    string script = Program.Decrypt2(cipherText, "soooooo_sorry_zis_is_not_ze_flag");
    System.IO.StreamWriter file = new System.IO.StreamWriter("zsud.ps1");
    file.WriteLine(script);
    file.Close();
    return 0;
}

public static void Main()
{
    string base64encoded = "Sbogppc38m/yviiq2 … [snip] … AyyAEQ2IwZPNoPEzE=";
    Smth(base64encoded);
    Console.ReadKey();
}

[ ... snip ... ]

The decrypted script can be viewed/downloaded here. If we run the powershell script on its own, we get the same UI depicted in the first image above.

First thing to notice in the script is the string 'http://127.0.0.1:9999/some/thing.asp' on line 814 which hints that the script communicates with a local web server embedded in the binary. This is confirmed by the following netstat result when zsud.exe is open:

Command Prompt
C:\>netstat -antp tcp | findstr 9999 TCP 127.0.0.1:9999 0.0.0.0:0 LISTENING InHost C:\>


Let's go back to the PS1 script and try to uncover what it's doing. The following is an excerpt of the important sections:


[ ... snip ... ]

$key = New-Thing "a key" "You BANKbEPxukZfP2EikF8jN04 ... [snip] ... PtEVBhQ==" @("key")

[ ... snip ... ]

function Invoke-XformKey([String]$keytext, [String]$desc) {
    $newdesc = $desc 

    Try {
        $split = $desc.Split()
        $text = $split[0..($split.Length-2)]
        $encoded = $split[-1]
        $encoded_urlsafe = $encoded.Replace('+', '-').Replace('/', '_').Replace('=', '%3D')
        $uri = "${script:baseurl}?k=${keytext}&e=${encoded_urlsafe}"
        $r = Invoke-WebRequest -UseBasicParsing "$uri"
        $decoded = $r.Content
        if ($decoded.ToLower() -NotContains "whale") {
            $newdesc = "$text $decoded"
        }
    } Catch {
        Add-ConsoleText "..."
    }
    
    return $newdesc
}

function Invoke-MoveDirection($char, $room, $direction, $trailing) {
    $nextroom = $null
    $movetext = "You can't go $direction."
    $statechange_tristate = $null

    $nextroom = Get-RoomAdjoining $room $direction
    if ($nextroom -ne $null) {
        $key = Get-ThingByKeyword $char 'key'
        if (($key -ne $null) -and ($script:okaystopnow -eq $false)) {
            $dir_short = ([String]$direction[0]).ToLower()

            ${N} = ${sC`Ri`Pt:MS`VcRt}::("{1}{0}" -f'nd','ra').Invoke() % 6

            if ($directions_enum[$dir_short] -eq ($n)) {
                $script:key_directions += $dir_short
                $newdesc = Invoke-XformKey $script:key_directions $key.Desc
                $key.Desc = $newdesc
                if ($newdesc.Contains("@")) {
                    $nextroom = $script:map.StartingRoom
                    $script:okaystopnow = $true
                }
                $statechange_tristate = $true
            } else {
                $statechange_tristate = $false
            }
        }

        $script:room = $nextroom
        $movetext = "You go $($directions_short[$direction.ToLower()])"

        if ($statechange_tristate -eq $true) {
            $movetext += "`nThe key emanates some warmth..."
        } elseif ($statechange_tristate -eq $false) {
            $movetext += "`nHmm..."
        }

        if ($script:autolook -eq $true) {
            $movetext += "`n$(Get-LookText $char $script:room $trailing)"
        }
    } else {
        $movetext = "You can't go that way."
    }

    return "$movetext"
}

[ ... snip ... ]

$baseurl = 'http://127.0.0.1:9999/some/thing.asp'
$directions_enum = @{'n' = 0; 's' = 1; 'e' = 2; 'w' = 3; 'u' = 4; 'd' = 5}

[ ... snip ... ]



The script does the following for each movement we perform in the game:
  1. Calls Invoke-MoveDirection
  2. Generates a random number between 0 and 5 (line 39)
  3. Translates our move to a number (lines 27 & 77)
  4. If these 2 numbers coincide, call Invoke-XformKey (line 43)
  5. Sends the list of directions (k param) and url-encoded key description (e param) to local web server (lines 15 & 16)
  6. If response does not contain 'whale', set it as the new key description (lines 18 & 19)
  7. If response contains '@', warp to starting room and stop game (lines 45 - 47)
The last bullet point hints that if we send the correct GET request to the local web server, it should spit the e-mail address. Using parts of the PS1 script above, we create another PS1 script to try and bruteforce the first 25 directions expected by the server:

iex ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("U0VULWl0RW
[ ... snip ... ]
"Xl9LCAke2ZpRWxkVmFgTGB1YGVzfSAgKTsgIHJldHVybiAke2FgVFRyfTsgICB9")))

.("{0}{1}{2}"-f 'SEt-IT','E','M') VariaBLE:q21s 
[ ... snip ...]
 ${T`ype`BU`IldEr}.("{2}{0}{1}"-f 'teTy','pe','Crea').Invoke(); }


[STring]::joIN( '', ('35h88w112_119}81r74r77h100J94<5
[ ... snip ...]
|%{[CHAR]($_ -BXoR  0x03  ) } ) )|.( $ShelliD[1]+$SheLLID[13]+'X')


$key = New-Thing "a key" "You BANKbEPxukZfP2EikF8jN04 ... [snip] ... PtEVBhQ==" @("key")

$baseurl = 'http://127.0.0.1:9999/some/thing.asp'

function Invoke-XformKey([String]$keytext, [String]$desc) {
    $newdesc = $desc 

    Try {
        $split = $desc.Split()
        $text = $split[0..($split.Length-2)]
        $encoded = $split[-1]
        $encoded_urlsafe = $encoded.Replace('+', '-').Replace('/', '_').Replace('=', '%3D')
        $uri = "${script:baseurl}?k=${keytext}&e=${encoded_urlsafe}"
        $r = Invoke-WebRequest -UseBasicParsing "$uri"
        $decoded = $r.Content

        # If response is different than previous one, set as new description
        if ($decoded.ToLower() -ne $encoded.ToLower()) {
            $newdesc = "$text $decoded"
            return $newdesc
        }
    } Catch {}
}

$directions_enum = @{0 = "n"; 1 = "s"; 2 = "e"; 3 = "w"; 4="u"; 5 = "d"}
$cumulativekey = ""

for ($i=0; $i -lt 25; $i++){
    for ($j=0; $j -lt 6; $j++){
        $testingchar = $directions_enum[$j]
        if ($newkey = Invoke-XformKey $cumulativekey$testingchar $key){
            $key = $newkey
            $cumulativekey += $testingchar
            break
        }
    }
}

echo "Directions: $cumulativekey"
echo "Key: $key"

Running the script:

Command Prompt
C:\>powershell -file bruteforcer.ps1 Directions: wnneesssnewne Key: You can start to make out some words but you need to follow the ZipRg2+UxcDPJ8T iemKk7Z9bUOfPf7VOOalFAepISztHQNEpU4kza+IMPAh84PlNxwYEQ1IODlkrwNXbGXcx/Q== C:\>


We notice 3 important things when we run the script:
  1. The key is incompletely decoded
  2. Only the first 13 direction attempts out of the 25 were executed
  3. Multiple runs produce the same output even though the expected input is decided by rand()
The first 2 happen because the server is bruteforce resistant and is expecting the right directions to spit out the answer. The 3rd bullet point is answered by looking at lines 431 - 433 of the extracted PS1 script:

if (($thing.Keywords -Contains "key") -and ($container_new -eq $script:char)){
    ${Msv`c`RT}::("{1}{0}"-f 'rand','s').Invoke(42)
}

When we pick up the 'key' in the game, srand(42) is executed, which explains why the rand() function always produces the same result and hence the server always expects the same directions. If we factor in the srand(42) though we notice that the output of 'rand() % 6' still does not match 'wnneesssnewne', which has been proven to be right else we would've never decoded parts of the key.

During the challenge it took me ages to realise the trick: both the rand() and srand() function are hooked in the binary and replaced with a custom implementation! This happens in function sub_1336530():


In the image we can see that the pointer to the real rand() is being hooked by new_rand() whilst srand() is being hooked by new_srand(). Let's take a look at both of these new functions. The following is the new_srand():


Recall that srand() is called from the PS1 when we collect the key. When this happens, it sets the global variable dword_138BD28 to 1, as displayed in the image above.

Let us take a look at the new implementation of rand(), i.e. new_rand():


If the global variable dword_138BD28 has been set to 1, i.e. new_srand() has been called, then it will end up in the green basic block which takes the expected direction from the array dword_1389CB8. This is NOT random at all!

Let's create a python script to extract all the expected directions:

import sys

directions_enum = {0:"n", 1:"s", 2 : "e", 3 : "w", 4:"u", 5 : "d"}

dword_1389CB8 = [0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x02,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x00,0x00,0x00,0x00]

dword_138BD24 = 0

for x in range(200):
    result = dword_1389CB8[dword_138BD24 * 4]
    dword_138BD24 = (dword_138BD24 + 1) % 0x35
    sys.stdout.write(directions_enum[result])

Running the script:

Command Prompt
C:\>python direction_extractor.py wnneesssnewneewwwdundundunsuneunsewdunsewsewsewsewdunwnneesssnewneewwwdundundun suneunsewdunsewsewsewsewdunwnneesssnewneewwwdundundunsuneunsewdunsewsewsewsewdu nwnneesssnewneewwwdundundunsuneunsewdunsew C:\>


Sw33t! Let's modify the PS1 script now to specifically send these rather than trying to bruteforce our way:


[ ... snip ... ]

$solutions = "wnneesssnewneewwwdundundunsuneunsewdunsewsewsewsewdunwnneesssnewneewwwdundundunsuneunsewdunsewsewse"

for ($j=1; $j -lt 54; $j++){
    $tempSolution = $solutions.substring(0,$j)
 if ($newkey = Invoke-XformKey $tempSolution $key){
        $key = $newkey
    }
}

echo "Directions: $cumulativekey"
echo "Key: $key"

And this time:

Command Prompt
C:\>powershell -file send_right_directions.ps1 Directions: wnneesssnewneewwwdundundunsuneunsewdunsewsewsewsewdun Key: You can start to make out some words but you need to follow the RIGHT_PATH!@66696e646b6576696e6d616e6469610d0a C:\>


Not exactly what we want by close enough. The long hex string at the end translated to 'findkevinmandia'. So let's go and find Kev ... nahh, let's extract the logic directly from the script.

The decrypter to this output can be found in the original PS1 file on lines 463 - 477 which are invoked when we talk to Kevin Mandia whilst wearing a helmet and the key is dropped in the room. Extracting the logic from the script and putting our response from the server we end up with the following script:

$key = "You can start to make out some words but you need to follow the RIGHT_PATH!@66696e646b6576696e6d616e6469610d0a"

$md5 = New-Object System.Security.Cryptography.MD5CryptoServiceProvider
$utf8 = New-Object System.Text.UTF8Encoding
$hash = [System.BitConverter]::ToString($md5.ComputeHash($utf8.GetBytes($key)))


$Data = [System.Convert]::FromBase64String("EQ/Mv3f/1XzW4FO8N55+DIOkeWuM70Bzln7Knumospan")
$Key = [System.Text.Encoding]::ASCII.GetBytes($hash)

# Adapated from the gist by harmj0y et al
$R={$D,$K=$Args;$H=$I=$J=0;$S=0..255;0..255|%{$J=($J+$S[$_]+$K[$_%$K.Length])%256;$S[$_],$S[$J]=$S[$J],$S[$_]};$D|%{$I=($I+1)%256;$H=($H+$S[$I])%256;$S[$I],$S[$H]=$S[$H],$S[$I];$_-bxor$S[($S[$I]+$S[$H])%256]}}
$x = (& $r $data $key | ForEach-Object { "{0:X2}" -f $_ }) -join ' '
$resp = "`nKevin says, with a nod and a wink: '$x'."
$resp += "`n`nBet you didn't know he could speak hexadecimal! :-)"

echo $resp

The output:

Command Prompt
C:\>powershell -file kevin_mandia.ps1 Kevin says, with a nod and a wink: '6D 75 64 64 31 6E 67 5F 62 79 5F 79 30 75 72 35 33 6C 70 68 40 66 6C 61 72 65 2D 6F 6E 2E 63 6F 6D'. Bet you didn't know he could speak hexadecimal! :-) C:\>


Converting the hex to ascii: mudd1ng_by_y0ur53lph@flare-on.com

Monday, 23 October 2017

CTF Writeup - Flare-On 2017 - 06: payload.dll


  • Name - payload.dll
  • Category - Reverse Engineering
  • Points - 1
  • Binary - Download here

The next challenge on the list is a 64-bit windows DLL. We create a small 64-bit program that loads the DLL and calls the only exported function by its ordinal number:

#include <windows.h>
#include <stdio.h>

int main(int argc, char* argv[])
{
    FARPROC func;

    HINSTANCE dllHandle = LoadLibraryA("payload.dll");

    if (dllHandle == NULL)
        return 0;

    printf("Loading payload.dll\n");
    func = GetProcAddress(dllHandle, 1);
    func();

    printf("Press Any Key to Continue\n");
    getchar();
    return 0;
}

Running the binary we get the following message box:


Interesting. It tells us to insert a clever message, yet the exported function doesn't accept any parameters. Let's take a look at function sub_7FEFAB95A50() which has an xref to this message:


If we put a break point on the JNZ instruction in the basic block at the top and manually alter the execution flow to the yellow basic blocks by flipping the Zero Flag, we reveal a single character from the key:


As already annotated in the previous image, the yellow basic blocks set a region as RWX, decrypt the memory region with key orphanedirreproducibleconfidences and call it to reveal a single of the character of the key.

The question now is, how do we force the DLL into revealing the other characters of the key when it doesn't accept any input? The answer lies in function sub_7FEFAA24710():

__int64 sub_7FEFAA24710()
{
    struct _SYSTEMTIME SystemTime; // [rsp+20h] [rbp-28h]

    GetSystemTime(&SystemTime);
    return (unsigned int)((SystemTime.wYear + SystemTime.wMonth) % 26);
}

If this function returns 0x19 (25), as it does in this case, it will use the 25th key to decrypt the 25th region and reveals the 25th character of the key. Manually setting the output of this function between 0 and 13, and manually forcing the execution to the yellow blocks as we have done before, we reveal the first 14 characters:

  • key[0] = 0x77
  • key[1] = 0x75
  • key[2] = 0x75
  • key[3] = 0x75
  • key[4] = 0x74
  • key[5] = 0x2d
  • key[6] = 0x65
  • key[7] = 0x78
  • key[8] = 0x79
  • key[9] = 0x30
  • key[10] = 0x72
  • key[11] = 0x74
  • key[12] = 0x73
  • key[13] = 0x40

As we reach the '@' character we can stop since we know that the email has to end with @flare-on.com.

The final key is therefore wuuut-exp0rts@flare-on.com.

CTF Writeup - Flare-On 2017 - 05: pewpewboat.exe


  • Name - pewpewboat.exe
  • Category - Reverse Engineering
  • Points - 1
  • Binary - Download here

The challenge is a 64-bit ELF binary, despite it's file extension. Launching it we get a Battleship game!


The goal is to beat the game by sinking all the ships, which we have no visibility of, in each level by inputting coordinate values. Completing the first level we are greeted with:


Note that the solution spells out the letter 'F'! We try to input the Md5 hash of the 4-char string to no avail. At this point we're not sure what's meant by NotMD5Hash so let's look at the binary under IDA. The main() function starts by loading some stack strings and initializes some stuff for the game; it then goes into a main loop which has 2 important functions (green basic block): sub_403047() which sets up the next level and sub_403C05() where we get to actually play the level:


The function sub_403C05() further contains another loop which iterates for each coordinate we supply:


Each time we input a coordinate, the loop displayed above does the following:
  1. Sets up the curses text-based user interface (sub_4031E1)
  2. Displays the grid (sub_403263)
  3. Processes the coordinate to determine if we've hit a ship or not, checks if we have completed the level and displays the appropriate messages (sub_4038D6)
  4. Asks the user to input the next coordinate (sub_40377D)
The first objective is to find what is meant by NotMd5Hash so we can progress further into the game. The function that deals with this is sub_403530(); the stack string NotMd5Hash("%s") at the beginning gives it away. Let's look at the first part of this function:


The green basic block is looped over 4 times and randomly generates the 4 characters for the NotMd5Hash string. It then goes to the yellow basic block which computes the MD5 hash of the 4-char string (whattt!???), prints out the NotMd5Hash string and waits for our input. But we've already tried the MD5 and it didn't work! Let's check what comes next:


The blue basic block loops over each byte of the 16-byte Md5 output, NOT's the value and stores the hex representation of it. The result is then compared to our input in the purple basic block. So this is what is meant by NotMd5Hash! The values of the output are NOT'd. The following is a python script to compute this:

import md5

m = md5.new("GHND").digest()
print ''.join(["%02X" % (256 + ~ord(x)) for x in m])

At this point we can continue playing the game and try to beat each level by guessing the coords. Of course we're not doing that as it is very time consuming and painful. So, the next step is to find a way to reveal where the ships are for each level. A good way to checking this is to trace what happens to our input coord and how a HIT or MISS is determined based on this input.

Our input is read in sub_40377D():

unsigned __int64 __fastcall sub_40377D(__int64 a1)
{

    [ .. snip .. ]

    printf(format, &v6);
    if ( fgets(&s, 17, stdin) )
    {
        row = (char)(s & 0xDF) - 0x41;
        col = v5 - 0x31;
        if ( row < 0 || row > 7 || col < 0 || col > 7 )
        {
            sub_403411(&s, 17LL);
        }
        else
        {
            *(_QWORD *)(a1 + 8) |= 1LL << (8 * (unsigned __int8)row + col);
            *(_BYTE *)(a1 + 28) = s & 0xDF;
            *(_BYTE *)(a1 + 29) = v5;
        }
    }
    return __readfsqword(0x28u) ^ v28;
}

The function reads our input at line 7 and maps the row and column to a number between 0 and 7 on lines 9 and 10. For example, 'B6' translates to row = 1 & col = 5 whilst 'E2' translates to row = 4 & col = 1. Both values are then validated on line 11 to ensure they're on the board. Finally, at line 17, the values are combined into a single value that represents the input coordinate. This is the important part as it will be used to determine a HIT or a MISS. The following is a python 1-liner, which we'll use later on, to compute this value:

singlify = 1 << (row * 8 + col)

We now move onto the part where a HIT or MISS is determined. This happens in sub_4038D6() which also decides what message is displayed to the user based on their coord input:


The decision happens in the yellow basic block. On the 4th line of this block, RAX holds a level configuration number which determines the location of all the ships. This value is AND'd with our coord number. If the number remains the same, then it goes to the green basic block which signifies a HIT, if not it goes to the red basic block which displays the message 'You missed :('. If we put a breakpoint and collect the configuration number for the first level we get: 0x0008087808087800.

We now combine everything we know. The following is a python script which, given a configuration number, determines all the hits and also displays them on a grid:

def draw_ships(input):    
    hits = []

    for row in range(8):
        for col in range(8):
            singlify = 1 << (row * 8 + col)
            if singlify & input == singlify:
                hits.append(chr(row + 0x41) + chr(col + 0x31))
                print "X",
            else:
                print "_",
        print
    print
    return hits
    
ship_config = 0x0008087808087800

print draw_ships(ship_config)

Running the script gives us:

Command Prompt
C:\>python solution.py _ _ _ _ _ _ _ _ _ _ _ X X X X _ _ _ _ X _ _ _ _ _ _ _ X _ _ _ _ _ _ _ X X X X _ _ _ _ X _ _ _ _ _ _ _ X _ _ _ _ _ _ _ _ _ _ _ _ ['B4', 'B5', 'B6', 'B7', 'C4', 'D4', 'E4', 'E5', 'E6', 'E7', 'F4', 'G4'] C:\>


Notice that this coincides perfectly with what we've gotten when we played the game manually (check 2nd image from above). With these 2 scripts we're now guaranteed to hit all the ships before running out of ammo and also solve the NotMd5hash challenge each time. Make sure to put a break point to collect the configuration number at each level and get a list of HIT coords from the script.

Going through the 10 levels, we collect the string 'FHGUZREJVO' from the solutions, and the game displays the following message:


Removing the PEWs we get:

Aye! You found some letters did ya? To find what you're looking for, you'll want to re-order them: 9, 1, 2, 7, 3, 5, 6, 5, 8, 0, 2, 3, 5, 6, 1, 4. Next you let 13 ROT in the sea! THE FINAL SECRET CAN BE FOUND WITH ONLY THE UPPER CASE.

This is referring to the string 'FHGUZREJVO'. After we do what we're told we end up with 'BUTWHEREISTHERUM'. Feeding this to the game instead of a coordinate:


Saturday, 21 October 2017

CTF Writeup - Flare-On 2017 - 04: notepad.exe


  1. Name - notepad.exe
  2. Category - Reverse Engineering
  3. Points - 1
  4. Binary - Download here

Running the binary notepad.exe we get:


Expected anything else from a binary called notepad.exe? When the binary is loaded into IDA, we even get the prompt to apply MS symbols to it! Interesting, so what's different from the normal notepad.exe? For starters, the Entry Point points at an executable .rsrc section at the bottom of the PE which also contains other functions. Additionally, the binary loads several strings onto the stack in the start() function, usually an indication that it is trying to hide some functionality:


The start() function looks a bit daunting so I've decided to take a different approach with this challenge and use ProcMon to deduce (guess?) it's inner workings. Filtering on notepad.exe and File System Activity we get the following:


The executable searches for path C:\Users\<user>\flareon2016challenge which is not present on the machine. Create the folder and put a dummy file in, say an empty text file called testing.txt. The function that searches for this file path is sub_1013F30() which does the following:
  1. Constructs the string 'C:\Users\<user>\flareon2016challenge'
  2. Calls FindFirstFileA on the path
  3. For each file found in the folder, invokes sub_1014E20() on it's file path
The first bit of function sub_1014E20() does some validation on the input file:


It performs the following:
  1. Checks that the file starts with 'MZ' (Green)
  2. Ensures that 'PE' offset is less than the size of the binary (Red)
  3. Checks that the file has 'PE' in the right place (Yellow)
  4. Ensures that the Target Machine is for Intel 386 and later (Blue)
  5. Passes the binary mapping to sub_10146C0()
The function sub_10146C0() then takes the timestamp of the input file and the timestamp of notepad.exe and branches the execution flow depending on these 2 values:


The blue basic blocks deal with notepad.exe whilst the yellow ones deal with the input file. Depending on these 2 variables, the execution flow will take 1 of the 5 relevant paths shown below:


Notice that 4 of them are very similar to each other. Let's go through one of them taking the 1st one (left most) as an example. The basic block does the following:
  1. Sub_1014350 converts 0x57D1B2A2 into the Date and Time: 2016/09/08 18:49:06 UTC
  2. Call ECX pops a message box with the resultant Date and Time
  3. Sub_10145B0 takes first 8 bytes from binary at offset 0x400 and writes them to key.bin
The other basic block (2nd from the right) reads the contents of key.bin, checks if it is 32 bytes long and XOR's it with another 32 bytes. The big question at this point is, what should key.bin contain? And hence what should the 4 PE files in 'C:\Users\<user>\flareon2016challenge' be?

The hint is in the folder name 'flareon2016challenge'. Download last year's Flare-On challenges and notice that challenge1.exe has a Date Modified of 08/09/2016 19:49 which matches with the timestamp we've analysed. Copying the challenge1.exe binary in the flareon2016challenge folder and running notepad.exe again we get the following Message Box and the first 8 bytes of the key in key.bin:


Cutting to the chase, the following are the 4 binaries from last year's challenge that match the 4 timestamps required by notepad.exe:
  1. challenge1.exe
  2. DudeLocker.exe
  3. khaki.exe
  4. unknown
We can now complete the challenge by placing these 4 binaries in the flareon2016challenge folder and run notepad.exe. Note that this requires us to change the timestamp of notepad.exe or to change the control flow during execution to ensure that all the branches are taken in the right order.

Alternatively we can extract the 8 bytes key from each binary at offsets 0x400, 0x410, 0x420 and 0x430 respectively (refer to image above). This will give us the key and we can create a small python script to print the solution:

key1 = '\x55\x8b\xec\x8b\x4d\x0c\x56\x57\x8B\x55\x08\x52\xFF\x15\x30\x20\xC0\x40\x50\xFF\xD6\x83\xC4\x08\x00\x83\xC4\x08\x5D\xC3\xCC\xCC'
key2 = '\x37\xE7\xd8\xbe\x7a\x53\x30\x25\xbb\x38\x57\x26\x97\x26\x6f\x50\xf4\x75\x67\xbf\xb0\xef\xa5\x7a\x65\xae\xab\x66\x73\xa0\xa3\xa1'

print ''.join([chr(ord(a) ^ ord(b)) for a,b in zip(key2,key1)])

Running the script:

Command Prompt
C:\>python solution.py bl457_fr0m_th3_p457@flare-on.com C:\>