Overview

We are going to take a look at the new Emotet 64-bit samples and see if we can generate a Yara rule and a config extractor.

Samples

Initial Triage

Payload Binary Overview

  • DLL with DllRegisterServer export (ord 1)
  • compiler is MASM
  • internal name Y.dll

Code

  • using the same llvm control flow flattening obfuscator as the 32-bit versions
  • obscuring function calls by passing constants that are not used
  • we have some encrypted strings in the .text section

Helper Functions

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)

String Decryption

The string decrypion works the same way as 32-bit emotet where the first DWORD is the key, the second DWORD is the encrypted string lenght, and the encrypted string follows. We can use the same code.

The strings table starts at the beginning of the .text section.

We can reuse our 32-bit string decryptor with some slight modificaitons.

import struct
import pefile

EMOTET_FILE = r'/tmp/work/emotet_b481_unpacked.bin'
data = open(EMOTET_FILE, 'rb').read()
pe = pefile.PE(data = data)

txt_data = None
for s in pe.sections:
    if b'.text' in s.Name:
        txt_data = s.get_data()

# Make sure we got the text section
assert txt_data is not None

# Strings are xor encrypted
def xor_decrypt(data, key):
    out = []
    for i in range(len(data)):
        out.append(data[i] ^ key[i%len(key)])
    return bytes(out)


def is_ascii(s):
    return all(c < 128 for c in s)


strings_table = []
ECS1_string = None
ECK1_string = None

# Check for the strings in the first 0x1000 bytes of the text section
for i in range(0,0x1000,4):
    candidate_1 = struct.unpack('<I',txt_data[i:i+4])[0] 
    candidate_2 = struct.unpack('<I',txt_data[i+4:i+8])[0] 
    if (candidate_1  & 0xffffff00) ^ (candidate_2 & 0xffffff00) == 0:
        # We have a match!
        key = txt_data[i:i+4]
        data_len = candidate_1 ^ candidate_2
        enc_data = txt_data[i+8:i+8+data_len]
        ptxt_data = xor_decrypt(enc_data, key)
        if is_ascii(ptxt_data):
            if ptxt_data != b'':
                strings_table.append(ptxt_data.decode('latin1'))
        if b'ECS1' == ptxt_data[:4]:
            ECS1_string = ptxt_data
        if b'ECK1' == ptxt_data[:4]:
            ECK1_string = ptxt_data

# Print our strings
print(ECS1_string)
print(ECK1_string)
for s in strings_table:
    print(s)
b'ECS1 \x00\x00\x00@_t\xb6\xc4\xd8\xdc\x0c=\x1f\x06z7\xdc\xb9\xf9\xb7\xbd^\x8a/\xa6\xa1\xf2\x0f\xa1y\r\x14\xe5\xf51\xe8\xb0\n\x1e<\x8b?{\x90\x1d&&1\x86e|\x1a\xad\xd9\xc3\\\xacH\xf0`\x87\x18\xd9t<X\xf9'
b'ECK1 \x00\x00\x00\xf3\xa35\xb5\x0e.+\xf45V\xcd\nL)>|\xf1\x10\xdd\xcb\xb0O \xb3\xfa\x02 \xceL\xb6\x0c\x1eD\x96\xbe\xb4\x0e\xe6\xc9[\x9a\xbdN\xbd\x9d\x8f\xcf\xe0\x10[4L\x82\x04&\x02\xd3\xba\xac\xf1\xfb\x9f,v'
%s%s.exe
%s%s.dll
%s\regsvr32.exe "%s" %s
shlwapi.dll
advapi32.dll
wininet.dll
crypt32.dll
userenv.dll
wtsapi32.dll
bcrypt.dll
urlmon.dll
shell32.dll
SHA256
Microsoft Primitive Provider
ECCPUBLICBLOB
HASH
ObjectLength
KeyDataBlob
AES
ECDH_P256
ECDSA_P256
RNG
POST
%u.%u.%u.%u

--%S--
Cookie: %s=%s

Content-Type: multipart/form-data; boundary=%s


--%S
Content-Disposition: form-data; name="%S"; filename="%S"
Content-Type: application/octet-stream


%s_%08X
nltest /dclist:
systeminfo
ipconfig /all
%s\%s
%s\regsvr32.exe "%s\%s"
SOFTWARE\Microsoft\Windows\CurrentVersion\Run
%s\%s%x
%s\regsvr32.exe "%s\%s" %s
WinSta0\Default
%s\%s
%s\*
%s:Zone.Identifier
%s\%s.exe
   

C2 Table

The c2 list is stored in the .data section in the exact same format as the 32-bit sample. We were able to re-use our 32-bit c2 extractor code.

data_data = None
for s in pe.sections:
    if b'.data' in s.Name:
        data_data = s.get_data()
print(data_data[:100])

key = data_data[:4]
data_len = struct.unpack('<I',data_data[:4])[0] ^ struct.unpack('<I',data_data[4:8])[0]
enc_data = data_data[8:8+data_len]
ptxt_data = xor_decrypt(enc_data, key)

print(tohex(ptxt_data))

print("\n== C2 List== ")
for i in range(0,len(ptxt_data),8):
    print("%d.%d.%d.%d:%d" % (ptxt_data[i+0],ptxt_data[i+1],ptxt_data[i+2],ptxt_data[i+3],struct.unpack('>H',ptxt_data[i+4:i+6])[0]))
b"8\xfd\xb9\x1e\xc0\xfc\xb9\x1e\x88\xe2\xf0D9F\xb9\x1f\x15\xb1&\xc8'm\xb9\x1f\xb28*{9F\xb9\x1fPU#Q'm\xb9\x1f\xad\xc5:\x02'm\xb9\x1f=\xf4\xcd\xe8'm\xb9\x1fu\xacN\x8e'm\xb9\x1f\x94\x95B\x84'm\xb9\x1f\n\xe3\x91\xda'm\xb9\x1f\x95)x\xe7'm\xb9\x1f\x0b\xa6\xf5G'm\xb9\x1f\xfd\x0f/\xea"
b'b01f495a01bb00012d4c9fd61f9000018ac5936501bb000168a89a4f1f9000019538831c1f900001050974f61f9000014d51f7901f900001ac68fb9a1f900001321e28c41f900001add4c1f91f900001335b4c591f900001c5f296f41f900001674bc90201bb000133fe8cee1ba800014f8923c61f900001480fc90f1f9000011b36593a1f900001bd7e6fc81ba80001c4da1e5301bb000152a5987f1f900001a44463031f900001b76fe3891f900001a7acfda21f900001997e92191ba8000181e8bc5d01bb0001976a70c41f900001bc2c141901bb0001a76373231f900001867a42c11f900001b90487a51f900001d41862631f900001335b07051f900001923be22d01bb0001836418e700500001d4ed11631f900001c95ea6a201bb00012db0e87c01bb00019f41580a1f900001a0108e381f900001d89ee2ce01bb0001cb726d7c01bb0001672b2eb601bb00012e37de0b01bb0001d17e62ce1f9000015bcf1c211f90000101ea02e81f9000012d7673631f900001cebd1cc71f9000015e172d56102f00019e45de6501bb000167461c661f9000016532005b1f9000013ae32aec0050000177c17c291ba800016bb6e18e1f900001b99d52d31f9000012deb081e1f9000016784f21a1f90000101ea15491ba800016ee875ba1f900001d161a3d601bb0001b908d4821ba80001d1faf6ce01bb0001'

== C2 List== 
176.31.73.90:443
45.76.159.214:8080
138.197.147.101:443
104.168.154.79:8080
149.56.131.28:8080
5.9.116.246:8080
77.81.247.144:8080
172.104.251.154:8080
50.30.40.196:8080
173.212.193.249:8080
51.91.76.89:8080
197.242.150.244:8080
103.75.201.2:443
51.254.140.238:7080
79.137.35.198:8080
72.15.201.15:8080
27.54.89.58:8080
189.126.111.200:7080
196.218.30.83:443
82.165.152.127:8080
164.68.99.3:8080
183.111.227.137:8080
167.172.253.162:8080
153.126.146.25:7080
129.232.188.93:443
151.106.112.196:8080
188.44.20.25:443
167.99.115.35:8080
134.122.66.193:8080
185.4.135.165:8080
212.24.98.99:8080
51.91.7.5:8080
146.59.226.45:443
131.100.24.231:80
212.237.17.99:8080
201.94.166.162:443
45.176.232.124:443
159.65.88.10:8080
160.16.142.56:8080
216.158.226.206:443
203.114.109.124:443
103.43.46.182:443
46.55.222.11:443
209.126.98.206:8080
91.207.28.33:8080
1.234.2.232:8080
45.118.115.99:8080
206.189.28.199:8080
94.23.45.86:4143
158.69.222.101:443
103.70.28.102:8080
101.50.0.91:8080
58.227.42.236:80
119.193.124.41:7080
107.182.225.142:8080
185.157.82.211:8080
45.235.8.30:8080
103.132.242.26:8080
1.234.21.73:7080
110.232.117.186:8080
209.97.163.214:443
185.8.212.130:7080
209.250.246.206:443

Binary Exploration with Dumpulator

Using Dumpultor

  • First we load the sample in x64dbg
  • Install mindump plugin
  • Then run to entrypoint of the DLL
  • Run mindump from the x64dbg command bar MiniDump <output_file.dmp>
from dumpulator import Dumpulator

DUMP_FILE = '/tmp/work/emotet_b481.dmp'

dp = Dumpulator(DUMP_FILE, quiet=True)
    
def decrypt_string(string_address):
    fn_decrypt = 0x07FFEA424B924
    result = dp.call(fn_decrypt, [0x9695E, string_address, 0xD71EB])
    ptxt_string = dp.read(result, 200)
    out = ptxt_string.split(b'\x00\x00')[0].replace(b'\x00',b'')
    return bytes(out).decode('utf-8')

blob_1 = 0x007FFEA4231000
ptxt_string = decrypt_string(blob_1)
print(ptxt_string)
%s%s.exe

Generate a Yara Rule

Hunting Traints

These are data that might be useful for hunting related samples, but not great for non-fp identification of Emotet

  • internal name Y.dll
  • export DllRegisterServer

String decryption loop

.text:00007FFEA424BAB9 C1 E9 10                                shr     ecx, 10h
.text:00007FFEA424BABC 66 C1 E8 08                             shr     ax, 8

Robust Rule Traits

We know that the .text and .data sections start with encrypted data that is in a set format: <DWORD:key><DWORD:encrypted len>. We also know that that encrypted length is XOR encrypted with the key. We can exploit this info combined with the fact that the encrypted data length is not going to be more than 255 for the strings in .text or 65536 for the c2 table in .data. This tells us that most significant 2-bytes or 3-bytes will be equal for the key and the encrypted length.

Our yara rule will just compare these bytes and also make sure that they are no null bytes.

This might be good, but we don't know??? Some considerations:

  • if there are common PE files that have repeating bytes at the start of both section this could cause a lot of FPs we should test
yara

import "pe"

rule Emotetx64
{


    condition:

        pe.is_64bit()
        and uint16(pe.sections[pe.section_index(".data")].raw_data_offset + 2) == uint16(pe.sections[pe.section_index(".data")].raw_data_offset + 6)
        and uint16(pe.sections[pe.section_index(".data")].raw_data_offset + 2) != 0
        and uint8(pe.sections[pe.section_index(".data")].raw_data_offset) != uint8(pe.sections[pe.section_index(".data")].raw_data_offset + 4)
        and uint8(pe.sections[pe.section_index(".text")].raw_data_offset + 1) == uint8(pe.sections[pe.section_index(".text")].raw_data_offset + 5)
        and uint8(pe.sections[pe.section_index(".text")].raw_data_offset + 1) != 0 
        and uint8(pe.sections[pe.section_index(".text")].raw_data_offset + 2) == uint8(pe.sections[pe.section_index(".text")].raw_data_offset + 6) 
        and uint8(pe.sections[pe.section_index(".text")].raw_data_offset + 2) != 0
        and uint8(pe.sections[pe.section_index(".text")].raw_data_offset + 3) == uint8(pe.sections[pe.section_index(".text")].raw_data_offset + 7)  
        and uint8(pe.sections[pe.section_index(".text")].raw_data_offset + 3) != 0
        and uint8(pe.sections[pe.section_index(".text")].raw_data_offset) != uint8(pe.sections[pe.section_index(".text")].raw_data_offset + 4)



}
data = b'\xAA\x8B\xDA\x35\xA2\x8B\xDA\x35\x8F\xF8\xFF\x46\x84\xEE\xA2\x50\x67\x9B\x33\xDD\xF6\xE0'

key = b'\xAA\x8B\xDA\x35'

data_len = b'\xA2\x8B\xDA\x35'