BlackMatter Ransomware ESXi ELF Config
Extracting the config from Blackmatter ESXi ELF Encryptors
- Overview
- Examining our ELF file
- Base64 Decoding
- Zlib decompression
- Custom 32 Byte Rolling XOR Algorithm
from __future__ import print_function
import sys
sys.path[0:0] = ['.', '..']
from elftools.elf.elffile import ELFFile
def process_file(filename):
print('Sections in ELF file:', filename)
with open(filename, 'rb') as f:
elffile = ELFFile(f)
for section in elffile.iter_sections():
print(' ' + section.name)
process_file("tmp/blackmatter_elf.inactive")
We can see that our ELF payload has a section named .cfgETD This is where the config data resides in current BlackMatter ELF payloads
def get_config_section_data(filename):
with open(filename, 'rb') as f:
elffile = ELFFile(f)
for section in elffile.iter_sections():
if section.name.startswith('.cfgETD'):
return section.data()
config_data = get_config_section_data("tmp/blackmatter_elf.inactive")
config_string = config_data.decode("utf-8") #we do not add the 'validate=True' parameter
print(config_string) #if this does not print out a clear base64 string, something has gone wrong
import base64
config_b64_decoded = base64.b64decode(config_string) #now we decode our base64 string into a bytearray
print(config_b64_decoded)
Throwing our bytearray into cyberchef, any kind of 'magic' tool, will tell us that our data is compressed by zlib's 'deflate' algorithm.
We can gleem this from reverse engineering the payload in a dissasembler to spot a single call to the compress function
We need to decompress this data with the zlib 'inflate' algorithm before we continue
import zlib
#as functions in code, zlib's deflate and inflate algorithms are referenced as compress and decompress
decompressed = zlib.decompress(config_b64_decoded) #a simple decompress function will uncompress our data
print(decompressed)
Reverse engineering our algorithm
We can see from our uncompressed data, that the beginning stands out as something 'key' like. However, if we try and use this as an xor key, we see that it only decodes the first 32 bytes succesfully, and is garbled after. From this, we know something more complicated is going on, but we know it must involve xor, and our key is definitely a key.
In a decompiled pseudocode view of our ELF payload, we can track the output from the base64 decoding, to the uncompress() call, and then onwards through some checks, before arriving at a 'do' statement as displayed below:
This loop conveniently leads into a string constructor, which is a pretty big hint that this is the final stage of decoding before our config string is constructed.
We can follow the outputs of our uncompress() function and mark up our variables to get to something like the following:
Recreating our algorithm
We can see that the algorithm functions similar to something called a rolling xor key encoding, or "multibyte XOR key" encoding, which is where byte 1 is XORd with the first byte of the key, byte 2 with the 2nd, byte 3 with the 3rd, and so on. This repeats until we get to the end of our key (_see if keyposition == 32) and then we loop back to the first byte of our key again (so the 33rd byte of the data is XORd with the 1st byte of the key)
However, two if statements throw in some additional complexities:
- If the encoded config byte is zero, do not encode it and move on
- If the encoded config byte is the same as whichever byte of the key we are on, do not encode it and move on
This is what was causing our problems when trying to perform a simple XOR, and is the reason why our key only worked up until a certain point.
We recreated the algorithm in python with some slight reordering and modification
When recreating the exact steps of the algorithm from our decompiler, we see that the output is a clean json file, with all of our blackmatter config values
data = bytearray(decompressed)
key = data[0:32]
data_position = 32
key_position = 0
#print(data[0:32].decode("utf-8","ignore"))
output = bytearray()
while data_position != len(data):
#print("Data position at " + str(data_position) + " and key position at " + str(key_position))
#print(str(data[data_position]) + str(key[key_position]))
if data[data_position] != 0:
if data[data_position] != key[key_position]:
data[data_position] = data[data_position] ^ key[key_position]
key_position += 1
if key_position == 32:
key_position = 0
data_position += 1
print(data.decode("utf-8"))