Emotet v5 Reversing (unpacking, basic analysis and Indiana Jones) [part. II]

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 :

  • le registre ESI prend pour valeur « 3CC159BB » (la première clé XOR) à l’adresse 0x00402550.
  • le call VirtualAllocEx, à l’adresse 0x0040257B, sert à allouer de l’espace mémoire qui contiendra le code d’unpacking.

  • la boucle décrypte et charge les données dans la mémoire.
  • le registre 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 :

  • Nous relançons le programme. Nous activons TitanHide sur le process et nous nous positionnons sur le point d’entrée de celui-ci, à l’adresse 0x00402404.
  • Nous posons un « break point » sur l’adresse 0x004025CB.
  • Nous sollicitons la touche 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 :

  • Nous positionnons un « Hardware break point » à l’adresse 0x00220028 (dans mon cas), puis F9 pour nous rendre à cette adresse.
  • Nous sollicitons la touche 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)

Les règles Yara et Python

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é || 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)

La génération des noms (exécutable et service)

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

Obtenir la configuration (les adresses IP)

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)

Conclusions

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 (https://github.com/PiratesRE/emoteted).

Dans le prochain article dédié à Emotet, nous étudierons le protocole de communication pour essayer de comprendre comment tout ceci fonctionne.

Ce contenu a été publié dans Reverse Engineering. Vous pouvez le mettre en favoris avec ce permalien.