MalwareTheFlag’s iHack 2020 challenges

On the 20th of June 2020, iHack took place as a virtual conference with two capture the flag games. The beginner CTF was aimed at starters, whereas the classic CTF was aimed at players who already played in a few CTF competitions. The MalwareTheFlag team created 9 challenges for the CTF. Four write-ups are given in this article. Links to the write-ups for the other five challenges are listed in the write-ups section.

Table of contents

Write-ups

Each write-up will include the challenge description, observations based on the description, the challenge’s solution in a step-by-step format, and a rationale that covers the creation process. The file(s) for each challenge can be downloaded freely. Lastly, the source code for some challenges is included in the write-up to further clarify the challenge’s explanation. Note that 100 points in the CTF should roughly equate to one hour of work for the players.

B1nary‘s Cant be real challenge can be found here. Kaido‘s The Doppler Effect, The Chronos Encrypter, Linkin the Flag, and Insider Trading can be found here.

Repetition is boring

This challenge was created by myself, it was worth 75 points and was solved 27 times. As such, 35% of the teams that scored points solved the challenge. One can download the file here. The challenge description is given below.

A strange file was discovered on our server, and it is up to you to find out what 
it is. We lost four interns over this, so you better come through, as we're in 
dire need for the solution! The interns left the building screaming, shouting 
something about "boring repetitive steps". Maybe we should ask the coming 
interns about their scripting skills. Never mind that, that is up to management 
to decide. What are you waiting for? Go!

Observations

The challenge consists of a single PowerShell script. Opening it in a text editor can be somewhat tedious due to its size. The code within the script seems to execute a decoded base64 encoded string. Upon executing the code, the message You have to dig deeper! is printed.

Analysis

The code, without the base64 encoded string, uses random capitalisation, as can be seen in the excerpt below.

ieX([systEm.tExt.EnCODIng]::Utf8.gEtSTriNg([syStEm.CONvErt]::FROMbasE64sTrIng("[...]"))

As the challenge name and description hint upon, this script is likely to repeat itself. To confirm this, one can manually decode the first stage. To automate this, one has to obtain the text between the quotes and decode it, until there is no such text anymore. Below, an example is given in Java.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
/**
 * Code to automatically retrieve the original value of 
 * the challenge script
 *
 * @author Max 'Libra' Kersten
 */
public class Base64VariantSolver {
 
    public static void main(String[] args) throws Exception {
        //Set the file name
        String fileName = "repetition-is-boring.ps1";
        //Load the file in a string
        String data = new String(Files.readAllBytes(Paths.get(fileName)));
        //Decode the first layer
        String base64 = getBase64(data);
        //Initialise the output variable
        String output = "";
        //Continue to decode additional layers until the function returns null
        while (base64 != null) {
            base64 = getBase64(base64);
            if (base64 != null) {
                output = base64;
            }
        }
        //Print the output once all layers have been removed
        System.out.println(output);
    }
 
    private static String getBase64(String input) {
        //Match a string between quotes
        String regex = "\"(\\S+)\"";
        //Make a pattern based on the given regular expression
        Pattern pattern = Pattern.compile(regex);
        //Match the pattern on the given string
        Matcher matcher = pattern.matcher(input);
        //Try to decode, if it succeeds, the matched string without quotes is to be returned
        //If it fails, null is to be returned, breaking the loop in the main function
        try {
            while (matcher.find()) {
                //Take the first matching group, as that is the value between the quotes
                return new String(Base64.getDecoder().decode(matcher.group(1)));
            }
            return null;
 
        } catch (Exception e) {
            return null;
        }
    }
}

In short, the program tries to decode the first full match. Once all layers have been removed, the original script is revealed, as can be seen in the output below.

$flag = "mtf{4ut0m4t10n_1s_k3y}"
$toPrint = "You have to dig deeper!"
Write-Host $toPrint

Rationale

The challenge consists of 31 base64 encoded layers, make manual decoding possible but repetitive. The capitalisation is random, as PowerShell is not case sensitive, and this slightly increases the challenge’s difficulty for those who are not used to automating tasks such as these.

The challenge name and description aim to solve it in a way that is similar to this write-up. There are many web shells in the wild that are encoded a few dozen times using the same encoding. The user does not notice this during runtime, but it does help to evade signature based detection on the server.

Vault

This challenge was created by myself, it was worth 175 points and was solved 23 times. As such, 29% of the teams that scored points solved the challenge. One can download the file here. The challenge description is given below.

Vault Systems recently broke the news that their systems cannot be started 
anymore, as a disgrunteld system administrator decided to change the 
administrator password in the main application. Vault Systems asked the 
internet for help, hoping that a vigilante will help them out. 
Maybe you can take a look?

Observations

Upon examining the file, one will see that it is a Java Archive. The output that is obtained when running the program, is printed one character at a time. The output is given below.

Welcome to Vault Systems version 0.1!
It seems that Vault Systems cannot function properly.
More information can be found in the manual, in chapter 82, section 17.

CTF challenges often take command line arguments. Adding one does not yield a different outcome, but adding two or more does, as can be seen below.

Welcome to Vault Systems version 0.1!

Analysis

To know how the program works, one can decompile it. This can be done with a variety of Java decompilers. The output in this write-up is obtained using JAD-X. The program contains only a single class, where three functions are present: main, rot13, and print. The print function is given below.

private static void print(String value) throws InterruptedException {
	for (char valueOf : value.toCharArray()) {
		System.out.print(Character.valueOf(valueOf));
		Thread.sleep(75);
	}
	System.out.print("\n");
}

The given string is printed character by character, where the program pauses for 75 miliseconds after writing a single character. The rot13 function is given below.

public static String rot13(String input) {
	StringBuilder sb = new StringBuilder();
	for (int i = 0; i < input.length(); i++) {
		char c = input.charAt(i);
		if (c >= 'a' && c <= 'm') {
			c = (char) (c + 13);
		} else if (c >= 'A' && c <= 'M') {
			c = (char) (c + 13);
		} else if (c >= 'n' && c <= 'z') {
			c = (char) (c - 13);
		} else if (c >= 'N' && c <= 'Z') {
			c = (char) (c - 13);
		}
		sb.append(c);
	}
	return sb.toString();
}

This function switches the characters 13 places. Its name refers to the amount that each character in the string is displaced. To obtain the original input, one can simply reuse the function with the output as its input. The main function is given below.

public static void main(String[] args) throws InterruptedException {
	byte[] toCompare = new byte[]{(byte) 101, (byte) 109, (byte) 100, (byte) 122, (byte) 101, (byte) 51, (byte) 99, (byte) 48, (byte) 97, (byte) 87, (byte) 53, (byte) 102, (byte) 99, (byte) 122, (byte) 66, (byte) 108, (byte) 88, (byte) 51, (byte) 107, (byte) 120, (byte) 99, (byte) 122, (byte) 78, (byte) 57};
	print("Welcome to Vault Systems version 0.1!");
	if (args.length < 2) {
		print("It seems that Vault Systems cannot function properly.\nMore information can be found in the manual, in chapter 82, section 17.");
	} else if (args[0].equals("--enable-vault")) {
		if (new String(Base64.getDecoder().decode(rot13(args[1]))).equals(new String(Base64.getDecoder().decode(rot13(new String(toCompare)))))) {
			print("The Vault system has started successfully!");
		} else {
			print("The Vault system has not started successfully!");
		}
	}
}

The welcome message is printed regardless of the amount of arguments. If there are less than two arguments, the manual reference is printed. Only if the first argument equals –enable-vault, the next if-else statement is reached. The user input is rotated using rot13, after which it is base64 decoded. The user input is then compared to the given byte array, which was rotated and decoded in the same way. One can copy the creation code from the decompiled code to obtain the value, or obtain the value manually. The code, which equals emdze3c0aW5fczBlX3kxczN9 is supposed to be used as the the second argument.

To test if the output is correct, one can enter the two found arguments, and the success message should be displayed. The output is given below.

java -jar ./Vault.jar --enable-vault emdze3c0aW5fczBlX3kxczN9
Welcome to Vault Systems version 0.1!
The Vault system has started successfully!

The provided argument is, however, base64 encoded and the characters have been rotated 13 places. Decoding the string results in zgs{w4in_s0e_y1s3}. After displacing the characters by 13 places, the flag becomes apparent: mtf{j4va_f0r_l1f3}.

Rationale

Malware often uses command line arguments that are embedded in the loader. This is done to avoid detection via automated sandboxes, as these do not provide the arguments. When reversing, it can be important to see how command line arguments are used.

Note that the used ROT13 function came from this answer on StackOverflow.

Go with the flow

This challenge was created by myself, it was worth 175 points and was solved 19 times. As such, 24% of the teams that scored points solved the challenge. One can download the file here. The challenge description is given below.

We found a strange JavaScript artifact on an infected corporate laptop. 
It gives a weird error when executed, so we need you to put it on the 
blockchain and analyse it, as we have heard this malware was able to 
bypass our next-generation artificial intelligence firewall, as well 
as our machine learning deception techniques that we put in place! 
We need the answer, no matter the cost!

Observations

The given script contains only a single function definition, which is called directly after the declaration. The function executes obfuscated code, which is the main point of interest for the analysis.

Analysis

The obfuscated code is given below. Note that the concatenation sequence is mostly omitted to provide a clear overview.

function jsFunction() {
eval(atob(atob((String.fromCharCode(90) + String.fromCharCode(71) + String.fromCharCode(49) + String.fromCharCode(71) + String.fromCharCode(101) + [...]))))
}
 
jsFunction();

To see what code is executed, one can simply replace eval with console.log, as that prints the deobfuscated result instead, as can be seen below. Note that beautification breaks the code execution in the browser’s console. Examples in this write-up will be using beautified code, but execution is done using the original form.

var _0x4545 = ['\\x77\\x35\\x58\\x44\\x6d\\x32\\x30\\x3d', '\\x77\\x70\\x48\\x44\\x6a\\x32\\x6f\\x3d', '\\x77\\x35\\x66\\x43\\x76\\x53\\x30\\x3d', '\\x4d\\x57\\x37\\x44\\x6d\\x67\\x3d\\x3d', '\\x57\\x73\\x4b\\x32\\x77\\x36\\x45\\x3d', '\\x46\\x30\\x68\\x73', '\\x77\\x6f\\x4c\\x43\\x70\\x4d\\x4b\\x36', '\\x4f\\x63\\x4b\\x73\\x77\\x37\\x55\\x3d', '\\x4c\\x73\\x4b\\x71\\x77\\x70\\x73\\x3d', '\\x77\\x36\\x41\\x2f\\x4a\\x77\\x3d\\x3d', '\\x4d\\x77\\x4c\\x44\\x67\\x77\\x3d\\x3d', '\\x48\\x4d\\x4f\\x61\\x77\\x36\\x63\\x3d', '\\x4e\\x4d\\x4b\\x35\\x77\\x6f\\x45\\x3d', '\\x77\\x34\\x7a\\x44\\x76\\x54\\x67\\x3d', '\\x77\\x36\\x72\\x43\\x74\\x4d\\x4f\\x43', '\\x77\\x34\\x62\\x43\\x70\\x63\\x4f\\x37', '\\x77\\x34\\x48\\x44\\x67\\x57\\x41\\x3d'];
(function(_0x5d0fdf, _0x4545dc) {
    var _0x7c8f2f = function(_0x151d5f) {
        while (--_0x151d5f) {
            _0x5d0fdf['push'](_0x5d0fdf['shift']());
        }
    };
    _0x7c8f2f(++_0x4545dc);
}(_0x4545, 0x1eb));
var _0x7c8f = function(_0x5d0fdf, _0x4545dc) {
    _0x5d0fdf = _0x5d0fdf - 0x0;
    var _0x7c8f2f = _0x4545[_0x5d0fdf];
    if (_0x7c8f['KNXoXp'] === undefined) {
        (function() {
            var _0x39da27 = typeof window !== 'undefined' ? window : typeof process === 'object' && typeof require === 'function' && typeof global === 'object' ? global : this;
            var _0x1f4b1b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
            _0x39da27['atob'] || (_0x39da27['atob'] = function(_0x50f30e) {
                var _0x4dcd43 = String(_0x50f30e)['replace'](/=+$/, '');
                var _0x44481e = '';
                for (var _0x2e1ede = 0x0, _0x5aec5a, _0x4f6479, _0x24c262 = 0x0; _0x4f6479 = _0x4dcd43['charAt'](_0x24c262++); ~_0x4f6479 && (_0x5aec5a = _0x2e1ede % 0x4 ? _0x5aec5a * 0x40 + _0x4f6479 : _0x4f6479, _0x2e1ede++ % 0x4) ? _0x44481e += String['fromCharCode'](0xff & _0x5aec5a >> (-0x2 * _0x2e1ede & 0x6)) : 0x0) {
                    _0x4f6479 = _0x1f4b1b['indexOf'](_0x4f6479);
                }
                return _0x44481e;
            });
        }());
        var _0x3f6685 = function(_0x3b26b8, _0x9fa303) {
            var _0x3ec96f = [],
                _0x4898a5 = 0x0,
                _0x4696d, _0x500f9b = '',
                _0x2681ab = '';
            _0x3b26b8 = atob(_0x3b26b8);
            for (var _0x137e74 = 0x0, _0x4afd26 = _0x3b26b8['length']; _0x137e74 < _0x4afd26; _0x137e74++) {
                _0x2681ab += '%' + ('00' + _0x3b26b8['charCodeAt'](_0x137e74)['toString'](0x10))['slice'](-0x2);
            }
            _0x3b26b8 = decodeURIComponent(_0x2681ab);
            var _0x50ff81;
            for (_0x50ff81 = 0x0; _0x50ff81 < 0x100; _0x50ff81++) {
                _0x3ec96f[_0x50ff81] = _0x50ff81;
            }
            for (_0x50ff81 = 0x0; _0x50ff81 < 0x100; _0x50ff81++) {
                _0x4898a5 = (_0x4898a5 + _0x3ec96f[_0x50ff81] + _0x9fa303['charCodeAt'](_0x50ff81 % _0x9fa303['length'])) % 0x100;
                _0x4696d = _0x3ec96f[_0x50ff81];
                _0x3ec96f[_0x50ff81] = _0x3ec96f[_0x4898a5];
                _0x3ec96f[_0x4898a5] = _0x4696d;
            }
            _0x50ff81 = 0x0;
            _0x4898a5 = 0x0;
            for (var _0x36bc7e = 0x0; _0x36bc7e < _0x3b26b8['length']; _0x36bc7e++) {
                _0x50ff81 = (_0x50ff81 + 0x1) % 0x100;
                _0x4898a5 = (_0x4898a5 + _0x3ec96f[_0x50ff81]) % 0x100;
                _0x4696d = _0x3ec96f[_0x50ff81];
                _0x3ec96f[_0x50ff81] = _0x3ec96f[_0x4898a5];
                _0x3ec96f[_0x4898a5] = _0x4696d;
                _0x500f9b += String['fromCharCode'](_0x3b26b8['charCodeAt'](_0x36bc7e) ^ _0x3ec96f[(_0x3ec96f[_0x50ff81] + _0x3ec96f[_0x4898a5]) % 0x100]);
            }
            return _0x500f9b;
        };
        _0x7c8f['SZgoTp'] = _0x3f6685;
        _0x7c8f['lVelro'] = {};
        _0x7c8f['KNXoXp'] = !![];
    }
    var _0x151d5f = _0x7c8f['lVelro'][_0x5d0fdf];
    if (_0x151d5f === undefined) {
        if (_0x7c8f['OUVSVd'] === undefined) {
            _0x7c8f['OUVSVd'] = !![];
        }
        _0x7c8f2f = _0x7c8f['SZgoTp'](_0x7c8f2f, _0x4545dc);
        _0x7c8f['lVelro'][_0x5d0fdf] = _0x7c8f2f;
    } else {
        _0x7c8f2f = _0x151d5f;
    }
    return _0x7c8f2f;
};
var _0x13afd3 = 0x3;
var _0x210b33 = 0x7;
if (_0x13afd3 > _0x210b33) {
    console[_0x7c8f('\\x30\\x78\\x62', '\\x47\\x78\\x53\\x74')](_0x7c8f('\\x30\\x78\\x66', '\\x39\\x75\\x57\\x6b') + _0x7c8f('\\x30\\x78\\x64', '\\x53\\x6f\\x5b\\x4d') + _0x7c8f('\\x30\\x78\\x34', '\\x39\\x75\\x57\\x6b') + _0x7c8f('\\x30\\x78\\x65', '\\x47\\x4b\\x31\\x32') + _0x7c8f('\\x30\\x78\\x33', '\\x25\\x32\\x5a\\x5b') + _0x7c8f('\\x30\\x78\\x37', '\\x66\\x68\\x34\\x4c') + _0x7c8f('\\x30\\x78\\x32', '\\x5d\\x61\\x75\\x28') + _0x7c8f('\\x30\\x78\\x39', '\\x78\\x61\\x51\\x64') + _0x7c8f('\\x30\\x78\\x35', '\\x33\\x74\\x63\\x50') + _0x7c8f('\\x30\\x78\\x61', '\\x40\\x36\\x35\\x30') + _0x7c8f('\\x30\\x78\\x63', '\\x40\\x65\\x31\\x21') + _0x7c8f('\\x30\\x78\\x36', '\\x40\\x55\\x46\\x51'));
} else {
    console[_0x7c8f('\\x30\\x78\\x30', '\\x72\\x30\\x52\\x24')](_0x7c8f('\\x30\\x78\\x38', '\\x66\\x4e\\x56\\x69') + _0x7c8f('\\x30\\x78\\x31\\x30', '\\x77\\x75\\x33\\x6d') + _0x7c8f('\\x30\\x78\\x31', '\\x5d\\x61\\x75\\x28') + '\\x21');
}

When executing the code, the message Try again! is displayed.

The strings within the code are unicode encoded. Additionally, a few references to the console can be observed. The string array is been rotated in the anonymous function directly after the array declaration. The function named _0x7c8f interacts with the string array named _0x4545 by removing the unicode encoding, decoding the base64 result, and decrypting the string array’s content using the RC4 algorithm. The _0x7c8f function requires two arguments. The first one is a number, which is used as the index to get the value from the string array. The second argument is the RC4 key to decrypt the value at the given index. Below, the refactored script without any calls to the decryption function is given.

var _0x13afd3 = 0x3;
var _0x210b33 = 0x7;
if (_0x13afd3 > _0x210b33) {
    console[`log`](`mtf` + `{j4` + `v4s` + `cr1` + `pt_` + `0bf` + `usc` + `4t1` + `0n_` + `1s_` + `f4n` + `cy}`);
} else {
    console[`log`](`Try` + ` ag` + `ain` + `!`);
}

If _0x13afd3 is less than _0x210b33, the message Try again! is printed, as was observed earlier on. If this is not the case, the flag is printed. As these values are static, the flag is never printed, meaning the user has to reverse the script up until this point. The concatenated flag is equal to mtf{j4v4scr1pt_0bfusc4t10n_1s_f4ncy}.

Rationale

The original script is, similar to the script that is given above, as can be seen in the original code below.

var x = 3;
var y = 7;
 
if(x > y) {
    console.log("mtf{j4v4scr1pt_0bfusc4t10n_1s_f4ncy}");
} else {
    console.log("Try again!");
}

The layer of obfuscation that is first encountered in the challenge, is generated with Genesis. This is a framework I made to generate unique test cases. The used JavaScript obfuscation profile within Genesis is medium. The second layer of obfuscation is created by using Obfuscator.io. This obfuscator is often used by malware authors, as the output varies based upon the used settings.

VBA hurts my eyes

This challenge was created by Danus, it was worth 175 points and was solved 5 times. As such, 6% of the teams that scored points solved the challenge. One can download the file here. The challenge description is given below.

This strange excel file was delivered within one of our clients mailbox, 
we extracted it before the client manged open it. It might be garbage but 
it might be something useful - perhaps it's those pesky APT's (or is it 
John from IT trying to mess with us again?). I should mention, that this 
Excel file does contain very cryptic VBA script and I can't seem to 
understand it at all. Maybe you can figure out what this is. 
Well, good luck.

Observations

Upon opening the document, one is greeted with a filled spreadsheet. More digging shows that there are three VBA functions within the document: FirstSub, SecondSub, and CountRecursive.

Analysis

The first function is only used to pass information onto the next function, as it does not contain any further logic. The code is given below.

Public Sub FirstSub()
   Dim array_bytes() As Byte
   Dim number_length As Integer
   number_length = WorksheetFunction.CountA(Worksheets(1).Columns(1))
   Call SecondSub(number_length, array_bytes)
End Sub

The second function iterates through all even columns, calling the recursive function at each step. The code is given below.

Public Sub SecondSub(number_linelen As Integer, ByRef array_bytes() As Byte)
    Dim current As Integer
    Dim current_string As String
    Dim index As Integer
    Dim index2 As Integer
    ReDim array_bytes(number_linelen)
 
    index2 = 0
    For index = 1 To 90 Step 2
        index2 = index2 + 2
        Call CountRecursive(number_linelen, array_bytes, 1, index, index2)
    Next index
End Sub

The CountRecursive function places some values in the spreadsheet’s cells when the found value is above 95. The code is given below.

Function CountRecursive(number_linelen As Integer, ByRef array_bytes() As Byte, current_line As Integer, num_col As Integer, char_col As Integer)
    Dim current_string As String
    Dim current_value As Integer
 
    If (current_line > number_linelen) Then
        Dim number_index As Integer
        Dim array_len As Integer
        array_len = UBound(array_bytes, 1) - LBound(array_bytes, 1)
        For number_index = 1 To array_len
            If (array_bytes(number_index) > 95) Then
                 Cells(Int((6000 - 5000 + 1) * Rnd + 5000), char_col).Value = array_bytes(number_index) Xor 6
            End If
        Next number_index
    Else
        current_string = Cells(current_line, char_col).Value
        current_value = Cells(current_line, num_col).Value
        current_value = CInt(current_value)
        array_bytes(current_value) = current_string
        Call CountRecursive(number_linelen, array_bytes, current_line + 1, num_col, char_col)
    End If
End Function

The xor within the for-loop’s if-statement’s body is of interest, as malware often retrieves values this way. The value within the selected cell is equal to the result of a xor operation between the value within the array_bytes at the given index and 6. Additionally, there is nothing else that the VBA function does. As such, one can print the values that are written to the cells by using a message box, as can be seen in the sample code below.

'omitted code
            If (array_bytes(number_index) > 95) Then
                 Cells(Int((6000 - 5000 + 1) * Rnd + 5000), char_col).Value = array_bytes(number_index) Xor 6
                 Msgbox array_bytes(number_index) Xor 6
            End If
'omitted code

Doing this will return the following values: 109, 116, 102, 123, 101, 120, 99, 101, 108, 99, 111, 100, 101, 105, 115, 104, 101, 99, 107, 105, 110, 102, 117, 110, and 125. When converting the integers into characters (as they correspond with the values in the ASCII table), the flag is given: mtf{excelcodeisheckinfun}.

Rationale

Macros are often used in malspam campaigns. Knowing where and how to look at a macro is essential. Most macros contain some sort of obfuscation, meaning that it is up to the analyst to find out what part of the code is essential. A high percentage of VBA macros can be defeated using a single messagebox to display the deobfuscated data, as was the case in this challenge.

Conclusion

First of all, I’d like to thank the HackFest organisation and the MalwareTheFlag team for the excellent cooperation. As we were still crystallising our way of working, we were also making challenges for iHack. As such, the track contained less challenges than we originally wanted to, but quality always prevails over quantity. The ideas we have left over will be used our next CTF, where we hope to see you as well!

All nine challenges within the track were solved within the CTF’s duration. The Vault challenge brought up the most questions, as the retrieval of the flag from the second command line argument was unclear to some players. This will be taken into account in future challenges.


To contact me, you can e-mail me at [info][at][maxkersten][dot][nl], send me a PM on Reddit or DM me on Twitter @LibraAnalysis.