Writeup: FlareOn 2021: 007 - spel

1. TLDR

spel graph

2. Input data

The challenge file is here. Hasło: flare.

The available file is spel.exe

3. Initial analysis

After launching, I saw an error message:

Spel Window

Is there something wrong with the application?

I thought there was something wrong with the setup of my environment. Maybe I had a problem with the sound? No access to some device? The wrong version of the system? While debugging the application, I found that the process continued to run after closing the window, hmmmm…..

In the disassembled code, I saw a lot of calls from the CWinApp class indicating that the application code was written using the Microsoft Foundation Class (MFC) library. Below is an example of the list of functions from this library used in the spel.exe application:

CWinApp

I looked through the list of imports for calls:

CreateThread
CreateProcessInternalW
VirtualAlloc
VirtualProtect

While doing so, I noticed an unusual (at least in my opinion) function VirutalAllocExNuma

An attempt to locate the code snippet calling this function (in the code graph view) failed:

VirutalAllocExNumaXRef

Locating the VirutalAllocExNuma function in the listing view revealed an interesting piece of code. My attention was drawn to the long string of mov commands arranging data in memory, which was completed by arranging the “flare” letter, followed by memory allocation, its copying and execution of the data as code:

ShellCode-Pre

4. Detailed analysis

Using x64dbg, I executed the code to the breakpoint set to VirutalAllocExNuma, which allocated the address 0x260000, and after calling the memmove function, I saw a potential shellcode at this address. It was probably fine, the first command was call (E8):

ShellCode-x64

Using Process Hacker, I dumped the entire memory area:

ShellCode-x64

In order to limit the size of the dump to the part that had any relevance to the shellcode, I opened a memory dump in HxD. I quickly noticed that there was a PE file right next to the shellcode, which I extracted, and without analyzing the shellcode I assumed it was loaded by it.

Stage-02-PE

The loaded file turned out to be a DLL file:

Stage-02-dll

After loading the dll into the IDA tool, I noticed two interesting things:

  1. the DLL had no exports except DllEntryPoint.
  2. the DLL function used a very long array of bytes, which it passed to a function I didn’t know. A very long array that was another PE file.

Stage-03-PE

So I extracted a nested file:

> emit spel-stage2.dll | carve-pe > spel-stage3.dll

Another loaded file turned out to be a DLL file:

Stage-03-dll

Static analysis of the code led me to several observations:

  1. Functions are dynamically imported
  2. The names of imported functions and libraries are obfuscated
  3. All strings are obfuscated: encrypted with xor, the key of which was each time found several instructions after the ciphertext was loaded into memory. The figure below shows the loading of the ciphertext and decryption, respectively:

String-xor

5. Deobfuscation

The names of libraries were mapped from integers, constructing an enumeration type made it easier to analyze such a method of code obfuscation:

Library-enum

The obfuscation of the loaded function names itself was not a problem, thanks to the use of HashDb and IDA environment plugin database

I solved the string obfuscation with the following script:

import idautils
import idc
import ida_ua
import ida_bytes
import idaapi
import struct
import malduck

def get_ciphertext(ea):
    input = list()
    for x in range(0,15):
        if ida_ua.ua_mnem(ea) == 'mov' and idc.get_numbered_type_name(idc.get_operand_type(ea, 1))=='DWORD':
            value = idc.get_operand_value(ea, 1)
            if value!=0:
                value = struct.pack("<I", value)
                input.append(value)
        else:
            break
        ea = ida_bytes.next_head(ea, ida_ida.cvar.inf.max_ea)
    return (ea,bytes().join(input))
    

def get_key(ea):                 
    while ida_ua.ua_mnem(ea) != 'xor' or idc.get_numbered_type_name(idc.get_operand_type(ea, 1))!='DWORD':
        ea = ida_bytes.next_head(ea, ida_ida.cvar.inf.max_ea)
    key = idc.get_operand_value(ea, 1)
    key = struct.pack("<I", key)
    return (ea,bytes(key))
    

ea = idc.here()
ea, ciphertext = get_ciphertext(ea)
ea, key = get_key(ea)

plaintext = malduck.xor(key, ciphertext)
plaintext_utf8 = plaintext.decode("UTF-8")
plaintext_utf16 = plaintext.decode("UTF-16")

print(f"key:{key}")
print(f"ciphertext:{ciphertext}")
print(f"plaintext_utf8:{plaintext_utf8}")
print(f"plaintext UTF-16:{plaintext_utf16}")

Further analysis of the program’s code made it possible to notice that the program used the bcrypt library and its provided implementation of the AES-128 algorithm. Thus, it loaded and decrypted a fragment of one of the resources with id 128:

findresource

6. Payload analysis

I recovered the contents of this resource with the CFF Explorer tool, the actual ciphertext is highlighted in red:

resource-128

The key used was one of the strings:

string-key

IV was a string: 80808080808080808080808080808080

So I decrypted the ciphertext using Binary Refinery

$ emit 80808080808080808080808080808080D7FB7E628DAB8765CD7185CE530F5A8C2D8A4537124B791D40DA768626D3D372 | hex | aes --mode cbc -i cut::16 d41d8cd98f00b204e9800998ecf8427e

l3rlcps_7r_vb33eehskc3

After decryption, the string was blacked out again with xorem and the order of the characters was changed:

plaintext2reg

7. Reading the flag

It was enough to ignore the stream cipher and read the correct order of characters:

p = "l3rlcps_7r_vb33eehskc3"
indexes = [12,13,6,8,7,6,5,1,0,3,4,17,15,20,19,21,2,10,16,11,14,2]
t=[]
for i in indexes:
	t.append(p[i])

t = ''.join(t)
print(t)

The flag:

b3s7_sp3llcheck3r_ev3r@flare-on.com