Ursnif: analyse du loader (partie 2)

Written by aaSSfxxx -

Cet article fait suite à celui-ci, et concernera l'analyse du dernier binaire extrait. Ce fichier se présente (encore) sous la forme d'une DLL. La DLL fait ses initialisations habituelles (création d'un objet Event pour notifier le bot lorsque la DLL est en train d'être déchargée, détection de la version de Windows et ouverture d'un handle sur son propre process).

Initialisation

Comme la fois précédente, on retrouve la section .bss chiffrée avec le même algorithme, mais ici, le déchiffrement est directement fait au lancement du bot et non au travers d'un Vectored Exception Handler, comme on peut le voir:

Un paramètre GetTickCount() & 0x1e est en plus rajouté à la clé, et le bot boucle tant que le déchiffrement n'est pas valide, c'est-à-dire tant que ce paramètre est différent de 0. Une fois la section .bss déchiffrée, le bot vérifie s'il tourne sur une machine 64bits ou 32bits, avant de passer la main à la fonction principale.

Fingerprinting de la machine

Une fois dans la fonction principale, le bot commence par récupérer le numéro de version de Windows en lisant les champs adéquats dans l'en-tête PE de ntdll.dll:

Le bot récupère ensuite le SID de l'utilisateur ayant lancé le process, et calcul la somme des sub-authorities.
Si le checksum est nul (c'est-à-dire qu'on a aucune sous-autorité, ce qui veut vraisemblablement dire que le bot est lancé depuis un compte système), alors la date d'installation du système récupérée depuis le registre est utilisée à la place.

La valeur ainsi récupérée va être xorée avec une constante pour former la "graine" qui servira à construire l'identificateur du bot, après avoir créé une structure de contexte qui stockera les informations du bot. Une fois cet identificateur généré, le bot va lire sa configuration avant de passer de communiquer avec son C&C.

Extraction de la configuration

Le bot commence tout d'abord par extraire ce qui ressemble à une clé RSA via le mécanisme de "ressources" vu dans l'article précédent (dont l'identifiant est 0xE1285E64). Puis, le bot extrait sa configuration depuis une "resource" d'identifiant 0x8FB1DDE1. Cette configuration a un format différent du système de ressources (ce ne serait pas amusant sinon), dont le pseudocode donnerait quelque chose du genre:

typedef struct {
    DWORD dwTag;
    DWORD dwFlags;
    DWORD dwRelativeOffset; /* offset relatif au début de l'entrée */
    DWORD dwUnused[3];
} BOT_CONFIG_ENTRY;

typedef struct {
    DWORD dwNumEntries;
    DWORD dwUnused;
    BOT_CONFIG_ENTRY entries[1];
} BOT_CONFIG;

char *ExtractConfig (BOT_CONFIG *pCfg, DWORD dwTag)
{
    BOT_CONFIG_ENTRY *pEntry;
    int i;
    pEntry = &pCfg->entries[0];
    for(i = 0; i < pCfg->dwNumEntries; i++)
    {
        if(pEntry->dwTag == dwTag)
        {
            return (char*)pEntry + pEntry->dwRelativeOffset;
        }
        i++;
        pEntry++;
    }
}

Une liste des identifiants de configuration se trouve dans le tableau ci-dessous:

Identifiant Utilité
11271C7F 1er timer
48295783 2ème timer
584E5925 3ème timer
656B798A ID du bot
556AED8F ID du serveur
4FA8693E Clé de chiffrement
D0665BF6 Liste d'URLs du loader

Une fois les éléments de sa configuration extraits, le bot va enfin contacter son C&C.

Communication avec le C&C

Après avoir créé son premier timer dont le délai d'attente est récupéré depuis la configuration, le malware va modifier la date du dernier lancement d'Internet Explorer, ainsi que désactiver la vérification du navigateur au démarrage. Comme nous le verrons ultérieurement, la communication avec le C&C se fait en pilotant Internet Explorer grâce à COM, ce qui est assez original pour être souligné.

Mais d'abord, regardons ce que le bot envoie:

Le bot transmet donc un paramètre "soft", sa version, l'identifiant de la machine générée précédemment dans le paramètre "user", un identifiant (probablement de botnet), ainsi qu'un paramètre "crc" indiquant le numéro de la requête faite au C&C (nous reviendrons sur ce point plus tard). Une fois ceci fait, l'URL va être transformée selon l'algorithme suivant:

  • Un paramètre aléatoire est préfixé à la query string
  • La chaîne générée est chiffrée avec l'algorithme "Serpent" en mode CBC
  • Le résultat est encodé en base64, puis les caractères "/" sont remplacés par "_" et les "=" à la fin sont supprimés grâce à la fonction StrTrimA
  • Des caractères "/" sont ajoutés aléatoirement dans le résultat
  • L'URL du C&C suivi de "/images/" est préfixée, et l'extension ".avi" est ajoutée pour construire l'URL finale

La requête sera donc envoyée au serveur en passant par ce bon vieux Internet Explorer. Le bot récupère d'abord une instance d'un objet InternetExplorer.Application, avant de récupérer les interfaces IWebBrowser et IWebBrower2, avant de cacher la fenêtre d'Internet Explorer.

Une fois l'objet IE et ses interfaces récupérés, le bot peut désormais envoyer sa requête au C&C:

Une fois la page chargée, le bot invoque la méthode get_Document de IWebBrowser, récupère l'interface IHTMLDocument2 et invoquer la méthode get_URL pour récupérer l'URL sur laquelle on atterit. Le bot vérifie ensuite si la chaîne "invalidcert" se trouve dans l'URL récupérée (ce qui signifie que la page a été redirigée vers res://ieframe.dll/invalidcert.htm). Si c'est le cas appelle une autre fonction (appelée "NavigateNext" dans l'IDB) afin de "cliquer" sur le lien permettant la connexion HTTPS avec un certificat invalide.

Une fois la bonne page chargée, le bot récupère le contenu de la page HTML reçue. Malheureusement, les C&C étant décédés au moment de leur analyse (paix à leur âme :'(), j'ai dû poursuivre l'analyse en me basant sur le .pcap enregistré par any.run lors de son analyse. En l'analysant, j'ai pu observer 2 connexions au C&C qui répond avec du contenu en base64.

Le base64 est donc décodé, puis, le bot déchiffre avec sa clé RSA les 256 derniers octets de la réponse reçue. Ce bloc contient entre autres la clé de chiffrement Serpent du reste du bloc, ainsi que la taille et le hash MD5 du reste de la réponse. Si la clé Serpent ne contient que des octets nuls, le bot se contente juste de recopier la réponse.

Le premier retour renvoyé par le serveur est un PE, vraisemblablement le bot final (qui fera peut-être l'objet d'un futur article). Le deuxième retour, lui, est un script Powershell qui aura pour but de charger le PE en mémoire.

Les réponses sont stockées dans une structure que l'on pourrait définir comme il suit:

struct ReturnSlot
{
  PVOID pData;
  DWORD dwSize;
  DWORD dwMappedSize;
  PVOID pMappedImage;
};

La communication avec le C&C se fait plusieurs étapes:

  • Le malware commence par récupérer un PE 32 bits (la charge finale) depuis le C&C (le paramètre envoyé est "crc=1")
  • Le malware, si nous somme sur une machine 64 bits, récupère un PE 64bits (le paramètre envoyé est "crc=2")
  • Enfin, le malware récupère un script powershell, qui servira à lancer le bot ("crc=3")

Persistance et Powershell

Une fois les binaires et le powershell récupérés sur le serveur, le bot va enfin mettre en place sa persistance. Le bot commence par générer des noms à partir des dlls présentes sur le système, puis un nom de clé de la forme "Software\AppDataLow\Software\Microsoft{GUID}", avant de vérifier le niveau d'intégrité du process, et sortir si jamais il est en processus "restreint".

La fonction appelée invoque donc QueryTokenInformation, avec le paramètre TokenIntegrityLevel, qui renvoie le SID du niveau d'intégrité souhaité, comme ci-dessous.
La réponse va ensuite être traitée: les deux PE récupérés vont être mappés directement en mémoire. Puis, une fois mappés, le bot récupère l'adresse du premier export (qui se chargera de résoudre l'IAT et les relocations), et patche le début de la zone mémoire par un saut relatif vers cette fonction.
Le binaire mappé va ensuite être converti en tableau d'entiers, puis injecté dans le dernier payload (Powershell), à la place du placeholder "@CODE@". Le fichier PE récupéré sera ensuite chiffré par un simple xor avec la "graine" qui a servi pour générer l'ID du bot.

Puis, le bot va énumérer toutes les clés sous HKEY_USERS afin d'assurer sa persistance. Pour chaque sous-clé (c'est-à-dire le profil de chaque utilisateur), une sous-clé du nom ci-dessus sera généré. Le bot va sauvegarder les PE téléchargés et chiffrés dans une valeur REG_BINARY, dans les valeurs nommées Client32 (pour le payload 32-bits) et Client64 (pour le payload 64 bits), ainsi que le chargeur powershell sous un nom aléatoire.

Ensuite, un launcher JScript sera lui aussi enregistré dans la sous-clé, et aura pour rôle d'invoquer le loader Powershell. Enfin, la commande ci-dessous formatée avec wsprintf sera enregistrée dans HKCU\Software\Microsoft\CurrentVersion\Run pour assurer la persistance.

aMshtaAboutHtaA:                        ; DATA XREF: WriteLauncher+F3↑o
.bss:1000DB68                                         ; WriteLauncher+119↑o
.bss:1000DB68                 text "UTF-16LE", 'mshta "about:<hta:application><script>resizeTo(1,1)'
.bss:1000DB68                 text "UTF-16LE", ';eval(new ActiveXObject(',27h,'WScript.Shell',27h,')'
.bss:1000DB68                 text "UTF-16LE", '.RegRead(',27h,'%S\\%S\\%s',27h,'));if(!window.flag'
.bss:1000DB68                 text "UTF-16LE", ')close()</script>"',0
.bss:1000DC92 aHost           db 'Host:',0

Puis le bot va invoquer la commande ainsi générée pour lancer la charge finale, avant de récupérer le nom du module par lequel il s'est chargé, pour supprimer le fichier droppé lors de la première phase.

Ainsi ce loader assure une persistence du bot sans laisser de fichiers, laissant uniquement des artefacts dans le registre de la machine infectée.