IV. Analyse du loader
- 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.
Name | VOffset | VSize | ROffset | RSize | Flags |
.text | 00001000 | 00000826 | 00000000 | 00000000 | E0000040 |
.rdata | 00002000 | 00000066 | 00000400 | 00000200 | E0000040 |
ZeGun0 | 00003000 | 00001000 | 00000600 | 00001000 | E0000040 |
ZeGun1 | 00004000 | 00001000 | 00001600 | 00000200 | E0000040 |
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é.
- 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é).
- 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.
On 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
- 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.
- 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.
Pour 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
Voici un point important. Le programme continuera son exécution uniquement si la date système est le 16 février 2011.
- 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.
- 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
- 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.
- 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.
On 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 :
- Ouvrir le fichier Virus.exe
- Lire les données compressées
- Décompresser les données
- Copier l'intégralité du fichier virus.exe en mémoire
- Corriger le PEHeader
- Décrypter le nom de l'api importée
- Créer un nouveau fichier
- Ecrire le PEHeader corrigé
- Ecrire la section décompressée
- Ecrire le reste du fichier
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/