BlackMatter Ransomware Version 3
Analysis of BlackMatter Ransomware Version 3 and Config Extraction
def unhex(hex_string):
import binascii
if type(hex_string) == str:
return binascii.unhexlify(hex_string.encode('utf-8'))
else:
return binascii.unhexlify(hex_string)
def tohex(data):
import binascii
if type(data) == str:
return binascii.hexlify(data.encode('utf-8'))
else:
return binascii.hexlify(data)
APLib
Credit: Sandor Nemes (snemes)
import struct
from binascii import crc32
from io import BytesIO
__all__ = ['APLib', 'decompress']
__version__ = '0.6'
__author__ = 'Sandor Nemes'
class APLib(object):
__slots__ = 'source', 'destination', 'tag', 'bitcount', 'strict'
def __init__(self, source, strict=True):
self.source = BytesIO(source)
self.destination = bytearray()
self.tag = 0
self.bitcount = 0
self.strict = bool(strict)
def getbit(self):
# check if tag is empty
self.bitcount -= 1
if self.bitcount < 0:
# load next tag
self.tag = ord(self.source.read(1))
self.bitcount = 7
# shift bit out of tag
bit = self.tag >> 7 & 1
self.tag <<= 1
return bit
def getgamma(self):
result = 1
# input gamma2-encoded bits
while True:
result = (result << 1) + self.getbit()
if not self.getbit():
break
return result
def depack(self):
r0 = -1
lwm = 0
done = False
try:
# first byte verbatim
self.destination += self.source.read(1)
# main decompression loop
while not done:
if self.getbit():
if self.getbit():
if self.getbit():
offs = 0
for _ in range(4):
offs = (offs << 1) + self.getbit()
if offs:
self.destination.append(self.destination[-offs])
else:
self.destination.append(0)
lwm = 0
else:
offs = ord(self.source.read(1))
length = 2 + (offs & 1)
offs >>= 1
if offs:
for _ in range(length):
self.destination.append(self.destination[-offs])
else:
done = True
r0 = offs
lwm = 1
else:
offs = self.getgamma()
if lwm == 0 and offs == 2:
offs = r0
length = self.getgamma()
for _ in range(length):
self.destination.append(self.destination[-offs])
else:
if lwm == 0:
offs -= 3
else:
offs -= 2
offs <<= 8
offs += ord(self.source.read(1))
length = self.getgamma()
if offs >= 32000:
length += 1
if offs >= 1280:
length += 1
if offs < 128:
length += 2
for _ in range(length):
self.destination.append(self.destination[-offs])
r0 = offs
lwm = 1
else:
self.destination += self.source.read(1)
lwm = 0
except (TypeError, IndexError):
if self.strict:
raise RuntimeError('aPLib decompression error')
return bytes(self.destination)
def pack(self):
raise NotImplementedError
def aplib_decompress(data, strict=False):
packed_size = None
packed_crc = None
orig_size = None
orig_crc = None
if data.startswith(b'AP32') and len(data) >= 24:
# data has an aPLib header
header_size, packed_size, packed_crc, orig_size, orig_crc = struct.unpack_from('=IIIII', data, 4)
data = data[header_size : header_size + packed_size]
if strict:
if packed_size is not None and packed_size != len(data):
raise RuntimeError('Packed data size is incorrect')
if packed_crc is not None and packed_crc != crc32(data):
raise RuntimeError('Packed data checksum is incorrect')
result = APLib(data, strict=strict).depack()
if strict:
if orig_size is not None and orig_size != len(result):
raise RuntimeError('Unpacked data size is incorrect')
if orig_crc is not None and orig_crc != crc32(result):
raise RuntimeError('Unpacked data checksum is incorrect')
return result
import struct
import pefile
RANSOMWARE_FILE = r'/tmp/blackmatter_v3.bin'
data = open(RANSOMWARE_FILE, 'rb').read()
pe = pefile.PE(data = data)
# Get resource data
r_data = None
for s in pe.sections:
if b'rsrc' in s.Name:
r_data = s.get_data()
print("Resource data length: %s" % hex(len(r_data)))
# Parse data from resource
seed = struct.unpack('<Q',r_data[:8])[0]
data_size = struct.unpack('<I',r_data[8:12])[0]
enc_data = r_data[12:]
print("Seed: %s" % hex(seed))
print("Size: %s" % hex(data_size))
print("Encrypted data example: %s..." % tohex(enc_data[:20]))
Decryption Routine
This PRNG algorithim is injected into memory at runtime -- maybe an attempt to hide the algorithm from statica analysis? Not a very good attempt though....
__int64 __stdcall sub_B45133(_DWORD *a1, _DWORD *a2)
{
__int64 v2; // rax
v2 = ((__int64 (__stdcall *)(_DWORD, _DWORD, int, int))sub_B450FD)(*a2, a2[1], 0x4C957F2D, 0x5851F42D)
+ 0x14057B7EF767814F;
*(_QWORD *)a2 = v2;
return ((__int64 (__stdcall *)(_DWORD, _DWORD, _DWORD, _DWORD))sub_B450FD)(*a1, a1[1], v2, HIDWORD(v2));
}
eax = 0x00a5f770 edx = 0x910fde29
import struct
def gen_key(seed, key_length):
out = b''
tmp_seed = seed
for i in range(key_length):
tmp_seed = (0x5851F42D4C957F2D * tmp_seed + 0x14057B7EF767814F) & 0xFFFFFFFFFFFFFFFF
out += struct.pack('<Q',(tmp_seed * seed)& 0xFFFFFFFFFFFFFFFF)
return out
tmp_seed = seed
tmp_seed = (0x5851F42D4C957F2D * tmp_seed + 0x14057B7EF767814F) & 0xFFFFFFFFFFFFFFFF
print("Test temp seed: %s" % hex(tmp_seed))
print("Test xor key: %s" % hex((tmp_seed * seed)& 0xFFFFFFFFFFFFFFFF))
#E2 02 10 B2 F6 64 C5 B1
print("Real Temp seed: %s\n\n" % hex(struct.unpack('<Q',unhex('E2 02 10 B2 F6 64 C5 B1'.replace(' ','')))[0]))
# 09 21 30 FB 18 EB 52 6A
print("Real Temp seed 2: %s" % hex(struct.unpack('<Q',unhex('09 21 30 FB 18 EB 52 6A'.replace(' ','')))[0]))
# FC9FF30B7AFBF8B7
tmp_seed = (0x5851F42D4C957F2D * tmp_seed + 0x14057B7EF767814F) & 0xFFFFFFFFFFFFFFFF
print("Test temp seed 2: %s" % hex(tmp_seed))
print("Test xor key 2: %s" % hex((tmp_seed * seed)& 0xFFFFFFFFFFFFFFFF))
key_data = gen_key(seed, data_size)
print("key: %s" % tohex(key_data[:32]))
print("data: %s" % tohex(enc_data[:32]))
# buff[0] ^ lo_al = 9e ^ 4f = d1
# buff[1] ^ lo_dh = 4c ^ 4f = 03
# buff[2] ^ lo_ah = bc ^ 8b = 37
# buff[3] ^ lo_dl = f9 ^ 81 = 78
# buff[4] ^ hi_al = fc ^ 61 = 9d
for i in range(0,10,8):
print(hex(enc_data[i] ^ key_data[i]))
print(hex(enc_data[i+1] ^ key_data[i+5]))
print(hex(enc_data[i+2] ^ key_data[i+1]))
print(hex(enc_data[i+3] ^ key_data[i+4]))
print(hex(enc_data[i+4] ^ key_data[i+2]))
print(hex(enc_data[i+5] ^ key_data[i+7]))
print(hex(enc_data[i+6] ^ key_data[i+3]))
print(hex(enc_data[i+7] ^ key_data[i+6]))
print("\n\n")
def decrypt(enc_data, data_size, seed):
out = []
keystream = gen_key(seed, data_size)
for i in range(0,data_size-8,8):
out.append(enc_data[i] ^ key_data[i])
out.append(enc_data[i+1] ^ key_data[i+5])
out.append(enc_data[i+2] ^ key_data[i+1])
out.append(enc_data[i+3] ^ key_data[i+4])
out.append(enc_data[i+4] ^ key_data[i+2])
out.append(enc_data[i+5] ^ key_data[i+7])
out.append(enc_data[i+6] ^ key_data[i+3])
out.append(enc_data[i+7] ^ key_data[i+6])
return bytes(out)
ap_data = decrypt(enc_data, data_size, seed)
ptxt_data = aplib_decompress(ap_data)
ptxt_data
import base64
ptr = 0
rsa_data = ptxt_data[ptr:128]
ptr += 128
affiliate_id_data = ptxt_data[ptr:ptr+32]
ptr+= 32
config_flags = ptxt_data[ptr:ptr+22]
ptr+= 9
config_values_offset = struct.unpack('<I',ptxt_data[ptr:ptr+4])[0]
config_values_buffer = ptxt_data[ptr+config_values_offset:]
config_values = []
for c in config_values_buffer.split(b'\x00'):
config_values.append(base64.b64decode(c))
def is_ascii(s):
return all(c < 128 for c in s)
print("RSA: %r\n" % rsa_data)
print("Affiliate ID: %r\n" % affiliate_id_data)
print("Flags: %s\n" % tohex(config_flags))
for c in config_values:
if not is_ascii(c):
c = new_data = decrypt(c,len(c), seed)
print("%s\n" % b' | '.join([s.replace(b'\x00',b'') for s in c.split(b'\x00\x00')]))