Ursnif: analyse du loader

Written by aaSSfxxx -

Après une période de hiatus encore plus longue que ceux de l'auteur de Hunter x Hunter, je me décide enfin à rescussiter mon blog (et mon dépôt). Cet article (ainsi que les suivants) redeviendront en français

Entrons directement dans le vif du sujet, c'est-à-dire l'analyse du bot Ursnif, en particulier du loader dans ce premier article. Le sample étudié est disponible sur any.run, s'appelle "osdjhbfowjndbherfvo.bin" (et hash 1da2adf65ab9d928cf4996a4151b0de2a5649d7454556f2c3ab81322d14213a4).

Unpacking du sample

L'unpacking du sample se fait sans grosse difficulté: il suffit de placer un point d'arrêt sur la fonction VirtualAlloc et exécuter jusqu'au point d'arrêt, ce qui nous amène à ceci:

Le malware utilise la mémoire allouée pour recopier le code d'unpacking (puisque l'exécutable en mémoire sera écrasée par le binaire packé). Nous continuons l'exécution step-by-step, et nous arrivons à un moment à cet endroit:

En regardant le début de la zone mémoire pointée par EAX, on observe clairement ceci:

Il s'agit du PE final compressé par apLib (classique), qu'il suffira donc de dumper et décompresser avec par exemple ce script python, ou dumper la mémoire une fois la fonction de décompression (A20189) exécutée. Maintenant munis de notre sample, nous pouvons l'ouvrir dans IDA Pro.

Techniques anti-debug

Le sample est un module DLL, qui se lance au DLL_PROCESS_ATTACH. Le module incrémente un compteur de référence, crée un heap avant de lancer le thread principal du bot, dont voici le début.

Le thread commence par récupérer un handle sur son propre processus, avant d'invoquer une autre fonction pour le moins "mystérieuse":

En effet, cette fonction commence par rechercher l'adresse de la section .bss, remplir une liste chaînée contenant des informations sur chaque page de la section, avant de placer la page en PAGE_NOACCESS. Ce comportement semble étrange au premier abord, mais en regardant de plus près, le loader définit un Vectored Exception Handler dans clsContext_init. En effet, lorsque le bot tentera d'accéder aux données contenues dans .bss, il déclenchera un EXCEPTION_ACCESS_VIOLATION qui sera traité par le gestionnaire d'exception, que voici ci-dessous:

Le gestionnaire d'exception va donc se charger de déchiffrer les données contenues dans .bss, puis restaurer les permissions d'origine stockée dans la mystérieuse liste chaînée.

En pseudo-code C++, le contexte serait défini comme ci-dessous:

struct pageContext {
    DWORD dwPageNumber;
    DWORD fOldProtect;
    DWORD decryptKey;
    DWORD shouldDecrypt;
}

struct memContext {
    CRITICAL_SECTION m_critSect;
    std::list<pageContext> m_list;
    HANDLE m_hExceptionHandler;
    DWORD m_dwTlsIndex;
}

L'analyse de l'algorithme de chiffrement sera laissé en exercice au lecteur.

Analyse du bot

Une fois ceci fait, le sample récupère son chemin d'exécution, crée un thread qui invoque SleepEx en continu avant d'invoquer QueueUserAPC. Si l'on en croit la documentation de Microsoft:

When a user-mode APC is queued, the thread is not directed to call the APC function unless it is in an alertable state. After the thread is in an alertable state, the thread handles all pending APCs in first in, first out (FIFO) order, and the wait operation returns WAIT_IO_COMPLETION. A thread enters an alertable state by using SleepEx, SignalObjectAndWait, WaitForSingleObjectEx, WaitForMultipleObjectsEx, or MsgWaitForMultipleObjectsEx to perform an alertable wait operation.

La fonction d'APC sera donc appelée après SleepEx.

Cette fonction invoque ensuite locateDataInBlob qui sert à récupérer des données incluses dans le binaire à partir d'un code, en l'occurence 0x408AF7E7.

Pour retrouver ces données, le bot a un tableau de structures à la suite de sa table des sections. La fonction locateDataInBlob va donc chercher ce tableau, puis analyser son contenu:

La structure peut être définie comme il suit:

struct botData {
    WORD magic; // vaut "JJ" dans notre exemple
    BYTE unused;
    BYTE flags;
    DWORD xorMask;
    DWORD tag;
    DWORD dataRVA;
    DWORD dwSize;
}

Si flags vaut 1, alors les données sont décompressées avec apLib, sinon il est juste extrait tel quel. Dans notre cas, il s'agit d'un autre "PE", compressé par apLib.

Une fois les données extraites, la fonction fait un xor avec le xorMask sur le 1er DWORD du binaire. Une fois le binaire décompressé, on remarque qu'il s'agit d'un PE Windows, mais dont les magic number pour l'en-tête DOS et PE ont été effacés. Les rajouter manuellement suffit à permettre le chargement du binaire obtenu.

Le bot quant à lui, va placer le chemin de lancement dans un file mapping de fichier dont le nom est le PID du process, puis créer de nouvelles sections pour mapper le PE obtenu avant de lui passer la main. Comme d'habitude, l'IDB est disponible sur le dépôt

L'analyse du loader touche à sa fin, à bientôt pour un nouvel article sur l'analyse de ce nouveau module !