GuLoader : Analyse | Part one | (French version)

January 14, 2023    malware guloader loader dropper

Hello,

Si vous avez suivi les différents billets de PROLOG de ce blog, alors le temps est venu de s’attaquer à un bon client pour l’analyse de Malware, et surtout des techniques d’évasion et d’anti : le loader GULOADER. GuLOADER c ’est un peu comme le boss de fin des techniques anti-*. En avant pour la Hard way

Cet article est inspiré d’une session Twitch de l’excellent Sergei Frankoff aka @herrcore. De toutes les personnes que je suis en Reverse Engineering, c’est bien lui le plus inspirant en analyse statique. Suivre Herrcore sur le statique et le prolifique @mrexodia sur l’analyse dynamique, c’est apprendre des meilleurs.

L’histoire de GuLoader

GuLoader est un dropper de Malware qui a été vu pour la première fois fin 2019. A l’époque il n’était encore qu’un ‘simple’ downloader utilisé pour diffuser des RAT tels que AgentTesla et Nanocore. Il est également connu et référencé sous le nom de CloudEyE).

Puis à partir de 2020, GuLoader se mit à intégrer un nombre très important de techniques d’évasion (cf. notre série sur ces techniques Anti). Notamment la technique anti qui consiste à ne pas appeler directement les fonctions d’API Windows par leur nom, mais par leur hash (cf. notre billet pour le détail de cette technique de API call by Hashing). Comme dans de nombreux malware, le dropper GuLoader utilise l’algorithme de Hash DJB2 qui a l’avantage d’être très simple et générant peu de collisions.

Puis en 2021 GuLoader s’est doté d’une armée de techniques d’Anti-debug, anti-Sandbox, anti-VM afin de rendre son analyse encore plus complexe. Cette complexification s’est accompagnée de l’utilisation de Visual Basic Script (VBS) pour se propager et de NSIS (Nullsoft Scriptable Install System) pour le packing et le chiffrement de son Payload.

Nous allons ici étudier une des très nombreuses versions de GuLoader. En effet, il en existe de très nombreuses car le groupe derrière ce dropper est très actif et scan tous les papiers concernant leur code. A chaque nouvelle analyse un peu sérieuse d’une de leur version, ils réagissent et modifient rapidement de manière radicale leur malware.

Par conséquent, nous devrions plutôt dire que nous allons analyser “une version” de GuLoader plutôt que GuLoader. En effet chaque update modifie complètement le profil du payload.

⚠️ DISCLAIMER ⚠️

A partir d'ici, je fais l'hypothèse que vous êtes familiers des précautions absolument nécessaires pour manipuler, ouvrir, analyser statiquement, debugger dynamiquement cette matière dangereuse que sont les MALWARES. Si cela n'est pas le cas, alors il faut tout de suite vous arrêter ici si vous ne voulez pas être infectés (et infecter) par les échantillons que nous allons manipuler. Ne le prenez pas à la légère car l'unique finalité des malwares et autres dropper que nous allons analyser est de vous faire du tort, et ce, en utilisant des mécanismes puissants, agressifs et furtifs.

Sur mon Blog, je ne souhaite pas écrire des articles sur comment monter son laboratoire d’analyse à base de VM. En effet, pour moi monter ce type de labo est une tache nécessaire mais dans laquelle je trouve peu d’intérêt intellectuel, je vous invite donc à suivre par exemple cet excellent guide afin de monter votre labo. Ainsi, vous pourrez faire exploser des malwares avec le plus de sécurité possible. Sachez néanmoins que même avec beaucoup de précautions (et surtout de rigueur), le risque zéro d’infection n’existe pas.

Juste pour votre information, mon labo d’analyse est constitué de la manière suivante:

  • Je n’utilise pas mon Mac de ‘daily’ pour cette activité
  • Utilisation d’un desktop orienté Gaming dédié, sous UBUNTU, nommé WOPR ;-)
  • Tout est VM VMWARE chez moi
  • Chaque Malware analysé en Debug l’est évidemment au sein d’une VM tournant sous Windows 11 avec 100% FLARE VM de MANDIANT d’installé (Host Based Indicators)
  • Les toutes dernières versions de FLARE VM fonctionnent parfaitement avec Windows 11 (contrairement aux anciennes qui avaient des problèmes en W10 et W11)
  • Les VM INFECTED ne communiquent pas avec le réseau du Desktop Hote (WOPR), ni avec le WIFI et internet : elles disposent de leur propre réseau virtuel privé en vase clos
  • J’utilise l’excellent RemNux pour émuler les services internet de base (HTTPx, SSHD, SMTP, SMB, …) et ainsi analyser les actions réseau des vilains canards (Network Based Indicators)
  • L’ensemble des VM INFECTED sont stockées sur un disque dur SSD externe et dédié
  • Le DD externe dédié avec son sticker BIOHAZARD est systématiquement débranché de l’hôte et rangé au tiroir
  • Définition et strict respect de Conventions de nommage des fichiers et des extensions au sein des VM INFECTED
  • Utilisation d’aucune extension dans les navigateurs des VM Infected (ex: n’allez pas y installer votre password manager pour gagner du temps )
  • ET SURTOUT, je ne télécharge JAMAIS mes samples sur le Darkweb (je préfère GITHUB, MALWARE BAZAR oy ANY.RUN)

Formez-vous sur tout cela AVANT de passer à la suite. Ne pas le faire vous expose de manière certaine à de gros problèmes.

GULOADER: On ouvre la boîte

Vous avez lu le Disclaimer ci-dessus ? Alors on y va.

Ce qui caractérise GuLoader, et donc fait son intérêt d’étude, ce sont les nombreuses techniques d’anti-* qu’il utilise:

Contre les analyses dynamiques :

  • Anti-VM : GuLoader vérifie l’absence de VMWare, QEMU, VirtualBox et se termine lors de toute détection
  • Anti-Sandbox : Le malware vérifie l’absence de système de sand-boxing tel que Cuckoo Sandbox
  • Anti-Debug : GuLoader vérifie si il est exécuté sous un debugger tel que WinDbg ou OllyDbg

Pour complexifier l’analyse statique :

  • Appels des API Windows, non pas par leur nom, mais par leur hash DJB2
  • Utilisation d’un VEH (Vectored Exception Handler) pour piloter le flux d’exécution (plutôt que par des JUMP et des CALL)
  • Offuscation du binaire et utilisation d’opaque predicates

Dans cette article, nous allons nous intéresser au Loader et à la désofuscation du shellcode. Nous traiterons la suite de l’analyse dans les articles suivants.

Le script NSIS

Nous allons vite passer cette partie car là n’est pas l’aspect intéressant ce GuLoader.

Depuis quelque temps, mes analyses statiques de binaires commencent toute par un scan avec l’excellent Detect It Easy codé par @horsicq.

Voici ce que DiE (lancé ici en mode console) pense de notre binaire mystère:

naptax@WOPR:~/diec 14d52119459ef12be3a2f9a3a6578ee3255580f679b1b54de0990b6ba403b0fe.7z 

PE32
    Installer: Nullsoft Scriptable Install System(3.08)[lzma,solid]
    Linker: Microsoft Linker(6.0*)[GUI32,signed]
    Overlay: NSIS data(-)[-]

DiE nous indique que le binaire pourrait être une archive compressée au format NSIS (Nullsoft Scriptable Install System). Qui dit NSIS, dit 7-Zip, alors ouvrons ce binaire avec 7-Zip:

Encore une fois, Detect It Easy avait vu juste. Intéressons-nous à ce gros fichier rudesbies.Par

Là encore, un premier scan avec DiE:

naptax@WOPR:~/diec rudesbies.Par

Mais cette fois, rien de connu dans la DB de signatures DiE.

Désofuscation du Shell Code Phase 1

Chargeons alors ce binaire dans BINARY NINJA. La première partie intéressante est ce petit bout de code, je vous ai mis en commentaire ce que nous pouvons en tirer: Jusqu’à l’adresse +0x00000025, que du JUNK Code n’ayant aucun impact sur le flux d’exécution.

La première instruction intéressante se situe à +0x00000025: un JMP qui nous mène sur un premier CALL +0x0000002A

Là encore, beaucoup de JUNK code, c’est pourquoi je n’ai conservé ci-dessous que les instructions de la fonction +0x0000002a qui nous intéressent:

**************************************************************
*                          FUNCTION                          *
**************************************************************
undefined FUN_0000002a()
   
   (...)
        00000048 5f              POP        EDI   
   
   (...)
        00000065 31 d2           XOR        EDX,EDX

LAB_00000090:

    (...)
        000000b5 81 34 17        XOR        dword ptr [EDI + EDX*0x1],0x919e1e2e
    
    (...)
        000000db 83 c2 04        ADD        EDX,0x4
        000000f7 81 fa 08        CMP        EDX,0x17208
        000000fd 75 91           JNZ        LAB_00000090
  • Le POP EDI stocke dans EDI la return address mis sur la pile au moment du CALL +0x0000002A
  • On met EDX à 0 (pour peut-être préparer une boucle …)
  • Ah :-) on XOR le code avec la clé 0x91 9e 1e 2e
  • Et on boucle 0x17208/4 fois, en avançant par 4 octets

Ok, on a donc une première boucle qui XOR tout le code à partir de l’adresse 0x0000014e avec 0x919e1e2e comme clé.

Créons un petit bout de RUST pour déchiffrer ce code offusqué:

use std::fs::File;
use std::io::prelude::*;
use std::io::{BufReader, BufWriter};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; 

fn main() {
    let file = File::open("/home/naptax/tmp/rudesbies.Par").unwrap(); // J ouvre le fichier

    let mut reader = BufReader::new(file); // J'en produis un Buffer
    let mut enc_code = vec![]; // Crée un nouveau vecteur contenant des u8

    reader.read_to_end(&mut enc_code).unwrap(); // Charge le contenu du buffer dans mon vecteur
    
    let code_offset = 0x0000014E; // offset du début du code à XORer 
    enc_code = enc_code[code_offset..].to_vec();
    
    let key: u32 = 0x919E1E2E; // la clé avec laquelle est réalisé le XOR

    let mut out = vec![]; // Crée un vecteur qui va recevoir le code déchiffré
    
    for i in 0..enc_code.len() {
        out.push(enc_code[i] ^ key.to_le_bytes()[i % 4]); // fait le XOR
    }
    
    let mut file = File::create("/home/naptax/tmp/stage2.bin").unwrap();
    let mut writer = BufWriter::new(file);
    
    writer.write_all(&out).unwrap();
}

Si vous préférez procéder au déchiffrement du code XORé en interne dans ❤️ BINARY NINJA ❤️, plutôt qu’en externe (ici en RUST), alors voici le code Python pour BINARY NINJA:

def decrypt(_address, _key, _len):
	xor_key = Transform['XOR']
	address = _address
	key = _key
	for i in range(_len):
		enc_str = bv.read(address, 4)
		decrypted_str = xor_key.decode(enc_str, {'key': key.to_bytes(4, 'little')})
		bv.write(address, decrypted_str)
		address += 4

key = 0x919E1E2E
code_offset = 0x0000014E

decrypt(0x0000014E,0x919E1E2E,0x16f55)

J’utilise ici le transformer XOR , directement fourni par l’API de BINARY NINJA ❤️

Allez, sauvegardons ce nouveau binaire fraîchement déchiffré et regardons ce stage2.bin avec ❤️ BINARY NINJA ❤️ (ou votre Disassembleur préféré).

Désofuscation du Shell Code Phase 2 : Arrivée du hashing

Une première lecture du code désassemblé laisse sceptique: Encore et encore du JUNK code :-(

Néanmoins, à force de JMP vers des JMP de CALL ;=) on fini par identifier tout de même plusieurs fonctions qui semblent procéder au calcul de constantes, et ce afin de les masquer. Et en RE, quand on constate que quelqu’un s’est donné du mal pour masquer une information : c’est là que l’on doit chercher ;-)

En 0x1308C, on voudrait nous masquer la constante 0x10000000 en la calculant par un hex((0x191AE730 ^ 0x320EB5D5 ^ 0xB8DB25E1) + 0x6C3088FC)

Puis en 0x127C1, on voudrait nous masquer la constante 0x539 en la calculant par un hex((0x96900857 + 0x10E451D0) ^ 0xAA6DFF89 ^ 0xD19A097)

Et à nouveau en 0x132C9, on voudrait nous masquer la constante 0x61 en la calculant par un hex((0xE22ECFA7 ^ 0xD05F809C ^ 0x4E1C381C) - 0x7C6D76C6))

Continuons le flux d’execution à la recherche d’autres “constantes indices” qui pourraient éclairer cette bien vilaine grotte toute obscure qu’est ce loader GuLoader …

Et on arrive enfin en 0x12AD5, avec la fonction suivante:

Cette fonction n’a pas l’apparence de JUNK code et semble implémenter un algo bien réel:

Si vous avez suivi mon petit article sur les techniques d’anti-disassembly, alors la constante 0x1505h (5381d) a du retenir votre attention.

En effet 5381 est une “constante signature” dans l’algorithme de hash DJB2. Cette algo de hash est très souvent utilisé pour faire du hashing dans les malwares (tout comme la valeur 256 caractérise un chiffrement RC4: cf mon article sur ce sujet). Il est simple, rapide et avec très peu de collisions.

On aurait donc cette fonction en 0x12AD5 qui implémenterait un hash DJB2. On va donc prendre cette hypothèse et nommer la fonction comme une maybe_calcul_hash().

Regardons maintenant qui appelle cette fonction de hash, pour voir si notre hypothèse tient la route:

Mais dites donc on ne serait pas en train d’essayer de nous masquer des appels API Windows par la technique d’API Hash Calling en utilisant un Hash DJB2 (ici sur mon blog) ??? !!

Dans tous les cas, les auteurs de ce machin se sont donnés bien du mal pour nous le cacher…

Continuons notre enquête …

Désofuscation du Shell Code Phase 3 : Les fonctions Windows appelées

Si les auteurs se sont donnés la peine d’implémenter un Hash DJB2, c’est certainement pour masquer leurs appels de fonctions Windows. Mais alors, il appelle quoi comme fonctions Windows et avec quels paramètres ce f..ing GuLoader (MD5:14d52119459ef12be3a2f9a3a6578ee3255580f679b1b54de0990b6ba403b0fe) ?

On continue notre exploration du JUNK Code, jusqu’à à nouveau tomber sur une fonction qui semble faire quelque chose de réel en +0x12fc1. Voici à quoi ressemble la fonction en IL une fois retravaillée sous BINARY NINJA (Du rename + élimination des Opaque Predicates):

Cette fois-ci c’est assez lisible dans le texte, il s’agit d’une fonction de XOR qui prend en paramètre un buffer et la clé à appliquer sur le XOR. On y voit bien les 2 boucles imbriquées du XOR (sur la longueur de la clé et sur la longueur du Buffer à ‘XORer’).

Regardons maintenant qui utilise cette fonction custom de XOR, avec quels buffers et clés en entrée pour comprendre ce, qu’à nouveau, on souhaite nous cacher ….

On constate alors que les auteurs de ce GuLoader utilisent toujours la même méthode pour passer les paramètres key, keyLen, buffer et bufferLen à notre fonction de calcul de XOR:

  1. Récupèration de la Return Address préalablement sauvegardée sur la pile par le CALL appelant
  2. Passage par la pile en argument de cette Return Address à la fonction de Hash en tant que Début de la clé de Hash

Puis, le mic-mac des XOR juste pour nous masquer une constant: la longueur de la clé (0x36):

push    0xd2faef
xor     dword [esp {var_4_1}], 0xd44408d8  {0xd496f237}
xor     dword [esp {var_4_2}], 0x5943447e  {0x8dd5b649}
xor     dword [esp], 0x8dd5b67f  {0x36}

Soit une longueur de clé 0x36 bytes

  1. L’adresse (et non pas la valeur) du Buffer est poussée sur la pile
  2. La taille du Buffer est aussi poussée sur la pile

Ca y est nous avons nos 4 paramètres pour faire notre XOR, en avant la musique

Pour retrouver la clé en analyse statique, c’est simple, on copie les 0x36 bytes juste derrière le CALL, c-à-d de 0x13a6e à 0x13a6e+0x36 (0x13aa4) Et nous avons bien la clé d’une longueur de 0x36 bytes:

b697cd32c7143eea5fb5fd3fa3dba8aaebe6226c89b9501c20806c888f58a2ba8ebc6b0a94e5bded795a2757109b8997d87e8080ee4aeb

La valeur du buffer à décoder avec cette clée est quant à elle logée sur la pile [ebp+0x140]:

000139bc  ffb540010000       push    dword [ebp+0x140]  

C’est bon, il ne nous reste plus qu’à passer en dynamique pour connaitre les valeurs dans la pile aux moments des appels de 0x1398f pour connaitre les valeurs du buffer qui sont envoyées au décodage avec la XOR Key.

Pour déterminer les valeurs d’entrée et de sortie des paramètres de cette fonction lors de ces appels, il existe plusieurs catégories de techniques:

  1. Continuer en Analyse Statique manuelle sous BINARY NINJA (c’est hard)
  2. Switcher en Analyse Dynamique manuelle par une exécution sous un debugger
  3. Utiliser un framework de simulation dynamique pour coder et simuler notre fonction (ex: le sublime UNICORN)
  4. Adopter une approche de Static Binary Instrumentation (SBI)
  5. Utiliser un moteur de Dynamic Binary Instrumentation (DBI), tel que Pin d’Intel, FRIDA ou QBDI de Quarklab

Je décide d’essayer une approche dynamique que je n’ai jamais utilisée auparavant mais dont des confrères m’ont vanté les mérites. Je vais produire un dump mémoire léger de ce binaire en Phase 2, puis le charger avec l’outil d’émulation Dumpulator. Je vais également faire la même chose (juste pour la gloire) avec FRIDA en y injectant un peu de code tracing ❤️

On passe en dynamique et on sort Dumpulator …

Avec ce type de code et Dumpulator, l’étape N°1 consiste à faire un Dump Mémoire au format Minidump.

Pour produire ce type de dump mémoire, à ce jour (Version 3.3.39) il n’y a rien dans la GUI du Debugger de Binary Ninja. Il faut savoir que l’introduction d’un debugger dans BINARY NINJA est somme toute assez récente et que son concepteur @PeterLaFosse a fait un choix qui me parait fort judicieux: Intégrer dans la GUI ‘juste’ un Front-end pour des moteurs de debugger existants. A ce jour BINARY NINJA en propose par défaut 2 sous Windows (LLDB et WinDbg) et un sous LINUX (LLDB).

Je vous donne une astuce pas très connue, Bien que la GUI BINARY NINJA ne donne accès pour le moment qu’à quelques fonctions de base WinDbg, son mode console sait exécuter/transmettre 100% des fonctions de winDbg. Par conséquent, vous pouvez générer votre Dump au format Minidump sans quitter votre cher BINARY NINJA :-)

Mais pour pouvoir réaliser ce dump, il nous faut charger le shellcode en Dynamique (mémoire + Thread): l’éxécuter. Or, souvenez vous que le fichier binaire que nous analysons actuellement ````GuLoader-Stage2.bin``` n’est pas un PE file et ne peut donc pas faire l’objet d’une exécution directe (Souvenez vous que ce ShellCode était au départ embarqué dans un autre fichier binaire (lui était un PE file) dont la seule fonction était de créer l’environnement d’éxécution (allocation mémoire + Creation Thread), de faire son XOR sur le code pour déchiffrer le Shellcode, puis de lui passer la main. Il nous faut donc un petit wrapper pour :

  1. Allouer de la mémoire exécutable
  2. Charger le Shellcode dans cet espace mémoire
  3. Créer un thread
  4. Lancer le thread
  5. En connaitre la Base Address pour pouvoir faire notre mapping entre la vision dynamique fournie par le Debugger et notre analyse statique (le coode source ;-) dans BINARY NINJA

Ok, maintenant exécutons tout ce petit monde sous debugger une première fois pour générer notre dump mémoire. Pour ça j’espère que vous avez bien lu le DISCLAIMER ci-dessus.

Ok, on a le DUMP mémoire au bon format. On peut donc maintenant commencer à le travailler avec Dumpulator

Ce qui va nous intéresser de visualiser lors de l’execution, c’est la stack AVANT chaque appel de la fonction (sub_0x12fc1 , que nous avons pour plus confort ;-) renommé Calcul_XOR.

Nous devrions y trouver tout en haut l’adresse du Buffer à décoder (et juste en dessous la longueur de ce buffer à décoder).

Plus globalement la stack avant chaque appel à la fonction de XOR sera construite pour avoir la structure suivante:

--------------------------------
| Adresse du buffer à déchiffer |
| ----------------------------- |
| Taille du buffer              |
| ----------------------------- |
| Taille de la clé              |
| ----------------------------- |
| Return Address                | <---- C'est ici que débute la Key 
| ----------------------------- |
|           (...)               |
---------------------------------

Voilà le secret de ces petits messieurs :-)

On se retrouve très vite pour la partie N°2 qui présentera l’analyse dynamique avec plusieurs approches (Dumpulator, FRIDA, …).