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 ...

and ...

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)

}

Config Extraction

We want to pull out the C2 IP addresses and ports.

Research

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}")
rel offset: 0x1e6f
match offset: 0x1651
match rva: 0x2251
key rva: 0x40c0
key offset: 0x2ec0
b'd78ad7388e21900836048ecf3f6dfd67349f5620d616d9f81e9d9c4892a159'
b'01bb0111bb010000001eba4105c94ba5875574b7'
Port: 443
c2: b'194.180.174.86'
c2: b'193.42.36.231'
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}")
rel offset: 0x2387
match offset: 0x1139
match rva: 0x1d39
key rva: 0x40c0
key offset: 0x2ac0
b'b54c55213e963325fba55eb30c987148f0e8b9165e884bf0f6ed190c1d3d4b'
b'01990501bb01015000013500000000f7eb5785b7'
Port: 409
c2: b'195.123.227.40'
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'))
        
[1433, 443, 80, 53]

Final Config Extractor

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))
['46.17.107.7:443', '185.250.151.33:443']
['46.17.107.32:443', '185.250.151.141:443']
['195.123.227.40:80', '195.123.227.40:1433', '195.123.227.40:443', '195.123.227.40:53']
['185.232.170.205:443', '94.140.114.173:443']
['185.172.129.35', '94.158.244.205']
['195.123.227.40:80', '195.123.227.40:1433', '195.123.227.40:443', '195.123.227.40:53']
['194.180.174.86:443', '193.42.36.231:443']
['194.180.174.86:443', '193.42.36.231:443']
['185.172.129.35', '94.158.244.205']
['185.172.129.35', '94.158.244.205']
['195.123.214.181:443']
['46.17.107.7:443', '185.250.151.33:443']
['185.172.129.35', '94.158.244.205']