Lors de ce second article dédié à Emotet
, nous allons construire un PoC (Proof of Concept), en nous basant sur le retour d’expériences de l’article précédent, qui nous permettra d’extraire la configuration (les adresses IP
).
Pour la beauté du geste, nous allons également regarder, plus en détail, la fonction d’unpacking pour les besoins de notre outil.
Nous avons observé dans l’article précédent que la portion de codes qui réalise le unpacking, est chargée et exécutée en mémoire. Mais si nous regardons plus en détail, nous comprenons que :
ESI
prend pour valeur 3CC159BB
(la première clé XOR
) à l’adresse 0x00402550
.call
VirtualAllocEx, à l’adresse 0x0040257B
, sert à allouer de l’espace mémoire qui contiendra le code d’unpacking.
ESI
prend la nouvelle clé XOR
à l’adresse 0x004025A6
.
Nous pouvons représenter ce travail avec le script Python
suivant :
#!/usr/bin/env python2.7
from sys import exit
def read_from_hex_offset(file_stream, hex_offset, bytes_to_read) :
offset = int(hex_offset, 16)
file_stream.seek(offset)
return(file_stream.read(bytes_to_read))
def unpack_stage1(path_to_file) :
from binascii import hexlify, unhexlify
from struct import pack
read_file = open(path_to_file,"rb")
written_file = open("stage1.bin", "wb")
## First XOR key
xor_key = 0x3cc159bb
## Decryption
start_offset = 0x29f0
max_offset = 1672
for i in range(0, max_offset, 4) :
bytes = hexlify(read_from_hex_offset(read_file, str(hex(start_offset)), 4))
bytes = hexlify(pack('<L', int(bytes, 16)))
bytes = (int(bytes, 16) + 0xffffffef) & 0xffffffff ## add eax, FFFFFFEF
bytes = (bytes ^ xor_key) & 0xffffffff ## xor eax, esi
bytes = (bytes + 0xffffffff) & 0xffffffff ## add eax, FFFFFFFF
bytes = hexlify(pack('<L', bytes))
xor_key = int(hexlify(pack('<L', int(bytes, 16))), 16)
written_file.write(unhexlify(bytes))
start_offset = start_offset + 4
written_file.close()
read_file.close()
def main() :
unpack_stage1("../malware.exe")
if(__name__ == '__main__') :
main()
exit(0)
Puis utiliser le logiciel IDA
pour avoir un aperçu global du résultat.
Nous poursuivons notre analyse, à la recherche d’Emotet
.
Pour contrer l’anti-debug (en mode gros bourrin), j’ai utilisé le plugin TitanHide en ayant pris soin d’appliquer à mon système de sandboxing, le patch pour supprimer la protection PatchGuard
.
J’ai réalisé une bonne partie du travail sans ce plugin. Mais, si je veux me faciliter un peu la vie, …
Pour les étapes suivantes, nous pouvons procéder comme ceci :
0x00402404
.break point
sur l’adresse 0x004025CB
.F9
pour nous rendre à l’adresse 0x004025CB
puis F7
pour atterrir au point d’entrée mémoire du processus d’unpacking.C’est à partir d’ici que TitanHide va nous faciliter la vie :
Hardware break point
à l’adresse 0x00220028
(dans mon cas), puis F9
pour nous rendre à cette adresse.F8
jusqu’à atteindre l’adresse 0x0023009F
. Puis nous suivons le call 230519
avec la touche F7
. La routine de déchiffrement du binaire commence ici.
Nous pouvons constater, dans le registre EAX
de la capture précédente, la valeur MZ
(header de tous les exécutables couramment utilisés par Windows).
C’est le moment de coder un script en nous basant sur nos observations.
#!/usr/bin/env python2.7
from sys import exit
def get_stage1(file_stream, at_offset, size, xor_key) :
from binascii import hexlify, unhexlify
from struct import pack
start_offset = at_offset
max_offset = size
data = ""
for i in range(0, max_offset, 4) :
bytes = hexlify(read_from_hex_offset(file_stream, str(hex(start_offset)), 4))
bytes = int(hexlify(pack("<L", int(bytes, 16))), 16)
bytes = xor_with_addition(bytes, xor_key)
bytes = hexlify(pack("<L", bytes))
xor_key = int(hexlify(pack("<L", int(bytes, 16))), 16)
data = data + unhexlify(bytes)
start_offset = start_offset + 4
return(data)
def parse_stage1(stage1) :
from binascii import hexlify
from struct import pack
xor_key = int(hexlify(pack("<L", int(hexlify(stage1[-284:-280]), 16))), 16)
sub_vector1 = int(hexlify(pack("<L", int(hexlify(stage1[-354:-350]), 16))), 16)
sub_fixed_value = int(hexlify(pack(">L", int(hexlify(stage1[-286:-285]), 16))), 16)
at_offset = int(hexlify(pack("<L", int(hexlify(stage1[-16:-12]), 16))), 16)
max_offset = int(hexlify(pack("<L", int(hexlify(stage1[-12:-8]), 16))), 16)
return((xor_key, sub_vector1, sub_fixed_value, at_offset, max_offset))
def read_from_hex_offset(file_stream, hex_offset, bytes_to_read) :
offset = int(hex_offset, 16)
file_stream.seek(offset)
return(file_stream.read(bytes_to_read))
def unpack(stage1, file_stream, size_of_raw_data) :
from binascii import hexlify, unhexlify
from struct import pack
xor_key, sub_vector1, sub_fixed_value, at_offset, max_offset = parse_stage1(stage1)
sub_key = (int(hexlify(pack("<L", int(hexlify(stage1[-4:]), 16))), 16) + sub_vector1) & 0xffffffff
start_offset = at_offset - size_of_raw_data
data = ""
for i in range(0, max_offset, 4) :
bytes = hexlify(read_from_hex_offset(file_stream, str(hex(start_offset)), 4))
next_sub_key = int(bytes, 16)
bytes = int(hexlify(pack("<L", int(bytes, 16))), 16)
bytes = xor_with_subtraction(bytes, xor_key, sub_key, sub_fixed_value)
bytes = hexlify(pack("<L", bytes))
sub_key = int(hexlify(pack("<L", next_sub_key)), 16)
data = data + unhexlify(bytes)
start_offset = start_offset + 4
return(data)
def xor_with_addition(bytes, xor_key) :
bytes = (bytes + 0xffffffef) & 0xffffffff ## add eax, FFFFFFEF
bytes = (bytes ^ xor_key) & 0xffffffff ## xor eax, esi
bytes = (bytes + 0xffffffff) & 0xffffffff ## add eax, FFFFFFFF
return(bytes)
def xor_with_subtraction(bytes, xor_key, sub_key, sub_fixed_value) :
bytes = (bytes - sub_fixed_value) & 0xffffffff ## sub eax, 7
bytes = (bytes ^ xor_key) & 0xffffffff ## xor eax, 954132AC
bytes = (bytes - sub_key) & 0xffffffff ## sub eax, edx
return(bytes)
def main() :
from pefile import PE
from binascii import hexlify
input_file = "../malware.exe"
read_file = open(input_file, "rb")
output_file = "unpacked.bin"
written_file = open(output_file,"wb")
pe = PE(input_file)
stage1 = get_stage1(read_file, 0x29f0, 1672, 0x3cc159bb)
raw_base = int(pe.sections[1].SizeOfRawData & 0xffff) ## TODO: fix that.
unpacked = unpack(stage1, read_file, raw_base)
written_file.write(unpacked)
written_file.close()
read_file.close()
if(__name__ == '__main__') :
main()
exit(0)
Pour nous aider dans notre travail, nous pouvons nous appuyer sur Yara
. Un outil très apprécié des chercheurs en sécurité informatique spécialisés dans la traque aux Malwares.
On pourrait utiliser des règles Yara
pour :
Dans un premier temps, je vais utiliser très basiquement Yara
pour détecter si le binaire d’Emotet
est packé ou unpacké.
Nous poursuivons en étudiant les signatures du loader
et du payload
utilisées par CAPE Sandbox. Si la signature du payload
est satisfaisante, celle du loader
ne donne aucun résultat.
Nous gardons donc la signature du payload
dont nous pouvons observer l’équivalence dans la capture d’écran ci-dessous.
Nous allons créer ensuite notre propre signature pour le packer
. Pour cela, nous nous baserons sur le code suivant :
Nous adoptons la signature de cette fonction qui contient toutes les informations nécessaires au développement de notre PoC (la localisation des données, les informations de décryptage).
Nous pouvons coder quelque chose comme ceci (Je sais … le code est de plus en plus moche.) :
#!/usr/bin/env python2.7
from sys import exit
_R_EMOTET_ = """
rule Emotet_Payload {
strings :
// .text:00FC6860 | 33C0 | xor eax,eax
// .text:00FC6862 | C705 ???????? ???????? | mov dword ptr ds:[FD24A0],sonicredist.FCF710
// .text:00FC686C | C705 ???????? ???????? | mov dword ptr ds:[FD24A4],sonicredist.FCF710
// .text:00FC6876 | A3 ???????? | mov dword ptr ds:[FD24A8],eax
// .text:00FC687B | A3 ???????? | mov dword ptr ds:[FD24AC],eax
// .text:00FC6880 | ???? ???????? | cmp dword ptr ds:[FCF710],eax
// .text:00FC6886 | ?? ?? | je sonicredist.FC68AA
// .text:00FC6888 | ?? ?? | jmp sonicredist.FC6890
// .text:00FC688A | ???? ??????00 | lea ebx,dword ptr ds:[ebx]
// .text:00FC6890 | 40 | inc eax
// .text:00FC6891 | A3 ???????? | mov dword ptr ds:[FD24A8],eax
// .text:00FC6896 | 833CC5 ???????? 00 | cmp dword ptr ds:[eax*8+FCF710],0
// .text:00FC689E | 75 F0 | jne sonicredist.FC6890
// .text:00FC68A0 | 51 | push ecx
// .text:00FC68A1 | E8 ???????? | call sonicredist.FC1FA0
// .text:00FC68A6 | 83C4 04 | add esp,4
// .text:00FC68A9 | C3 | ret
$unpacked = { 33 C0 C7 05 ?? ?? ?? ?? ?? ?? ?? ?? C7 05 ?? ?? ?? ?? ?? ?? ?? ?? A3 ?? ?? ?? ?? A3 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 00 40 A3 ?? ?? ?? ?? 83 3C C5 ?? ?? ?? ?? 00 75 F0 51 E8 ?? ?? ?? ?? 83 C4 04 C3 }
condition :
// check for MZ Signature at offset 0
(uint16(0) == 0x5A4D) and $unpacked
}
rule Emotet_Packed {
strings :
// .text:00402549 | BA ???????? | mov edx,malware.4037F0
// .text:0040254E | 29F6 | sub esi,esi
// .text:00402550 | 81F6 ???????? | xor esi,3CC159BB
// .text:00402556 | 52 | push edx
// .text:00402557 | B9 ???????? | mov ecx,40
// .text:0040255C | 51 | push ecx
// .text:0040255D | BA ???????? | mov edx,1000
// .text:00402562 | 52 | push edx
// .text:00402563 | BA ???????? | mov edx,688
// .text:00402568 | 52 | push edx
// .text:00402569 | B9 ???????? | mov ecx,0
// .text:0040256E | 51 | push ecx
// .text:0040256F | B9 ???????? | mov ecx,FFFFFFFF
// .text:00402574 | 51 | push ecx
// .text:00402575 | 8D1D ???????? | lea ebx,dword ptr ds:[<&VirtualAllocEx>]
// .text:0040257B | FF13 | call dword ptr ds:[ebx]
// .text:0040257D | 5A | pop edx
// .text:0040257E | 83F8 00 | cmp eax,0
// .text:00402581 | 0F84 ???????? | je malware.402A07
// .text:00402587 | 29DB | sub ebx,ebx
// .text:00402589 | 4B | dec ebx
// .text:0040258A | 21C3 | and ebx,eax
// .text:0040258C | 53 | push ebx
// .text:0040258D | 81FF ???????? | cmp edi,688
// .text:00402593 | 74 ?? | je malware.4025B7
// .text:00402595 | ???? | xor eax,eax
// .text:00402597 | ???? | sub eax,dword ptr ds:[edx]
// .text:00402599 | ???? | neg eax
// .text:0040259B | 8D52 ?? | lea edx,dword ptr ds:[edx+4]
// .text:0040259E | 83C0 ?? | add eax,FFFFFFEF
// .text:004025A1 | ???? | xor eax,esi
// .text:004025A3 | 83C0 ?? | add eax,FFFFFFFF
// .text:004025A6 | ???? | mov esi,eax
// .text:004025A8 | 8943 ?? | mov dword ptr ds:[ebx],eax
// .text:004025AB | 8D?? ?? | lea ebx,dword ptr ds:[ebx+4]
// .text:004025AE | 8D?? ?? | lea edi,dword ptr ds:[edi+4]
// .text:004025B1 | 68 ???????? | push malware.40258D
// .text:004025B6 | C3 | ret
$packed = { BA ?? ?? ?? ?? 29 F6 81 F6 ?? ?? ?? ?? 52 B9 ?? ?? ?? ?? 51 BA ?? ?? ?? ?? 52 BA ?? ?? ?? ?? 52 B9 ?? ?? ?? ?? 51 B9 ?? ?? ?? ?? 51 8D 1D ?? ?? ?? ?? FF 13 5A 83 F8 00 0F 84 ?? ?? ?? ?? 29 DB 4B 21 C3 53 81 FF ?? ?? ?? ?? 74 ?? ?? ?? ?? ?? ?? ?? 8D 52 ?? 83 C0 ?? ?? ?? 83 C0 ?? ?? ?? 89 43 ?? 8D ?? ?? 8D ?? ?? 68 ?? ?? ?? ?? C3 }
condition :
// check for MZ Signature at offset 0
(uint16(0) == 0x5A4D) and $packed
}
"""
def convert_bytes(size) :
for x in ["Bytes", "Kb", "Mb", "Gb", "Tb"] :
if size < 1024.0 :
return("%3.1f %s" % (size, x))
size /= 1024.0
def digest(algorithm, data) :
if algorithm == "md5" :
from hashlib import md5
return(md5(data).hexdigest())
elif algorithm == "sha1" :
from hashlib import sha1
return(sha1(data).hexdigest())
elif algorithm == "sha256" :
from hashlib import sha256
return(sha256(data).hexdigest())
def file_size(path_to_file) :
from os import stat
from os.path import isfile
if isfile(path_to_file) :
file_info = stat(path_to_file)
return(convert_bytes(file_info.st_size))
return(0)
def get_stage1(file_stream, at_offset, size, xor_key, add_value1, add_value2) :
from binascii import hexlify, unhexlify
from struct import pack
start_offset = at_offset
max_offset = size
data = ""
for i in range(0, max_offset, 4) :
bytes = hexlify(read_from_hex_offset(file_stream, str(hex(start_offset)), 4))
bytes = int(hexlify(pack("<L", int(bytes, 16))), 16)
bytes = xor_with_addition(bytes, xor_key, add_value1, add_value2)
bytes = hexlify(pack("<L", bytes))
xor_key = int(hexlify(pack("<L", int(bytes, 16))), 16)
data = data + unhexlify(bytes)
start_offset = start_offset + 4
return(data)
def parse_stage0(signature, image_base, raw_base) :
from binascii import hexlify
from struct import pack
xor_key = int(hexlify(pack("<L", int(hexlify(signature[0].strings[0][2][9:13]), 16))), 16)
print_message("xor_key:\t" + str(hex(xor_key)))
add_value1 = int(hexlify(signature[0].strings[0][2][-23:-22]), 16)
print_message("add_value1:\t" + str(hex(add_value1)))
add_value2 = int(hexlify(signature[0].strings[0][2][-18:-17]), 16)
print_message("add_value2:\t" + str(hex(add_value2)))
at_offset = (int(hexlify(pack("<L", int(hexlify(signature[0].strings[0][2][1:5]), 16))), 16) - image_base) - raw_base
print_message("at_offset:\t" + str(hex(at_offset)))
max_offset = int(hexlify(pack("<L", int(hexlify(signature[0].strings[0][2][27:31]), 16))), 16)
print_message("max_offset:\t" + str(hex(max_offset)))
return((xor_key, add_value1, add_value2, at_offset, max_offset))
def parse_stage1(stage1) :
from binascii import hexlify
from struct import pack
print("\n[+] parse_stage1")
xor_key = int(hexlify(pack("<L", int(hexlify(stage1[-284:-280]), 16))), 16)
print_message("xor_key:\t" + str(hex(xor_key)))
sub_vector1 = int(hexlify(pack("<L", int(hexlify(stage1[-354:-350]), 16))), 16)
print_message("sub_vector1:\t" + str(hex(sub_vector1)))
sub_fixed_value = int(hexlify(pack(">L", int(hexlify(stage1[-286:-285]), 16))), 16)
print_message("sub_vector2:\t" + str(hex(sub_fixed_value)))
at_offset = int(hexlify(pack("<L", int(hexlify(stage1[-16:-12]), 16))), 16)
print_message("at_offset:\t" + str(hex(at_offset)))
max_offset = int(hexlify(pack("<L", int(hexlify(stage1[-12:-8]), 16))), 16)
print_message("max_offset:\t" + str(hex(max_offset)))
return((xor_key, sub_vector1, sub_fixed_value, at_offset, max_offset))
def print_message(message) :
print(" --> %s" % (message))
def read_file_to_buffer(path_to_file, mode) :
from os.path import isfile
if not(isfile(path_to_file)) : return(False)
try :
with open(path_to_file, mode) as file_stream :
file_content = file_stream.read()
except Exception as Exception_Error :
print("%s", (Exception_Error))
return(False)
return(file_content)
def read_from_hex_offset(file_stream, hex_offset, bytes_to_read) :
offset = int(hex_offset, 16)
file_stream.seek(offset)
return(file_stream.read(bytes_to_read))
def run_yara(rule_code, buffer_to_sample) :
from yara import compile
try :
yar_object = compile(source = rule_code)
matches = yar_object.match(data = buffer_to_sample)
except Exception as Exception_Error :
print("%s", (Exception_Error))
return(False)
if len(matches) != 0 :
return(matches)
return(False)
def show_pe_info(path_to_file) :
from pefile import PE
print("filename:\t%s\nSize:\t\t%s\nArchitecture\t%s\n\nMD5:\t\t%s\nSHA1:\t\t%s\nSHA256:\t\t%s\n" % (
path_to_file.split('/')[::-1][0],
file_size(path_to_file),
hex(PE(path_to_file).FILE_HEADER.Machine),
digest("md5", read_file_to_buffer(path_to_file, "rb")),
digest("sha1", read_file_to_buffer(path_to_file, "rb")),
digest("sha256", read_file_to_buffer(path_to_file, "rb"))
))
def unpack(stage1, file_stream, size_of_raw_data) :
from binascii import hexlify, unhexlify
from struct import pack
xor_key, sub_vector1, sub_fixed_value, at_offset, max_offset = parse_stage1(stage1)
sub_key = (int(hexlify(pack("<L", int(hexlify(stage1[-4:]), 16))), 16) + sub_vector1) & 0xffffffff
start_offset = at_offset - size_of_raw_data
data = ""
for i in range(0, max_offset, 4) :
bytes = hexlify(read_from_hex_offset(file_stream, str(hex(start_offset)), 4))
next_sub_key = int(bytes, 16)
bytes = int(hexlify(pack("<L", int(bytes, 16))), 16)
bytes = xor_with_subtraction(bytes, xor_key, sub_key, sub_fixed_value)
bytes = hexlify(pack("<L", bytes))
sub_key = int(hexlify(pack("<L", next_sub_key)), 16)
data = data + unhexlify(bytes)
start_offset = start_offset + 4
return(data)
def xor_with_addition(bytes, xor_key, add_value1, add_value2) :
bytes = (bytes + 0xffffffef) & 0xffffffff ## add eax, FFFFFFEF
bytes = (bytes ^ xor_key) & 0xffffffff ## xor eax, esi
bytes = (bytes + 0xffffffff) & 0xffffffff ## add eax, FFFFFFFF
return(bytes)
def xor_with_subtraction(bytes, xor_key, sub_key, sub_fixed_value) :
bytes = (bytes - sub_fixed_value) & 0xffffffff ## sub eax, 7
bytes = (bytes ^ xor_key) & 0xffffffff ## xor eax, 954132AC
bytes = (bytes - sub_key) & 0xffffffff ## sub eax, edx
return(bytes)
def main() :
from binascii import hexlify
from pefile import PE
from struct import pack
path_to_sample = "../malware.exe"
output_file = "unpacked.bin"
read_file = open(path_to_sample, "rb")
print("Emotet 2019 - Get Configuration Tools\n\t by mekhalleh [www.pirates.re]\n")
print("%s" % ("=" * 40))
show_pe_info(path_to_sample)
pe = PE(path_to_sample)
raw_base = int(pe.sections[1].SizeOfRawData & 0xffff) ## TODO: fix that?
image_base = int(pe.OPTIONAL_HEADER.ImageBase)
signature = run_yara(_R_EMOTET_, read_file_to_buffer(path_to_sample, "rb"))
if (signature != False) :
if (signature[0].strings[0][1] == "$packed") :
print("emotet:\t\tPACKED\nsignature:\t%s (at offset)\n" % (hex(signature[0].strings[0][0])[:-1]))
print("%s" % ("=" * 40))
print("[+] Start unpacking Emotet")
xor_key, add_value1, add_value2, at_offset, size = parse_stage0(signature, image_base, raw_base)
stage1 = get_stage1(read_file, at_offset, size, xor_key, add_value1, add_value2) ## TODO: add xor vectors!
unpacked = unpack(stage1, read_file, raw_base)
s = run_yara(_R_EMOTET_, unpacked)
if (s[0].strings[0][1] == "$unpacked") :
written_file = open(output_file, "wb")
print("\n%s" % ("=" * 40))
print("[+] Write extracted file to <%s>" % (output_file))
print("%s" % ("=" * 40))
written_file.write(unpacked)
written_file.close()
show_pe_info(output_file)
print("emotet:\t\tUNPACKED\nsignature:\t%s (at offset)\n" % (hex(s[0].strings[0][0])[:-1]))
else :
print("[!] No Emotet signature found!")
exit(0)
elif (signature[0].strings[0][1] == "$unpacked") :
print("emotet:\t\tUNPACKED\nsignature:\t%s (at offset)\n" % (hex(signature[0].strings[0][0])[:-1]))
else :
print("[!] No Emotet signature found!")
exit(0)
## Read Emotet configuration:
print("%s" % ("=" * 40))
## TODO
read_file.close()
if(__name__ == '__main__') :
main()
exit(0)
Dans l’article précédent j’avais émis des hypothèses sur la génération du nom de l’exécutable (et du service). Voyons comment ceci fonctionne réellement.
Pour générer son nom d’exécutable (et de service), Emotet n’utilise qu’une seule et unique liste de noms et applique un algorithme pour choisir une première valeur dans cette liste. Puis un second tour permet de choisir une autre valeur qui est concaténée avec la première.
Le vecteur de taille fixe utilisé est la valeur de retour de la fonction GetVolumeInformationW.
Nous pouvons réaliser le script suivant pour illustrer ce travail.
#!/usr/bin/env python2.7
def get_name(pos, candidates) :
if candidates[pos] == ',' :
pos = pos + 1
else :
for i in reversed(range(pos)) :
if candidates[i] == ',' :
break
pos = pos - 1
name = ""
for i in range(len(candidates)) :
if i >= pos :
name = name + candidates[i]
if candidates[i] == ',' :
break
return(name)
def main() :
candidates = "rel,tables,glue,impl,texture,related,key,nis,langs,iprop,exec,wrap,matrix,dump,phoenix,ribbon,sorting,pinned,lics,bit,unpack,adt,rep,jobs,acl,title,sound,events,targets,scrn,mheg,lines,prompt,adjust,xian,ser,cycle,redist,its,boxes,dma,small,cloud,flow,guiddef,whole,parent,bears,random,bulk,idebug,viewer,starta,comment,sel,source,hotspot,pnf,portal,sitka,iell,slide,typ,sonic"
VolumeInfo = 0xe42f18a6
name = ""
for i in range(2) :
pos = VolumeInfo % len(candidates)
VolumeInfo = VolumeInfo / len(candidates)
VolumeInfo = ~VolumeInfo & 0xffffffff
name = name + get_name(pos, candidates)
print name[:-1]
main()
Dans mon cas, le nom du service s’appelle sonicredist
.
Au cours du processus d’installation, Emotet génère un autre nom, depuis une liste différente, pour supprimer peut-être une ancienne version.
La fonction de suppression commence à l’adresse mémoire 0x107CCE0
(w/ ASLR
) qui fait appel à DeleteFileW.
Dans mon cas, le fichier à supprimer est C:\\Windows\\SysWOW64\\satavg.exe
.
#!/usr/bin/env python2.7
def get_name(pos, candidates) :
if candidates[pos] == ',' :
pos = pos + 1
else :
for i in reversed(range(pos)) :
if candidates[i] == ',' :
break
pos = pos - 1
name = ""
for i in range(len(candidates)) :
if i >= pos :
name = name + candidates[i]
if candidates[i] == ',' :
break
return(name)
def main() :
candidates = "not,ripple,svcs,serv,wab,shader,single,without,wcs,define,eap,culture,slide,zip,tmpl,mini,polic,panes,earcon,menus,detect,form,uuidgen,pnp,admin,tuip,avatar,started,dasmrc,alaska,guids,wfp,adam,wgx,lime,indexer,repl,dev,mapi,resw,daf,diag,iss,vsc,turned,neutral,sat,source,enroll,mfidl,idl,based,right,cbs,radar,avg,wordpad,metagen,mouse,iprop,mdmmcd,jersey,thunk,subs"
VolumeInfo = 0xe42f18a6
name = ""
for i in range(2) :
pos = VolumeInfo % len(candidates)
VolumeInfo = VolumeInfo / len(candidates)
VolumeInfo = ~VolumeInfo & 0xffffffff
name = name + get_name(pos, candidates).replace(',', '')
print name
main()
Voici donc pour ce qui est de cette subtilité.
Cette opération va être rapide car nous avons effectué ce travail lors de notre première analyse. Il ne reste donc plus qu’à vérifier la condition de sortie de la boucle et coder …
#!/usr/bin/env python2.7
from sys import argv, exit
_R_EMOTET_ = """
rule Emotet_Payload {
strings :
// .text:00FC6860 | 33C0 | xor eax,eax
// .text:00FC6862 | C705 ???????? ???????? | mov dword ptr ds:[FD24A0],sonicredist.FCF710
// .text:00FC686C | C705 ???????? ???????? | mov dword ptr ds:[FD24A4],sonicredist.FCF710
// .text:00FC6876 | A3 ???????? | mov dword ptr ds:[FD24A8],eax
// .text:00FC687B | A3 ???????? | mov dword ptr ds:[FD24AC],eax
// .text:00FC6880 | ???? ???????? | cmp dword ptr ds:[FCF710],eax
// .text:00FC6886 | ?? ?? | je sonicredist.FC68AA
// .text:00FC6888 | ?? ?? | jmp sonicredist.FC6890
// .text:00FC688A | ???? ??????00 | lea ebx,dword ptr ds:[ebx]
// .text:00FC6890 | 40 | inc eax
// .text:00FC6891 | A3 ???????? | mov dword ptr ds:[FD24A8],eax
// .text:00FC6896 | 833CC5 ???????? 00 | cmp dword ptr ds:[eax*8+FCF710],0
// .text:00FC689E | 75 F0 | jne sonicredist.FC6890
// .text:00FC68A0 | 51 | push ecx
// .text:00FC68A1 | E8 ???????? | call sonicredist.FC1FA0
// .text:00FC68A6 | 83C4 04 | add esp,4
// .text:00FC68A9 | C3 | ret
$unpacked = { 33 C0 C7 05 ?? ?? ?? ?? ?? ?? ?? ?? C7 05 ?? ?? ?? ?? ?? ?? ?? ?? A3 ?? ?? ?? ?? A3 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 00 40 A3 ?? ?? ?? ?? 83 3C C5 ?? ?? ?? ?? 00 75 F0 51 E8 ?? ?? ?? ?? 83 C4 04 C3 }
condition :
// check for MZ Signature at offset 0
(uint16(0) == 0x5A4D) and $unpacked
}
rule Emotet_Packed {
strings :
// .text:00402549 | BA ???????? | mov edx,malware.4037F0
// .text:0040254E | 29F6 | sub esi,esi
// .text:00402550 | 81F6 ???????? | xor esi,3CC159BB
// .text:00402556 | 52 | push edx
// .text:00402557 | B9 ???????? | mov ecx,40
// .text:0040255C | 51 | push ecx
// .text:0040255D | BA ???????? | mov edx,1000
// .text:00402562 | 52 | push edx
// .text:00402563 | BA ???????? | mov edx,688
// .text:00402568 | 52 | push edx
// .text:00402569 | B9 ???????? | mov ecx,0
// .text:0040256E | 51 | push ecx
// .text:0040256F | B9 ???????? | mov ecx,FFFFFFFF
// .text:00402574 | 51 | push ecx
// .text:00402575 | 8D1D ???????? | lea ebx,dword ptr ds:[<&VirtualAllocEx>]
// .text:0040257B | FF13 | call dword ptr ds:[ebx]
// .text:0040257D | 5A | pop edx
// .text:0040257E | 83F8 00 | cmp eax,0
// .text:00402581 | 0F84 ???????? | je malware.402A07
// .text:00402587 | 29DB | sub ebx,ebx
// .text:00402589 | 4B | dec ebx
// .text:0040258A | 21C3 | and ebx,eax
// .text:0040258C | 53 | push ebx
// .text:0040258D | 81FF ???????? | cmp edi,688
// .text:00402593 | 74 ?? | je malware.4025B7
// .text:00402595 | ???? | xor eax,eax
// .text:00402597 | ???? | sub eax,dword ptr ds:[edx]
// .text:00402599 | ???? | neg eax
// .text:0040259B | 8D52 ?? | lea edx,dword ptr ds:[edx+4]
// .text:0040259E | 83C0 ?? | add eax,FFFFFFEF
// .text:004025A1 | ???? | xor eax,esi
// .text:004025A3 | 83C0 ?? | add eax,FFFFFFFF
// .text:004025A6 | ???? | mov esi,eax
// .text:004025A8 | 8943 ?? | mov dword ptr ds:[ebx],eax
// .text:004025AB | 8D?? ?? | lea ebx,dword ptr ds:[ebx+4]
// .text:004025AE | 8D?? ?? | lea edi,dword ptr ds:[edi+4]
// .text:004025B1 | 68 ???????? | push malware.40258D
// .text:004025B6 | C3 | ret
$packed = { BA ?? ?? ?? ?? 29 F6 81 F6 ?? ?? ?? ?? 52 B9 ?? ?? ?? ?? 51 BA ?? ?? ?? ?? 52 BA ?? ?? ?? ?? 52 B9 ?? ?? ?? ?? 51 B9 ?? ?? ?? ?? 51 8D 1D ?? ?? ?? ?? FF 13 5A 83 F8 00 0F 84 ?? ?? ?? ?? 29 DB 4B 21 C3 53 81 FF ?? ?? ?? ?? 74 ?? ?? ?? ?? ?? ?? ?? 8D 52 ?? 83 C0 ?? ?? ?? 83 C0 ?? ?? ?? 89 43 ?? 8D ?? ?? 8D ?? ?? 68 ?? ?? ?? ?? C3 }
condition :
// check for MZ Signature at offset 0
(uint16(0) == 0x5A4D) and $packed
}
"""
def cli_args() :
import argparse
parser = argparse.ArgumentParser(
add_help = False,
description = "Emotet 2019 - Get Configuration Tools"
)
optional = parser._action_groups.pop()
required = parser.add_argument_group("required arguments")
required.add_argument(
"--sample", "-s", action = "store",
help = "Path to sample file to input."
)
required.add_argument(
"--out-file", "-o", action = "store",
help = "Path to unpacked file out."
)
optional.add_argument(
"--in-stage1", "-i1", action = "store",
help = "Path to stage1 file to input."
)
optional.add_argument(
"--help", "-h", action = "store_true",
help = argparse.SUPPRESS
)
parser._action_groups.append(optional)
return(cli_args_helper(parser.parse_args(), parser))
def cli_args_helper(arguments, parser) :
# Help message and exit.
if((arguments.help) or (len(argv) <= 1)) :
parser.print_help()
exit(0)
return(arguments)
def convert_bytes(size) :
for x in ["Bytes", "Kb", "Mb", "Gb", "Tb"] :
if size < 1024.0 :
return("%3.1f %s" % (size, x))
size /= 1024.0
def digest(algorithm, data) :
if algorithm == "md5" :
from hashlib import md5
return(md5(data).hexdigest())
elif algorithm == "sha1" :
from hashlib import sha1
return(sha1(data).hexdigest())
elif algorithm == "sha256" :
from hashlib import sha256
return(sha256(data).hexdigest())
def exiting(message, ret_code) :
print("[!!] %s\nExiting..." % (message))
exit(ret_code)
def file_size(path_to_file) :
from os import stat
from os.path import isfile
if isfile(path_to_file) :
file_info = stat(path_to_file)
return(convert_bytes(file_info.st_size))
return(0)
def get_stage1(file_stream, at_offset, size, xor_key, add_value1, add_value2) :
from binascii import hexlify, unhexlify
from struct import pack
start_offset = at_offset
max_offset = size
data = ""
for i in range(0, max_offset, 4) :
bytes = hexlify(read_from_hex_offset(file_stream, str(hex(start_offset)), 4))
bytes = int(hexlify(pack("<L", int(bytes, 16))), 16)
bytes = xor_with_addition(bytes, xor_key, add_value1, add_value2)
bytes = hexlify(pack("<L", bytes))
xor_key = int(hexlify(pack("<L", int(bytes, 16))), 16)
data = data + unhexlify(bytes)
start_offset = start_offset + 4
return(data)
def hex_to_ip(ip) :
from socket import inet_ntoa
from struct import pack
return(inet_ntoa(pack("<L", ip)))
def parse_stage0(signature, image_base, raw_base) :
from binascii import hexlify
from struct import pack
print("\n ++ [ parse_stage0 ] ++")
xor_key = int(hexlify(pack("<L", int(hexlify(signature[0].strings[0][2][9:13]), 16))), 16)
v1_add = int(hexlify(signature[0].strings[0][2][-23:-22]), 16)
v2_add = int(hexlify(signature[0].strings[0][2][-18:-17]), 16)
at_offset = (int(hexlify(pack("<L", int(hexlify(signature[0].strings[0][2][1:5]), 16))), 16) - image_base) - raw_base
max_offset = int(hexlify(pack("<L", int(hexlify(signature[0].strings[0][2][27:31]), 16))), 16)
print(" xor_key:\t%s\n v1_add:\t%s\n v2_add:\t%s\n at_offset:\t%s\n max_offset:\t%s" % (
str(hex(xor_key)),
str(hex(v1_add)),
str(hex(v2_add)),
str(hex(at_offset)),
str(hex(max_offset)),
))
return((xor_key, v1_add, v2_add, at_offset, max_offset))
def parse_stage1(stage1) :
from binascii import hexlify
from struct import pack
print("\n ++ [ parse_stage1 ] ++")
xor_key = int(hexlify(pack("<L", int(hexlify(stage1[-284:-280]), 16))), 16)
v1_sub = int(hexlify(pack("<L", int(hexlify(stage1[-354:-350]), 16))), 16)
v2_sub = int(hexlify(pack(">L", int(hexlify(stage1[-286:-285]), 16))), 16)
at_offset = int(hexlify(pack("<L", int(hexlify(stage1[-16:-12]), 16))), 16)
max_offset = int(hexlify(pack("<L", int(hexlify(stage1[-12:-8]), 16))), 16)
print(" xor_key:\t%s\n v1_sub:\t%s\n v2_sub:\t%s\n at_offset:\t%s\n max_offset:\t%s" % (
str(hex(xor_key)),
str(hex(v1_sub)),
str(hex(v2_sub)),
str(hex(at_offset)),
str(hex(max_offset)),
))
return((xor_key, v1_sub, v2_sub, at_offset, max_offset))
def pe_info(path_to_file) :
from pefile import PE
pe = PE(path_to_file)
image_base = int(pe.OPTIONAL_HEADER.ImageBase)
data = read_file_to_buffer(path_to_file, "rb")
print("filename:\t%s\nSize:\t\t%s\nArchitecture\t%s\n\nMD5:\t\t%s\nSHA1:\t\t%s\nSHA256:\t\t%s\n" % (
path_to_file.split('/')[::-1][0],
file_size(path_to_file),
hex(pe.FILE_HEADER.Machine),
digest("md5", data),
digest("sha1", data),
digest("sha256", data)
))
pe.close()
signature = run_yara(_R_EMOTET_, data)
if (signature) :
print("emotet:\t\t%s\nsignature:\t%s (at offset)\n" % (signature[0].strings[0][1], hex(signature[0].strings[0][0])[:-1]))
return((image_base, signature))
def read_file_to_buffer(path_to_file, mode) :
from os.path import isfile
if not(isfile(path_to_file)) : return(False)
try :
with open(path_to_file, mode) as file_stream :
file_content = file_stream.read()
except Exception as Exception_Error :
print("%s", (Exception_Error))
return(False)
return(file_content)
def read_from_hex_offset(file_stream, hex_offset, bytes_to_read) :
offset = int(hex_offset, 16)
file_stream.seek(offset)
return(file_stream.read(bytes_to_read))
def run_yara(rule_code, buffer_to_sample) :
from yara import compile
try :
yar_object = compile(source = rule_code)
matches = yar_object.match(data = buffer_to_sample)
except Exception as Exception_Error :
print("%s", (Exception_Error))
return(False)
if len(matches) != 0 :
return(matches)
return(False)
def unpack(stage1, file_stream, size_of_raw_data) :
from binascii import hexlify, unhexlify
from struct import pack
xor_key, sub_vector1, sub_fixed_value, at_offset, max_offset = parse_stage1(stage1)
sub_key = (int(hexlify(pack("<L", int(hexlify(stage1[-4:]), 16))), 16) + sub_vector1) & 0xffffffff
start_offset = at_offset - size_of_raw_data
data = ""
for i in range(0, max_offset, 4) :
bytes = hexlify(read_from_hex_offset(file_stream, str(hex(start_offset)), 4))
next_sub_key = int(bytes, 16)
bytes = int(hexlify(pack("<L", int(bytes, 16))), 16)
bytes = xor_with_subtraction(bytes, xor_key, sub_key, sub_fixed_value)
bytes = hexlify(pack("<L", bytes))
sub_key = int(hexlify(pack("<L", next_sub_key)), 16)
data = data + unhexlify(bytes)
start_offset = start_offset + 4
return(data)
def write_file(path_to_file, mode, data) :
try :
with open(path_to_file, mode) as written_file :
written_file.write(data)
except IOError :
return(False)
return(True)
def xor_with_addition(bytes, xor_key, v1_add, v2_add) :
v1_add = int("0xffffff" + str(hex(v1_add)).replace("0x", ''), 16)
v2_add = int("0xffffff" + str(hex(v2_add)).replace("0x", ''), 16)
bytes = (bytes + v1_add) & 0xffffffff ## add eax, FFFFFFEF
bytes = (bytes ^ xor_key) & 0xffffffff ## xor eax, esi
bytes = (bytes + v2_add) & 0xffffffff ## add eax, FFFFFFFF
return(bytes)
def xor_with_subtraction(bytes, xor_key, v1_sub, v2_sub) :
bytes = (bytes - v2_sub) & 0xffffffff ## sub eax, 7
bytes = (bytes ^ xor_key) & 0xffffffff ## xor eax, 954132AC
bytes = (bytes - v1_sub) & 0xffffffff ## sub eax, edx
return(bytes)
def get_config(data, signature, image_base, raw_base) :
from binascii import hexlify
from struct import pack
print("[+] IP adresses list")
pos_to_ip = (int(hexlify(pack("<L", int(hexlify(signature[0].strings[0][2][8:12]), 16))), 16) - image_base) - raw_base
ip = -1
while ip != 0 :
ip = int(hexlify(read_from_hex_offset(data, str(hex(pos_to_ip)), 4)), 16)
if ip != 0 :
print(" 0x%X\t<->\t%s" % (ip, hex_to_ip(ip)))
pos_to_ip = pos_to_ip + 8
def main() :
from pefile import PE
params = cli_args()
print("Emotet 2019 - Get Configuration Tools\n\t by mekhalleh [www.pirates.re]\n")
read_file = open(params.sample, "rb")
## Get image base address, Yara signature and show PE informations:
print("%s" % ("=" * 40))
image_base, signature = pe_info(params.sample)
if (not(signature) and not(params.in_stage1)) : exiting("No supported version of Emotet or signature no found!", -1)
if ((signature != False) and (str(signature[0]) == "Emotet_Packed")) or ((signature != False) and (str(signature[0]) == "Emotet_Packed") and params.in_stage1) or ((signature == False) and (params.in_stage1)) :
print("%s" % ("=" * 40))
print("[+] Start unpacking Emotet")
pe = PE(params.sample)
raw_base = int(pe.sections[1].SizeOfRawData & 0xffff) ## section:.joi -> TODO: fix that?
pe.close()
if(params.in_stage1) :
print("\n ++ [ load from exported stage1 ] ++")
stage1 = read_file_to_buffer(params.in_stage1, "rb")
else :
if ((signature != False) and (signature[0].strings[0][1] == "$packed")) :
xor_key, v1_add, v2_add, at_offset, max_offset = parse_stage0(signature, image_base, raw_base)
stage1 = get_stage1(read_file, at_offset, max_offset, xor_key, v1_add, v2_add)
unpacked = unpack(stage1, read_file, raw_base)
if not(write_file(params.out_file, "wb", unpacked)) :
exiting("Unable to create/write file on disk!", -1)
print("\n[+] Write extracted file to <%s>\n" % (params.out_file))
image_base, signature = pe_info(params.out_file)
if ((signature) and (signature[0].strings[0][1] == "$unpacked")) :
read_file = open(params.out_file, "rb")
else : exiting("No supported version of Emotet or signature no found!", -1)
## Read Emotet configuration:
if (signature[0].strings[0][1] == "$unpacked") :
print("%s" % ("=" * 40))
pe = PE(params.out_file)
raw_base = int(pe.sections[2].SizeOfRawData & 0xffff) ## section:.data -> TODO: fix that?
pe.close()
get_config(read_file, signature, image_base, raw_base)
if(__name__ == '__main__') :
main()
exit(0)
Le mérite en revient aux éditeurs d’antivirus qui doivent avoir un travail d’analyse énorme. J’ai testé mon script sur plusieurs souches téléchargées sur Hybrid Analysis avec un résultat plutôt satisfaisant, jusqu’à ce que … je trouve une autre variante d’Emotet
.
En analysant un peu rapidement, nous pouvons supposer que cette variante est plus récente que la précédente. Elle est basée sur le même schéma d’exécution mais avec un algorithme un peu différent (pour rendre encore plus compliquer le travail de l’analyste).
Ceci dit, avant de finir, j’ai quand même eu très envie de savoir si le payload
(le binaire unpacké) avait changé ! Et c’est bien une nouvelle souche d’Emotet
.
Nous pouvons le vérifier avec le plugin diaphora
et IDA
…
Pour réaliser la signature, nous pouvons (sans doute) nous baser sur la même méthode que la version 5
et en cherchant un peu…
Nous pouvons utiliser l’outil mkYARA et extraire la signature depuis IDA
(en tant que plugin) ou directement en ligne de commande.
Les scripts finaux sont disponibles sur Github.