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 ...
There was a 'Moved Permanently' error fetching URL: 'https://twitter.com/Arkbird_SOLG/status/1310966874352635907'
and ...
There was a 'Moved Permanently' error fetching URL: 'https://twitter.com/MrDanPerez/status/1347185968500109313'
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\\%08xthat 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
.datasection 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))