Post

HTB: Lovely Malware

HTB: Lovely Malware

Lovely Malware is an insane difficulty malware RE sherlock, which includes a PE file, pcap file, and an encrypted .txt.naso file. I’ll start with covering what I did to decrypt config data and resolve windows API hashes, as these were an easy first place to start, and curcial to figuring out where to look. I’ll then go into how the key is generated, how files are encrypted, and how the key is transformed before being sent back to the TA with the victim host information. From the knowledge gained, if provided with a capure file that contains the network traffic from the sample, one would be able to decrypt their files.

Scenario Description: An employee at NeirCyber Security discovered a suspicious file named employee_benefits.exe on their desktop. The employee found the file after returning from lunch and immediately reported it to the IT security team, suspecting that it could be malicious. The objective is to reverse engineer the file and dissect its inner workings.

https://labs.hackthebox.com/achievement/sherlock/1229827/762

Initial Analysis

update.exe SHA256 (Task 1): 83cb5e8b7455fcb3b6c2d45269b08b3ae003dfed4ce8ca942cd007c1ebf17cf2

I started off by looking around in some online sandbox reports, then launching the sample in a VM with procmon, regshot etc. When executing the binary from the terminal, my VM shut down, and when double clicking the exe (launching under explorer.exe) it ran without issue (indication that it is probably checking for explorer as a parent process). Files in the VM got encrypted with the .naso extension, and a ransom note poped up in notepad that contained the address for sending the TA bitcoin (only 2k, what a deal).

From simple dynamic analysis and sandboxes we can see the mutex name that’s used, as well as the commands utilizing vssadmin and bcdedit (this used to be common with ransomware for deleting/disabling OS recovery features). Heres an Elastic rule for detection of utilies used to tamper with system recovery settings like whats used in this sample. Some ways of avoiding command line detections that ransomware groups are using currently include using COM providers, reimplement the actual IOCTL’s used by the providers/utilites, or rebooting into safe mode.

Task 2: Global\On3_S1d3d_hard Task 7: exe Task 9: cmd.exe /c vssadmin.exe Delete Shadows /All /Quiet & bcdedit /set {default} recoveryenabled No & bcdedit /set {default} bootstatuspolicy ignoreallfailures Task 12: .naso Task 13: bc1qntgwduujdjnfv6txwxugdsx6nw5mfhgqa4nn82

Next I took a look through the cap.pcapng file and the only packet of any value that I found was 14696, which contains appears to be a unique victim ID, host information, and the key used for encryption of the victim’s files, getting sent out to the TA.

Task 11: 192.168.1.104:8000

Decrypting Config Strings

This sample is pretty easy to analyse statically once you mark up the Win API hashes and the config information. Luckly the hash resolving function and the config can be found very early on in the process. You can easily spot a string getting dynamically constructed in a user TLS callback, with the string coming out to equal Love_Her. Each character of the string is found in a loop while being put together, and after that a series of moves are made on elements in the .data section to another area of .data.

Cross referencing the location of where these unknown data elements get passed reveals a large amount of calls to what turns out to be a simple XOR encryption function, which contains the Love_Her string as the key. Reimplementing this function in python and running these elements through reveals config data. I made a script for Binary Ninja to resolve all of these at once, by iterating the instructions in the callback, finding where the strings were getting moved from, reading them in, decrypting and setting comments.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from binaryninja import *

def love_her_sd(idata: bytes) -> bytes:
	key = b"Love_Her"
	odata = bytearray(idata)
	for i in range(len(idata)):
		odata[i] = (odata[i] - i) & 0xFF
		odata[i] ^= key[i % 8]
	return bytes(odata)

tls_callback = bv.get_function_at(0x7ff636eb24a0) # Edit me
instructions = list(tls_callback.instructions)

resolved_strs = []
for i, (instr_parts, addr) in enumerate(instructions):
	instr_text = ''.join(token.text for token in instr_parts).strip()
	if "lea" in instr_text and "rdx" in instr_text:
		enc_str_addr = instr_text.split("[rel")[-1].split("]")[0].split()
		enc_str_addr = enc_str_addr[0]
		enc_str_addr = int(enc_str_addr, 16)

		size = 0
		max_read = 1024
		for offset in range(max_read):
			b = bv.read(enc_str_addr + offset, 1)
			if not b or b == b'\x00':
				break
			size += 1

		enc_str = bv.read(enc_str_addr, size)
		dec_str = love_her_sd(enc_str)
		resolved_strs.append((hex(addr), dec_str))
		tls_callback.set_comment_at(addr, f"Decrypted to: {dec_str}")

print(">>> Resolved strings <<<")
for addr, dec_str in resolved_strs:
	print(f"{addr}: {dec_str}")

From the output we now see vssadmin/bcdedit command strings, the format for the viticim information packet, the mutex, an IP, etc. The ransome note strings get decryped in the final routine by a different function, and there are a few other strings that are not setup here that also get decrypted with the same routine when needed. All strings are decrypted right before being used.

Task 3: Love_Her Task 10: {‘Name’ : %s , ‘Computer’ : %s , ‘Proc’ : %s , ‘Key’ : %s , ‘Id’ : %s}

Resolving Hashes

Now starting to look at the main routine, checking inside the first call we can see our string decryption function resolve the mutex name, then pass it to the next function. This function appears to be a wrapper for CreateMutexA. Inside the wrapper is a function that takes a hardcoded hash value and the base of kernel32, matches the hash with the correct export and returns the adderss to the desired API function.

Based on the DLL names from the derypted strings, I used the hash function to generate a dictionary of the export_name:hash_value for every export in these libraries.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import json
import pefile

def compute_hash(func):
	hash = 0
	for char in func:
		hash = (hash + ord(char) + ((hash >> 13) | (hash << 19))) & 0xFFFFFFFF
	return hash

def list_exports(dll_path):
	pe = pefile.PE(dll_path)
	if hasattr(pe, 'DIRECTORY_ENTRY_EXPORT'):
		exports = [entry.name.decode() for entry in \ 
		pe.DIRECTORY_ENTRY_EXPORT.symbols if entry.name]
		return exports
	else:
		return []
  
def create_hash_dict(dll_paths):
	hash_dict = {}
	for path in dll_paths:
		exports = list_exports(path)
		for name in exports:
			h = compute_hash(name)
			hash_dict[name] = hex(h)
	return hash_dict

dll_paths = [
	"C:\\Windows\\System32\\kernel32.dll",
	"C:\\Windows\\System32\\user32.dll",
	"C:\\Windows\\System32\\advapi32.dll",
	"C:\\Windows\\System32\\ntdll.dll",
	"C:\\Windows\\System32\\shlwapi.dll",
	"C:\\Windows\\System32\\crypt32.dll",
	"C:\\Windows\\System32\\ws2_32.dll",
	"C:\\Windows\\System32\\shell32.dll"
]

hash_dict = create_hash_dict(dll_paths)
with open("dict.json", 'w') as f:
	json.dump(hash_dict, f, indent=4)

Next I made a script for Binary Ninja to get the hardcoded hash values from every cross reference from the resolver function and set a comment above the value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import json
from binaryninja import *

# Change path
with open("/home/rb3nzr/Desktop/dict.json", "r") as f:
	hash_dict = json.load(f)

# Change address
resolver = bv.get_function_at(0x7ff7b3e33ec0)
for xref in resolver.callers:
	for block in xref.low_level_il:
		for instr in block:
			if instr.operation == LowLevelILOperation.LLIL_CALL:
			  call_target = instr.dest
				if call_target.operation == LowLevelILOperation.LLIL_CONST_PTR and call_target.constant == resolver.start:
				  arg_instrs = [xref.get_instruction_containing_address(instr.address - offset) for offset in range(1, 10)]

				  for arg_instr in arg_instrs:
				    if arg_instr:
				      disasm = bv.get_disassembly(arg_instr)
				      if "mov" in disasm and "edx," in disasm:
				        hash_val = int(disasm.split(",")[-1].strip(), 16)
				        hash_val = hex(hash_val)

				        for name, hv in hash_dict.items():
				        if hv == hash_val:
				          print(f"Resolved {hash_val} to: {name} at: 0x{instr.address:08X}")
				          xref.set_comment_at(instr.address, f"Resolves: {name}")

Now that all the Win API functions can be seen, and the function wrappers can all be labled, it’s much easier to see what the sample is doing.

What Launched Me

As mentioned in initial analysis, when launching this sample from anything other than explorer my VM shutdown. Shortly after the mutex check, the sample runs a check on the parent process by getting the current process’s executable path with GetModuleFileNameExA, the path of explorer.exe and checking if the current exeutable path contains explorer.

If explorer is not in that path then it will attempt to obtain SeShutDownPrivilege and call ExitWindowsEx with EWX_SHUTDOWN | EWX_FORCE flags.

If trying to debug this: once the function that checks the executable path with GetModuleFileNameExA returns, the return value can be changed, or the zero flag (ZF) bit can be flipped on the test instruction right after, in order to take the jump (if doing this then you also want to take the jump right after).

Task 5: ExitWindows

Main Encryption Routine

After using cmd.exe to run the vssadmin/bcdedit commands to delete shadowcopies and prevent recovery, there is a function that starts the main crypto/file operations. It starts by calling CryptAquireContextA (with PROV_RSA_FULL and CRYPT_VERIFYCONTEXT) and CryptGenRandom to generate a 256-bit AES key. Then it resolves extensions that it wants to encrypt, the custom .naso extension to append to encrypted files, and enumerates the SYSTEM_INFO struct for CPU information to determine thread count before creating new threads, each starting a crypto/file operation routine. Operations happen to only to files under the Users directory and subdirectories.

Depending on the size of the file, one of two encryption routines is picked, determined if the file is over or under 0x10000 bytes. The smaller file routine (as this applies to our file for the challenge) maps the file to memory with CreateFileMappingW and MapViewOfFile, generates an IV and combines it with the AES key, prepends a 17-byte header to the top of the encrypted data (where 1 byte is added to the top of the 16-byte IV), uses AES in CBC mode to encrypt the data after the header, then writes header + data back to the file.

The other routine for larger files is similar, yet processes in chunks. Where the first chunk does the same prepending of the 17-byte header contianing an extra byte and the IV, and only encrypting the first 65KB, the remaining 17-bytes of the previous ciphertext are used as the new IV, and a 17-byte footer is added for padding on the remaining <64KB bytes.

Task 6: Users Task 7: .exe Task 8: AES-256

Decrypting Files

After the file encryption routines end, the AES key bytes are transformed with a single XOR operation against 0xbb, then base64 encoded with CryptBinaryToString, before getting added to the node that includes victim/host information.

This is now all the information we need to decrypt the provided attachement.txt.naso, as we know the IV is located after the first byte of the file and how the key was transformed. The script below will take the key found in the cap.pcapng file and print the contents of the decrypted attachment with it’s SHA256 hash.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import hashlib
from base64 import b64decode
from Crypto.Cipher import AES

def xor_key(key) -> bytes:
	key = b64decode(key.encode())
	dec = bytes([b ^ 0xBB for b in key])
	return dec

enc_aes_key = "ZcxYj5wFbcbbd4/mDhQRSQMfcqoFsRywp9+qPVeUUwA="
aes_key = xor_key(enc_aes_key)

with open("Attachement.txt.naso", "rb") as f:
	data = f.read()
	
iv = data[1:17]
data = data[17:]
cipher = AES.new(aes_key, AES.MODE_CBC, iv)
data = cipher.decrypt(data)
data = data.rstrip(b'\x00')
sha256 = hashlib.sha256(data).hexdigest()
print(f"Contents: {data.decode('utf-8')}\n")
print(f"SHA256: {sha256}")

Task 14: e9348cfdb3dcb8f6ab4b6c439750d8a28e65bb8cd71a22472fd01b783e98a126

In the final function called in main, the sample will use a seperate XOR decryption function for the ransom note, collect system information, construct the ransom note, pop the ransom note in notepad, then resolve and use winsock functions to send the packet with the ID, Key etc..

This post is licensed under CC BY 4.0 by the author.