Nicolas SURRIBAS

Développement / Réseau / Sécurité Informatique

Analyse du malware Podnuha.ql : première partie

Rédigé par devloop - -

Introduction

Les différents articles que l'on peut trouver sur ce blog offrent une assez bonne vision des sous domaines existants de la sécurité informatique. Mais ce qui manquait réellement sur ce blog c'était un article sur l'analyse de malwares Windows.

Cette lacune va être comblée par le présent article et les articles qui devront suivre sur l'analyse du même exécutable. Dans chaque article nous avancerons un peu plus dans le fonctionnement du malware jusqu'à (si tout se passe bien) parvenir à découvrir sa charge finale ou comprendre son fonctionnement dans sa totalité.

Toute remarque ou observation est la bienvenue.

Présentation du malware

Le malware a été trouvé dans le code HTML d'une page malicieuse il y a un bon temps maintenant. Cette page exploitait une vulnérabilité dans Apple Quicktime : RTSP Response Header Content-Type Remote Stack Based Buffer Overflow.
Si l'exploitation fonctionnait ce malware était exécuté sur la machine vulnérable.


Il est détecté avec AVG comme BackDoor.Generic9.AZFP, par F-Secure comme Rootkit.Win32.Podnuha.ql et par McAfee comme "Generic Dropper". Quand à ClamAV, il n'y voit aucun danger :|
Lr programme fait 132Ko (135168 octets), sa somme de controle MD5 est b4f3f938ef77ae369c13fcc26006658b.

Logiciels utilisés pour l'analyse

L'analyse en dead-listing (désassemblage) s'est faite depuis HT Editor sous Linux. Je l'utilise principalement parce qu'il tourne sur cette plateforme et que ses fonctionnalités correspondent généralement à mes attentes. On peut naviguer dans le code assembleur, voir les références (appels, sauts, adresses mémoires), renommer et insérer des labels, placer des commentaires...
En revanche il rencontre des difficultés sur les exécutables non conventionnels (headers réduits, obfuscation du code...) ce qui peut être un problème pour l'analyse d'un malware.
Dans notre cas, et bien que le malware analysé intègre toute sorte de protections, la lecture depuis HT Editor ne pose pas de problème significatifs.

Pour épauler HTE, OllyDBG a été utilisé pour tracer l'exécution du programme. Cette analyse "live" s'est effectuée depuis un Windows XP de base (sans service pack installé) virtualisé par VirtualBox depuis un Win7.
OllyDBG n'a été lancé sur des portions de code qu'une fois leur lecture effectuée précédemment depuis HTE.

Analyse rapide du programme

L'analyse des sections ne révèle rien de particulier. Trois sections avec des droits d'accès standards : .text (droits RX), .data (RW) et .rdata (R). Toutefois la taille de la section .data est impressionante comparée aux autres sections, ce qui laisse entrevoir la présence de ressources dissimulées...



La table des imports indique l'utilisations de fonctions diverses destinées à la gestion de chaines de caractères (lstrcpynA, lstrcatA, MultiByteToWideChar...), la gestion des dossiers (FindFirstFileA, GetTempPathA, MoveFileA...), des fonctions de gestion de la mémoire, des fonctions de temps (GetTickCount, Sleep) ou encore des fonctions de gestion d'évennements (CreateEventA, WaitForSingleObject, TranslateMessage, DispatchMessageA, GetMessageA).

Un "strings" sur le binaire ne retourne pas plus que le nom des fonctions importées, ce qui laisse supposer que les chaines de caractères sont encodées dans l'exécutable. De même aucune fonction concernant la création de fichiers, l'accès à la base de registre ou des appels réseau ne semble appelée par l'exécutable, ce qui est un peu "gros" pour un fichier considéré comme "backdoor" ou "trojan" par certains logiciels antivirus.
Pour rester dans le louche, un appel est fait à la fonction VirtualProtect qui pourrait bien dissimuler un code auto-modifiable ou la présence d'un packer.

Première plongée dans le code

Le point d'entré est typique d'une application graphique compilée avec un compilateur Microsoft. On y retrouve un prologue qui met en place un SEH puis appelle dans l'ordre les fonctions GetCommandLineA, GetStartupInfoA et GetModuleHandleA. Les résultats obtenus de ces commandes sont passées en argument au vrai "début" du programme en suivant le prototype classique d'une fonction WinMain.
Au sortir de cette fonction, la valeur de retour est là aussi passée à une fonction d'épilogue classique insérée par le compilateur et qui termine le processus.

PEiD confirme d'ailleurs mon impression en détectant le compilateur Microsoft Visual C++ 6 et indique que le malware est de type Windows + GUI.
Si aucun packer n'est détecté, PEiD calcule tout de même une entropie de 7.2.

Dès l'arrivé dans le main (situé à l'adresse 0x004024a7), le programme effectue plusieurs appels à la fonction GetTickCount.
Les premiers appels sont effectués depuis une fonction que j'ai baptisé "calcul_delais_temps" dont le code assembleur commenté est le suivant :

...... ! calcul_delais_temps:            ;xref c4024b4
...... !   push        ebp
40243e !   mov         ebp, esp
402440 !   sub         esp, 8
402443 ! time ref #1
...... !   call        dword ptr [KERNEL32.DLL:GetTickCount]
402449 !   mov         [ebp-4], eax
40244c ! sleep(100ms)
...... !   push        64h
40244e !   call        dword ptr [KERNEL32.DLL:Sleep]
402454 ! time ref #2
...... !   call        dword ptr [KERNEL32.DLL:GetTickCount]
40245a !   mov         [ebp-8], eax
40245d !   mov         eax, [ebp-8]
402460 !   sub         eax, [ebp-4]
402463 !   mov         [ebp-8], eax
402466 ! delais <= 51ms ?
...... !   cmp         dword ptr [ebp-8], 33h
40246a ! on saute
...... !   jnc         loc_402470
40246c !   xor         eax, eax
40246e !   jmp         loc_4024a3
402470 ! 401ms
...... ! loc_402470:                     ;xref j40246a
...... !   cmp         dword ptr [ebp-8], 191h
402477 !   jnc         loc_402480
402479 ! on retourne 6 et on quitte
...... !   mov         eax, 6
40247e !   jmp         loc_4024a3
402480 ! 801ms
...... ! loc_402480:                     ;xref j402477
...... !   cmp         dword ptr [ebp-8], 321h
402487 !   jnc         loc_402490
402489 !   mov         eax, 2
40248e !   jmp         loc_4024a3
402490 ! 1801ms
...... ! loc_402490:                     ;xref j402487
...... ! loc_402490:                     ;xref j402487
...... !   cmp         dword ptr [ebp-8], 709h
402497 !   jnc         loc_4024a0
402499 !   mov         eax, 1bh
40249e !   jmp         loc_4024a3
4024a0 !
...... ! loc_4024a0:                     ;xref j402497
...... !   or          eax, 0ffffffffh
4024a3 !
...... ! loc_4024a3:                     ;xref j40246e j40247e j40248e
...... !                                 ;xref j40249e
...... !   mov         esp, ebp
4024a5 !   pop         ebp
4024a6 !   ret


Cette fonction fait deux appels à GetTickCount avec, entre les deux, un appel à la fonction Sleep. Les résultats des appels successifs à GetTickCount sont soustrait l'un à l'autre et comparés à différentes valeurs. En fonction du résultat des comparaisons, une valeur différente sera placée dans le registre eax (valeur de retour). Bien sûr il n'y a pas de raisons que cette soustraction donne un résultat différent de 100ms (à quelques millisecondes près) car c'est la période durant laquelle le programme va effectuer le Sleep()... à moins que bien sûr le programme soit débogué et que l'exécution soit ralentie.
Les valeurs possibles retournées par le fonction sont 0, 2, 6 et 27. Si tout est ok (temps d'exécution normal), la fonction doit retourner 6.

Ca se complique un (tout) petit peu plus ensuite. Le programme utilise 3 variables :
  • une variable locale qui semble destinée à contenir un pointeur sur fonction, initialisé avec la valeur 0x004023b0.
  • une variable locale contenant le résultat de la fonction calcul_delais_temps() vue précédemment. Cette variable ne sera pas modifiée.
  • une variable globale que j'ai baptisé "magic_int" car appellée à différents endroits du code et initialisée à 79h (79 en hexa soit 121 en décimal).


Notre adresse de fonction est rapidement xorée avec la valeur 5ee2h, elle devient donc 0x00407D52.
Une première référence de temps est ensuite gardée de côté par GetTickCount().

On entre ensuite dans deux boucles.
La première a un compteur initialisé à 4 et incrémenté de 1 à chaque passage jusqu'à 4008 (exclu). A chaque passage de cette boucle une fonction que j'ai nommé "calcul_sur_magic_int" (vous devinez pourquoi) est appellée avec comme argument la valeur du compteur.
On passe ensuite à une seconde boucle, compteur initialisé à 1 pour aller jusqu'à 1001 (exclu). Cette boucle appelle toujours calcul_sur_magic_int mais cette fois avec la valeur 12 comme argument. Elle fait aussi appelle à une fonction qui peut paraître étrange au début que j'ai baptisé "wait_10ms".
Une fois ces deux boucles passées, un nouvel appel à GetTickCount (l'auteur semble aimer cette fonction) est effectué pour obtenir une seconde référence de temps.

4024d1 ! Get time ref #1
...... !   call        dword ptr [KERNEL32.DLL:GetTickCount]
4024d7 !   mov         [ebp-0ch], eax
4024da ! i = 4
...... !   mov         dword ptr [ebp-1ch], 4
4024e1 !   jmp         boucle_4008
4024e3 ! boucle while
...... ! inc_boucle_4008:                ;xref j402502
...... !   mov         ecx, [ebp-1ch]
4024e6 ! i++
...... !   add         ecx, 1
4024e9 !   mov         [ebp-1ch], ecx
4024ec ! i >= 4008 ? quite la boucle : boucle
...... ! boucle_4008:                    ;xref j4024e1
...... !   cmp         dword ptr [ebp-1ch], 0fa8h
4024f3 !   jnl         fin_boucle_4008
4024f5 !   mov         dx, [ebp-1ch]
4024f9 !   push        edx
4024fa !   call        calcul_sur_magic_int
4024ff !   add         esp, 4
402502 !   jmp         inc_boucle_4008
402504 !
...... ! fin_boucle_4008:                ;xref j4024f3
...... !   mov         dword ptr [ebp-18h], 0
40250b !   jmp         boucle_1001
40250d !
...... ! inc_boucle_1001:                ;xref j402533
...... !   mov         eax, [ebp-18h]
402510 !   add         eax, 1
402513 !   mov         [ebp-18h], eax
402516 !
...... ! boucle_1001:                    ;xref j40250b
...... !   cmp         dword ptr [ebp-18h], 3e9h
40251d !   jnl         fin_boucle_1001
40251f !   push        0ah
402521 !   call        wait_10ms
402526 !   add         esp, 4
402529 !   push        0ch
40252b !   call        calcul_sur_magic_int
402530 !   add         esp, 4
402533 !   jmp         inc_boucle_1001
402535 ! Get time ref #2
...... ! fin_boucle_1001:                ;xref j40251d
...... !   call        dword ptr [KERNEL32.DLL:GetTickCount]


La question est bien sûr de savoir ce que fait la fonction calcul_sur_magic_int : elle fait des maths ! A noter que cette fonction est appellée à 8 endroits différents du programme.

...... ! calcul_sur_magic_int:           ;xref c401455 c401474 c4014dc
...... !                                 ;xref c4014fa c401518 c40180b
...... !                                 ;xref c4024fa c40252b
...... !   push        ebp
402110 !   mov         ebp, esp
402112 !   mov         eax, [magic_int]
402117 !   imul        eax, eax, 7d17h
40211d !   add         eax, 13h
402120 !   mov         [magic_int], eax
402125 !   mov         ecx, [magic_int]
40212b !   and         ecx, 7fffh
402131 !   mov         [magic_int], ecx
402137 !   mov         eax, [magic_int]
40213c !   shr         eax, 2
40213f !   mov         ecx, [ebp+8]
402142 !   and         ecx, 0ffffh
402148 !   xor         edx, edx
40214a !   div         eax, ecx
40214c !   mov         ax, dx
40214f !   pop         ebp
402150 !   ret


On remarque que les modifications effectuées sur magic_int s'arrêtent à l'adresse 402131 et aussi que l'argument passé à la fonction n'intervient en rien dans sa modification !
La fonction retourne une valeur calculée en fonction de cet argument et de magic_int.
Pour le moment la valeur de retour de calcul_sur_magic_int (et par conséquent la valeur de son argument) ne nous intéressent pas car les boucles n'en ont pas l'utilité !

Il ne nous reste plus qu'à étudier "wait_10ms", fonction appellée dans la seconde boucle. Cette fonction est équivalente à un Sleep() de 10ms mais avec les fonctions utilisées c'est moins visible car ça revient à attendre un temps maximum (10ms de timeout) que se réalise un événement dont on sait à l'avance qu'il n'aura jamais lieu :

...... ! wait_10ms:                      ;xref c4021eb c4021fe c40241d
...... !                                 ;xref c402521
...... !   push        ebp
401386 !   mov         ebp, esp
401388 !   push        ecx
401389 !   push        data_4200a0
40138e !   push        0
401390 !   push        1
401392 !   push        0
401394 !   call        dword ptr [KERNEL32.DLL:CreateEventA]
40139a !   mov         [ebp-4], eax
40139d !   mov         eax, [ebp+8]
4013a0 ! time to wait = 0x0a = 10ms
...... !   push        eax
4013a1 !   mov         ecx, [ebp-4]
4013a4 !   push        ecx
4013a5 !   call        dword ptr [KERNEL32.DLL:WaitForSingleObject]
4013ab !   mov         edx, [ebp-4]
4013ae !   push        edx
4013af !   call        dword ptr [KERNEL32.DLL:CloseHandle]
4013b5 !   mov         esp, ebp
4013b7 !   pop         ebp
4013b8 !   ret


Après être sorti de cette seconde boucle, une nouvelle référence de temps est prise et un délais est calculée à l'aide de la première.
Si vous avez bien suivi, vous savez que la fonction wait_10ms est appellée 1001 fois... Il se passe donc normalement un délais de 10 secondes (à quelques millisecondes près) entre les deux appels à GetTickCount.

Après ces deux boucles la mystèrieuse adresse (qui était restée à 0x00407D52) est xoré avec magic_int. L'objectif de ces étapes a pour seul but principal d'obscurcir la valeur de cette adresse mais aussi de faire en sorte que le programme agisse différemment s'il est débogué.
C'est malgré tout assez trivial car il n'y a pas d'inconnues : tout est connu à l'avance et l'adresse obtenue à ce stade du programme sera inlassablement la même, quelque soit le délais d'exécution.

L'instruction à l'adresse 402559 se charge de placer cette adresse en mémoire selon le code suivant :
mov [eax*4+magic_buffer], ecx

"magic_buffer" est une adresse mémoire (41da3c), eax correspond au résultat de calcul_delais_temps (c'est à dire 6) et ecx à notre (déjà moins) mystérieuse adresse.

Mais cette zone mémoire peut à nouveau être réécrite juste après le calcul du delais (qui rappellons le doit faire 10 secondes). Ce délais est à nouveau comparé à différentes valeurs et en fonction de cela l'adresse écrite peut être remplacée par une autre adresse.
Parmis les adresses candidates, l'un effectue un Sleep() de 3 secondes avant de quitter (adresse = 402300) et l'autre vaut 0.

Il ne reste alors dans le WinMain que deux appels à des fonctions.
La première que j'ai baptisé poétiquement "antifuck", car mesure supplémentaire du malware pour ne pas se faire analyser, prend deux arguments : le 4ème argument du WinMain (ici SW_SHOWDEFAULT soit 10) et la valeur 0.
La seconde fonction appelée est la fameuse adresse écrite en mémoire :
call dword ptr [eax*4+magic_buffer]


Quelle est cette adresse ? On peut bien sûr l'avoir en placant un breakpoint sur ce call ou le calculer à la main.
J'ai écrit pour l'occasion un code en C qui réimplémente les boucles et la fonction de calcul :

#include <stdio.h>
unsigned int magic = 0x79;

unsigned int calcul(unsigned int x)
{
  unsigned int ret;
  magic = ((magic * 0x7d17) + 0x13) & 0x7fff;
  ret = magic >> 2;
  ret = ret / (x & 0xffff);
  return (ret & 0xffff);
}

int main(int argc, char *argv[])
{
  unsigned int addr = 0x4023b0;
  unsigned int i;

  addr = addr ^ 0x5ee2;
  for (i=4; i<4008; i++)
  {
    calcul(i);
  }

  for (i=0; i<1001; i++)
  {
    calcul(12);
  }
  addr = addr ^ magic;

  printf("Adresse cachee = %p\n", addr);
  return 0;
}


Le résultat est le suivant :

Adresse cachee = 0x4023b0



A suivre

Dans le prochain épisode nous étudierons le fonctionnement de la fonction "antifuck".
D'ici là j'aurais sans doute rajouté quelques petits éléments à cet article.

Classé dans : Non classé - Mots clés : malware

Les commentaires sont fermés.