Darkside Ransomware
Analysis of Darkside Ransomware and Config Extraction
- Overview
- Helper Functions
- API Hashing
- API String Decryption
- Config Decryption Functions
- Config Decryption
- Parsing The Config
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)
kernel32_hash = 999818334
string = b'k\x00e\x00r\x00n\x00e\x00l\x003\x002\x00.\x00d\x00l\x00l\x00'
hash_high = 0xffff
hash_low = 0xffff
for ptr in range(len(string)):
hash_low = (hash_low + string[ptr])
hash_high = (hash_high + hash_low)
hash_high %= 0xFFF1
hash_low %= 0xFFF1
hash = (hash_high << 16) + hash_low
print(hex(hash))
print('===')
print(hex(kernel32_hash))
import struct
def gen_key_buffer(buf1, buf2):
key_buffer = [0]*256
v3 = 240
v4 = buf1[:4]
v5 = buf1[4:8]
v6 = buf1[8:12]
result = buf1[12:]
v3 = 240
while v3 >= 0:
for i in range(4):
key_buffer[v3 + i + 12 ] = v4[i]
key_buffer[v3 + i + 8 ] = result[i]
key_buffer[v3 + i + 4 ] = v5[i]
key_buffer[v3 + i] = v6[i]
v4 = struct.pack('<I', (struct.unpack('<I',v4)[0] - 0x10101010) & 0xffffffff);
result = struct.pack('<I', (struct.unpack('<I',result)[0] - 0x10101010) & 0xffffffff);
v5 = struct.pack('<I', (struct.unpack('<I',v5)[0] - 0x10101010) & 0xffffffff);
v6 = struct.pack('<I', (struct.unpack('<I',v6)[0] - 0x10101010) & 0xffffffff);
v3 -= 16
lo_v8 = 0
v9 = 0
v10 = 0
flag_return = False
while True:
if flag_return:
break
while True:
lo_result = key_buffer[v9] & 0xff
lo_v8 = (lo_result + ((buf2[v10] + lo_v8) & 0xff)) & 0xff
hi_result = key_buffer[lo_v8]
v10 += 1
key_buffer[lo_v8] = lo_result
key_buffer[v9] = hi_result
if v10 >= 16:
break
v9 += 1
v9 &= 0xff
if v9 == 0:
flag_return = True
break
v10 = 0
v9 += 1
v9 &= 0xff
if v9 == 0:
break
return key_buffer
def decrypt_data(data, key_buffer):
data = list(data)
data_len = len(data)
key = key_buffer.copy()
edx = 0
cl = 0
curr_index = 0
eax = 0
while data_len != 0:
cl = (key[(1 + edx) & 0xff] + cl) & 0xFF
eax = key[(1 + edx) & 0xff] & 0xFF
ch = key[cl] & 0xFF
key[cl] = eax
key[(1 + edx) & 0xff] = ch
eax = (ch + eax) & 0xFF
edx = (edx + 1) & 0xff
data[curr_index] ^= key[eax]
curr_index += 1
data_len -= 1
return bytes(data)
KEY_BUFFER = gen_key_buffer(unhex('edf9e5ed8640fd53ab185838646bd9df'),unhex('92b2801a9c19867db6a5002936c1084a'))
decrypt_data(unhex('7b0d2ddb284b'),KEY_BUFFER)
Config Decryption Functions
Because the config file is so large it needs it's own custom decryption wrapper to decrypt 256 bytes at a time. The decryption routine also needs to handle 256 blocks of data. The config is also compressed using aplib and the values are base64 encoded.
The following functions will aid in the config decryption.
def decrypt_large_data(data, key_buffer):
out = b''
for ptr in range(0,len(data),255):
out += decrypt_data(data[ptr:ptr+255],key_buffer)
return out
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
Config Decryption
The config is stored in the data
or ndata
section of the PE file. The first two 16-byte blocks are the components of the key buffer. Following the key material is a DWORD that indicates the size of the encrypted config. This is followed by the encrypted config itself.
The actual config decryption process is as follows.
- Find the
data
section - Locate the size of the config at offset 0x20
- Extract the encrypted config
- Decrypt config using the custom decryption algorithm
- Decompress the resulting data using aplib
import pefile
import struct
RANSOMWARE_FILE = r'/tmp/darkside.bin'
data = open(RANSOMWARE_FILE, 'rb').read()
pe = pefile.PE(data=data)
# Get section with config
section_data = None
for s in pe.sections:
if b'ndata' in s.Name:
section_data = s.get_data()
break
# Extract config
config_length = struct.unpack('<I',section_data[0x20:0x24])[0]
enc_data = section_data[0x24:0x24+config_length]
# Decrypt config
ap_data = decrypt_large_data(enc_data, KEY_BUFFER)
# Decompress config data with aplib
ptxt_data = aplib_decompress(ap_data)
ptxt_data
Parsing The Config
The first 128
bytes of the config are the RSA exponent followed by the 128
bytes RSA modulus.
After the modulus is a 32
buffer containing a null terminated ascii string representing the affiliate ID followed by some random data.
Next is a 22
bytes buffer containing a series of binary configuration flags.
Next is a DWORD
indicating where the start of the next configuration value.
The following config values are base64 encoded and seperated by null bytes.
import base64
ptr = 0
rsa_exponent = ptxt_data[ptr:128]
ptr += 128
rsa_mod = ptxt_data[ptr:ptr+128]
ptr += 128
affiliate_id_data = ptxt_data[ptr:ptr+32]
affiliate_id = affiliate_id_data.split(b'\x00')[0]
ptr+= 32
config_flags = ptxt_data[ptr:ptr+22]
ptr+= 22
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).split(b'\x00\x00'))
print("Affiliate ID: %s\n" % affiliate_id)
for c in config_values:
print("%s\n" % b' | '.join([s.replace(b'\x00',b'') for s in c]))