Nicolas SURRIBAS

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

Analyse du malware Podnuha.ql : troisième partie

Rédigé par devloop - -

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 :
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 :
  1. on lui ajoute 0x11
  2. on la multiplue par (0x11 + 0x448h)
  3. 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).

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

Les commentaires sont fermés.