Résolution d'API Hashing avec BinaryNinja

Utilisation de l'API de BinaryNinja pour automatiser la résolution d'APIs

Introduction

Récemment j’ai abandonné IDA Pro, logiciel de désassemblage professionnel, pour Binary Ninja. Binary Ninja (ou Binja) est un désassembleur complet, offrant plus ou moins les mêmes capacités qu’IDA Pro.

Une des forces de ce logiciel, outre le fait qu’il intègre sur la licence basique la prise en charge de toutes les architectures communes (x86, x64, ARM), un débugger et un decompiler, réside dans son API. Elle permet d’automatiser beaucoup de choses dans le workflow d’un analyste de malwares, il est possible d’interagir avec le binaire en mode statique (cela va de soit) mais également avec le débugger, ce qui permet par exemple de créer des unpackers automatisés.

Pour apprendre efficacement à utiliser ce logiciel et son API, quoi de mieux que d’analyser un sample d’IcedID, trojan particulièrement en vogue en ce moment, et voir ce que Binja a à nous offrir.

Unpacking

Au départ, je pensais réaliser une analyse complète sur ce sample. IcedID est comme revenu d’entre les morts et je pensais bon de voir ce qui avait changé.

Mais je suis tombé sur un sample buggé. Je l’ai unpacké manuellement la première fois, avant d’envoyer le sample à Unpac.me pour économiser du temps.

Pour faire bref, voici comment l’unpacking de ce sample se déroule :

  • Déchiffre un shellcode et l’exécute
  • Le shellcode vient remplacer les sections du malware original, remplaçant ainsi la totalité du code initial
  • Le malware hook ensuite ZwCreateProcess avec une fonction custom
  • Un appel à CreateProcessW est effectué pour le programme svchost.exe
  • La fonction hookée va créer le processus svchost.exe en mode suspendu, puis injecter du code à l’intérieur
  • Svchost va déchiffrer le prochain stage et créer une tâche planifiée chargée d’exécuter le binaire, déclenchée au prochain login de l’utilisateur.

Il va répéter ce processus plusieurs fois, chaque fois avec la même technique, mais tout en retirant de plus en plus d’obfuscation sur les binaires. Cette technique fonctionne très bien, car elle empêche une sandbox d’avoir accès au code “en clair” du binaire, car plusieurs logins de l’utilisateur sont nécessaires.

Mais alors pourquoi n’ai je pas pu continuer l’analyse?

1
2
3
4
5
6
00401c79  push    dword [esp+0x4 {size}] {var_4}
00401c7d  push    0x8 {var_8}
00401c7f  call    dword [GetProcessHeap]
00401c85  push    eax {var_c}
00401c86  call    dword [HeapAlloc]
00401c8c  retn     {__return_addr}

Cette fonction est très simple, elle est chargée d’allouer de la mémoire dans le processus. Or, HeapAlloc va toujours retourner 0 à un moment dans le code, ce qui va empêcher le programme d’allouer de la mémoire supplémentaire et ainsi continuer le processus.

Si on se réfère à l’analyse Joe Sandbox, nous pouvons constater qu’aucun domaine ou IP n’a été contacté, ou encore qu’aucun comportement autre que celui présent pendant l’unpacking n’a eu lieu, ce qui me donne à penser que ce sample spécifique est éventuellement buggé.

Mais je n’ai pas pour autant perdu du temps en l’analysant.

Api Hashing

Ce sample utilise de l’API Hashing, technique très utilisée par les malwares pour obscurcir leur fonctionnement aux antivirus ou aux analystes.

Pour rappel, une API Windows est une fonction proposée par Microsoft pour interagir avec le système d’exploitation. Par exemple, l’API CreateThread est la seule façon de créer un thread sous Windows.

Au lieu d’appeler les APIs Windows normalement (ce qui les feraient apparaître dans la table des imports et ainsi pouvant être utilisées à des fins de détection), les malwares utilisent une technique permettant de résoudre les adresses de ces APIs dynamiquement.

Les APIs sont des fonctions exportées par les DLL Windows (kernel32.dll, NTDLL.dll pour ne citer que les plus courantes), ainsi il est possible de récupérer leurs addresses pour les utiliser dynamiquement sans avoir à les importer.

Voici comment fonctionne l’API hashing:

  • Lors de la phase de développement du malware, l’auteur va choisir un algorithme de hashing pour hasher les noms des différentes APIs qu’il souhaite utiliser (exemple, CreateProcessW devient 5c856c47 en utilisant CRC32)
  • Lors de l’exécution, le malware va d’abord récupérer l’adresse de la DLL contenant les fonctions à importer. Cela peut se faire grâce à l’API GetModuleHandle(’nom_dll’) ou au PEB.
  • Ensuite, le programme va parcourir la liste des fonctions exportées par la DLL, récupérer leur nom et leur adresse.
  • Pour chaque nom d’API, le malware hashera celui-ci avec le même algorithme de hash utilisé pour précompiler les APIs à utiliser, puis le comparera au hash recherché.
  • Si ils correspondent, l’adresse de la fonction sera retournée au programme ou stockée dans une variable.

Comment reverse une fonction d’API hashing efficacement?

Dans ce sample, voici comment est utilisée l’API hashing :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
    HINSTANCE eax = GetModuleHandleA("NTDLL.DLL");
    if (eax != 0)
    {
        data_402000 = 1;
        int32_t esi_3 = (((sub_4012c2(eax, eax, 0, 0xe50cd451, &data_402030, 0x402076) | sub_4012c2(eax, eax, 0, 0xb0d89fb2, &data_402028, 0x402070)) | sub_4012c2(eax, eax, 0, 0xd37bdaeb, &data_402008, 0x402058)) | sub_4012c2(eax, eax, 0, 0xf4b15f66, &data_402040, 0x402082));
        int32_t esi_7 = ((((esi_3 | sub_4012c2(eax, eax, 0, 0x8c795ddf, &data_402018, 0x402064)) | sub_4012c2(eax, eax, 0, 0xc5509c94, &data_402010, 0x40205e)) | sub_4012c2(eax, eax, 0, 0xae46d1e4, &data_402020, 0x40206a)) | sub_4012c2(eax, eax, 0, 0xfd06b77e, &data_402048, 0x402088));
        int32_t esi_8 = (esi_7 | sub_4012c2(eax, eax, 0, 0x2d7fdd26, &data_402038, 0x40207c));
        int32_t eax_11 = (sub_4012c2(eax, eax, 0, 0x530c1aee, &data_402050, 0x40208e) | esi_8);
        int32_t eax_12 = (-eax_11);
        return ((eax_12 - eax_12) + 1);
    }
    return eax;
}

Nous pouvons voir dans un premier temps que l’adresse de la librairie NTDLL.dll est récupérée.

Ensuite, plusieurs appels vers la même fonction, avec des arguments clairs (les 2 premiers représentent le handle de NTDLL, le 4 ème correspond au hash à résoudre et le 5ème est la variable où sera stockée l’adresse de l’API résolue).

Lorsque vous identifiez une fonction avec des arguments similaires, vérifiez les cross-references. Si elle est appelée de nombreuses fois, il se peut fortement qu’il s’agisse d’une fonction d’API hashing.

Trouver l’algorithme de hashing

La partie la plus importante sera de trouver l’algorithme de hashing utilisé. Il s’agit souvent d’un algorithme commun, des outils comme capa pourront éventuellement le détecter. Mais parfois, les auteurs peuvent modifier des algorithmes déjà connus ou en créer un de toutes pièces.

C’est pourquoi il est utile d’utiliser un débugger lorsque vous avez trouvé la fonction de hashing, ce qui facilite la tâche.

Dans mon cas, la fonction utilisée pour créer un hash du nom de l’API était ROT13, couplée à un XOR dont la valeur est hardcodée dans le binaire :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
while (true)
    {
        int32_t eax;
        eax = *esi;
        if (eax == 0)
        {
            break;
        }
        ecx = ((RORD(ecx, 0xd)) + eax);
        esi = &esi[1];
    }
...
if (*eax_1 != (ecx ^ 0x401056))

Répliquer l’algorithme de hashing et créer une lookup table

Une fois l’algorithme compris, j’ai répliqué son fonctionnement en Python et créé une lookup table. Celle-ci contiendra tous les noms des APIs de la DLL choisie (ici NTDLL) et leur hash correspondant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import json

def ROR(x, n, bits = 32):
    mask = (2**n) - 1
    mask_bits = x & mask
    return (x >> n) | (mask_bits << (bits - n))

def hash_api(api):

    h = 0
    for a in api:
        h = ROR(h, 13)
        h += ord(a)

    return (hex(4198486 ^ h))

apis = []

with open("NTDLL_apis.txt", 'r') as f:
    lines = f.readlines()
    for line in lines:
        api_couple = {}
        api_couple["api"] = line[:-1]
        api_couple["icedid_hash"] = hash_api(line[:-1])
        apis.append(api_couple)
      
apis = json.dumps(apis)

with open("hash_apis.json", 'a') as f:
    f.write(apis)

Ce script parcourt une liste des APIs, extraite grâce à un petit programme que j’ai codé en C (mais il est aussi possible de le faire en Python, et bien plus simple) et hash toutes les APIs avant de produire un fichier json qui sera notre lookup table :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
...
{"api": "EtwEnumerateProcessRegGuids", "icedid_hash": "0x91246a5e"}, 
{"api": "EtwEventActivityIdControl", "icedid_hash": "0xd5072de7"}, 
{"api": "EtwEventEnabled", "icedid_hash": "0xf0906dff"}, 
{"api": "EtwEventProviderEnabled", "icedid_hash": "0xb51b3f6e"}, 
{"api": "EtwEventRegister", "icedid_hash": "0xfb73ec0e"}, 
{"api": "EtwEventSetInformation", "icedid_hash": "0x4f3be0bb"}, 
{"api": "EtwEventUnregister", "icedid_hash": "0xf55690dc"}, 
{"api": "EtwEventWrite", "icedid_hash": "0x2007d3b8"}, 
{"api": "EtwEventWriteEndScenario", "icedid_hash": "0x72faa875"}, 
{"api": "EtwEventWriteEx", "icedid_hash": "0x1458ec56"}, 
{"api": "EtwEventWriteFull", "icedid_hash": "0xbdeefe7"}, 
{"api": "EtwEventWriteNoRegistration", "icedid_hash": "0xdd452084"}
...

Automatisation

Maintenant que nous avons la lookup table, il est temps de résoudre ces APIs sur BinaryNinja. Bien sûr, ce processus n’aura aucune incidence sur le binaire, il nous permettra juste de mieux nous y retrouver durant l’analyse statique.

Nous avons toutes les informations nécessaires:

  • L’adresse de la fonction de résolution des APIs
  • Le hash recherché
  • La variable où sera stockée l’adresse de l’API

La première étape consiste à récupérer la liste de toutes les cross-references de la fonction de résolution :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> refs = bv.get_code_refs(0x4012c2)
>>> for ref in refs:
... 	print(ref.mlil)
... 
eax_1 = 0x4012c2(var_24, var_20, 0, 0xe50cd451, 0x402030, 0x402076)
eax_2 = 0x4012c2(var_3c, var_38, 0, 0xb0d89fb2, 0x402028, 0x402070)
eax_3 = 0x4012c2(var_54, var_50, 0, 0xd37bdaeb, 0x402008, 0x402058)
eax_4 = 0x4012c2(var_24_1, var_20_1, 0, 0xf4b15f66, 0x402040, 0x402082)
eax_5 = 0x4012c2(var_3c_1, var_38_1, 0, 0x8c795ddf, 0x402018, 0x402064)
eax_6 = 0x4012c2(var_54_1, var_50_1, 0, 0xc5509c94, 0x402010, 0x40205e)
eax_7 = 0x4012c2(var_24_2, var_20_2, 0, 0xae46d1e4, 0x402020, 0x40206a)
eax_8 = 0x4012c2(var_3c_2, var_38_2, 0, 0xfd06b77e, 0x402048, 0x402088)
eax_9 = 0x4012c2(var_54_2, var_50_2, 0, 0x2d7fdd26, 0x402038, 0x40207c)
eax_10 = 0x4012c2(var_24_3, var_20_3, 0, 0x530c1aee, 0x402050, 0x40208e)
eax_1 = 0x4012c2(nullptr, var_24_1, var_20_1, 0xe50cd451, 0x4020c8, 0x40210e)
eax_2 = 0x4012c2(nullptr, var_3c_1, var_38_1, 0xb0d89fb2, 0x4020c0, 0x402108)
eax_3 = 0x4012c2(nullptr, var_54_1, var_50_1, 0xd37bdaeb, 0x4020a0, 0x4020f0)
eax_4 = 0x4012c2(nullptr, var_24_2, var_20_2, 0xf4b15f66, 0x4020d8, 0x40211a)
eax_5 = 0x4012c2(nullptr, var_3c_2, var_38_2, 0x8c795ddf, 0x4020b0, 0x4020fc)
eax_6 = 0x4012c2(nullptr, var_54_2, var_50_2, 0xc5509c94, 0x4020a8, 0x4020f6)
eax_7 = 0x4012c2(nullptr, var_24_3, var_20_3, 0xae46d1e4, 0x4020b8, 0x402102)
eax_8 = 0x4012c2(nullptr, var_3c_3, var_38_3, 0xfd06b77e, 0x4020e0, 0x402120)
eax_9 = 0x4012c2(nullptr, var_54_3, var_50_3, 0x2d7fdd26, 0x4020d0, 0x402114)
eax_10 = 0x4012c2(nullptr, var_24_4, var_20_4, 0x530c1aee, 0x4020e8, 0x402126)

Là où BinaryNinja se révèle plus efficace qu’IDA, c’est que nous pouvons interagir directement en mode IL (Intermediate language) où le code ressemble ainsi plus à du C, ce qui permet de récupérer plus facilement les arguments d’une fonction par exemple.

Voici comment récupérer le hash de la fonction à résoudre pour chaque appel à la fonction :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
for ref in refs:
	print(ref.mlil.params[3])

0xe50cd451
0xb0d89fb2
0xd37bdaeb
0xf4b15f66
0x8c795ddf
0xc5509c94
0xae46d1e4
0xfd06b77e
0x2d7fdd26
0x530c1aee
0xe50cd451
0xb0d89fb2
0xd37bdaeb
0xf4b15f66
0x8c795ddf
0xc5509c94
0xae46d1e4
0xfd06b77e
0x2d7fdd26
0x530c1aee

Enfin, voici comment récupérer la variable où l’adresse de la fonction sera stockée :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
for ref in refs:
	print(ref.mlil.params[4].constant)
 
4202544
4202536
4202504
4202560
4202520
4202512
4202528
4202568
4202552
4202576
4202696
4202688
4202656
4202712
4202672
4202664
4202680
4202720
4202704
4202728

Si nous mettons tout cela ensemble à l’aide d’un script Python :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from binaryninja import BinaryView
import json

ADDRESS = 0x4012c2

#Load la lookup table à partir du fichier json
with open('/Users/lordtmk/Malwares/IcedID/hash_apis.json', 'r') as f:
    lookup = json.load(f)

refs = bv.get_code_refs(0x4012c2)

log_info("Starting...")

for ref in refs:
    hash_value = ref.mlil.params[3]
    address_pointer = ref.mlil.params[4].constant
    key = [x for x in lookup if x["icedid_hash"] == str(hash_value)] #Recherche la clé de la lookup table pour le hash en cours
    log_info(f"Found api {key[0]['api']} for this hash !")
    bv.define_data_var(address_pointer, "void*", key[0]['api']) #Renomme la variable avec le nom de l'API trouvée

Voici le résultat après l’utilisation du script :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[Python Console] Running script from file: /Users/lordtmk/Malwares/IcedID/decode_api_hashing.py
[Default] Starting...
[Default] Found api LdrGetProcedureAddress for this hash !
[Default] Found api LdrLoadDll for this hash !
[Default] Found api NtAllocateVirtualMemory for this hash !
[Default] Found api NtCreateUserProcess for this hash !
[Default] Found api NtProtectVirtualMemory for this hash !
[Default] Found api NtWriteVirtualMemory for this hash !
[Default] Found api NtWaitForSingleObject for this hash !
[Default] Found api RtlDecompressBuffer for this hash !
[Default] Found api RtlExitUserProcess for this hash !
[Default] Found api NtFlushInstructionCache for this hash !
[Default] Found api LdrGetProcedureAddress for this hash !
[Default] Found api LdrLoadDll for this hash !
[Default] Found api NtAllocateVirtualMemory for this hash !
[Default] Found api NtCreateUserProcess for this hash !
[Default] Found api NtProtectVirtualMemory for this hash !
[Default] Found api NtWriteVirtualMemory for this hash !
[Default] Found api NtWaitForSingleObject for this hash !
[Default] Found api RtlDecompressBuffer for this hash !
[Default] Found api RtlExitUserProcess for this hash !
[Default] Found api NtFlushInstructionCache for this hash !
[Analysis] Analysis update took 0.025 seconds
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
    HINSTANCE eax = GetModuleHandleA("NTDLL.DLL");
    if (eax != 0)
    {
        data_402000 = 1;
        int32_t esi_3 = (((mw_resolve_api_by_hash(eax, eax, 0, 0xe50cd451, &LdrGetProcedureAddress, 0x402076) | mw_resolve_api_by_hash(eax, eax, 0, 0xb0d89fb2, &LdrLoadDll, 0x402070)) | mw_resolve_api_by_hash(eax, eax, 0, 0xd37bdaeb, &NtAllocateVirtualMemory, 0x402058)) | mw_resolve_api_by_hash(eax, eax, 0, 0xf4b15f66, &NtCreateUserProcess, 0x402082));
        int32_t esi_7 = ((((esi_3 | mw_resolve_api_by_hash(eax, eax, 0, 0x8c795ddf, &NtProtectVirtualMemory, 0x402064)) | mw_resolve_api_by_hash(eax, eax, 0, 0xc5509c94, &NtWriteVirtualMemory, 0x40205e)) | mw_resolve_api_by_hash(eax, eax, 0, 0xae46d1e4, &NtWaitForSingleObject, 0x40206a)) | mw_resolve_api_by_hash(eax, eax, 0, 0xfd06b77e, &RtlDecompressBuffer, 0x402088));
        int32_t esi_8 = (esi_7 | mw_resolve_api_by_hash(eax, eax, 0, 0x2d7fdd26, &RtlExitUserProcess, 0x40207c));
        int32_t eax_11 = (mw_resolve_api_by_hash(eax, eax, 0, 0x530c1aee, &NtFlushInstructionCache, 0x40208e) | esi_8);
        int32_t eax_12 = (-eax_11);
        return ((eax_12 - eax_12) + 1);
    }
    return eax;
}

Les APIs ont été correctement résolues, ce qui permet ensuite de comprendre plus facilement les capacités du malware.

Conclusion

Ainsi s’achève mon tout premier article utilisant le logiciel Binary Ninja, il me reste encore beaucoup de choses à découvrir sur celui-ci, et son API me donne beaucoup d’idées pour la suite.

Pour ceux qui veulent tester BinaryNinja, il existe une version demo plutôt complète.

comments powered by Disqus
Généré avec Hugo
Thème Stack conçu par Jimmy