Emotet 64-bit
Initial Triage of new Emotet 64-bit sample
- Overview
- Initial Triage
- Helper Functions
- String Decryption
- C2 Table
- Binary Exploration with Dumpulator
- Generate a Yara Rule
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
-
Packed (
b481ac05ea9a59eedf6233166327057279babef26c913a8e89536472b192e86c
) -
Unpacked (
ed2640be5ed0a4486ecf7ac97b125e26b9d263624251eae1c9a42e9998ca1e68
)The PS1 has zero detections on VT 👀
— Max_Malyutin (@Max_Mal_) April 28, 2022
Distro compromised URL:
hxxp://ciencias-exactas[.]com[.]ar/old/w/#Emotet DLL payload, MD5: 71675a9a8abbce8ba524f8f6ef3735ed pic.twitter.com/95Na6SzguN🚨#Emotet Update🚨 - Looks like Ivan laid an egg for easter and has been busy. As of about 14:00UTC today 2022/04/18 - Emotet on Epoch 4 has switched over to using 64-bit loaders and stealer modules. Previously everything was 32-bit except for occasional loader shenanigans. 1/x
— Cryptolaemus (@Cryptolaemus1) April 19, 2022 - Yara rule plugin for IDA/Binja/Cutter
- @MaxMal emotet delivery analysis
- Emotet botnet switches to 64-bit modules, increases activity
- EmoCheck now detects new 64-bit versions of Emotet malware
- Emotet Tests New Delivery Techniques
- Malpedia Emotet Info (Yara)
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
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)
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]))
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)
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'