Voila un titre bien pompeux. Je vais consacrer une série d’articles à l’art du dépacking (Oui c’est un ART !), et sachant que c’est en forgeant qu’on devient forgeron dessoudeur (Proverbe Ukrainien), je me rend compte qu’il est nécessaire de maitriser quelques concept de base. (PE, PEB, TIB, CPU Context, ViewOfFile etc..) avant de pouvoir pleinement comprendre ce que l’on va faire.
Je ne vais pas revenir non plus sur qu’est – ce qu’un exécutable packé et comment réussir à le dépacker de façon générique (Et pour l’instant magiques). Cela à déjà été couvert par de précédents articles :
Donc allons y. On reprend depuis le début. Nous allons déjà voir comment Windows représente les exécutables en mémoire. Première étape, on reprend depuis le début, je pense que tous le mode était au courant mais soit;
Chaque exécutable pense qu’il est seul au monde. Si on est en 32 bits par exemple, chaque exécutable dispose devant lui de potentiellement de 4Go de ram de 0x00000000 à 0xFFFFFFFF. Et peut importe si la station ne dispose de 256 Mo de mémoire tout est remappé et virtuel. On remercie au passage l’operating system pour cela. Dans cette plage mémoire, il n’a quand même pas tout pour lui. le Kernel de windows se réserve un pan de mémoire, cette mémoire n’est pas atteignable par le process. C’est de 0x8000000 à 0xFFFFFFF en 32 bits (bref 2Go pour le Process et 2 Go pour le Kernel).
En 64 Bits c’est encore plus la foire; Dans l’absolu l’adressage 64 bits permet d’adresser 16 Exaoctets ( bref 16484 Petaoctets, soit 16777216 Teraoctets rhaa rhaaa…) Bref y a de quoi voir venir. Le découpage à été moins magnanime. en 64 Bits le process dispose de 0x0 à 0x7FF’FFFFFFFF soit 8 Teraoctets et ensuite il y a un énorme No man’s land et enfin les 248 derniers Teraoctets de 0xFFF0800’00000000 à 0xFFFFFFFFFFFFFFFF sont réservés au kernel. Voila pour la base.
Maintenant faisons un exécutable 32 bits qui fait un hello world. On le compile et on le strip pour ne garder que le strict minimum des sections.
$ cat hello.c #include <windows.h> #include <stdio.h> #include <string.h> char mystring []="Hello World"; int main () { printf("%s is at 0x%X, main is at 0x%X.\n", mystring,&mystring,&main); return(0); } $ i586-mingw32msvc-gcc hello.c -o hello.exe $ strip hello.exe
Cet exécutable va afficher notre string «mystring» ainsi que l’offset (l’emplacement mémoire) de la dite string et de la fonction main. Une fois compilé. J’obtient un EXE. Tous le monde dit c’est un EXE, mais en fait c’est un fichier Portable Executable (PE). Il est constitué de deux parties, une partie compatible MS-DOS (On la traine la compatibilité ascendante) qui généralement affiche juste le message “JE TOURNE PAS SOUS MS-DOS ( This program cannot be run in MS-DOS mode). Au cul de cette partie DOS, il y a une partie pour Windows avec des segments mémoires mis bout à bouts. Et oui, notre exécutable est constitué de 3 segments de mémoire qui vont être mappés dans cette mémoire virtuelle. (.text, .data, .rdata ). Une table contenu dans cet EXE nous indique qui va où et comment.
$ objdump -h hello.exe hello.exe: file format pei-i386 Sections: Idx Name Size VMA LMA File off Algn 0 .text 0000004a 00401000 00401000 00000200 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 .rdata 00000024 00402000 00402000 00000400 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 2 .data 0000005a 00403000 00403000 00000600 2**2 CONTENTS, ALLOC, LOAD, DATA
Faisons simple on s’intéresse au stricte minimum pour l’instant. Admettons on a un segment de code (.text) avec le programme et un autre segment de data (.data) avec notre string “hello world”. (Oubliez Rdata pour l’instant, ne demandez pas svp. :) )
Sous windows il est fort probable qu’un segment de programme soit mappé dans notre mémoire virtuelle en 0x401000. La raison est simple, généralement tous les exécutable fait pour Windows NT et > on leur adresse de base mappé en 0X400000. Sous windows 95 ils étaient prévus pour être mappé en 0x1000000. Mais rien n’est aussi simple si on regarde Notepad.exe de windows XP, il est toujours en 0x1000000 lui.
$ objdump -h notepad.exe notepad.exe: file format pei-i386 Sections: Idx Name Size VMA LMA File off Algn 0 .text 00007748 01001000 01001000 00000400 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 .data 00000800 01009000 01009000 00007c00 2**2 CONTENTS, ALLOC, LOAD, DATA 2 .rsrc 00008958 0100b000 0100b000 00008400 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA
Ca c’est pour l’historique. Ce qu’il faut savoir c’est que dans le segment de code tout est relatif. Quand je dit a mon programme “saute là” ou “call ici”. les sauts sont relatifs par rapport à là ou je suis. Un “saute là” sera en fait un saute à +X octets.
Exemple non tiré de notre hello.exe :
Premier exemple, je suis en 0x40113E et le code assembleur est E8 DF FE FF FF. «0xE8» c’est l’opcode de «CALL». Ensuite il y a où sauter, le saut est relatif à l’instruction suivante. Ici le saut c’est 0xFFFFFEDD (Il faut lire de droite à gauche pour convertir bytes (octets) en integer (entier 32 Bits)). Si on convertit cet entier en entier signé sur 32 bits ça fait -291. Et je vous le donne en mille 0x401143 – 291, ça fait 0x401020. Bref un saut en arrière.
Idem pour le Jump if Above “JA”. instruction 0x77, Suivis de son saut relatif à la prochaine instruction sur 8 bits. Donc 0x401165 + 0x3B = 0x4011A0.
On l’a compris, le code et surtout les instructions de saut et branchement sont relatives et se foutent de connaitre à quel emplacement ils tournent. C’est ce qu’il faut retenir. Le code peut être posé potentiellement à n’importe quel offset il fonctionnera. C’est d’ailleurs ce que fait l’ASLR. L’ASLR randomise a chaque lancement l’emplacement mémoire du segment de code (et aussi de la stack). Ce qui complique les exploitations des exécutables du style : “crash pas et saute plutôt ici à met moi tous les droits”. L’ASLR est dispo depuis Vista.
Mais reprenons notre hello world, le printf va nous afficher l’offset mémoire de la procédure MAIN et l’adresse ou est stocké la chaine de texte.
Sur un XP sans ASLR
Hello World is at 0x403000, main is at 0x401004.
Sur un Seven avec ASLR
Hello World is at 0x403000, main is at 0x401004.
Damned, Fichtre.. Et oui, sous Windows, c’est pas tout d’avoir un OS ASLR. Ils faut que les exécutables le soient. Et ça c’est une compilation spécifique et un petit flag dans l’exécutable qui le dit. On y reviendra. Mais retenez aussi cela ! C’est comme les antibiotiques, c’est pas automatique.
Regardons les data maintenant; Voici comment mon programme appelle mon «printf»
Et oui, Il pose sur la pile toutes les chose à imprimer puis l’offset de la string, 0x401004, 0x403000, x402000 et oui c’est bien là où se situe mes «string» comme nous l’avons constaté précédemment, mais on constate que ces emplacements sont “en dur” dans l’exécutable. Autre constat donc, les emplacement mémoires des DATA sont en durs et non relatifs.
Ensuite en 0x40102E il appelle printf. Ollydbg ici nous aide et nous dit que ça appelle un JMP qui lui saute vers printf de MSVCRT. Bien on est prêt à le croire Ollydbg, mais détaillons ceci;
En 0x40102E, J’ai un saut , opcode «E8» vers 0x11 (17 en décimal) octets plus loin à partir de la prochaine instruction. J’arrive donc en 0x401044 et là j’ai un saut «long» opcode «FF». le «25» suivant indique, tu va sauter à la valeur qu’il y a dans l’emplacement mémoire qui va venir. Et enfin l’emplacement mémoire, 0x40303C . Ca ressemble à une adresse dans le segment DATA et pour cause. C’en est bien une ! Si on regarde ce qu’il y a dans cette mémoire on tombe sur une valeur fort étrange 0x77C4186A.
Vous venez de découvrir l’IAT. l’Import Address Table. Expliquons;
Notre exécutable utilise printf, mais printf est pas inclus dans notre exécutable. Printf sous windows est une fonction (un bout de code quoi) incluse dans une DLL (Dynamic Linked Library) nommé MSVCRT.DLL (Microsoft Visual C RunTime). Alors comment cela marche. Quand j’ai créé mon exécutable, il a été noté dedans «Heps j’aurai besoins pour fonctionner de printf de la dll msvcrt». C’est noté dans la table d’import qui est aussi une dans une section de notre PE. On peut le voir avec objdump -p.
There is an import table in .data at 0x40300c The Import Tables (interpreted .data section contents) vma: Hint Time Forward DLL First Table Stamp Chain Name Thunk 0000300c 00003034 00000000 00000000 0000304e 0000303c DLL Name: msvcrt.dll vma: Hint/Ord Member-Name Bound-To 3044 641 printf
Et voila, tout est là dans le PE. Au démarrage de l’exécutable Windows va donc savoir quel dll j’ai besoin et de quelles fonctions. Il va donc charger la DLL en mémoire, trouver l’offset du code des fonctions que j’ai besoin et les poser dans cette plage mémoire (l’IAT) avant de lancer le programme.
Mais alors, cette DLL elle est aussi en mémoire… Et oui, une DLL c’est comme un PE tout à fait normal, ça a une adresse de base et des segments, des table d’import IAT, et aussi des table d’export. Une table d’export (EAT) c’est une table qui dit quelles fonctions sont exportée (rendues disponible par la DLL aux autre programmes) et où on peut les trouver. regardons msvcrt.dll.
$ objdump -h msvcrt.dll msvcrt.dll: file format pei-i386 Sections: Idx Name Size VMA LMA File off Algn 0 .text 0004bd36 77c11000 77c11000 00000400 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 .data 00004800 77c5d000 77c5d000 0004c200 2**2 CONTENTS, ALLOC, LOAD, DATA 2 .rsrc 000003e0 77c64000 77c64000 00050a00 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 3 .reloc 00002d74 77c65000 77c65000 00050e00 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA
Et oui l’adresse de base de la DLL est 0x77C11000. Étonnamment dans le même segment mémoire que là ou notre IAT va nous faire sauter (0x77C4186A) pour le printf. Cette DLL sera elle aussi mappée en mémoire un peut plus loin que notre process. Notre process pourra utiliser ce code à son aise.
Et oui, maintenant tout s’éclaire. Si on regarde les segments mémoire sous Ollydbg, tout est plus clair.
On comprend comment tout (presque tout) est goupillé. Ici j’ai donc mon programme avec ses 3 sections, et 3 DLL, msvcrt.dll, kernel32.dll et ntdll.dll. Et l’IAT de mon programme a été tripotée par windows pour savoir où sauter. On remarque que les headers du PE (les EXE quoi) eux aussi sont mappés à l’addresse de base de l’exécutable.
A retenir que même si on demande rien dans l’IAT, ntdll et kernel32 sont deux DLL toujours chargée.
Passer d’un fichier PE à un exécutable placé aux bon offsets en mémoire s’appelle «Map a View of File» On comprend mieux l’existence des fonctions windows UnmapviewOfFile et MapViewOfFile. Pour une DLL que l’on souhaite charger en mémoire même si l’IAT ne dit pas de le faire, on préfèrera la fonction LoadLibrary.
Voila, j’espère que la vue d’un exécutable et des DLL en mémoire et la magie qui les passe d’executable sur le disque au mapping en mémoire, ainsi que le miracle de l’IAT est beaucoup plus clair pour tous le monde.
Prochainement, avant de désosser le format PE, on regardera en détail comment retrouver une DLL et une fonction en mémoire.
A+