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”. Quant à 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èmes 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’utilisation 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 :
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
...... ! 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 la fonction sont 0, 2, 6 et 27. Si tout est ok (temps d’exécution normal), la fonction doit retourner 6.
Ça 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 appelé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). À chaque passage de cette boucle une fonction que j’ai nommé calcul_sur_magic_int
(vous devinez pourquoi) est appelé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.
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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 ! À noter que cette fonction est appelée à 8 endroits différents du programme.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...... ! 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 appelé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 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...... ! 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élai 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élai 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élai d’exécution.
L’instruction à l’adresse 402559
se charge de placer cette adresse en mémoire selon le code suivant :
1
mov [eax*4+magic_buffer], ecx
où 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 délai (qui rappellons le doit faire 10 secondes). Ce délai est à nouveau comparé à différentes valeurs et en fonction de cela l’adresse écrite peut être remplacée par une autre adresse.
Parmi 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 :
1
call dword ptr [eax*4+magic_buffer]
Quelle est cette adresse ? On peut bien sûr l’avoir en plaçant 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 :
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
31
32
#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.
Published January 11 2011 at 14:30