-------------------------------------------------------------------------------- vx-reversing: basis silmaril -------------------------------------------------------------------------------- 0 - introduction ============ Pour ce premier article, nous allons analyser un fichier infecté par un virus vraiment basique utilisé pour l'occasion, peu virulent, mais néanmoins fonctionnel sur les OS Windows 9x, 2000, XP. Le but est de récolter assez d'informations pour être capable de nettoyer le fichier infecté à la main, puis de coder un petit désinfecteur qui automatisera la procédure. Ainsi, le fichier infecté pourra être exécuté sans lancer le virus. 1 - prérequis ========= -pour les connaissances, ce qui se trouve dans le vx-guide [1] est quasiment suffisant. Quelques notions sur le PEB/TEB ne seront pas inutiles, ce sujet est abordé (indirectement) dans un tuto de Neitsa [2]. -Pour les outils, j'ai utilisé IDA, un puissant désassembleur [3], un éditeur PE [4], et enfin un éditeur hexadecimal. Une partie du listing du fichier désassemblé est également joint pour les curieux. Le dossier binaries contient un fichier propre, un fichier infecté et le désinfecteur. 2 - structure du fichier infecté ============================= Le fichier cible est aussi un fichier codé pour l'occasion, un truc qui dit 'helloworld' codé avec TASM. Lorsque l'on ouvre ce fichier infecté avec l'éditeur de PE, on voit tout de suite qu'entre les sections et l'entrypoint, quelque chose ne colle pas: entrypoint RVA: 5000 sections: Name VOffset Vsize ROffset RSize Flags ----------+----------+----------+----------+----------+----------+ CODE | 00001000 | 00001000 | 00000600 | 00000200 | 60000020 DATA | 00002000 | 00001000 | 00000800 | 00000A00 | C0000040 .idata | 00003000 | 00001000 | 00001200 | 00000200 | C0000040 .reloc | 00004000 | 00001000 | 00001400 | 00000200 | E00000C0 .larva | 00005000 | 00001000 | 00001600 | 00001000 | A0000020 Le point d'entrée du programme se trouve en dehors de la section CODE; c'est quelque chose de systématique lorsque l'on étudie des exécutables protégés par des packers/protectors. Ici point de packer, mais un principe identique: un bout de code exécuté avant le code réel du programme (situé dans la section CODE). Pour les packer, ce bout de code est chargé de décompresser l'exécutable en mémoire afin qu'il puisse être exécuté correctement; pour nous, ce bout de code est le virus, qui effectuera toutes ses petites affaires avant de rendre la main au programme infecté. On remarque aussi que cette dernière section (clic droit --> edit section header --> flags) possède les attributs: executable as code+write+contain executable code, représentés pas l'opération suivante: 20000000 OR 80000000 OR 00000020 = A0000020. Ce qui est peu courant pour un exécutable normal, à savoir la présence des attributs code et write dans une même section, est une nécessité pour un virus: il a besoin d'un accès en écriture car code et datas sont regroupés en cette seule section et il manipule les datas (en particulier des sauvegardes de valeurs importantes du PE et des calculs d'addresses). Si cet attribut write est absent, le virus ne peut tout simplement pas s'exécuter. 3 - les entrailles du virus ======================= 3.1 - chargement ---------------- Si vous vous souvenez un peu du vx-guide, je vous avais parlé du delta handle (bien que ce ne soit pas un handle, mais peu importe); ce delta contient une valeur qui permet au virus de se situer dans le programme hôte, et de ce fait il peut savoir où se trouvent les données qu'il va utiliser, où les sauvegarder, etc... Dans le cas où nous procédons à l'étude de la génération 0 d'un virus (virus fraîchement compilé), il n'y a pas besoin de se soucier de ce delta, il vaut 0. Par contre, et c'est notre cas, le delta est différent de 0 dès lors que le virus a infecté un fichier (génération X du virus). L'astuce, c'est de modifier l'image_base à laquel le fichier sera désassemblé, de façon à annuler ce delta et ainsi simuler la génération 0 du virus. En ayant ainsi notre fake generation 0, on va avoir de beaux labels comme à l'origine, et pouvoir les renommer afin de comprendre ce virus plus facilement. Pour calculer notre nouvelle image_base, c'est simple: on soustrait la valeur du delta. Et pour calculer ce delta, je connais 2 techniques: la première est de supposer que la VA de l'entrypoint du programme non infecté est 401000 et de soustraire cette valeur à la VA de l'entrypoint du programme infecté (peu fiable, même si c'est vrai pour le fichier étudié), et la seconde est de charger le fichier infecté sous IDA un première fois, afin de voir de que vaut ce delta. J'ai choisi la seconde méthode: __________________________________________________________ .larva:00401000 public start .larva:00401000 start proc near .larva:00401000 call $+5 .larva:00401005 .larva:00401005 delta: .larva:00401005 pop ebp .larva:00401006 sub ebp, offset delta __________________________________________________________ 3 lignes qui devraient vous êtres familières... Il nous faut la valeur de EBP: après le POP, il vaut 405005 et après le SUB il vaut 405005-401005=4000. EBP n'est plus modifié pas la suite, nous avons notre delta qui vaut 4000. On quitte IDA sans sauvegarder, et on recharge le fichier, cette fois-ci en cochant manual load. Comme new image_base, on soustrait 4000 à celle proposée, ce qui nous donne 3FC000. On charge tout le reste normalement, et nous avons un beau fake generation 0. 3.2 - analyse du virus ---------------------- L'une des forces d'IDA, est de permettre de renommer des labels, et aujourd'hui on ne va pas s'en priver... on va commencer tout de suite en renommant le premier label, qui permet de calculer le delta handle; appelons le delta: clic droit --> rename. Le code qui suit le calcul du delta permet de récupérer l'image_base de la DLL kernel32. dll tout en identifiant le type de noyau auquel le virus est confronté (noyau de type 9x ou de type NT). Ensuite, nous passons en 401037, où l'adresse du kernel32 est stockée dans une variable; comme pour tout à l'heure, on donne à cette variable un nom plus explicite, par exemple Addr_kernel32. Une fois que l'adresse de kernel32 est connue, le virus va scanner l'export_table de kernel32 à la recherche de l'API GetProcAddress: ________________________________________________________________________________ .larva:00401037 .larva:00401037 end_identification: .larva:00401037 mov ss:ADDR_kernel32[ebp], eax .larva:0040103D mov edx, ss:ADDR_kernel32[ebp] .larva:00401043 mov eax, [eax+3Ch] .larva:00401046 mov edx, [edx+eax+78h] .larva:0040104A add edx, ss:ADDR_kernel32[ebp] .larva:00401050 mov ecx, [edx+18h] ; ECX=nombre d'exports .larva:00401053 mov ebx, [edx+20h] .larva:00401056 add ebx, ss:ADDR_kernel32[ebp] .larva:0040105C ; nom des exports .larva:0040105C API_suivante: .larva:0040105C jecxz short CetProc_non_trouvee .larva:0040105E dec ecx .larva:0040105F mov esi, [ebx+ecx*4] .larva:00401062 add esi, ss:ADDR_kernel32[ebp] ; ESI pointe sur le nom d'un export de kernel32 .larva:00401068 lea edi, byte_401577[ebp] .larva:0040106E .larva:0040106E lettre_suivante: .larva:0040106E cmpsb .larva:0040106F jnz short API_suivante .larva:00401071 cmp byte ptr [edi], 0 .larva:00401074 jz short calcule_adresse .larva:00401076 jmp short lettre_suivante ________________________________________________________________________________ En 401068, l'adresse de la chaîne GetProcAddress (la fonction recherchée) est placée dans EDI. Ensuite, la chaîne pointée pas l'adresse contenue dans ESI est comparée à GetProcAddress, et le virus boucle tant qu'il ne l'a pas trouvé. S'il ne trouve pas cette fonction, il quitte la routine. A ce propos, j'ai remarqué une incohérence: le virus quitte cette boucle, mais il continue son exécution à un autre endroit dans le code qui est illogique. . . puisque GetProcAddress n'a pas été trouvée, il aurait tout simplement fallut rendre la main à l'hôte. Une fois que le nom de l'API a été trouvé, le virus procède au calcul de son adresse via l'algorythme qui avait été présenté dans le vx-guide, à savoir: (position*2) + ordinal_table_VA = ordinal de l’API (ordinal*4) + address_table_VA + image_base de kernel32 = adresse de l’API GetProcAddress permet de retrouver l'adresse d'une autre fonction, elle a juste besoin qu'on lui fournisse son nom et l'adresse de la dll qui l'exporte. Une fois que GetProcAddress peut être utilisée, le virus s'en sert pour appeler les APIs GetWindows/System/CurrentDirectory, afin d'obtenir les répertoires où il recherchera des fichiers à infecter. Ensuite, il appelle l'API lstrcat afin de concaténer les répertoires avec la chaîne \target*. exe. Ainsi, pour un répertoire donné, il recherchera les fichiers de type target*. exe. On entre ensuite dans une boucle classique de type FindFirstFile/FindNextFile, où le virus recherche des fichiers vulnérables. Nous passons rapidement ces étapes afin de trouver les informations importantes qui nous permettront de désinfection le fichier. Ces infos se trouvent dans la procédure d'infection des fichiers, qui commence en 4011B0. Il n'y a rien de très important pour nous, jusqu'en 401317. A partir de cette adresse, le virus sauvegarde des informations importantes sur le fichier hôte: ________________________________________________________________________________ .larva:004012F5 cmp word ptr [esi], 5A4Dh ; MZ header .larva:004012FA jnz unmappe_fichier .larva:00401300 mov ebx, [esi+3Ch] .larva:00401303 cmp word ptr [esi+ebx], 4550h ; signature "PE" .larva:00401309 jnz unmappe_fichier .larva:0040130F add esi, ebx ;ESI = VA du PE_header .larva:00401311 mov ss:PE_header[ebp], esi .larva:00401317 mov edx, [esi+28h] ; EDX = OEP du fichier .larva:0040131A mov ss:original_EP[ebp], edx .larva:00401320 mov edx, [esi+34h] ; EDX = image_baser .larva:00401323 mov ss:image_base[ebp], edx .larva:00401329 mov edx, [esi+38h] ; EDX=section alignment .larva:0040132C mov ss:section_alignment[ebp], edx .larva:00401332 mov edx, [esi+50h] ; EDX= image_size .larva:00401335 mov ss:image_size[ebp], edx ________________________________________________________________________________ Grace à ces informations, nous savons que le virus va modifier l'entrypoint et l'image_size du fichier (sinon il ne les sauvegarderait pas). L'alignement des sections lui permettra d'ajouter la section dans laquelle il va se répliquer, et l'image_base lui permettra de rendre la main à l'hôte. Le code qui suit est intéressant, puisqu'il sert à localiser la dernière section du fichier et à en ajouter une nouvelle. Il n'y a néanmoins pas d'informations intéressantes pour la désinfection du fichier, nous pouvons donc passer cette étape pour aller en 4013C4, zone où le virus se réplique dans la nouvelle section et met à jour les champs importants du PE de l'hôte: ________________________________________________________________________________ .larva:004013C4 mov eax, ss:virtual_address[ebp] .larva:004013CA mov ss:new_entrypoint[ebp], eax .larva:004013D0 mov edi, ss:ptr_to_raw_data[ebp] .larva:004013D6 add edi, ss:adresse_fichier_mappe[ebp] ; EDI=destination_de_la_copie .larva:004013DC lea esi, start[ebp] ; ESI=source_de_la_copie .larva:004013E2 mov ecx, 0B27h ; nombre d'octets ; à copier (longueur du virus) .larva:004013E7 cld .larva:004013E8 repe movsb ; copie octets après octets .larva:004013EA mov esi, ss:PE_header[ebp] .larva:004013F0 inc word ptr [esi+6]; met à jour le nombre de sections .larva:004013F4 mov eax, ss:new_entrypoint[ebp] .larva:004013FA mov [esi+28h], eax ; met à jour le nouvel EP .larva:004013FD mov eax, 0B27h .larva:00401402 add ss:dword_40183E[ebp], eax .larva:00401408 mov eax, ss:image_size[ebp] .larva:0040140E mov [esi+50h], eax ; met à jour la taille du ; fichier en mémoire .larva:00401411 inc ss:compteur_d_infections[ebp] ; incrémente le ; nombre d'infections ________________________________________________________________________________ Le reste du code ne nous sera pas utile, il sert à sauvegarder les modifications apportées au fichier, fermer les handle et rendre la main à l'hôte. Pour désinfecter le fichier, nous allons donc devoir remettre l'entrypoint d'origine, supprimer la section où se trouve le virus, mettre à jour les entrées du PE relatives à cette section ainsi que l'image du fichier en mémoire. 4 - nettoyage ========= Pour retrouver l'entypoint originel du fichier, j'ai choisi une méthode que je trouve triviale, mais c'est la seule que je connaisse: à partir de l'entrypoint du fichier infecté, on soustrait le delta précédemment calculé: 405000-4000=401000. pour le nombre de sections, c'est le nombre actuel moins 1: 5-1=4 pour la taille de l'image, c'est la taille actuelle moins la taille de la section du virus: 6000-1000=5000 avec LordPE, on note le RawOffset de la section du virus (1600h) et sa taille (1000h), puis met les champs à jours avec les informations que nous avons obtenus: entrypoint: 1000 size of image: 5000 number of sections: 4 Bien, il reste le plus difficile, à savoir supprimer définitivement la section: avec un éditeur hexa, on se rend à l'offset 1600 (début de la dernière section), on sélectionne un bloc de 1000h octets (taille de la dernière section), et on le delete rageusement,. Reste plus qu'à sauvegarder, et nous avons à nouveau un fichier clean. J'ai aussi codé un petit désinfecteur qui automatise la procédure, comme me l'ont suggéré kaze et Codasyl. Son code source est normalement joint au mag. 5 - conclusion-greetz ================= Bah... j'espère que ça vous à plus... Comme d'habitude, tout est copyleft. je passe le bonjour aux membres actuels de la NAS: Mastermatt29, Zave, Koderez, Necrophiliac, haiklr. Je salut nos 3 anciens membres: Kef, w0rp et ++meat le fondateur de cette équipe, ainsi que virtualabs et les teams FRET et RedKod. Enfin, je remercie toute l'équipe FAT pour sa sympathie et pour la publication de ce modeste article... rassurez vous, je n'oublie personne :p 6 - références/liens ================ [1] http://everyones-world.info/0x00.%20NAS%20productions/ [2] http://neitsabes.online.fr/RE/HIRE/OwnGetProc1.html [3] http://www.programmersheaven.com/search/Download.asp?FileID=37637 [4] http://y0da.cjb.net/ silmaril, juin 2006