Diceloader Triage Notes
Researching malware downloaders, detection and triage
Overview
According to Mandiant..
the DiceLoader framework, a known toolkit that helps attackers gain a foothold in infected systems and perform reconnaissance
According to Twitter...
twitter:https://twitter.com/c3rb3ru5d3d53c/status/1348667319665487874
and ...
#FIN7
— Arkbird (@Arkbird_SOLG) September 29, 2020
As reported by @KorbenD_Intel, the initial powershell script use DeflateStream method for uncompress the zip in memory and extract it. This execute the second layer that heavily obfuscated. More 70 functions are used for reorder the data for sensible strings and the implant pic.twitter.com/f8CTvtaf2m
and ...
For those tracking #FIN7 - #TAKEOUT is the powershell memory dropper that has been seen dropping #CARBANAK OR #DICELOADER - there's some confusion going on..
— Dan Perez (@MrDanPerez) January 7, 2021
ClearTemp.ps1 files == TAKEOUT
Payloads embedded == CARBANAK || DICELOADER
According to Dan Perez at Mandiant...
For those tracking #FIN7 - #TAKEOUT is the powershell memory dropper that has been seen dropping #CARBANAK OR #DICELOADER - there's some confusion going on.. ClearTemp.ps1 files == TAKEOUT Payloads embedded == CARBANAK || DICELOADER
References
Triage Notes
Sample: 2d88767c424d05330839e568b32f9f52962df56b1d3021f69930167fe623efd1
Creation Time: 2021-07-15 15:46:07 UTC
First Submission: 2022-03-20 23:22:32 UTC
- The sample is an x64 DLL with two exports
- One of the exports is a reflective PE loader (possibly copy-paste from metasploit)
- There is a mutex string
Global\\%08x
that is built as a stack string
.text:0000000180001749 C7 44 24 30 47 6C 6F 62 mov dword ptr [rsp+228h+var_mutex], 626F6C47h
.text:0000000180001751 C7 44 24 34 61 6C 5C 25 mov dword ptr [rsp+228h+var_mutex+4], 255C6C61h
.text:0000000180001759 C7 44 24 38 30 38 78 00 mov dword ptr [rsp+228h+var_mutex+8], 783830h
- The
.data
section containst two encrypted blobs with a 32-byte key sandwitched between them. - The first encrypted blob seems to contain a port number
01 bb 01 11 bb 01 00 00 00 90 f1
- The second encrypted blob contains C2 IP addresses separated with a
|
46.17.107.7|185.250.151.33\x00
Yara
Initial Attempt
import "pe"
rule diceloader {
meta:
description = "Identifies diceloader"
strings:
// gobal stack string
// C7 44 24 30 47 6C 6F 62 mov dword ptr [rsp+228h+var_mutex], 626F6C47h
// C7 44 24 34 61 6C 5C 25 mov dword ptr [rsp+228h+var_mutex+4], 255C6C61h
// C7 44 24 38 30 38 78 00 mov dword ptr [rsp+228h+var_mutex+8], 783830h
$x1 = { C7 ?? ?? ?? 47 6C 6F 62 C7 ?? ?? ?? 61 6C 5C 25 C7 ?? ?? ?? 30 38 78 00 }
// fnv1
// 48 FF C1 inc rcx
// 33 C2 xor eax, edx
// 69 C0 93 01 00 01 imul eax, 1000193h
$x2 = { 48 FF C1 33 C2 69 C0 93 01 00 01 }
// sleep(1000)
// B9 10 27 00 00 mov ecx, 2710h ; dwMilliseconds
// FF 15 B3 13 00 00 call cs:Sleep
$x3 = {B9 10 27 00 00 ff}
// hash for NtFlushInstructionCache
// 81 F9 B8 0A 4C 53 cmp ecx, 534C0AB8h
// 75 16 jnz short loc_7FFA3BA4121E
$x4 = { 81 F9 B8 0A 4C 53 75 }
condition:
pe.is_64bit() and
all of ($x*)
}
Hunt
rule diceloader_hunt {
meta:
description = "Identifies diceloader - danger hunt rule"
strings:
// Mod 31 for key length
// C1 FA 04 sar edx, 4
// 8B C2 mov eax, edx
// C1 E8 1F shr eax, 1Fh
// 03 D0 add edx, eax
// 6B C2 1F imul eax, edx, 1Fh
$x1 = { C1 FA 04 8B C2 C1 E8 1F 03 D0 6B C2 1F }
condition:
all of ($x*)
}
Final Rule
import "pe"
rule diceloader {
meta:
description = "Identifies diceloader"
strings:
// Mod 31 for key length
// C1 FA 04 sar edx, 4
// 8B C2 mov eax, edx
// C1 E8 1F shr eax, 1Fh
// 03 D0 add edx, eax
// 6B C2 1F imul eax, edx, 1Fh
$mod = { C1 FA 04 8B C2 C1 E8 1F 03 D0 6B C2 1F }
// Reflective loader - not in all versions
// B8 4D 5A 00 00 mov eax, 'ZM'
// 66 41 39 07 cmp [r15], ax
// 75 1B jnz short loc_18000106D
// 49 63 57 3C movsxd rdx, dword ptr [r15+3Ch]
// 48 8D 4A C0 lea rcx, [rdx-40h]
// 48 81 F9 BF 03 00 00 cmp rcx, 3BFh
// 77 0A ja short loc_18000106D
// 42 81 3C 3A 50 45 00 00 cmp dword ptr [rdx+r15], 'EP'
// 74 05 jz short loc_180001072
$reflective = { B8 4D 5A 00 00 66 41 39 07 75 ?? 49 63 57 3C 48 8D 4A C0 48 81 F9 BF 03 00 00 77 ?? 42 81 3C 3A 50 45 00 00 }
// Fnv1 Algrithm - only in new versions
$fnv1 = {33 ?? 69 ?? 93 01 00 01}
condition:
pe.is_64bit() and
$mod and
($reflective or $fnv1)
}
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)
import pefile
import struct
import re
def xor_decrypt(data, key):
out = []
for i in range(len(data)):
out.append(data[i] ^ key[i%len(key)])
return bytes(out)
FILE_PATH = '/tmp/087435ef6cddc58d40ba6ba8b4bb60b2ead9873bf1cb5c0702bbf31e3199b343.bin'
file_data = open(FILE_PATH, 'rb').read()
pe = pefile.PE(data=file_data)
data_section_offset= None
data_section_end_offset = None
for s in pe.sections:
if b'.data\x00' in s.Name:
data_section_offset = s.PointerToRawData
data_section_end_offset = s.PointerToRawData + s.SizeOfRawData
break
# Find the key based on it being passed as an arg
#
# BD 1F 00 00 00 mov ebp, 1Fh
# 4C 8D 05 68 1E 00 00 lea r8, key
# 44 8B CD mov r9d, ebp
# 49 8B CE mov rcx, r14
# 8D 75 F7 lea esi, [rbp-9]
# 8B D6 mov edx, esi
# E8 50 FE FF FF call sub_7FFA3BA420B8
egg = rb'\xBD\x1F\x00\x00\x00\x4C\x8D\x05(....)'
key = None
for m in re.finditer(egg, file_data):
rel_offset = struct.unpack('<I',m.group(1))[0]
# Remember to add 7 for the 7 bytes in the instruction
rel_offset += 7
print(f"rel offset: {hex(rel_offset)}")
# Remember that this is in the .text section which is loaded before the .data
# so the relative will be postive displacement
match_offset = m.start() + 5
print(f"match offset: {hex(match_offset)}")
match_rva = pe.get_rva_from_offset(match_offset)
print(f"match rva: {hex(match_rva)}")
key_rva = match_rva + rel_offset
print(f"key rva: {hex(key_rva)}")
key_offset = pe.get_offset_from_rva(key_rva)
print(f"key offset: {hex(key_offset)}")
key = file_data[key_offset:key_offset+31]
print(tohex(key))
# Use the key position to locate the other two data blobs
blob1 = file_data[data_section_offset:key_offset]
blob2 = file_data[key_offset+31:data_section_end_offset]
out_blob1 = xor_decrypt(blob1, key)
print(tohex(out_blob1[:20]))
port = struct.unpack('>H',out_blob1[:2])[0]
print(f"Port: {port}")
out_blob2 = xor_decrypt(blob2.lstrip(b'\x00'), key)
c2_ips = out_blob2.split(b'\x00')[0].split(b'|')
for c2 in c2_ips:
print(f"c2: {c2}")
FILE_PATH = '/tmp/7747707df3951b0de6b9a18f7597fb2819009163f61186393798a6167897e2f8.bin'
file_data = open(FILE_PATH, 'rb').read()
pe = pefile.PE(data=file_data)
data_section_offset= None
data_section_end_offset = None
for s in pe.sections:
if b'.data\x00' in s.Name:
data_section_offset = s.PointerToRawData
data_section_end_offset = s.PointerToRawData + s.SizeOfRawData
break
# 48 8D 15 80 23 00 00 lea rdx, key
# 8A 04 10 mov al, [rax+rdx]
# 41 30 01 xor [r9], al
egg = rb'\x48\x8D\x15(....)\x8A\x04\x10\x41\x30\x01'
key = None
for m in re.finditer(egg, file_data):
rel_offset = struct.unpack('<I',m.group(1))[0]
# Remember to add 7 for the 7 bytes in the instruction
rel_offset += 7
print(f"rel offset: {hex(rel_offset)}")
# Remember that this is in the .text section which is loaded before the .data
# so the relative will be postive displacement
match_offset = m.start()
print(f"match offset: {hex(match_offset)}")
match_rva = pe.get_rva_from_offset(match_offset)
print(f"match rva: {hex(match_rva)}")
key_rva = match_rva + rel_offset
print(f"key rva: {hex(key_rva)}")
key_offset = pe.get_offset_from_rva(key_rva)
print(f"key offset: {hex(key_offset)}")
key = file_data[key_offset:key_offset+31]
print(tohex(key))
# Use the key position to locate the other two data blobs
blob1 = file_data[data_section_offset:key_offset]
blob2 = file_data[key_offset+31:data_section_end_offset]
out_blob1 = xor_decrypt(blob1, key)
print(tohex(out_blob1[:20]))
port = struct.unpack('>H',out_blob1[:2])[0]
print(f"Port: {port}")
out_blob2 = xor_decrypt(blob2.lstrip(b'\x00'), key)
c2_ips = out_blob2.split(b'\x00')[0].split(b'|')
for c2 in c2_ips:
print(f"c2: {c2}")
def parse_ports(data):
out_ports = []
for ptr in range(0,len(data)-3,3):
if data[ptr] == 0:
break
tmp_port = struct.unpack('<H',data[ptr+1:ptr+3])[0]
out_ports.append(tmp_port)
return out_ports
parse_ports(unhex('01990501bb01015000013500000000f7eb5785b7'))
import pefile
import struct
import re
def xor_decrypt(data, key):
out = []
for i in range(len(data)):
out.append(data[i] ^ key[i%len(key)])
return bytes(out)
def get_key_offset(pe, file_data):
# Try new method first -- key is passed as an argument
#
# BD 1F 00 00 00 mov ebp, 1Fh
# 4C 8D 05 68 1E 00 00 lea r8, key
# 44 8B CD mov r9d, ebp
# 49 8B CE mov rcx, r14
# 8D 75 F7 lea esi, [rbp-9]
# 8B D6 mov edx, esi
# E8 50 FE FF FF call sub_7FFA3BA420B8
egg = rb'\xBD\x1F\x00\x00\x00\x4C\x8D\x05(....)'
for m in re.finditer(egg, file_data):
try:
rel_offset = struct.unpack('<I',m.group(1))[0]
# Remember to add 7 for the 7 bytes in the instruction
rel_offset += 7
# Remember that this is in the .text section which is loaded before the .data
# so the relative will be postive displacement
match_offset = m.start() + 5
match_rva = pe.get_rva_from_offset(match_offset)
key_rva = match_rva + rel_offset
key_offset = pe.get_offset_from_rva(key_rva)
# Return if we have something
return key_offset
except Exception:
continue
# If we are here the new method didn't work let's try the old method
# The key is directly referenced in a crypto routine
# 48 8D 15 80 23 00 00 lea rdx, key
# 8A 04 10 mov al, [rax+rdx]
# 41 30 01 xor [r9], al
egg = rb'\x48\x8D\x15(....)\x8A\x04\x10\x41\x30\x01'
for m in re.finditer(egg, file_data):
try:
rel_offset = struct.unpack('<I',m.group(1))[0]
# Remember to add 7 for the 7 bytes in the instruction
rel_offset += 7
# Remember that this is in the .text section which is loaded before the .data
# so the relative will be postive displacement
match_offset = m.start()
match_rva = pe.get_rva_from_offset(match_offset)
key_rva = match_rva + rel_offset
key_offset = pe.get_offset_from_rva(key_rva)
# Return if we have something
return key_offset
except Exception:
continue
# If we got here we failed
return None
def get_data_section_bounds(pe):
data_section_offset= None
data_section_end_offset = None
for s in pe.sections:
if b'.data\x00' in s.Name:
data_section_offset = s.PointerToRawData
data_section_end_offset = s.PointerToRawData + s.SizeOfRawData
break
return data_section_offset,data_section_end_offset
def parse_ports(data):
out_ports = []
for ptr in range(0,len(data)-3,3):
if data[ptr] == 0:
break
tmp_port = struct.unpack('<H',data[ptr+1:ptr+3])[0]
out_ports.append(tmp_port)
return out_ports
def get_config(data):
c2s = []
pe = pefile.PE(data=file_data)
key_offset = get_key_offset(pe, file_data)
if key_offset is None:
print("Error - no key offset found!")
return c2s
key = file_data[key_offset:key_offset+31]
data_section_offset,data_section_end_offset = get_data_section_bounds(pe)
if data_section_offset is None:
print("Error - no .data section")
return c2s
ports_data = file_data[data_section_offset:key_offset]
ips_data = file_data[key_offset+31:data_section_end_offset]
ports_ptxt_data = xor_decrypt(ports_data, key)
ports = parse_ports(ports_ptxt_data)
ports = set(ports)
ips_ptxt_data = xor_decrypt(ips_data.lstrip(b'\x00'), key)
c2_ips = ips_ptxt_data.split(b'\x00')[0].split(b'|')
# Sometimes the port data is messed up
# In this case just skip it
if len(ports) == 0 or len(ports) > 5:
return [ip.decode('ascii') for ip in c2_ips]
for ip in c2_ips:
for port in ports:
c2s.append(ip.decode('ascii')+":"+str(port))
return c2s
targets = ["/tmp/diceloader/2d88767c424d05330839e568b32f9f52962df56b1d3021f69930167fe623efd1.exe",
"/tmp/diceloader/0f76768f65775329d7a0ddb977ea822d992d086ce48a23679cef66e3b4d2f4ed.exe",
"/tmp/diceloader/extracted_implant.bin",
"/tmp/diceloader/bc3ce7f2ea9d33374a1373965625b7d0d6a010a2de5ddccbb8e1d819622187c6.exe",
"/tmp/diceloader/f32b8b0530ed068b47817d165064e99800a44a943ccae0e32f6b6b3e40c79638.exe",
"/tmp/7747707df3951b0de6b9a18f7597fb2819009163f61186393798a6167897e2f8.bin",
"/tmp/diceloader2.bin",
"/tmp/087435ef6cddc58d40ba6ba8b4bb60b2ead9873bf1cb5c0702bbf31e3199b343.bin",
"/tmp/met.bin",
"/tmp/diceloader.bin",
"/tmp/4d933b6b60a097ad5ce5876a66c569e6f46707b934ebd3c442432711af195124.bin",
"/tmp/2d88767c4.bin",
"/tmp/dicefail.bin"]
for fp in targets:
file_data = open(fp, 'rb').read()
print(get_config(file_data))