Retour au sommaire
IV. Analyse du loader
  1. Identification de la cible

    Le fichier à analyser peut être téléchargé ici : Virus.zip.

    Ce programme étant dangereux, je décide de l'étudier de manière statique à l'aide du désassembleur IDA Freeware 4.3. Ce qui est, de plus, une bonne occasion pour découvrir cet outil dont l'interface m'a toujours rebuté. Avant de commencer l'analyse, je préfère m'assurer que le PE Header n'a pa été altéré, pour éviter tout problème de chargement sous IDA. J'inspecte donc la structure du fichier à l'aide de Lord PE.

    NameVOffsetVSizeROffsetRSizeFlags
    .text00001000000008260000000000000000E0000040
    .rdata00002000000000660000040000000200E0000040
    ZeGun000003000000010000000060000001000E0000040
    ZeGun100004000000010000000160000000200E0000040

    Rien à signaler pour le PE Header, les valeurs semblent correctes. La table d'import ne contient qu'une seule fonction, ce qui fait penser que les autres seront importées dynamiquement. On remarque que les 2 champs ROffset et RSize de la première section sont nuls. De plus, l'EntryPoint se trouve dans la 3ème section (00003010) et les 4 sections autorisent l'accès en écriture (E0000040 signifie que la section autorise l'accès en lecture, écriture et exécution). C'est souvent le cas lorsqu'un programme est compressé/crypté.

  2. Retour au sommaire
  3. Chargement dans IDA

    On charge le programme dans IDA en laissant les options par défaut dans un premier temps, et première chose frappante, le code semble crypté après les 15 premières instructions.


    ZeGun0:00403010                     public start
    ZeGun0:00403010     start:
    ZeGun0:00403010                     pusha                       ; Sauvegarde des registres
    ZeGun0:00403011                     call    $+5                 ; Place l'adresse 00403016
    ZeGun0:00403016                     pop     ebp                ; dans EBP
    ZeGun0:00403017                     sub     ebp, 6              ; EBP = EntryPoint
    ZeGun0:0040301A                     sub     ebp, 401010h  ; - le vrai EntryPoint du loader
    ZeGun0:0040301A                                                         ; Pour caclculer le delta du code relogeable
    ZeGun0:00403020                     lea     ebx, [ebp+4016F8h]
    ZeGun0:00403026                     mov     eax, [esp-10h]
    ZeGun0:0040302A                     mov     [ebx], eax
    ZeGun0:0040302C                     lea     edi, [ebp+401042h]
    ZeGun0:00403032                     lea     ecx, [ebp+401073h]
    ZeGun0:00403038                     sub     ecx, edi
    ZeGun0:0040303A     
    ZeGun0:0040303A     Decrypt_Layer01:                        ; CODE XREF: ZeGun0:00403040
    ZeGun0:0040303A                     add     [edi], cl
    ZeGun0:0040303C                     xor     byte ptr [edi], 0D2h
    ZeGun0:0040303F                     inc     edi
    ZeGun0:00403040                     loop    Decrypt_Layer01
    ;-------------------- Code crypté -----------------------------------------------------------------
    ZeGun0:00403042                     db      2Eh
    ZeGun0:00403042                     daa
    ZeGun0:00403044                     sub     al, 94h
    ZeGun0:00403046                     db      65h

    Les premières instructions sont typiques des loaders et permettent d'avoir un code indépendant de l'adressage. C'est à dire pouvant être exécuté à n'importe quel endroit (code relogeable). Au cours de l'exécution du programme, ebp contiendra un delta qui sera ajouté aux constantes pour pointer vers la bonne adresse. Ici, le delta est de 2000h (403016 - 6 - 401010).

    Par exemple, en 0040302C, l'adresse 00401042 est placée dans edi. 00401042 correspond à l'adresse d'une variable lorsque le loader a été assemblé. Pour retrouver la bonne adresse dans ce nouvel adressage, le delta est ajouté ce qui donne 00403042 (le début du code crypté).

  4. Retour au sommaire
  5. Suppression du Delta

    Pour bénéficier pleinement de la puissance de IDA, il serait préférable d'avoir les adresses directement calculées avec le delta. Je suis d'abord parti sur un script IDC qui calculait les adresses avant de les patcher mais ce n'était pas très propre et un peu trop long à coder. Finalement, après une petite discussion avec Deicide et une profonde réflexion, j'ai trouvé une méthode beaucoup plus simple et efficace.

    IdaOn ferme IDA sans sauvegarder l'IDB et on recharge le programme mais cette fois en cochant Manual Load. Pour supprimer le delta, il suffit que la section du loader soit chargée à son adresse d'origine (00401000). Pour se faire, on va simplement retrancher le delta (2000) à l'ImageBase (400000). On entre donc dans le champ de IDA, la nouvelle ImageBase (003FE000) et on clique sur OK pour le reste.

    Maintenant que le delta (ebp) vaut 0, on va pouvoir renommer les constantes en label. Voici la même partie de code maintenant plus compréhensible :


    ZeGun0:00401010     
    ZeGun0:00401010                     public start
    ZeGun0:00401010     start:                                    ; DATA XREF: ZeGun0:0040101A
    ZeGun0:00401010                     pusha                   ; Sauvegarde des registres
    ZeGun0:00401011                     call    $+5             ; Calcul du delta pour le code relogeable
    ZeGun0:00401016                     pop     ebp            ; EBP = 00403016
    ZeGun0:00401017                     sub     ebp, 6         ; EBP = 00403010
    ZeGun0:0040101A                     sub     ebp, offset start ; EBP = 00403010 - 00401010 = 2000 = Delta
    ZeGun0:0040101A                                                  ; En modifiant l'ImageBase à 003FE000, Delta = 0
    ZeGun0:00401020                     lea     ebx, NTDLL_ImageBase[ebp]
    ZeGun0:00401026     
    ZeGun0:00401026                                     ; Sous Win2000, [ESP-10h] contient l'ImageBase de NTDLL
    ZeGun0:00401026                     mov     eax, [esp-10h]
    ZeGun0:0040102A                     mov     [ebx], eax      ; ImageBase sauvegardée
    ZeGun0:0040102C                     lea     edi, Layer01_Start[ebp]
    ZeGun0:00401032                     lea     ecx, Layer01_End[ebp]
    ZeGun0:00401038                     sub     ecx, edi         ; ECX = Taille du Layer01 = 31h
    ZeGun0:0040103A     
    ZeGun0:0040103A     DecryptLayer01:                         ; CODE XREF: ZeGun0:00401040
    ZeGun0:0040103A                     add     [edi], cl
    ZeGun0:0040103C                     xor     byte ptr [edi], 0D2h
    ZeGun0:0040103F                     inc     edi
    ZeGun0:00401040                     loop    DecryptLayer01
    ZeGun0:00401042     
    ZeGun0:00401042     Layer01_Start:                           ; DATA XREF: ZeGun0:0040102C
  6. Retour au sommaire
  7. Layers de cryptage

    Juste après le cacul du delta, le programme sauvegarde la valeur contenue dans [esp-10h]. En regardant plusieurs programmes sous OllyDbg, j'ai remarqué que cette valeur est l'ImageBase de ntdll.dll sous Windows 2000 (mais pas sous XP). S'ensuit une simple boucle (DecryptLayer01) qui décrypte les 49 octets de Layer01_Start (en 00401042) à Layer01_End (en 00401073).

    On ne peut pas continuer puisque le reste du code est crypté. On va donc le décrypter à l'aide d'un script IDC que voici :

    #include <idc.idc>                              // Bibliothèque de fonctions

    static main() {                                 // Point d'entrée du script
           auto J,address,count;                    // Déclaration des variables
           address = 0x401042;                      // Adresse de début de décryptage
                                                            // Décrypte 49 octets
           for (count = 0x31; count>0; count--) {
               J = Byte(address);                   // Récupère la valeur de l'octet à l'adresse courante
               J = J + count;                               // Ajoute la valeur du compteur de boucle
               J = J ^ 0xD2;                                // Xor 0D2h
               PatchByte(address,J);                // Patche l'adresse avec l'octet décrypté
               address++;                           // Octet suivant
           }
           Message("Layer décrypté\n");
    }

    Et voici le code fraichement décrypté et analysé (touche C) :


    ZeGun0:00401042    Layer01_Start:                          ; DATA XREF: ZeGun0:0040102C
    ZeGun0:00401042                     lea     eax, Layer02_Start[ebp]
    ZeGun0:00401048                     lea     ecx, Layer02_End[ebp]
    ZeGun0:0040104E                     sub     ecx, eax
    ZeGun0:00401050     
    ZeGun0:00401050     DecryptLayer02:                         ; CODE XREF: ZeGun0:00401068
    ZeGun0:00401050                     neg     byte ptr [eax]
    ZeGun0:00401052                     push    eax
    ZeGun0:00401053                     add     byte ptr [eax], 33h
    ZeGun0:00401056                     sub     byte ptr [eax], 81h
    ZeGun0:00401059                     pop     eax
    ZeGun0:0040105A                     sub     byte ptr [eax], 0C3h
    ZeGun0:0040105D                     push    ecx
    ZeGun0:0040105E                     neg     byte ptr [eax]
    ZeGun0:00401060                     ror     byte ptr [eax], 2
    ZeGun0:00401063                     xor     [eax], cl
    ZeGun0:00401065                     pop     ecx
    ZeGun0:00401066                     inc     eax
    ZeGun0:00401067                     dec     ecx
    ZeGun0:00401068                     jnz     short DecryptLayer02
    ZeGun0:0040106A                     mov     eax, ebp
    ZeGun0:0040106C                     add     eax, offset Layer02_Start
    ZeGun0:00401071                     push    eax
    ZeGun0:00401072                     retn                    ; Jmp Layer02_Start
    ZeGun0:00401073     ; ---------------------------------------------------------------------------
    ZeGun0:00401073     
    ZeGun0:00401073     Layer01_End:   

    Ce code est exécuté juste après la boucle de décryptage et se trouve être une nouvelle boucle qui va décrypter un autre bout de code.

    Le loader contient 15 couches de cryptage toutes différentes. Je ne vais pas les copier ici mais vous trouverez dans le dossier IDC les 15 scripts pour décrypter les 15 layers séparément.

    Après 5 couches de décryptage successives, la 6ème boucle va décrypter plusieurs fonctions utilitaires : une émulation de GetProcAddress, 3 fonctions pour résoudre les imports, et la fonction de décompression. Puis avant de sauter sur la prochaine boucle de décryptage, le loader récupère l'adresse de IsDebuggerPresent grâce à la fonction GetProcAddress émulée en 00401393, avant de l'appeler. Si un debugger est détecté, le programme quitte. Je ne copie pas le code qui ne présente aucun intérêt particulier.

  8. Retour au sommaire
  9. Vérification de la date

    Après quelques décryptages supplémentaires, on arrive sur cette partie de code qui va vérifier la date du système. L'adresse de la fonction GetSystemTime est récupérée dynamiquement (toujours avec l'émulation de GetProcAddress) puis appélée. Cette fonction va remplir une structure SYSTEMTIME déclarée plus haut.

    IdaPour déclarer une structure dans IDA, on se place dans la fenêtre Structures (Alt+9) et on crée une nouvelle définition (Inser). Dans la boîte de dialogue, on clique sur Add standard structure puis on choisit dans la liste la structure qui nous intéresse (ici SYSTEMTIME). Il ne reste plus qu'à cliquer sur l'adresse représentant le début de la structure (004011AC) puis Alt+Q pour afficher la liste des structures disponibles et choisir SYSTEMTIME. Les membres sont renommés automatiquement dans tout le listing.


    ZeGun0:004011AC     SysTime    dw 0                    ; wYear
    ZeGun0:004011AC                     dw 0                    ; wMonth
    ZeGun0:004011AC                     dw 0                    ; wDayOfWeek
    ZeGun0:004011AC                     dw 0                    ; wDay
    ZeGun0:004011AC                     dw 0                    ; wHour
    ZeGun0:004011AC                     dw 0                    ; wMinute
    ZeGun0:004011AC                     dw 0                    ; wSecond
    ZeGun0:004011AC                     dw 0                    ; wMilliseconds
    ZeGun0:004011BC     ; ---------------------------------------------------------------------------
    ZeGun0:004011BC     
    ZeGun0:004011BC     GetTime:                                ; CODE XREF: ZeGun0:004011AA
    ZeGun0:004011BC                     lea     eax, SysTime.wYear[ebp]
    ZeGun0:004011C2                     push    eax
    ZeGun0:004011C3                     call    Push_strGetSystemTime
    ZeGun0:004011C3     ; ---------------------------------------------------------------------------
    ZeGun0:004011C8     str_Getsystemtime db 'GetSystemTime',0
    ZeGun0:004011D6     ; ---------------------------------------------------------------------------
    ZeGun0:004011D6     
    ZeGun0:004011D6     Push_strGetSystemTime:                  ; CODE XREF: ZeGun0:004011C3
    ZeGun0:004011D6                     lea     ebx, Kernel32_ImageBase[ebp]
    ZeGun0:004011DC                     mov     ebx, [ebx]
    ZeGun0:004011DE                     push    ebx
    ZeGun0:004011DF                     lea     ebx, GetProcAddress[ebp]
    ZeGun0:004011E5                     call    ebx
    ZeGun0:004011E7                     call    eax             ; Call GetSystemTime
    ZeGun0:004011E9                     lea     eax, SysTime.wYear[ebp]
    ZeGun0:004011EF                     cmp     word ptr [eax], 2011 ; Année = 2011
    ZeGun0:004011F4                     jnz     short Quit_BadDate
    ZeGun0:004011F6                     add     eax, 2          ; wMonth
    ZeGun0:004011F9                     cmp     word ptr [eax], 2 ; Mois = Février
    ZeGun0:004011FD                     jnz     short Quit_BadDate
    ZeGun0:004011FF                     add     eax, 4          ; wDay
    ZeGun0:00401202                     cmp     word ptr [eax], 16
    ZeGun0:00401206                     jnz     short Quit_BadDate ; Si la date du jour n'est pas le 16 février 2011, le programme quitte
    ZeGun0:00401208                     lea     edx, Layer08_End[ebp]
    ZeGun0:0040120E                     push    edx
    ZeGun0:0040120F                     retn                    ; JMP Layer08_End
    ZeGun0:00401210     ; ---------------------------------------------------------------------------
    ZeGun0:00401210     
    ZeGun0:00401210     Quit_BadDate:                           ; CODE XREF: ZeGun0:004011F4
    ZeGun0:00401210                                             ; ZeGun0:004011FD
    ZeGun0:00401210                     popa
    ZeGun0:00401211                     retn
    ZeGun0:00401212     ; ---------------------------------------------------------------------------
    ZeGun0:00401212     
    ZeGun0:00401212     Layer08_End:                            ; DATA XREF: ZeGun0:004010EE

    IdaVoici un point important. Le programme continuera son exécution uniquement si la date système est le 16 février 2011.

  10. Retour au sommaire
  11. Décompression du code

    Les adresses des fonctions LoadLibraryA, GetProcAddress et VirtualAlloc (cette dernière n'est pas utilisée) sont récupérées de la manière suivante :


    ZeGun0:004012A3                     call    Push_StrLoadLibrayA
    ZeGun0:004012A3     ; ---------------------------------------------------------------------------
    ZeGun0:004012A8     str_Loadlibrarya db 'LoadLibraryA',0
    ZeGun0:004012B5     ; ---------------------------------------------------------------------------
    ZeGun0:004012B5     
    ZeGun0:004012B5     Push_StrLoadLibrayA:                    ; CODE XREF: ZeGun0:004012A3
    ZeGun0:004012B5                     lea     ebx, Kernel32_ImageBase[ebp]
    ZeGun0:004012BB                     mov     ebx, [ebx]
    ZeGun0:004012BD                     push    ebx
    ZeGun0:004012BE                     lea     ebx, GetProcAddress[ebp]
    ZeGun0:004012C4                     call    ebx             ; GetProcAddress(hKernel32, "LoadLibraryA");
    ZeGun0:004012C6                     lea     ebx, @LoadLibraryA[ebp]
    ZeGun0:004012CC                     mov     [ebx], eax

    La fonction appelée ensuite ressemble fortement à la fonction aP_depack_asm_fast de aPLib.


    ZeGun0:00401324                     lea     edi, VOffset[ebp] ; VOffset de la 1ère section
    ZeGun0:0040132A                     mov     edi, [edi]
    ZeGun0:0040132C                     lea     edx, ImageBase[ebp] ; + ImageBase
    ZeGun0:00401332                     add     edi, [edx]
    ZeGun0:00401334                     push    edi             ; Push VA 1ere section
    ZeGun0:00401335                     lea     esi, PackedCode[ebp]
    ZeGun0:0040133B                     push    esi
    ZeGun0:0040133C                     call    aP_depack_asm_fast ; aP_depack_asm_fast(Source, Destination);

    La première section .text est décompressée à sa place en 00401000.

  12. Retour au sommaire
  13. Résolution des imports

    Après avoir récupéré l'ImageBase de User32.dll, la fonction de résolution des imports est appelée. Celle-ci est très confuse du fait de nombreux JMP dans tous les sens. Elle a été découpée en plusieurs blocs qui ont ensuite été mélangés. Lorsque l'on a compris l'ordre d'exécution, elle ne pose pas de problèmes particuliers. A part une fonction que j'ai appelé CheckKernelUser car elle teste les ImageBases de Kernel32 et de User32. Cependant le résultat n'est pas utilisé. Le seul intérêt de cette fonction, à la fin, est qu'elle fait pointer le nom de la dll courante vers la suivante.

    Ici, il n'y a qu'un seul import, Kernel32.EraseTape. On note également que le nom de l'api est décrypté (xor byte ptr [edi], 0C5h) avant d'en déterminer l'adresse (script IDC pour décrypter le nom de l'api).


    ZeGun0:00401341                     call    Push_strUser32_dll ; LoadLibraryA("User32.dll");
    ZeGun0:00401341     ; ---------------------------------------------------------------------------
    ZeGun0:00401346     str_User32_dll  db 'User32.dll',0
    ZeGun0:00401351     ; ---------------------------------------------------------------------------
    ZeGun0:00401351     
    ZeGun0:00401351     Push_strUser32_dll:                     ; CODE XREF: ZeGun0:00401341
    ZeGun0:00401351                     call    ss:LoadLibraryA[ebp] ; LoadLibraryA("User32.dll");
    ZeGun0:00401357                     lea     ebx, User32_ImageBase[ebp]
    ZeGun0:0040135D                     mov     [ebx], eax
    ZeGun0:0040135F                     lea     ebx, FixImports[ebp]
    ZeGun0:00401365                     call    ebx
  14. Retour au sommaire
  15. Saut sur l'OEP

    L'Original EntryPoint est calculé simplement puis le loader patche l'adresse 0040138E pour donner push 00401004h et le retn saute sur l'OEP du programme démarrant ainsi l'exécution du virus. Juste avant de sauter, le loader restaure la valeur sauvegardé en début de programme.


    ZeGun0:00401367                     lea     eax, OriginalEntryPoint[ebp]
    ZeGun0:0040136D                     mov     eax, [eax]
    ZeGun0:0040136F                     add     eax, ss:ImageBase[ebp] ; eax = OEP VA
    ZeGun0:00401375                     lea     ebx, (OEPJmp+1)[ebp]
    ZeGun0:0040137B                     mov     [ebx], eax      ; Patche le PUSH pour le faire pointer sur l'OEP
    ZeGun0:0040137D                     add     esp, 8
    ZeGun0:00401380                     lea     ebx, NTDLL_ImageBase[ebp]
    ZeGun0:00401386                     mov     ebx, [ebx]
    ZeGun0:00401388                     mov     [esp-10h], ebx  ; Restaure la valeur sauvegardée au début
    ZeGun0:0040138C                     popa
    ZeGun0:0040138D     
    ZeGun0:0040138D     OEPJmp:                                 ; DATA XREF: ZeGun0:00401375
    ZeGun0:0040138D                     push    0
    ZeGun0:00401392                     retn                    ; JMP OEP

    Vous trouverez la base LoaderVirus.idb du listing complet, décrypté et commenté dans le dossier IDB.

  16. Retour au sommaire
  17. Programmation d'un décompresseur

    D'après l'analyse, on a vu que le virus est simplement compressé avec aPLib. Le cryptage n'est là que pour le loader. La décompression sera donc simple.

    IdaOn va d'abord copier la fonction de décompression depuis IDA en faisant menu File -> Produce -> Create ASM file.... On obtient un fichier asm contenant le listing complet de ida. Il ne reste plus qu'à copier la fonction aP_depack_asm_fast et la coller dans notre code (éventuellement corriger certaines lignes si l'assembleur n'est pas content, j'ai utilisé MASM).

    Voici la structure de mon programme :

    Voici les corrections apportées au PEHeader :

                   Mov eax, NewFile
                   Lea edx, Dword ptr [eax+3Ch]            ; edx = @PEHeader
                   Mov edx, Dword ptr [edx]
                   Add edx, eax                                            ; edx = PEHeader
                   Mov Dword ptr [edx+28h], 1004h          ; Corrige l'EntryPoint
                   Mov Dword ptr [edx+80h], 2010h          ; Corrige l'ImportDirectoryRVA
                   Add edx, 0F8h
                   Push Dword ptr [UnpackedSize]
                   Pop Dword ptr [edx+10h]                 ; Corrige la RSize de la 1ère section
                   Mov Dword ptr [edx+14h], 400h           ; Corrige le ROffset de la 1ère section
                   Mov ebx, [UnpackedSize]
                   Add edx, 28h                                            ; Décale les sections suivantes pour prendre en compte
                   Add [edx+14h], ebx                              ; la taille de la nouvelle section
                   Add edx, 28h
                   Add [edx+14h], ebx
                   Add edx, 28h
                   Add [edx+14h], ebx

    Le code source du décompresseur est disponible dans le dossier Sources/Unpacker/


Retour au sommaire