Analyse du malware Podnuha.ql : troisième partie
Saturday, May 15, 2010 1:02:19 PM
Le précédent article nous avait ammené à étudier l'avant dernière fonction dans le WinMain qui était utilisée pour détecter les environnements mutualisés.
Dans le présent article et les suivants, nous édudierons 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 :
sub_402859 est appellé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 :
Pour résumer cette fonction en appelle une autre (sub_40250e) dont le prototype est le suivant :
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.
A l'adresse data_41daa4 (passée en argument), on trouve la suite d'octets suivante :
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 appellée 23 fois à divers endroit du code
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 :
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 :
Où l'argument c qui est le dernièr 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 :
On l'appelle avec la suite d'octets encodés :
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).
Dans le présent article et les suivants, nous édudierons 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 :
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 appellé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 :
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 :
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.
A 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 appellée 23 fois à divers endroit du code
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 multiplue par (0x11 + 0x448h)
- on effectue un AND avec le masque 0x000000ff 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 :
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 dernièr 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 :
#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 :
./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).
