Flare-On 6 CTF WriteUp (Part 12)

This is the twelfth and final part of the Flare-On 6 CTF WriteUp Series.

12 - help

The challenge reads

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!!!!!!

We have two files -

  • help.dmp - A 2 GB memory dump
  • help.pcapng - Packet capture

Identifying the image

For analyzing the memory dump we will be using Volatility along with WinDbg. Make sure to use the bleeding edge version of Volatility on GitHub and not the 2.6 release which is quite old.

At first we need to identify the image with imageinfo or kdbgscan command. Unfortunately, for this specific image none of the commands work out of the box as shown in the snippet below.

$ vol.py -f help.dmp imageinfo
Volatility Foundation Volatility Framework 2.6.1
INFO    : volatility.debug    : Determining profile based on KDBG search...
WARNING : volatility.debug    : Alignment of WindowsCrashDumpSpace64 is too small, plugins will be extremely slow
WARNING : volatility.debug    : Alignment of WindowsCrashDumpSpace64 is too small, plugins will be extremely slow
WARNING : volatility.debug    : Alignment of WindowsCrashDumpSpace64 is too small, plugins will be extremely slow
WARNING : volatility.debug    : Alignment of WindowsCrashDumpSpace64 is too small, plugins will be extremely slow
WARNING : volatility.debug    : Alignment of WindowsCrashDumpSpace64 is too small, plugins will be extremely slow
WARNING : volatility.debug    : Alignment of WindowsCrashDumpSpace64 is too small, plugins will be extremely slow
WARNING : volatility.debug    : Alignment of WindowsCrashDumpSpace64 is too small, plugins will be extremely slow

In such cases where Volatility is unable to infer on its own, we have to manually specify the image profile to use with the --profile flag. For example using the profile Win7SP1x64 volatility correctly identifies the image.

$ vol.py -f help.dmp --profile=Win7SP1x64 imageinfo
Volatility Foundation Volatility Framework 2.6.1
INFO    : volatility.debug    : Determining profile based on KDBG search...
          Suggested Profile(s) : Win7SP1x64, Win7SP0x64, Win2008R2SP0x64, Win2008R2SP1x64_24000, Win2008R2SP1x64_23418, Win2008R2SP1x64, Win7SP1x64_24000, Win7SP1x64_23418
                     AS Layer1 : WindowsAMD64PagedMemory (Kernel AS)
                     AS Layer2 : WindowsCrashDumpSpace64 (Unnamed AS)
                     AS Layer3 : FileAddressSpace (/home/bb/Documents/RCE-InfoSec/Flare-on 2019/12 - help/help.dmp)
                      PAE type : No PAE
                           DTB : 0x187000L
                          KDBG : 0xf80002c390a0L
          Number of Processors : 1
     Image Type (Service Pack) : 1
                KPCR for CPU 0 : 0xfffff80002c3ad00L
             KUSER_SHARED_DATA : 0xfffff78000000000L
           Image date and time : 2019-08-02 14:38:33 UTC+0000
     Image local date and time : 2019-08-02 10:38:33 -0400

Preliminary Analysis

Volatility supports a whole slew of different analysis commands. For brevity, we will only be mentioning the relevant commands but the reader is encouraged to try them all. For each of the commands we need to specify the profile using the --profile=Win7SP1x64 flag.

Screenshot command

This command takes a screenshot from each desktop on the system. Running the command generates several screenshots one of which looks interesting.

Figure 1: An interesting screenshot

As shown in Figure 1, there is Google Chrome and KeyPass running. A database named keys.kdb is currently open in the KeyPass instance.

Modules command

This command displays the list of kernel modules that were loaded in the system at the time the memory dump was captured.

$ vol.py -f help.dmp --profile=Win7SP1x64 modules

Offset(V)          Name                 Base                             Size File
------------------ -------------------- ------------------ ------------------ ----
0xfffffa800183e890 ntoskrnl.exe         0xfffff80002a49000           0x5e7000 \SystemRoot\system32\ntoskrnl.exe
0xfffffa800183e7a0 hal.dll              0xfffff80002a00000            0x49000 \SystemRoot\system32\hal.dll

-- snip--

0xfffffa800428ff30 man.sys              0xfffff880033bc000             0xf000 \??\C:\Users\FLARE ON 2019\Desktop\man.sys

There's a driver named man.sys which was loaded from the path C:\Users\FLARE ON 2019\Desktop\man.sys. Since system drivers do not usually reside on the Desktop we can be pretty sure that this is related to the challenge.

Dumping man.sys

man.sys is loaded at the address 0xfffff880033bc000. We can either use Volatility or WinDbg for dumping the module. Here I have used WinDbg as I found it to work better than Volatility as far as memory dumping is concerned. Ensure that the path to the symbol server is properly set in WinDbg.

Figure 2: Missing MZ header

We can use the db command to hex dump a memory region. However as shown in Figure 2, the MZ header is missing from man.sys which indicate that the corresponding page must have been paged out from memory. Regardless, we can still use the .writemem command to dump a considerable chunk of memory (say 100KB) and load it as a binary file in IDA.

Figure 3: Dumping man.sys

The dumped file is of size 60 KB. Searching for strings we locate the path to the PDB file embedded in the PE.

Figure 4: Embedded PDB file path

Dumping other drivers and DLLs

Grepping for the strings "flareon_2019" and "pdb" we can find other relevant files that are related to the challenge as shown in Figure 5.

Figure 5: DLLs and drivers related to the challenge

List of drivers

  • stmedit
  • shellcodedriver
  • man

List of DLLs

  • cd
  • cryptodll
  • filedll
  • keylogdll
  • networkdll
  • screenshotdll

Among the drivers we already have man.sys. The other two - shellcodedriver and stmedit can be located in memory using the yarascan volatility command as shown in Figure 6.

Figure 6: Using yarascan

stmedit is located within memory of process svchost.exe with pid 876. Using the !process windbg extension, svchost.exe with pid 876 has its EPROCESS at fffffa80034a4b30

Figure 7: Finding EPROCESS of svchost

Knowing the address of EPROCESS we can set the process context using the .process command.

Figure 8: Setting the process context

Navigating to b260c9 we can cross check that it indeed contains the stmedit string as found in Volatility using yarascan.

Figure 9: Cross checking the output of yarascan

From here we can search backwards to locate the start of the MZ header as shown in Figure 10.

Figure 10: Searching backwards for MZ header

Next, we can use the .writemem command to dump stmedit.sys in the same way as we did earlier.

Figure 11: Dumping stmedit

Apart from shellcodedriver (it has the MZ header missing), the same technique can be reused to dump each of the 6 DLLs. (The complete shellcodedriver file will later be found in the pcap).

Analyzing the DLLs & drivers

Before we look at the DLLs separately there are several techniques that are common across all the DLLs and drivers.

None of the DLLs import WinAPI functions statically using the IAT. Instead functions are resolved dynamically using LoadLibrary or by parsing PEB_LDR_DATA. For obtaining the addresses of the API, either GetProcessAddress is used or in some cases the export table of the module is parsed. Further to harden analysis, the names of the functions are encrypted and are only decrypted at run-time before it's about to be called.

For example in Figure 12, the rc4 function decrypts the stringCreateFileA. It is worth noting that each such string is encrypted with a different key.

Figure 12: Decrypting API names at runtime

The rc4 function takes four parameters -

  • A pointer to the key
  • Size of the key in bytes
  • A pointer to the encrypted buffer. After the function returns this will hold the decrypted contents
  • Size of the encrypted buffer

In Figure 12, the key and encrypted buffer are {0x91, 0xe8, 0xa5, 0x7d} and {0xbd, 0x64, 0x20, 0x46, 0xad, 0xad, 0xe8, 0x7a, 0x39, 0x7c, 0x26} respectively. This can be decrypted using the PyCryptodome Python library.

>>> from Crypto.Cipher import ARC4
>>> key = ''.join(map(chr, [0x91, 0xe8, 0xa5, 0x7d]))
>>> ct = ''.join(map(chr, [0xbd, 0x64, 0x20, 0x46, 0xad, 0xad, 0xe8, 0x7a, 0x39, 0x7c, 0x26]))
>>> 
>>> ARC4.new(key*2).decrypt(ct)
'CreateFileA'

Lastly another technique common across all the DLLs is the use of a function dispatcher to make the WinAPI function call. The dispatcher takes a variable number of arguments depending on the WinAPI function it wants to call. Let's look at two examples to make it clear.

Figure 13: Calling socket

In Figure 13, the method call_function is the function dispatcher we are talking about. Here it wants to call socket which is exported from Ws2_32.dll. The first parameter is a handle to the module containing the function; the second parameter is a pointer to a buffer containing the name of the function. The name of the function is decrypted at runtime as we saw just a while ago. The third parameter indicates the number of arguments that the function requires which is 3 as socket takes three arguments. After the third comes the actual arguments to the function.

Figure 14: Calling htons

Let's consider another example as in Figure 14. The htons function exported from Ws2_32.dll takes a single parameter. Correspondingly the third parameter passed to call_function is 1. After that we have the actual argument to htons - the binding port number.

cd.dll

  • Exports a function named c
  • Sets up a listener on port 4444 and spawns a thread for each incoming connection
  • Reads 4 bytes from the socket. This indicates the size of the payload about to follow
  • The next 4 bytes are some sort of code (Figure 15) based on which it sends a IOCTL to a driver.
Figure 15: Various IOCTL codes
  • The driver to which it sends the IOCTLs is named FLID (Figure 16).
Figure 16: Driver has the name FLID

cryptodll

  • Exports a function named e
  • This function takes in a single parameter - a pointer to a structure of  the following form
struct Buffer_info
{
    QWORD src_buffer_size;
    LPBYTE src_buffer;
    QWORD dst_buffer_size;
    LPBYTE dst_buffer;
}
  • The function compresses (LZNT1) and encrypts (RC4) src_buffer to dst_buffer
  • For Compression it uses the NT function RtlCompressBuffer
  • The key used for encryption is the current username obtained from GetUserNameA

filedll

  • Exports a function named i
  • Contains functionality to  create, read, write and search for a file as shown in Figure 17.
Figure 17: Functionality in filedll

keylogdll

  • Exports a function named l
  • As its name suggests, the dll implements key logging functionality

networkdll

  • Exports a function named s
  • Contains functionality to send data to the host 192.168.1.243 at a configurable port as shown in Figure 18.
Figure 18: Networkdll can send a piece of data to 192.168.1.243

screenshotdll

  • Exports a function named t
  • Contains functionality to capture a bitmap screenshot of the desktop

shellcodedriver

  • This is a 32-bit driver whose sole purpose is to execute a piece of shellcode in kernel space

stmedit

Figure 19: Hardcoded XOR key

man

  • This driver handles IOCTLs from cd.dll as in Figure 20.
  • Compete understanding of this driver is not necessary to complete the challenge
Figure 20: man.sys handles IOCTLs from cd.dll

Decrypting the pcap traffic

Figure 21: NetworkMiner

NetworkMiner is a great tool to get a quick summary of a packet capture. Using the tool we can see there are too many hosts involved. However not all of the traffic in the pcap is relevant to this challenge. By analyzing networkdll we already know that traffic to host 192.168.1.243 is related to this challenge. From cd.dll we also know any traffic to port 4444 is also relevant. All in all the following TCP streams in the pcap are important.

  • Traffic to host 192.168.1.243 on ports 6666, 7777, 8888
  • Traffic to host 192.168.1.244 on port 4444

To extract the TCP streams from the pcap we can use tcpflow which automatically groups them by host and port. We get 285 flows in total out of which we only need to consider the relevant traffic as just discussed.

Decrypting traffic to 192.168.1.244:4444

This traffic is just XOR encryted with the 8 byte key (5d f3 4a 48 48 48 dd 23) which we found earlier. There are 20 such TCP streams. After decrypting, one stream by virtue of its large size (4 KiB) stands out. This stream contains the complete shellcodedriver at offset 12 as shown in Figure 22.

Figure 22: shellcode driver

Decrypting traffic to 192.168.1.243:6666

There are 2 such streams of sizes 50 bytes and 4.53 KiB respectively. First we need to figure out the 8 byte XOR key. Lets have a look at the 50 bytes sized stream as in Figure 23.

Figure 23: Stream of size 50 bytes to 192.168.1.243:6666

The first 4 bytes (encrypted) are cc 69 94 fa. We can assume that these bytes indicates the length of the stream, i.e. the data about to follow. This is because as otherwise the receiving side will have no way to know how many bytes to recv. The size of the stream is 50 (0xcc) which takes up 1 bytes of space. If the size is indicated by 4 bytes, the other three bytes will be zero. Xoring with zero has no effect which indirectly means the last three bytes will reveal a part of the key.

Indeed, if we do a yarascan for the bytes 69 94 fa in the svchost.exe process, we get exactly 1 hit which is the XOR key (d5 69 94 fa 25 ec df da) as shown in Figure 24.

Figure 24: XOR key for port 6666

The traffic here is doubly encrypted. After XOR decrypting, we need to RC4 decrypt followed by LZNT1 decompression. The key for RC4 is the username obtained from GetUserNameA. The username is FLARE ON 2019 which we can obtain from hashdump. We need to append a null byte to the username as per the docs.

Figure 25: Obtaining the username

The following Python script RC4 decrypts with the username followed by LZNT1 decompression.

from Crypto.Cipher import ARC4
import ctypes
import sys

def decompress(data):
	ntdll = ctypes.windll.ntdll
	RtlDecompressBuffer = ntdll.RtlDecompressBuffer

	COMPRESSION_FORMAT_LZNT1 = 0x2
	COMPRESSION_ENGINE_MAXIMUM = 256
	STATUS_SUCCESS  = 0

	RtlDecompressBuffer.argtypes = [
	            ctypes.c_ushort, # USHORT CompressionFormat
	            ctypes.c_void_p, # PUCHAR UncompressedBuffer
	            ctypes.c_ulong,  # ULONG  UncompressedBufferSize
	            ctypes.c_void_p, # PUCHAR CompressedBuffer
	            ctypes.c_ulong,  # ULONG  CompressedBufferSize
	            ctypes.c_void_p, # PULONG FinalUncompressedSize
	]

	RtlDecompressBuffer.restype = ctypes.c_uint	
	finaluncompsize = ctypes.c_ulong(0)
	comp_buffer = ctypes.create_string_buffer(data)
	uncomp_buffer = ctypes.create_string_buffer(len(comp_buffer)*1)

	res = RtlDecompressBuffer(
		COMPRESSION_FORMAT_LZNT1, 
		ctypes.byref(uncomp_buffer), 
		ctypes.c_ulong(len(uncomp_buffer)), 
		ctypes.byref(comp_buffer), 
		ctypes.c_ulong(len(comp_buffer)), 
		ctypes.byref(finaluncompsize)
	)
	print res
	if res == 0:
		return uncomp_buffer[0:finaluncompsize.value]
	else:
		return None

def decrypt(ct, key):
	arc4 = ARC4.new(key)
	pt = arc4.decrypt(ct)
	return pt

def decrypt_and_decompress(data, key):
	decry = decrypt(data, key)
	return decompress(decry)

key = 'FLARE ON 2019' + '\x00'
ct = open(sys.argv[1], 'rb).read()
pt = decrypt_and_decompress(ct, key+'\x00')
open('output.bin', 'wb').write(pt)

After double decryption, one of the files contain the text "C:\keypass\key.kdb"

Figure 26: Path to keys.kdb

Decrypting traffic to 192.168.1.243:7777

There are 12 such streams. The 8 byte XOR key can similarly be found as we did for 6666. It is 4a 1f 4b 1c b0 d8 25 c7. It's also doubly encrypted. After decrypting we get bitmap files some of which are shown in Figure 27 and Figure 28.

Figure 27: Four decrypted bitmaps shown tiled
Figure 28: Four decrypted bitmaps shown tiled

Looking at the images we come to know about our objective. Our much coveted flag is in a KeePass database named keys.kdb. The password of the KeyPass database is of size 18 characters.

Decrypting traffic to 192.168.1.243:8888

There are 5 streams. The XOR key is f7 8f 78 48 47 1a 44 9c. It's also doubly encrypted. The traffic consists of keylogger captured data as we can see in Figure 29.

Figure 29: The keylogger captured some keystrokes wrong

Also note that the keylogger captured some keystrokes wrong. For instance from Figure 26, the user typed nslookup some_blog.com whereas the keylogger captured nslookup soeblogcom.

In another file we find the string "th1sisth33nd111" which looks to be the password for keys.kdb

Figure 30: Likely password of the keypass database

Retrieving the KDB

A KDB 1.x file begins with the bytes 03 D9 A2 9A 65 FB 4B B5. Searching for these bytes in the dump we can locate the kdb file.

Figure 31: Locating the KDB file

Unfortunately, using the password "th1sisth33nd111" fails to open the kdb. This is possible as the keylogger is buggy and doesn't capture all key strokes correctly. Also from the screenshots we know the length of the password is 18 whereas the keylogg'd password is of size 15.

Figuring out the correct password

Searching for "th3" we locate a 16 character string which looks to be a part of the correct password.

Figure 32: A part of the correct password

The correct password to the keypass database can be thus inferred and is "Th!s_iS_th3_3Nd!!!". Opening the kdb file we finally have the much coveted flag.

Figure 33: The flag

Flag: f0ll0w_th3_br34dcrumbs@flare-on.com

Final Words

With this we come to end of the Flare-on 2019 CTF write-up series. Barring the final two, the challenges this year were slightly easier that the last year. Hope you liked the write-ups. If you have any queries or suggestions, feel free to leave a comment below.