Le précédent article nous avait amené à étudier l’avant-dernière fonction dans le WinMain
qui était utilisée pour détecter les environnements virtualisés.
Dans le présent article et les suivants, nous étudierons du code se situant ou étant appelé depuis la toute dernière fonction dont nous avions retrouvé l’adresse dans le premier article (0x4023b0
).
Dès l’entrée dans cette fonction, le codeur du malware nous fait à nouveau poireauter en utilisant les fonctions CreateEventA / WaitForSingleObject / CloseHandle
avec un timeout de 12 secondes… sans aucun objectif !
On entre ensuite dans une fonction où l’on remarque tout de suite des appels répétés à GetProcAddress
après un appel à GetModuleHandleA
.
Il y a aussi beaucoup d’appels à différentes fonctions toutes écrites sous le même profil.
Pour donner un autre ordre d’idée, quand on entre dans la fonction contenant tous ces GetProcAddress (elle se trouve à l’adresse 401000
, début de la section .text
) on rencontre l’appel suivant :
1
2
3
4
5
6
7
8
9
401003 ! sub esp, 1d8h
401009 ! push 50h
40100b ! lea eax, [ebp-178h]
401011 ! push eax
401012 ! call sub_402859
401017 ! add esp, 8
40101a ! lea ecx, [ebp-178h]
401020 ! push ecx
401021 ! call dword ptr [KERNEL32.DLL:GetModuleHandleA]
sub_402859
est appelée avec deux arguments : une adresse mémoire locale où va être stocké un résultat et la valeur hexadécimale 50h
.
Le code assembleur de sub_402859
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
402859 !
...... ! ;-----------------------
...... ! ; S U B R O U T I N E
...... ! ;-----------------------
...... ! sub_402859: ;xref c401012
...... ! push ebp
40285a ! mov ebp, esp
40285c ! sub esp, 8 ; reserve deux variables locales type int
40285f ! ; var2 = 12
...... ! mov dword ptr [ebp-8], 0ch
402866 ! mov eax, [ebp-8]
402869 ! xor ecx, ecx
40286b ! ; récupère l'octet à l'adresse 0x41daa4 + 12
...... ! mov cl, [eax+data_41daa4]
402871 ! mov [ebp-4], ecx
402874 ! mov edx, [ebp-8]
402877 ! cmp edx, [ebp+0ch]
40287a ! jng loc_402882
40287c ! ; arg2 soit 50h
...... ! mov eax, [ebp+0ch]
40287f ! mov [ebp-8], eax
402882 !
...... ! loc_402882: ;xref j40287a
...... ! mov ecx, [ebp-4]
402885 ! ; var1
...... ! push ecx
402886 ! mov edx, [ebp-8]
402889 ! ; var2 = 12
...... ! push edx
40288a ! mov eax, [ebp+8]
40288d ! ; buffer de sortie
...... ! push eax
40288e ! ; pointe vers une chaîne de caractères prédéfinie
...... ! push data_41daa4
402893 ! call sub_40250e
402898 ! add esp, 10h
40289b ! ; la valeur de retour est le buffer de sortie passé dans arg1
...... ! mov eax, [ebp+8]
40289e ! mov esp, ebp
4028a0 ! pop ebp
4028a1 ! ret
Pour résumer cette fonction en appelle une autre (sub_40250e
) dont le prototype est le suivant :
1
int * sub_40250e(int * data_in, int * data_out, 12, var1);
var1
a pour valeur l’octet à l’adresse [0x41daa4 + 12]
à moins que cette valeur soit supérieure à 0x50 dans ce cas, elle est remplacée par 0x50.
À l’adresse data_41daa4
(passée en argument), on trouve la suite d’octets suivante :
ae 03 2d 9e 3c b6 80 16 43 aa eb b4 3c 00 00 00
L’octet à l’adresse [0x41daa4 + 12]
est donc 3c
, le dernier octet présent.
Puisque l’on connait maintenant tous les arguments, étudions la fonction sub_40250e
qui est appelée 23 fois (!!) à divers endroit du code.
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
4025e0 !
...... ! ;-----------------------
...... ! ; S U B R O U T I N E
...... ! ;-----------------------
...... ! sub_40250e:
...... ! push ebp
4025e1 ! mov ebp, esp
4025e3 ! sub esp, 0ch ; réserve 3 variables locales type int
4025e6 ! push ebx ; sauvegarde des registres
4025e7 ! push esi
4025e8 ! push edi
4025e9 ! ; 4ème argument soit le dernier (et 12ème) caractère de data_41daa4 = 3c ou 50h
...... ! mov eax, [ebp+14h]
4025ec ! mov [ebp-0ch], eax ; var1 = 0x3c ou 0x50
4025ef ! ; [ebp-8] = compteur initialisé à 0
...... ! mov dword ptr [ebp-8], 0
4025f6 ! jmp loc_402601
4025f8 !
...... ! loc_4025f8: ;xref j402648
...... ! mov ecx, [ebp-8]
4025fb ! add ecx, 1 ; incrémente le compteur, i++
4025fe ! mov [ebp-8], ecx
402601 !
...... ! loc_402601: ;xref j4025f6
...... ! mov edx, [ebp-8]
402604 ! ; compare le compteur à 12 soit la longueur de data_41daa4 en octets
...... ! cmp edx, [ebp+10h]
402607 ! jnl loc_40264a ; sort de la boucle si compteur >= len(data_41daa4)
402609 ! push ecx
40260a ! ; var1
...... ! mov eax, [ebp-0ch]
40260d ! mov ecx, 11h
402612 ! add eax, ecx
402614 ! add ecx, 448h
40261a ! imul eax, ecx
40261d ! and eax, 1ffffh
402622 ! mov [ebp-0ch], eax
402625 ! pop ecx
402626 ! mov eax, [ebp-0ch]
402629 ! and eax, 0ffh
40262e ! mov [ebp-4], al
...... ! ; ecx = [i + 0x41daa4]
402631 ! mov ecx, [ebp+8]
402634 ! add ecx, [ebp-8]
402637 ! ; edx = caractère encodé
...... ! movsx edx, byte ptr [ecx]
40263a ! movsx eax, byte ptr [ebp-4]
40263e ! ; edx = caractere decodé
...... ! xor edx, eax
402640 ! mov ecx, [ebp+0ch]
402643 ! add ecx, [ebp-8]
402646 ! mov [ecx], dl
402648 ! jmp loc_4025f8
40264a !
...... ! loc_40264a: ;xref j402607
...... ! mov edx, [ebp+0ch]
40264d ! add edx, [ebp+10h]
402650 ! ; place un caractère terminal NULL
...... ! mov byte ptr [edx], 0
402653 ! pop edi
402654 ! pop esi
402655 ! pop ebx
402656 ! mov esp, ebp
402658 ! pop ebp
402659 ! ret
Au début du code, on remarque deux initialisations de variables locales. La première est un compteur initialisé à 0. La seconde variable locale se voit affecter le dernier caractère de la chaine de caractère encodée qui a été passée en argument.
On remarque ensuite une boucle où le compteur est incrémenté de 1 à chaque passage jusqu’à valoir 12 soit la longueur de la chaine de caractère à décoder.
Des calculs sont ensuite effectués sur la seconde variable locale :
- on lui ajoute
0x11
- on la multiplie par (
0x11
+0x448h
) - on effectue un
AND
avec le masque0x000000ff
pour obtenir l’octet de poids faible
Ce n’est alors qu’avec ce résultat que le i-ème octet de la chaine de caractère est xor-é pour obtenir le vrai caractère dissimulé.
On pourrait réécrire cette fonction en C de cette façon :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
char * decrypt(char *in, char *out, unsigned int len, int c)
{
unsigned int i;
int x = c;
for (i=0;i<len;i++)
{
x += 0x11;
x *= (0x448 + 0x11);
x &= 0x000000ff;
out[i] = in[i] ^ (char)x;
}
return out;
}
Où l’argument c
qui est le dernier caractère de la chaine est utilisé comme vecteur d’initialisation pour le déchiffrement.
J’ai préféré écrire un programme plus facile d’utilisation qui prend pour argument le chaine encodée (suite de caractères hexa) et retourne la chaine décryptée :
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int char_to_byte(int c)
{
if(isxdigit(c))
{
if(c>90)
{
c -= 32;
}
else if(c<58)
{
c -= 48;
}
if(c>9)
{
c -= 55;
}
return c;
}
return -1;
}
int main(int argc,char *argv[])
{
int len;
int i;
int ch, cl;
char *out;
int x;
if(argc != 2)
{
printf("Usage: %s <hex_string>\n", argv[0]);
return 1;
}
len = strlen(argv[1]);
if(len % 2 == 1)
{
printf("Invalid input string\n");
return 1;
}
out = (char*)malloc(len/2 + 1);
for(i=0;i<len;i+=2)
{
ch = char_to_byte(argv[1][i]);
if(ch == -1)
{
printf("Invalid input string\n");
return 1;
}
ch = ch << 4;
cl = char_to_byte(argv[1][i+1]);
if(cl == -1)
{
printf("Invalid input string\n");
return 1;
}
x = ch | cl;
out[i/2] = x;
}
out[i/2+1] = 0;
len = len/2;
for (i=0;i<len;i++)
{
x += 0x11;
x *= (0x448 + 0x11);
x &= 0x000000ff;
out[i] = out[i] ^ (char)x;
}
out[i-1] = 0;
printf("%s.\n",out);
free(out);
return 0;
}
On l’appelle avec la suite d’octets encodés :
1
2
$ ./decode_hexa ae032d9e3cb6801643aaebb43c
kernel32.dll.
La mystérieuse chaine était donc kernel32.dll
:)
Ce qui est étonnant dans le code du malware c’est que l’auteur a défini pour chaque chaine de caractère une fonction chargée de passer comme argument la longueur de la chaine encodée, son dernier caractère et l’adresse de la chaine elle-même. On trouve ainsi dans la même zone les 23 fonctions de décodage correspondant aux 23 chaines de caractères que le programme décode durant son exécution !
Il aurait été bien plus efficace d’initialiser un tableau et de créer une fonction décodant directement toutes les chaines utilisées…
Les chaines de caractères encodées dans le programme se révèlent être les suivantes :
kernel32.dll
, dat
, .exe
, .dll
, %d-%d
, teatimer.exe
, \???*.dll
, \???*.exe
, SetFileTime
, CreateProcessA
, TerminateProcess
, WriteFile
, MoveFileExA
, CreateToolhelp32Snapshot
, Process32First
, Process32Next
, OpenProcess
, LoadLibraryA
, GetSystemDirectoryA
, GetModuleFileNameA
, GetTickCount
, CreateFileA
, CloseHandle
et ..
.
On remarque parmi ces chaines certaines fonctions déjà importées “proprement” par le malware qui ont été dissimulées pour être réutilisées à d’autres endroits du code.
La présence de teatimer.exe
et des fonctions destinées à gérer les processus laisse supposer que le malware va tenter de désactiver TeaTimer
, un logiciel “qui surveille sans cesse les processus qui sont appelés/lancés. Il détecte immédiatement les processus connus pour être malveillants qui veulent démarrer et les arrête, en vous donnant quelques options sur la façon de traiter ces processus à l’avenir.”
Enfin on trouve des fonctions destinées à créer des fichiers mais toujours pas de fonctions réseau. Le malware correspondrait donc bien à un “dropper” qui se charge de déposer un fichier sur le disque dur.
Mais revenons à nos moutons. Au tout début, on a vu qu’après avoir appelé la fonction chargée de décoder kernel32.dll
, le programme récupère son handle avec GetModuleHandle
.
Il s’ensuit des appels aux différentes fonctions de décodage et à nombre égal des appels à GetProcAddress
pour récupérer les adresses des fonctions cachées. Adresses qui sont stockées en mémoire à partir de l’adresse 420044
.
On sort ensuite de cette fonction et l’adresse obtenue pour la fonction GetTickCount
est vérifiée : si elle vaut 0 (NULL) alors le programme quitte sinon (résolution de noms de fonction ok) il poursuit son fonctionnement dont nous verrons une partie dans le prochain article (terminaison de teatimer et peut-être plus encore).
Published January 11 2011 at 15:58