HELP: Flare-On 6 Challenge 12

Flare-On is Fireeye’s annual CTF which mainly focused on reverse engineering and this year (2019) is the 6th. I got a chance to finish all the challenges and the last challenge (challenge 12) is quite interesting and educational so I decided to write something about it.

First of all, the prize for finishers looks very nice ☺:

Ok, let’s begin. The challenge provides the following two files for analysis:

File Name help.dmp
Size 1.99 GB (2,146,959,360 bytes)
MD5 26a2fd022c3a49e474b82014c0b95c14
SHA256 b2da27e9b349b0b9e04496fd0d00782691271fc91fdcdbc24ef0471287b2181a
Description A crash dump file.
File Name help.pcapng
Size 30.7 MB (32,244,036 bytes)
MD5 1f639d457d46c0163b82f10bda64063a
SHA256 91db1e1f3db0d0220451abe121841cea4888b70519982aef3ec220df6205d07b
Description A network traffic dump file.

Along with the two files, there is a Message.txt file contains the information below:

You’re my only hope FLARE-On player! One of our developers was hacked and we’re not sure what they took. We managed to set up a packet capture on the network once we found out but they were definitely already on the system. I think whatever they installed must be buggy – it looks like they crashed our developer box. We saved off the dump file but I can’t make heads or tails of it – PLEASE HELP!!!!!!

Apparently, the challenge wants us to analyze the crash dump and decrypt something from either the crash dump or the network traffics as the flag for this challenge.

Let’s look at the network traffics first since it is easier. Open the help.pcapng with the Wireshark and examine the network communications, you will find some suspicious packages being sent to port 4444, 6666, 7777 and 8888, however, the content seems not in plaintext so that we need to find out how to decrypt them.

Tips: to get a statistic of the network communications, you may save the .pcapng file as .pcap file and use the Python script I wrote below:

from scapy.all import *

if __name__ == '__main__':

    packets = rdpcap('help.pcap')
    
    for name, session in packets.sessions().items():
        if TCP in session[0]:
            print name

The output will be like below:

There seems no more infomrtion could be extracted from the network traffics, so it’s time to move to the crash dump (help.dump) file. There are a lot of tools for crash dump analysis and in this article I will use Windbg. There is a Windbg command !analyze -v which can be used to generate some summary information from the crash, according to those information we can see that it is the code from a driver named man.sys that lead to the crash:

Using the lmvm command on the module man, we can locate its loading address in memory:

After knowing the module address we can dump out this module for further analysis:

Take a quick look at the dump file, you will find there is a PE file embedded at offset 0x7110:

Let’s extract this PE file and see what it does. The file is a 64bit DLL file which exported a function named c, in this export function it will listen on port 4444 to receive and handle a bunch of commands as shown in the screenshot below:

You will soon find out that most of the commands are actually passed to a driver named FLID to process (through API DeviceIoControl), so there seems not too much information we can get directly from the DLL.

Let’s go back to the man.sys dump file, although the dump has no PE header, you will see it won’t be a barrier for our analysis. Firstly, loading the man.sys into IDA, the IDA will automatically resolve some functions for us. After finishing the auto-analysis, we can look into the Strings window (Shift + F12) , if you scroll down you will spot a string \??\FLID, familar? Yes, it is the name of the driver that the DLL tries to communicate with! Cross-referencing this string we may be able to find the entry point of the driver because driver usually initializes its name at a place not far away from the entry point. Using this way we can successfully locate the entry point of the man.sys, which is at address 0xFFFFF880033C1110:

Tips: When loading memory dumps into IDA for analysis, it is recommended to rebase the address to the same one as it seen in Windbg, this can be done through IDA->Edit->Segments->Rebase program.

Now that we have found the entry point, reverse engineering the driver code will lead you to the place where the IoControlCode get handled:

The handler for IoControlCode 0x22AF2C got my attention first, because in its handler (0xFFFFF880033BEC50) it tries to load a user mode module into memory, and the most interesting thing is that the loaded module will be encrypted if not in use. There is a data structure which I named it as ModuleInfo to store the information of the user mode module:

Lucky enough there is a global variable which will point to the first ModuleInfo entity and the Next pointer of the ModuleInfo structure allows us to enumerate all the modules:

Below is the data of the first user mode module from the crash dump:

With above information we will be able to dump the data of the module:

The module is encrypted by RC4 with a 44 (0x2C) bytes long key located at the offset 0x50 of the ModuleInfo structure:

Below is the decryption key for the first module:

Knowing the key we can decrypt the first module (already dumped as payload1.bin) with the Python script below:

def rc4_crypt(data , key):
    
    S = [x for x in xrange(256)]
    j = 0
    out = []
    
    for i in range(256):
        j = (j + S[i] + ord( key[i % len(key)] )) % 256
        S[i] , S[j] = S[j] , S[i]
    
    i = j = 0
    for char in data:
        i = ( i + 1 ) % 256
        j = ( j + S[i] ) % 256
        S[i] , S[j] = S[j] , S[i]
        out.append(chr(ord(char) ^ S[(S[i] + S[j]) % 256]))
        
    return ''.join(out)


if __name__ == '__main__':

    key = '00001002000000006016000000000000007000000000000000016f000000000060d05f0380faffff00000000'.decode('hex')
    data = open('payload1.bin', 'rb').read()
    ourput = rc4_crypt(data , key)
open('payload1.bin.dec', 'wb').write(ourput)

You can use the same way to extracted other modules. There are totally 5 payload modules could be found:

Name Tag Port Description
Payload1.bin 0xbebebebe N/A Compress and encrypt data.
Payload2.bin 0xdededede N/A Send data to C2.
Payload3.bin 0xfabadada 8888 Log keystrokes.
Payload4.bin 0xbeda4747 7777 Capture screenshot.
Payload5.bin 0xdefa8474 6666 Manipulate files.

Note: The Name is just a reference name used by myself during the analysis; the Tag is the first Dword of the ModuleInfo structure which is used by the malware to identify the payload module; the Port is the port number used by the payload module for network communication.

It looks that the keystroke logs and screenshots may contain the information needed to solve this challenge and we did see some network traffics send to port 7777 and 8888 in the pcap file. However, there is still a puzzle need to be solved: what is the algorithm used to encrypt/decrypt the network traffics?

The Payload1.bin contains part of the answer, in this module it retrieves the user name of the victim machine as a RC4 key to encrypt the data, and before the encryption it will also compress the data with Windows API RtlCompressBuffer().

To get the user name of the victim machine we can use the Windbg command below:

Then we can create the following Python script for the decryption:

import sys
import struct
import lznt1_test


def rc4_crypt(data , key):
    
    S = [x for x in xrange(256)]
    j = 0
    out = []
    
    for i in range(256):
        j = (j + S[i] + ord( key[i % len(key)] )) % 256
        S[i] , S[j] = S[j] , S[i]
    
    i = j = 0
    for char in data:
        i = ( i + 1 ) % 256
        j = ( j + S[i] ) % 256
        S[i] , S[j] = S[j] , S[i]
        out.append(chr(ord(char) ^ S[(S[i] + S[j]) % 256]))
        
    return ''.join(out)


if __name__ == '__main__':
    
    key = 'FLARE ON 2019\x00'
    data = open(sys.argv[1], 'rb').read()
    size = struct.unpack('<I', data[0:4])[0]
    out = rc4_crypt(data[4:], key)
    out = lznt1_test.decompress_data(out[:size-4])
    open(sys.argv[1] + '.dec', 'wb').write()

Till now, it looks we are very close to the goal, however, by running above script we still cannot decrypt the network traffics correctly, there seems a second layer encryption, but where is it?

By reviewing those IO control handlers in man.sys, you can find that when passing IoControlCode 0x2337BC, another driver will be installed under name \Driver\FLARE_Loaded_%d:

With above information, we can check if any additional drivers has been installed by man.sys. It turns out there are two:

Now let’s check the FLARE_Loaded_0:

The entry point of the drive is located at address 0xfffffa80042d1184 and there is a customized dispatch routine located at address 0xfffffa80042d5ef8, to facilitate the analysis, you may dump the memory based on the entry point and then load it into IDA:

The FLARE_Loaded_0 is a driver based on Windows Filtering Platform, it creates a sublayer into the network stack and modifies the network traffics on the fly. By looking at the dispatch routine we found earlier (0xfffffa80042d5ef8) we can see that there is only one IoControlCode being accepted, which is 0x13FFFC. After analyzing the handler for this IoControlCode, the flowing conclusion could be draw:

  • The IoControlCode is used to configurate the traffic filtering rules.
  • A port number and a key will be passed to the driver and the driver will create a sublayer to monitor the network traffics from or to the specified port and encrypt/decrypt the data using the key.
  • The encryption/decryption algorithm is a simple XOR.

So how can we find the port-key relationships? It turns out that the driver will store the rules into a data structure which I named it as HookInfo during the analysis:

There is a global variable that points to the first HookInfo structure:

Given those information now we can get all the port-key mapping by emulating the HookInfo list:

Finally, with the XOR keys for each port and combining with previous analysis results, we can write the Python script below to decrypt all the traffics:

import os
import lznt1_test
from scapy.all import *


def rc4_crypt(data , key):
    
    S = [x for x in xrange(256)]
    j = 0
    out = []
    
    for i in range(256):
        j = (j + S[i] + ord( key[i % len(key)] )) % 256
        S[i] , S[j] = S[j] , S[i]
    
    i = j = 0
    for char in data:
        i = ( i + 1 ) % 256
        j = ( j + S[i] ) % 256
        S[i] , S[j] = S[j] , S[i]
        out.append(chr(ord(char) ^ S[(S[i] + S[j]) % 256]))
        
    return ''.join(out)


def xor_decrypt(data, key):
    
    out = ''
    for i in range(len(data)):
        out += chr(ord(data[i]) ^ ord(key[i % len(key)]))
        
    return out
    
    
def decrypt_data(data):

    key = 'FLARE ON 2019\x00'
    size = struct.unpack('<I', data[0:4])[0]
    out = rc4_crypt(data[4:], key)
    out = lznt1_test.decompress_data(out[:size-4])
    
    return out
    
    
def adjust_bmp_off(data):
    
    return data[4:]
    

if __name__ == '__main__':

    packets = rdpcap('help.pcap')
    
    port_key_mapping = {4444: {'key': '5df34a484848dd23'.decode('hex'), 'offset': 0, 'suffix': '', 'callback': None},
                        6666: {'key': 'd56994fa25ecdfda'.decode('hex'), 'offset': 6, 'suffix': '', 'callback': decrypt_data},
                        7777: {'key': '4a1f4b1cb0d825c7'.decode('hex'), 'offset': 6, 'suffix': '.png', 'callback': adjust_bmp_off},
                        8888: {'key': 'f78f7848471a449c'.decode('hex'), 'offset': 6, 'suffix': '', 'callback': decrypt_data}}
                        
    result_folder = 'decrypted_traffics'
    if not os.path.isdir(result_folder):
        os.makedirs(result_folder)
    
    for port, config in port_key_mapping.items():
        i = 0
        for name, session in packets.sessions().items():
            payload = ''
            for packet in session:
                if TCP in packet:
                    if packet[TCP].dport == port:
                        payload += str(packet[TCP].payload)
            if payload != '':
                dec_data = xor_decrypt(payload[config['offset']:], config['key'])
                if  config['callback'] is not None:
                    dec_data = config['callback'](dec_data)
                open(os.path.join(result_folder, 'port_' + str(port) + '_' + str(i) + config['suffix']), 'wb').write(dec_data)
                i += 1

From one of the screenshot decrypted from the network traffics we can see that the flag we are seeking for is in a KeyPass database file:

From another screenshot we can understand that the length of the KeyPass password is likely to be 18 bytes or longer:

So how can we get the keys.kdb file? In fact, it has already been stolen by the malware through its file manipulation payload module (Payload5.bin), and if you look at the decrypted network traffics you will find the data sent to port 6666 just contains the file!

Ok, now only one question left, what is the password for the keys.kdb file? Remember that we have a keylogger payload module (Payload3.bin) which will send the keystrokes to port 8888, below is what we can get from the network traffics:

The string “th1sisth33nd111” looks very much like the password, however the length is not same as the one we saw in the screenshot, so what was missing?

By looking at the keylogger payload module (Payload3.bin) again carefully, you will realize that special characters were not recorded, and the log data is case insensitive. That probably the missing things!

Initially I was trying to brute-force a full set of special characters but soon I realized that it is impossible because it requires a very long time, then I tried to only add an underscore (_) between each word and generated a password dictionary with the Python script below:

key = 'th1s_is_th3_3nd111'

wordlist = list()
wordlist.append(key)

start = 0
for r in range(1000):
    pre_len = len(wordlist)
    for i in range(start, len(wordlist)):
        for j in range(len(wordlist[i])):
            keys = list(wordlist[i])
            is_new = False
            if ord(keys[j]) >= ord('a') and ord(keys[j]) <= ord('z'):
                keys[j] = chr(ord(keys[j]) ^ 0x20)
                is_new = True
            elif ord(keys[j]) >= ord('A') and ord(keys[j]) <= ord('Z'):
                keys[j] = chr(ord(keys[j]) ^ 0x20)
                is_new = True
            elif keys[j] == '1':
                keys[j] = '!'
                is_new = True
            elif keys[j] == '3':
                keys[j] = '#'
                is_new = True
            elif keys[j] == '!':
                keys[j] = '1'
                is_new = True
            elif keys[j] == '#':
                keys[j] = '3'
                is_new = True
            if is_new:
                new_key = ''.join(keys)
                if new_key not in wordlist:
                    open('wordlist.txt', 'ab').write(new_key + '\n')
                    wordlist.append(new_key)
    start = pre_len
    print r, pre_len, len(wordlist)
    if len(wordlist) == pre_len:
        break

Fortunately, with only a few tries I got the correct password Th!s_iS_th3_3Nd!!!:

Open the keys.db file with the password you will get the flag: f0ll0w_th3_br34dcrumbs@flare-on.com

Hopefully this writeup will give you some fresh ideas on how to analyze malware from a crash dump, if you want to try it by yourselves you can find all the challenges from the link below:

http://flare-on.com/

You can also find the offical writeups from the link below:

https://www.fireeye.com/blog/threat-research/2019/09/2019-flare-on-challenge-solutions.html

This entry was posted in CTF, WinDbg and tagged , , , . Bookmark the permalink.

2 Responses to HELP: Flare-On 6 Challenge 12

  1. failwest says:

    Hi, how do you know keys.hash?

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.