Maximize
Bookmark

VX Heaven

Library Collection Sources Engines Constructors Simulators Utilities Links Forum

Total encryption for PE infectors

kaze
2004

[Back to index] [Comments]

Introduction

Tres tôt, pour échapper aux scanners, les virus ont adopté une méthode de camouflage aujourd'hui très répendue: l'encryption. Cependant, aucun virus ne peut s'encrypter totalement, le décrypteur devant toujours être en clair. Enfin, ne pouvait ... :p C'est désormais possible, gr^qce à la mgie Windows. Imaginez ... votre virus est entièrement crypté sur le disque et c'est Windows, au chargement du PE, qui le décrypte à votre place. Sympa non ? Cette méthode n'est pas originale, ayant déjà été présentée dans 29A#5 par Tcp, mais très brievement, avec peu d'explications et zéro code. De plus, il me semble que cette technique n'a jamais été reutilisée dans des virii connus. J'ai donc essayé d'y remédier en apportant un exemple fonctionnel et plutot efficace (pour l'instant, aucun AV ne l'a détecté, alors qu'il reste très très simple).

Cet article n'est pas un tutoriel sur la programmation de PE infecteurs, il assume que vous savez coder ce type d'infecteur et que vous avez quelques bases en encryption. Si ce n'est pas le cas, allez lire le tut de rikenar. Pour les remarques ou autre, n'hésitez pas:

[email protected] #vxers: irc.epiknet.org

I. Description

Le format PE permet de prendre en charge un mécanisme assez interessant: la relocation. Voyez plutôt: lorsqu'un executable (ou un dll) est compilé, le linkeur assume que le programme sera chargé à une certaine adresse en RAM. Pour windows, cette adresse appelée ImageBase est généralement 0x400000. 99% du temps cette adresse est disponible dans le process et les relocations n'auront pas lieu. C'est pour cela que quelques virus se recopient sur la section des relocs, étant tres rarement utilisée. Mais lorsque par exemple un executable et un dll ayant tous deux une ImageBase de 0x400000 sont chargés en RAM, ils ne peuvent être chargés tous deux à 0x400000. L'un des deux devra donc être chargé à une adresse différente, ce qui faussera alors tout son adressage. C'est le même principe que pour le delta offset:

                __________ 0x400000 _________ 
   -------------                             -------------     
   | MZ HEADER |                             | MZ HEADER |
   |-----------|                             |-----------|
   | PE HEADER |                             | PE HEADER |
   |-----------|                             |-----------|     
   |           |                             |           | 
   |           |                             |           | 
   |    EXE    |                             |    DLL    | 
   | variable1 |<---  adresse variable1  --->| ........  |---? 
   | variable2 |<---  adresse variable2  --->| ........  |   |  Decalage
   -------------       _______|________      |-----------|   |  
                       etablies lors de      | MZ HEADER |   |
                        la compilation       |-----------|   |
                                             | PE HEADER |   |
                                             |-----------|   |
                                             |           |   |
                                             |           |   |
                                             |    EXE    |   |
                                             | variable1 |---Ù 
                                             | variable2 |
                                             -------------

C'est pour remedier à ce problème que sont apparues les relocs. Elles se caracterisent sous forme d'une section généralement nommée ".reloc" qui est en gros une liste d'adresses pointant vers chaque dword de l'executable qui contient une adresse absolue. Si l'executable est chargé à une adresse differente de son ImageBase, alors le décalage sera appliqué à toutes les adresses absolues de cette executables. Exemple:

Mettons nous dans le cas d'un executable compilé avec une ImageBase de 0x400000, et qui ne peut être chargé qu'à 0x500000. Dans son code, si il y avait un:

                        ...
 
        reloc:  mov eax,[0x401009]      ; B8 09 10 40 00
                        ...             ;= mov eax, offset variable1

        variable1       dd 'VAR1'
 

avec offset variable1 égal à par exemple 0x401009, une relocation serait appliquée lors de son chargement sur le dword à l'emplacement reloc+1, en ajoutant 0x500000-0x400000 soit 0x100000. Ce qui donnerait finalement en memoire:

	0x500000:
	         		...   relocation ( + 0x100000 )
                                  ____|___               
        reloc:           mov eax,[0x501009]      ; B8  09 10 50 00 
                 		...                     
	                                               
        0x501009:            'VAR1'              ; "VAR1"  L'adresse est ok.
	        		...

Ainsi, bien que le code ait été chargé à une adresse non prévue par le compilateur, les relocations lui ont permis de fonctionner comme si de rien n'était, en ajoutant le décalage nécessaire à chaque adresse de l'executable. Et ce qui est intéressant, c'est que ces additions sont effectuées automatiquement au chargement de l'executable...

Autre chose d'encore plus interressant est que quand windows rencontre une ImageBase à laquelle il ne peut charger l'executable (par exemple < 0x400000) ou >0x80000000), Windows le charge automatiquement à l'adresse 0x400000 et applique les relocs en conséquence.

Si l'on arrivait à modifier les relocs d'un PE pour qu'elles pointent vers chaque dword de notre decrypteur, et si l'on forçait les relocs à s'executer en changeant l'ImageBase en disons NewImageBase, que se passerait-il ? Et bien tout d'abord, au chargement de l'executable, à chaque dword de notre decrypteur serait ajoutée la valeur ImageBase-NewImageBase (il faudrait donc bien sur l'encrypter avant en otant la valeur ImageBase-NewImageBase), et quand notre virus rendrait le contrôle au PE hôte, il crasherait. Pourquoi ? Parce qu'en faisant pointer les relocs vers notre decrypteur, nous avons écrasé les anciennes relocs du PE. Et comme vu precedemment, si il est chargé à une adresse différente de son ImageBase sans relocations, son adressage devient faux: il crash.

Mais si l'on infectait uniquement les PE ayant une ImageBase de 0x400000 et que l'on changeait leur ImageBase en une NewImageBase à laquelle windows ne peut pas charger l'executable (par ex. <0x400000), que se passerait-il ? Et bien l'executable serait chargé à l'adresse 0x400000 et les relocs seraient appliquées, et donc notre decrypteur decrypté. Mais là, le fichier ne crasherait pas vu qu'il serait chargé à son ImageBase et que ses relocs ne seraient pas appliquées, vu qu'on les a écrasé avec les notres. Voila donc ce qu'il nous reste à faire...

II. Programmation

Tout d'abord, voyons comment se présentent ces relocations: Une section reloc est constituée d'une suite de "RelocChunk", chacun portant les informations de relocation pour 4k de code:

[ RELOC_CHUNK ]

	DWORD	RelocBase	;Base à partir de laquelle les relocs
				;seront appliquées.
	DWORD	RelocLen	;Taille du RELOC_CHUNK 
	WORD	Reloc1		;Une reloc. Voir plus loin ...
	WORD	Reloc2		;Une autre reloc. Voir plus loin ...
	...

La fin des relocs est marquée par un RELOC_CHUNK ayant pour Reloc_base le dword 0.

Une reloc en elle-même est un word, constitué comme suit: Les 4 premiers bits (MSB) représentent le type de reloc à appliquer, à savoir:

RELOC_ABSOLUTE	EQU	0

Pas de signification, utilisé pour aligner le RELOC_CHUNK sur 32bits.

RELOC_16BITS_HIGH	EQU	1

La relocation doit être appliquée sur le word de poids fort (MSB) du dword en question.

RELOC_16BITS_LOW	EQU	2

La relocation doit être appliquée sur le word de poids faible (LSB) du dword en question.

RELOC_32BITS	EQU	3

La relocation doit étre appliquée au dword, en entier.

Les 12 derniers bits (LSB) eux contiennent le décalage par rapport à RelocBase où la reloc doit être appliquée. D'où le fait que chaque RELOC_CHUNK ne peut adresser plus de 2^12 soit 4096 bytes. Par exemple, si vous tombez sur un RELOC_CHUNK de ce type:

	0x00001000		;RelocBase = 0x1000
	0x00000010		;Taille du RELOC_CHUNK = 16 bytes
	0x3016			;reloc
	0x2020			;reloc
	0x3088			;reloc
	0x0000			;reloc

	0x00000000		;RelocBase du RELOC_CHUNK suivant

On remarque que 3 relocations s'appliquent:

La premiere à l'adresse 0x1000 + 0x0016 soit à 0x1016 sur un dword

La deuxième à l'adresse 0x1020 sur le mot de poids faible du dword

La troisième à l'adresse 0x1088, sur un dword.

La quatrième n'indique rien, servant juste à aligner le RELOC_CHUNK sur 32bits.

Le RELOC_CHUNK qui suit termine la liste vu qu'il possède pour RelocBase le dword 0.

Maintenant que nous avons toutes les informations en main, il ne nous reste plus qu'à appliquer le tout. Voila ce qu'il devrait se passer lors de l'infection d'un fichier:

  1. Vérifier que son ImageBase est bien 0x400000, sinon exit.
  2. Modifier son ImageBase en une NewImageBase à laquelle win ne pourra le charger
  3. Soustraire la valeur (ImageBase-NewImageBase) à chaque dword du décrypteur
  4. Si il n'y a pas de section .reloc, en creer une
  5. Modifier les relocs pour qu'elles pointent vers chaque dword du décrypteur
  6. Infecter le fichier
  7. exit

Ainsi, le virus sera entièrement crypté sur le disque, et automatiquement décrypté lors de son chargement en RAM. Plutot sympa ... Au lieu de juste crypter un décrypteur on pourrait bien sur crypter ainsi tout le virus, mais cela necessiterait une section .reloc bien plus importante... La meilleure solution à mon goût reste donc d'utiliser une encryption même simple et d'encrypter uniquement le decrypteur via cette technique.

A partir de maintenant, j'assumerai que votre décrypteur est de la forme:

debut:  call    delta
delta:  pop     ebp
        sub     ebp,offset delta                                ;calcul le delta offset
        lea     esi,[ebp+virus]                                 ;esi --> code à décrypter
        mov     ecx,virus_len-(offset virus-offset debut)       ;taille du virus -
        mov     edi,esi                                         ;taille du decrypteur
encrypt:
        lodsb
        xor     al,[ebp+encrypt_val]                            ;simple 8bits xor encryption
        stosb
        loop    encrypt
        jmp     virus

encrypt_val     db      0                                                      

        crypteur_len    equ ($ - offset debut)/4+1              ;nombre de dword du
                                                                ;decrypteur + 1
ALIGN DWORD
virus:                                                          ;début du virus ...
 

Il peut être différent bien sur, mais il faudrait conserver les constantes debut, virus et crypteur_len + le ALIGN DWORD.

Pour l' étape 1, rien de bien compliqué, il suffit de regarder à l'offset 34h du PE Header pour obtenir l'ImageBase.

;edx --> PE Header du fichier en train d'être infecté

        cmp     dword ptr [edx+34h],0400000h
        jnz     pasbon
 

Pour l'étape 2, il va nous falloir trouver une adresse à laquelle windows ne pourra charger l'executable. Les valeurs possibles vont de 0 à 0x400000 et de 0x80000000 à 0xFFFFFFFF. Le mieux serait d'en obtenir une aléatoirement. Pour obtenir un dword aléatoire, chacun sa méthode. Moi j'ai choisi de lire le TimeStamp à l'offset 08h du PE Header. Ca nous donne un nombre différent pour chaque fichier infecteur, ce qui est largement suffisant à mon goût. Si cette valeur est à 0, alors on peut prendre comme valeur 0x100000, qui est l'ImageBase des vieilles applis windows, comme ca ca ne fera pas trop suspect.

Tcp nous dit que WinNT n'admet que des ImageBase multiples de 64k, donc on arrondira le résulta final sur 64k.

;edx --> PE Header du fichier en train d'être infecté

        mov     eax,[edx+8]             ; TimeDateStamp (random...)
        test    eax,eax
        jz      hcrandom                ; Si 0, valeur par défault.
findrandom:
        js      ncrandom                ; Si >= 0x80000000 c'est bon.
        cmp     eax,400000h
        jb      ncrandom                ; si < 0x400000 c'est bon aussi.
        ror     eax,1                   ; On ror la valeur jusqu'à que ce soit bon.
        jmp     findrandom
hcrandom:
        mov     eax,0100000h            ; Old windows applications ImageBase
ncrandom:
        xor     ax,ax                   ; NT/2000 Compatibility ?
        mov     [edx+34h],eax           ; NewImageBase
 

Ensuite, il nous faut encrypter notre décrypteur. Rien de bien compliqué là encore.

;edi = Offset du virus sur le disque (pas en ram hein).

        mov     esi,edi
        mov     ebx,0400000h
        mov     ecx,crypteur_len
        sub     ebx,eax                 ; Ebx = ImageBase - NewImageBase
encryptdécrypteur:
        lodsd
        sub     eax,ebx                 ; Encrypte le decrypteur.
        stosd
        loop    encryptdécrypteur
 

Voila, c'est l'application bête et méchante de ce qui a été dit plus haut.

Pour l'étape 4, je ne proposerai pas de code car cela dépend énormément de votre virus. Si vous voulez des exemples, allez voir le code source. Juste quelques indices: pour savoir si une section de relocs est présente, il suffit de regarder à l'offset 0xA0 du PE Header. Vous trouverez la RVA des relocations. Si elle est égale à 0, alors il n'y a pas de relocs et il vous faudra alors creer une nouvelle section pour les y mettre. J'ai déjà essayé de mettre les relocs dans la même section que le virus, mais pour des raisons obscures que j'aimerais bien connaitre, ca n'a jamais fonctionné. L'unique solution que j'ai trouvé a été de creer une nouvelle section.

Nommez cette section ".reloc", avec pour flags 0x50000040. Pour VirtualAddress et RawAddress, à vous de voir. Pour VirtualSize, c'est simple, c'est:

	Relocs.VirtualSize = (crypteur_len*2) + 8.

*2 car une reloc fait deux bytes et + 8 pour le header du RELOC_CHUNK. Pour la RawSize, il suffit juste d'arrondir la VirtualSize sur le FileAlignement

Maintenant que nous sommes surs qu'une section .reloc existe, qu'elle soit déjà là ou qu'elle ait été crée par notre virus, il ne nous reste plus qu'à la remplir pour que notre décrypteur soit décrypté au chargement du PE infecté. Il nous faut localiser la section contenant les relocs et récupérer son RawOffset (adresse sur le disque), puis y écrire un RELOC_CHUNK qui appliquera des RELOC_32BITS sur chaque dword de notre decrypteur. Pour localiser la section des relocs (section crée ou déjà présente), la solution la plus sure/simple consiste à regarder à l'offset 0xA0 du PE Header où se trouve la RVA des relocs. Il suffit ensuite de parcourir chaque section header et celui qui possède comme VirtualAddress cette RVA est le header de la section des relocs. On regarde son RawOffset et c'est gagné.

;edx --> PE Header du fichier en train d'être infecté.
;esi = VirusRVA (RVA du virus dans le PE infecté, souvent égal à SizeOfImage).
;edi = Adresse où est mappé le fichier (grace a MapViewOfFile)
       
        push    edi
        mov     eax,[edx+0A0h]                  ;Relocs RVA.
        lea     edi,[edx+18h]                   ;edi --> OptionalHeader.
        movzx   ecx,word ptr[edx+14h]           ;SizeOfOptionalHeader.
        add     edi,ecx                         ;edi --> sections headers.

        movzx   ecx,word ptr [edx+6]            ;ecx=nombre de sections
        imul    ecx,ecx,28h/4                   ;Un header de section = 28h bytes.
        repnz   scasd                           ;Cherche dans les sections celle qui a pour
        jnz     erreur                          ;VirtualAdress RelocRVA
                                                ;(section .reloc deja là ou prealablement crée)
        mov     ebx,[edi+4]                     ;RawOffset (VirtualAddress+8)
        pop     edi
        add     ebx,edi                         ;edi--> Fichier mappé grâce à MapViewOfFile.
                                                ;ebx--> Relocs (sur le disque toujours hein).
        mov     [ebx],esi                       ;esi = VirusRVA = RelocBase.
        mov     [ebx+4],dword ptr crypteur_len*2+8
                                                ;taille des relocs (meme un peu +)
        mov     ecx,crypteur_len                ;nombre de relocs a effectuer
        xor     eax,eax                         ;1ere reloc a l'offset (relatif a RelocBase) 0
        add     ebx,8                           ;on commence ...
        or      ax,3000h                        ;type de reloc: RELOC_32bits
relloop:
        mov     [ebx],ax                        ;écrit une reloc
        add     ax,4                            ;un dword plus loin: prochaine reloc à effectuer
        add     ebx,2                           ;1 reloc = 1 word
        loop    relloop
        and     [ebx],dword ptr 0               ;marque la fin des relocs en mettant là
relocsdone:                                     ;RelocBase du prochain RELOC_CHUNK à 0
 

III. Code Source

Reloc.asm

IV. Conclusion

Voila qui devrait donner un petit peu de boulot aux AVers. Néanmoins, cette technique possède ses faiblesses: en effet, vu que l'ImageBase doit être un multiple de 64k, il n'y à que le mot de poids fort de chaque dword de notre (dé)crypteur qui est (dé)crypté, rendant possible le scan. Une solution serait d'appliquer une RELOC_32bits non pas à chaque dword du décrypteur mais à chaque word, ou mieux encore, d'ajouter un peu de polymorphisme à l'affaire. Je ne sais pas ce qu'il en est des émulateurs, si ils prennent en charge les relocations ou non. Si vous etes au courant, mailez-moi.

Le virus en lui-meme reste tres tres simple et vous ne devriez pas avoir de problemes pour le comprendre. Si c'est le cas, la encore, n'hesitez pas a me demander.

kaze/FAT
[email protected]
Epiknet #vxers
[Back to index] [Comments]
By accessing, viewing, downloading or otherwise using this content you agree to be bound by the Terms of Use! vxheaven.org aka vx.netlux.org
deenesitfrplruua