Unpacking 4 Newbies #1



Bonjour à tous ! Nous allons attaquer aujourd'hui le 1er défi d'une suite consacrée à l'unpacking, et crée par Kharneth (que nous remercions déjà pour tous les jolis défis qu'il nous concocte).

Nous allons séparer ce tutoriel en quatre parties, correspondant aux quatre objectifs à atteindre pour réussir l'exercice. Aussi, j'annonce :

    I. Réalisation d'un dump fonctionnel

      1) Réalisation du dump

      2) Recherche de l'OEP

      3) Correction du PE Header

      4) Reconstruction de l'Import Table

    II. Analyse du loader

      1) Décompression

      2) Résolution des Imports

    III. Reconstruction des Imports

    IV. Suppression du loader

Pré-requis : Savoir débugger, une « bonne » connaissance du PE Header est appréciable. Si vous avez réussi le défi #0 de Kharneth, c'est parfait.

Petite introduction sur les packeurs :

Un packeur est un petit programme qui va servir à compresser un fichier exécutable. La subtilité c'est que le programme, une fois packé, sera toujours exécutable. Mais comment tout cela est possible ? Je vais essayer de vous expliquer ça. Déjà, rappelons la structure de base d'un fichier exécutable (win32), je vais parler ici des fichiers qu'on trouve la plupart du temps (et pas des fichiers exotiques :-) ). Au départ, on trouve le PE Header, qui contient un certain nombre d'informations utile au chargement du fichier. On y trouve notamment la Section Header, qui contient les informations concernant la ou les sections de notre programme. Après ce fameux PE Header, on trouve les sections, avec tout ce qu'elles contiennent.

En principe, chaque section a un rôle bien particulier. On va par exemple trouver la section .code, qui va contenir des instructions (c'est la section qu'on voit lorsqu'on va débugger le fichier). On trouve aussi la section .rdata, qui va contenir l'Import Table. La section .rsrc qui va contenir toutes les ressources du programme (l'icône, des fichiers images, des fichiers .wav...tout ce qu'on veut !), etc.... Il est d'ailleurs bon de préciser que ces noms ne sont pas fixés, on peut très bien appeler la section contenant du code « .rsrc », ça ne pose pas de problèmes. On peut aussi avoir deux sections qui contiennent du code (ce sera d'ailleurs le cas avec un certain nombre de packeurs), voire beaucoup plus :-)

L'exécution d'un programme a lieu en mémoire.Ce qu'il vous faut savoir (c'est pas très important, mais c'est toujours ça), c'est que le fichier exécutable n'est qu'un modèle, un modèle qui va servir à fabriquer une copie, et c'est cette copie qui va être exécutée en mémoire. Mais cette copie n'est pas vraiment conforme ! Comme je commence à avoir du mal à m'en tirer, je vais tenter d'expliquer (attention, je ne suis pas sûr de tout ce que je vais avancer) ça concrètement :

- Lorsqu'on double clique sur un fichier (ou qu'on le lance, qu'importe la manière), un petit programme, qu'on appelle le loader de Windows (loader = chargeur), va lire le PE Header et va préparer le terrain.En fait,  il va négocier avec le Saint Patron pour réserver suffisamment de place en mémoire afin d'y faire la copie du fichier présent sur le disque dur (en tenant compte de Section Alignement, par exemple, ainsi que des Virtual Offsets, histoire de savoir à quelle adresse commencera chacune des sections). Il va aussi s'occuper de l'IAT. Pour cela il va charger les différentes .dll dont le fichier aura besoin pour y utiliser certaines fonctions. Il va donc dans un premier temps lire l'Import Table (le champ Name d'un IMAGE_IMPORT_DESCRIPTOR) utiliser la fonction LoadLibraryA, qui va charger la .dll en mémoire et renvoyer l'adresse à laquelle elle sera accessible. Le loader va donc garder cette adresse sous le coude. Il va ensuite s'intéresser au champ ORIGINAL First Thunk, ou First Thunk, pour connaître la fonction dont on aimerait bien avoir l'adresse. Puis il appellera l'API GetProcAddress en lui passant en paramètre l'adresse de la .dll en mémoire (celle qui contient la fonction qu'on souhaite appeler hein), puis le nom de la fonction qu'on aimerait utiliser (ou pas d'ailleurs). En retour on aura l'adresse de la fonction, et le loader la mettra dans l'IAT, comme ça, lorsque dans le programme on va appeler la fonction, le programme sera capable d'y accéder.

Voilà voilà, je vous ai concocté un petit schéma, tout moche et pas spécialement très pédagogique mais le voilà :




Bon, on critique pas, s'il vous plaît :-)

Maintenant, que va faire notre gentil packeur ? (Oui, un packeur c'est gentil, un protecteur c'est méchant, un crackme avec des maths c'est méchant aussi). Et bien notre packeur va greffer un morceau de son code sur le programme qu'on veut compresser (le fameux loader). Il va donc créer un nouveau fichier exécutable, en y mettant son propre PE Header, sa propre section .code (qui va contenir le loader, c'est à dire le programme qui va décompresser ce qui a été compressé et qui va remplir l'IAT), et une autre section (ou plusieurs, tout dépend) où il stockera l'équivalent de toutes les sections du programme original, mais compressées. Grosso modo, ça donne quelque chose comme ça :





Le packeur en profite aussi pour changer l'Entrypoint, en le faisant pointer vers le début du loader. Comme ça dès qu'on lance le programme, c'est le loader qui va s'exécuter en premier, puis il ira décompresser les sections et remplir l'IAT. Bébé parenthèse (comme dirait quelqu'un :(p), on trouve parfois plusieurs sections vides en dur, et le packeur décompressera les sections dans la section vide qui lui correspondra.

Voilà, le problème est posé. En fait le but de cette première partie sera d'avoir un fichier qui ne sera plus packé (les sections seront donc décompressées) et qui se lancera correctement (mwarf !). Ce qui nous amène à différencier quatre étapes bien distinctes :

    1) Réalisation du dump

    Mais avant tout, qu'est-ce qu'un dump ? En fait, un dumpeur (programme capable de réaliser un dump) va être capable de faire une copie du fichier alors qu'il se trouve en mémoire. C'est à dire que tout ce qui sera en mémoire (à un instant donné) sera copié vers un nouveau fichier, c'est ce qu'on appelle un « dump ». L'astuce ici sera de faire le dump lorsque les sections seront décompressées. On aura ainsi un fichier qui contiendra nativement les sections décompressées. On n'aura donc plus besoin du loader :-) Et c'est ce qui nous amène à la seconde partie.

    2) Recherche de l'OEP

    L'OEP, c'est l'Original Entry Point, c'est à dire l'endroit où va commencer la première instruction du programme original (non packé). On a vu précédemment que le packeur changeait l'entry point pour charger le loader en premier. Mais comme le loader ne nous est plus d'aucune utilité, il va falloir changer cet Entry point, pour le faire pointer au début de la première section décompressée (ce que faisait le programme non packé, ce qui est logique). Nous aurons donc à déterminer cet OEP, pour le corriger.

    3) Correction du PeHeader

    Ben là c'est tout con. L'entry point étant un champ du PE Header et c'est ici qu'il faudra le corriger (pour que ce soit le programme décompressé qui se lance et plus le loader).

    4) Reconstruction de l'Import Table

    Là ça devient déjà un peu plus compliqué à expliquer. Reprenons grossièrement le fonctionnement de l'Import Table, et de la résolution des imports, en tous cas la méthode utilisée par ce packeur. Ici sur les 5 champs de l'IID, on utilise le premier et les deux derniers,à savoir ORIGNIAL First Thunk, Name et First Thunk. Ici on se fiche du champ Name qui pointe simplement vers le nom de la .dll que l'IID concerne. Sur le disque dur (on dit aussi « en dur ») le champ FirstThunk va pointer vers une zone plus ou moins grande, correspondant à l'IAT. Cette IAT contient (toujours en dur) des adresses qui vont pointer vers le nom des fonctions importées de cette .dll (par exemple si l'IID concerne user32.dll, l'adresse pointera vers une fonction de user32.dll, MessageBoxA par exemple). Donc en résumé, First Thunk pointe vers deux choses différentes, mais localisées au même endroit, à savoir l'IAT (qui, contiendra les adresses des fonctions en mémoire, une fois la .dll chargée) et vers les adresses des noms des fonctions. Lorsque le loader procédera à la résolution des imports (Résoudre un import revient à trouver son adresse et la stocker dans l'IAT) il placera donc les adresses des fonctions dans l'IAT, et donc écrasera les données écrites précédemment (mais comme tout ceci se passe en mémoire - donc sur une copie – le programme en dur ne subira pas de modifications). Et c'est bien là le problème. En effet, le dumpeur va copier ce qui se trouve en mémoire, dans l'IAT on aura donc les adresses des fonctions, et pas d'adresses pointant vers le nom des fonctions. Lorsqu'on va vouloir lancer le dump, le loader va lire l'IAT dans l'espoir d'y trouver les adresses pointant vers les noms, et ne trouvera à la place que le contenu « logique » de l'IAT. Et le champ ORIGNAL First Thunk alors ? On s'en fiche ? Non...non, le champ ORIGINAL First Thunk va pointer sur cette même liste de pointeurs, qui va indiquer le nom des fonctions à résoudre, mais cette liste sera placée ailleurs, ceci a pour conséquence d'augmenter la taille du fichier packé pour pas grand chose, pour rien même :-)

    Si vous avez bien suivi, vous trouverez ce qui cloche. Pour y voir un peu plus clair, je vous ai fait deux p'tits schémas illustrant grossièrement le fonctionnement de l'Import Table, l'un avec un champ Orignal First Thunk non nul, et l'autre (celui qui nous intéresse ici) nul :


Voici le second :




    Et dans les deux cas ça fonctionne. Il est logique après ça de se poser des questions sur l'utilité du champ Orignial First Thunk, et bien c'est pas compliqué, il ne sert à rien :-)

    Après toute cette théorie je pense qu'on est prêt à remplir tous les objectifs, alors passons à la pratique !

    I. Réalisation d'un dump fonctionnel

      1) Réalisation du dump

      Pour débugger je vais utiliser la dernière version d'OllyDBG. On charge donc notre programme avec, et on arrive en 004061BC, voici une jolie capture de ce qu'on obtient :




Et là, on a tout le loader, c'est y pas beau ?

Bon, ne perdons pas de vue notre objectif. Pour réaliser notre dump on doit avoir toutes les sections décompressées, et là c'est pas encore le cas :-) Il faut trouver la routine de décompression. On sait que Kharneth a utilisé l'ApLib pour compresser/décompresser les données. Pour faire court, pour qu'ApLib puisse décompresser des données quelque part, il faut lui passer deux paramètres. Un premier push pour indiquer l'endroit où l'on veut qu'ApLib décompresse les données, et un second push pour indiquer à ApLib l'endroit où se trouvent les données compressées.

Dans le loader on trouve 4 Call. Le premier est un appel à GetModuleHandleA, le deuxième est indéterminé pour le moment, le troisième est un appel à LoadLibraryA et enfin le dernier est un appel à GetProcAddress. On a de très bonnes raisons de penser que le Call en 004061E1 appelle la routine de décompressions des données, surtout qu'on a deux push juste au dessus. Ah oui tiens, on peut aussi étudier le PE Header (enfin, la Section Header surtout) avec LordPE, histoire de voir si on a pas déjà une section vide en dur...Voici une capture :




Et bien si ! On a bien une section vide (Raw Size = 0), qui commencera à la RVA 1000. On est presque sûrs maintenant que le loader va décompresser les données à partir de l'adresse 00401000 (en tenant compte de l'ImageBase). Alors toujours sous Olly, on va faire un clic droit > Go To > Expression et dans la boite de dialogue qui s'affiche, on tape « 00401000 » sans les guillemets. On tombe bien sur une suite de 00.

Posons un breakpoint sur le Call (F2) en 004061E1 et laissons le programme continuer son exécution (F9). Olly va breaker sur le Call. Regardons un peu ce qu'on a sur la pile....




La première valeur pushée à été 00401000, c'est à dire le début de la section vide, et la seconde valeur pushée a été 00406282. Pas besoin d'être devin. Le Call qu'on va prendre permet de décompresser les données en 00406282 à l'adresse 00401000. On va d'ailleurs s'en rendre compte tout de suite. Appuyez sur F8 et le Call va remplir sa fonction. Faites maintenant CTRL+G (Go to Expression) et retapez 00401000. Et là, on a du code ! La section .code vient d'être décompressée. On va maintenant poser un breakpoint à la fin de la boucle, en 004061F5 et appuyer sur F9. A ce moment là ApLib aura fini sa décompression. La prochaine boucle va s'occuper de remplir l'IAT, pour l'instant on s'en fout.

Bon, les sections sont bien décompressées, on va pouvoir passer au dump proprement dit ! Dégainez LordPE (ou ce que vous voulez, du moment que vous savez l'utiliser...) et trouvez votre fichier en mémoire (en principe U4N1.exe si vous n'avez pas changé le nom) puis faites un clic droit dessus > dump Full. Puis sélectionnez le nom du fichier et son chemin, et Enregistrez....

Voilà un beau dump, qui n'est pas encore fonctionnel, mais c'est un dump ! Et du coup on peut passer à la prochaine étape...


            2) Recherche de l'OEP

L'heure est maintenant venue de vous parler de l'instruction RET. Kesskaifé ? Bon, c'est pas compliqué. En fait, exécuter un RET, ça revient à poper puis à sauter. Je m'explique : A la fin de chaque Call normalement constitué, on va trouver un RET, pour RETour (sauf que c'est en anglais :p). Ce Ret va prendre la dernière valeur pushée dans la pile, et va sauter dessus ! Le Call va donc pusher l'adresse de retour, puis sauter sur la routine à exécuter. A la fin de cette routine, on va tomber sur le RET qui va récupérer l'adresse de retour et nous y emmener. On peut donc remplacer un Call par :

Push Adresse_de_retour
Jmp Adresse_de_la_routine
RET

Pour résumer tout ça, si on fait :

Push 00406666
RET

Et bien on sautera à l'adresse 00406666. Et que fait un loader quand il a fini de loader ? Et bien il va sauter à l'OEP, c'est à dire l'endroit où le programme « véritable » va commencer. Et après le POPAD on a quoi ? Et ben oui, un PUSH/RET, qui nous fait sauter en 00401000. On vient de trouver l'OEP, c'est cool :-)


                 3) Correction du PE Header

Donc là, on va devoir modifier le champ Entrypoint du PE Header. Tout le monde sort LordPE, cliquez sur Pe Editor, allez chercher notre dump. Puis modifiez comme ci :



        4) Reconstruction de l'Import Table

Comme pour l'instant on doit pas le faire à la main, on va pas se priver d'utiliser Imprec :-) Pour information, notre programme doit être lancé pour qu'Imprec puisse agir, donc réouvrez-le si vous aviez fermé Olly.

Voici une petite capture qui nous aidera à y voir un peu plus clair (certains la reconnaîtront :-P) :



Passons aux différents points :

  1. Sélectionnez notre programme (comme pour le dump)

  2. Entrez la RVA de l'OEP, à savoir 00001000

  3. Cliquez sur IAT AutoSearch

  4. Cliquez sur Get Imports, une liste des imports s'affiche

  5. Cliquez sur Fix dump et allez sélectionné notre dump

La modification s'effectue, et normalement, lorsqu'on lance notre dump, et bien ça plante pas :-) Bravo, on vient de terminer la première partie de l'exercice !


II. Étude du loader

Nous voici à la seconde partie de l'exercice. L'objectif ici sera de comprendre tout ce que va faire le loader, de le décortiquer, de se l'approprier :-) Alors pour ça il n'y a pas de secrets, va falloir débugger ! Il est bon aussi de dégainer l'éditeur Hexadécimal, pour voir plus facilement ce qui se passe en mémoire, surtout en ce qui concerne la seconde sous-partie, et sa petite surprise :-)

L'étude au débugger se fera avec le fichier packé, et l'étude à l'éditeur hexadécimal se fera avec le dump (ben oui, on verra rien si tout est encore compressé). Donc lancez Olly, et on arrive à l'Entrypoint en 004061BC. Tracez tranquillement jusqu'à :


004061C3 Push 0

004061C5 Call GetModuleHandleA

004061CB Mov Dword ptr SS:[EBP-4], EAX

Concrètement, l'appel à GetModuleHandleA va permettre de récupérer l'ImageBase. La valeur de retour (L'ImageBase donc) est stockée dans EAX. La ligne suivante permet de mettre l'ImageBase dans la pile, en EBP-4.


004061CE xor ecx, ecx

Là on met ECX à 0, ce qui laisse supposer qu'il va nous être utile pour la première partie de notre étude...


        I) Décompression

Voici une petite capture d'écran de la boucle qui va décompresser les sections :



On saute directement en 004061EA avec l'instruction :


004061EA Mov EAX, Dword ptr DS:[ECX*8+40625A]

Comme pour l'instant ECX est à 0, cela revient à mettre le Dword contenu en 40625A dans EAX, et cette valeur c'est 1000. Pour voir ce qu'il y a après l'adresse 004061EA faites un clic droit sur l'instruction > Follow In dump > Memory Address/Address Constant. Et voici :



On voit bien la première valeur, 1000. Pas de problèmes. On continue :-)


004061F1 TEST EAX,EAX

004061F3 JNZ SHORT U4N1.004061D2

Si EAX n'est pas à 0, on saute en 004061D2, c'est à dire au début de la boucle.


004061D2 ADD EAX,DWORD PTR SS:[EBP-4]

004061D5 PUSH EAX

On ajoute à EAX, ce qu'on a stocké au préalable en EBP-4, c'est à dire l'ImageBase et on push le résultat, à savoir : 00401000, le début de la section vide.


004061D6 MOV EAX,DWORD PTR DS:[ECX*8+40625E]

004061DD ADD EAX,DWORD PTR SS:[EBP-4]

004061E0 PUSH EAX

Donc là c'est un peu le même topo qu'au début, ECX est à 0 donc c'est ce qu'il y a en 40625E qui va être mis dans EAX, à savoir : 6282, on se doute que c'est là que se trouve la première section compressée.

Ensuite on ajoute l'ImageBase et on pushe le tout.


004061E1 CALL U4N1.00406078

004061E6 ADD ESP,8

004061E9 INC ECX

On appelle la routine située en 00406078, qui va décompresser les données. Je ne m'étalerais pas sur cet algorithme, bien trop compliqué pour mon petit cerveau. BeatriX en a fait une jolie analyse que vous trouverez ici.

Le ADD ESP, 8 permet de placer le pointeur de la pile avant les deux paramètres pushés.

Ensuite on incrémente ECX, qui est donc à 1.

La boucle continue et on retombe en :


004061EA Mov EAX, Dword ptr DS:[ECX*8+40625A]

Cette fois ci ECX n'est pas à 0. Donc on veut mettre dans EAX ce qui est situé en [40625A + 8]. Si on regarde notre capture, on remarque que les valeurs pour chaque catégorie (endroit où décompresser et données compressées) sont séparées par 8 octets. Donc à chaque fois qu'on va incrémenter ECX c'est pour décompresser une autre section.

Voilà comment va faire notre packeur pour décompresser les données, la boucle va se répéter autant de fois qu'il y a de sections à décompresser, et là, il y en a 3.


    2) Résolution des Imports

Maintenant que tout est décompressé, on va vouloir exécuté notre programme, mais avant cela, il va falloir résoudre les imports, pour que le code décompressé puisse faire appel aux fonctions dont il a besoin. C'est ce que va faire ce morceau de code :




004061F5 MOV EBX,DWORD PTR DS:[406256]

004061FB ADD EBX,DWORD PTR SS:[EBP-4]

004061FE JMP SHORT U4N1.00406244

La première instruction va mettre ce qu'il y a en 406256 dans EBX. Et en 406256 il y a 40C8. Hum, ça ne m'évoque rien ce 40C8. Allons voir ce qu'on va y trouver !




Bon, en 40C8, on va trouver 4190. Ca ressemble beaucoup à des RVA ça, on tient peut-être quelque chose....Suivons le fil :-)

En 4190 on va trouver 4332, bien...

En 4332 on tombe sur l'ordinal de ShowWindow ! Et si ce qui était surligné dans la capture était l'ensemble des IID ? Si c'est le cas, on pourra dire que nous sommes chanceux. En effet, le packeur n'a pas détruit l'ORIGINAL First Thunk. Le contenu de l'IAT n'étant plus rempli de pointeurs, on ne pourra pas l'utiliser. Mais là, on dispose de l'ORIGINAL First Thunk, c'est à dire une « copie » de la liste des pointeurs, et celle ci n'est pas écrasée par le loader.

Mais bon, ce n'est qu'une supposition pour le moment. Essayons de déterminer la valeur des champs du premier IID, si c'en est vraiment un, on le reconnaître tout de suite :


ORIGINAL First Thunk : 4190

Date Time Stamp : 0000

Forwarder : 0000

Name : 4354

First Thunk : 4064

Bon, que trouve-t-on en 4354 ? Et oui, vous avez bien lu, on trouve la chaîne « user32.dll ».

Nous sommes bien en présence d'une IID, même de plusieurs, de toutes ! L'ORIGINAL First Thunk a été conservée pour chacun d'entre eux, ce qui laisse présager de bonnes choses :-) Mais passons, le moment n'est pas encore venu......


Donc EBX contient 40C8 (début de la première IID), très bien. On lui ajoute l'ImageBase et on saute en plein dans la boucle !


00406244 MOV EAX,DWORD PTR DS:[EBX+C]

00406247 TEST EAX,EAX

00406249 JNZ SHORT U4N1.00406200

Maintenant qu'on sait qu'on à affaire directement aux IID la compréhension va être beaucoup plus aisée. En EBX+C on va directement dans le champ Name. On met le contenu de ce champ dans EAX. Ensuite on regarde si EAX est à 0 (Vous n'avez pas oublié qu'une Import Table se termine par un IID nul j'espère ? Bon...) et là c'est pas le cas, donc on saute en 00406200 :


00406200 ADD EAX,DWORD PTR SS:[EBP-4]

00406203 PUSH EAX ; /FileName

00406204 Call LoadLibraryA

On ajoute l'ImageBase à EAX, et là on pointe directement sur la string user32.dll. On push cette adresse et on appelle LoadLibraryA. Cette API va charger la dll user32.dll en mémoire, pour qu'on puisse y avoir accès, et en retour, il place l'adresse à laquelle on pourra y accéder en EAX.


0040620A MOV DWORD PTR SS:[EBP-8],EAX

On va stocker cette adresse en EBP-8.


0040620D MOV EAX,DWORD PTR DS:[EBX]

0040620F TEST EAX,EAX

00406211 JNZ SHORT U4N1.00406216

00406213 MOV EAX,DWORD PTR DS:[EBX+10]

00406216 ADD EAX,DWORD PTR SS:[EBP-4]

00406219 MOV EDI,EAX

0040621B MOV ESI,DWORD PTR DS:[EBX+10]

0040621E ADD ESI,DWORD PTR SS:[EBP-4]

00406221 JMP SHORT U4N1.0040623B

On remet le contenu de l'adresse pointée par EBX dans EAX, soit 4190, l'ORIGINAL First Thunk.

Si c'est nul on met dans EAX le contenu de EBX+10 (First Thunk) , mais là c'est pas le cas donc on se contente d'ajouter l'ImageBase et de mettre le tout dans EDI.

On met dans ESI le contenu du champ First Thunk et on lui ajoute l'ImageBase.

Pour résumer :

EDI contient un pointeur vers un tableau contenant les adresses pointant vers le nom des fonctions importées (Original first thunk).

ESI contient un pointeur vers l'IAT (et accessoirement vers un tableau contenant les adresses pointant vers le nom des fonctions importées, bouh, j'ai même pas fait de copier/coller).

On prend le saut et on se retrouve en 0040623B


0040623B MOV EAX,DWORD PTR DS:[EDI]

0040623D TEST EAX,EAX

0040623F JNZ SHORT U4N1.00406223

On place la première adresse pointant vers un nom de fonction dans EAX. Si EAX n'est pas à 0 (ce qui signifierait qu'on a passé toutes les adresses dans la routine) on saute en 00406223.


00406223 ADD EAX,DWORD PTR SS:[EBP-4]

00406226 ADD EAX,2

00406229 PUSH EAX ; /ProcNameOrOrdinal

0040622A PUSH DWORD PTR SS:[EBP-8] ; |hModule

0040622D CALL DWORD PTR DS:[<&Kernel32.GetProcA>; \GetProcAddress

00406233 MOV DWORD PTR DS:[ESI],EAX

00406235 ADD ESI,4

00406238 ADD EDI,4

On ajoute l'ImageBase à EAX. EAX pointe maintenant sur l'ordinal de la fonction dont on veut récupérer l'adresse. Un ordinal tient sur deux octets, après ces deux octets on tombe directement sur le nom de la fonction.

Comme ici on se fout de l'ordinal, on ajoute directement 2 à EAX qui pointe maintenant vers le nom de la fonction qu'on veut importer.

On pushe cette adresse, puis on pushe ce qu'on avait stocké en EBP-8, vous vous souvenez ? C'était l'adresse en mémoire de user32.dll.

Une fois les paramètres passés, on appelle GetProcAddress qui renvoie dans EAX l'adresse à laquelle on pourra accéder à notre fonction.

Maintenant qu'on a récupéré notre adresse chérie, on la stocke à l'endroit pointé par ESI (donc l'IAT).

On ajoute 4 à ESI pour qu'il pointe vers l'endroit où on stockera la deuxième adresse récupérée.

On ajoute 4 à EDI pour récupérer le nom de la seconde fonction à importer.


Et on repart en 0040623B, où l'on va récupérer le nom de la seconde fonction importer et pour finir son adresse qu'on stockera dans l'IAT. On la boucle se répétera jusqu'à ce qu'on ai récupéré toutes les adresses des fonctions de user32.dll qu'on va utiliser. A ce moment là, on ne prendra pas le saut en 0040623F et on arrivera ici :


00406241 ADD EBX,14


00406244 MOV EAX,DWORD PTR DS:[EBX+C]

00406247 TEST EAX,EAX

00406249 JNZ SHORT U4N1.00406200

J'ai volontairement séparé ces deux groupes d'instructions car on est déjà passé sur le deuxième :-)

Notre EBX national pointe toujours sur le champ Name du premier IID, mais comme on en a fini avec cette dll, on lui ajoute 14, ce qui lui fait pointer vers le début de la prochaine IID.

On place le contenu de EBX+7 (le champ Name de l'IID qu'on va traiter) en EAX, et c'est reparti. On fait un passage dans LoadLibraryA pour charger la ..dll, puis on va résoudre tous ses imports et on passera à une autre .dll jusqu'à ce qu'on les ait toutes traitées. A ce moment là notre IAT sera entièrement remplie et le programme pourra fonctionner normalement :-)

D'ailleurs ce n'est pas terminé :) Une fois toutes les .dll chargées, leurs fonctions résolues etc....on arrive en 0040624B :


0040624B POPAD

0040624C LEAVE

0040624D PUSH U4N1.00401000

00406252 RETN

L'instruction POPAD permet de restaurer les valeurs des registres, enregistrées avec le PUSHAD en 00401C2. LEAVE permet de restaurer la pile comme c'était tout joli avant :-)

Et enfin, on a notre fameux push/ret qui nous fait sauter en 00401000, et notre programme pourra se lancer !


    III.Reconstruction des Imports


Vous pouvez ranger votre débugger maintenant, vous n'en aurez plus besoin :-)


On va devoir faire un nouveau dump, donc allez-y, je vous attend...Très bien. Donc pour le rendre fonctionnel, c'est toujours le même problème. Il va falloir corriger le champ Entrypoint du PE Header et reconstruire l'Import Table.

On va s'occuper de l'Entrypoint pour le moment. Alors on sort l'éditeur hexadécimal et on ouvre notre dump. On se rend à l'offset 34, et on remplace ce qui s'y trouve par 0010.

Passons maintenant à l'Import Table...Que faut-il pour qu'une Import Table soit fonctionnelle ?

Déjà, il faut autant d'IID que de .dll importées, et en plus de ça il faut un IID vide pour que le loader de Windows comprenne que c'est fini. Ensuite pour ces IID on a en gros, deux possibilités. Soit ORIGINAL First Thunk ne pointe sur rien, soit il pointe vers la même chose que First Thunk, mais placé ailleurs. Ensuite, bien sûr, tout doit être correct. C'est à dire que le champ Name pointe bien vers le nom de la bonne .dll, les adresses pointant vers les ordinaux des fonctions pointent bien vers les ordinaux des fonctions, dans le bon ordre etc...

Bon, et concrètement, une Import Table ça se met où dans le fichier ? Et bien, où on veut ! Vraiment, n'importe où. Et Windows, comment il sait où ça se trouve ? Il scanne tout le fichier à la recherche de ce qui ressemblerait à une IID ? Bah non hein.

Dans le PE Header on trouve ce qu'on appelle le DataDirectory. C'est un tableau plus ou moins grand qui contient les adresses (en RVA) de tout un tas de chose se trouvant dans le fichier, dont l'Import Table. Bien...

Si on se souvient bien, dans la partie précédente, on avait trouvé tous les IID. Et la façon dont le packeur résolvait toutes les adresses nous confirmait le fait que l'Import Table était correcte (le packeur résolvait les imports de la même façon que le loader de windows, donc...).

Comme on a pas touché au PE Header, sauf pour changer l'Entrypoint, on est en droit de penser que le champ Import Table du DataDirectory pointe vers l'Import Table non pas du programme original, mais du packeur, et c'est ce champ qu'il va nous falloir changer pour retrouver une Import Table fonctionnelle.

Ah oui autre chose, le champ d'un IID tient sur un DWORD, soit 4 octets. On trouve 5 champs par IID, donc un IID fait 5*4 = 20 octets. Là, on a 5 IID (4 .dll et un nul), ce qui fait 5*20 = 100 octets, soit en héxa : 64.


Le champ Import Table du DataDirectory se trouve à l'offset 8C. Et si on se souvient bien (si on se souvient pas on relit, c'est pas grave) l'Import Table commence en 40C8, soit C840 si on veut parler ordinateur. Comme tout ça tient sur un Dword ça fait : C8 40 00 00, on pourrait aussi s'éclater à changer le champ qui vient juste après, indiquant la taille de l'Import Table, mais ce champ ne sert à rien, donc on laisse. Les maniaques pourront mettre 64 00 00 00. N'oubliez pas d'enregistrer vos modifications.


Maintenant lancez le programme, et tout fonctionne correctement :-) M'enfin c'est pas ce qu'on fait de plus compliqué non plus hein ;)


    IV Suppression du Loader


C'est l'étape la plus marrante :-) Va falloir modifier le dump pour obtenir un fichier proche de ce qu'aurait pu être le programme avant d'être packé. On a vu plus haut que le fichier d'origine était composé d'au moins 3 sections (voir la valeur d'ECX à la sortie de la routine de décompression). Bon, toujours en reprenant la pseudo analyse (oui, je sais...) de la routine de décompression, on se souvient qu'un des paramètres passé à ApLib était l'adresse où on voulait que ce soit décompressé, et que chacune des adresses de cette catégorie était l'adresse du début d'une section. On va donc reprendre Olly et noter les 3 adresses, qui sont 00401000, 00404000 et 00405000.

On a donc au moins trois sections. Le Virtual Offset de la première (qui est la section .code, on le sait car elle contient du code exécutable) est de 1000, celui de la seconde est de 4000 et celui de la troisième est 5000.

Maintenant qu'on sait ça il va falloir penser à modifier la Section Header, en modifiant les sections déjà présentes.

Pour ça on va avoir besoin de notre éditeur hexadécimal :



Voilà la tronche de notre PE Header, ce qui est surligné correspond à la Section Header. Petit rappel sur la section header :


Alors commençons notre charcutage....On va écraser les sections déjà existantes pour les remplaces par les nôtres :-)


On commence par changer le nom de la première section, qui était sûrement .code, mais de toutes façons c'est pas le problème vu qu'on peut mettre n'importe quel nom (on peut même les appeler toutes de la même façon si ça nous chante :-) ).

Ensuite, en Virtual Size....Rendez-vous en 1000, c'est là que la section .code commence. La Virtual Size c'est la taille « réellement occupée » par le code, en gros : combien il y-t-il d'octets dans notre section ? Il ne faudra bien sûr pas tenir compte du padding (le remplissage de 00 qui sert à aligner correctement les sections).

Sous HexWorshop il suffit de placer le curseur au début de la section, puis de maintenant la touche SHIFT en cliquant sur le dernier octet (à priori le dernier word est 4000), et il nous indique la taille de ce qu'on a sélectionné, ici 27A6 octets.

La Virtual Offset c'est l'adresse (sans tenir compte de l'ImageBase) à laquelle commencera notre section en mémoire. Ici ce qui est sympa, c'est qu'on bosse avec un dump, une copie d'un programme tournant en mémoire. Donc les adresses en mémoire et en dur correspondent (la section .code commence en 1000 en mémoire, donc à l'offset 1000, en principe ce n'est pas le cas :-) ). Nous savons que la section commence en 1000.

La Raw Size c'est la taille de la section .code en tenant compte du padding. Le padding est utilisé pour que les sections soient alignées correctement, donc lorsqu'il n'y a plus de padding, la section suivante commence. Comme on l'a vu un peu plus haut, le Virtual Offset de la section suivante est 4000. il suffit de faire la différence entre les deux pour trouver le Raw Offset, qui ici, est de 3000.

Le Raw Offset correspond à l'adresse où commence la section en dur. Comme nous travaillons sur un dump, cette valeur est égale à la VirtualSize, soit 1000.

On ne touche pas aux caractéristiques, étant donné que la première section a été faite pour recevoir du code (de par la décompression des données), le packeur aura mis les caractéristiques adéquat, donc pas besoin d'y toucher :)


Deuxième section maintenant, ça devrait être rapide si vous avez compris ce que l'on vient de faire.

Virtual Size : 500

Virtual Offset : 4000

Raw Size : 1000

Raw Offset : 4000

Pour les caractéristiques, on a besoin de connaître l'utilité de cette section. Je ne sais pas si vous vous souvenez, mais pendant l'analyse du loader on a vu passer des adresses qui commençaient par 00404000 (après l'ajout de l'ImageBase), c'était à quel moment ? Ouep, c'est dans cette section que se trouve notre Import Table, ainsi que notre IAT.

On a donc besoin que cette section soit Readable (logique) et Writable (pour qu'on puisse remplir l'IAT). En caractéristiques, ça donne : C0000000.

Tiens d'ailleurs, que diriez-vous du nom .rdata pour cette section hein ? :-P


Passons à la troisième section :

Virtual Size : 364

Virtual Offset : 5000

Raw Size : 1000

Raw Offset : 5000

Si on retrace un peu notre code chéri (par exemple notre dump fonctionnel) on remarque (par exemple en 00401018, qui met en jeu l'adresse 5374 contenant le chemin de l'application) que cette section contient des données initialisées, elle a donc besoin d'être Readable et Writable, donc encore C0000000. On pourrait appeler cette section .data


On sait aussi de source sûre (c'est Kharneth qui l'a dit, si si, il a dit : « Indice : la section des ressources commençait à l'origine, en 406000 ») qu'il existe une section des ressources.

On va pas commencer à se prendre la tête tout de suite avec :-)

Pour l'instant on va s'occuper de supprimer notre Loader....


Prenons la Virtual Offset de la section contenant le loader (sur l'exe packé donc), elle est égale à 6000, celle de la section ressource est de 8000. On va donc sélectionner tous les octets entre 6000 et 8000, et appuyer gentillement sur la touche « Suppr » (enfin ça c'est sous HexWorshop hein).

Voilà, maintenant notre section ressource commence en 6000 :-) On va pouvoir déterminer la valeur de ses champs...

Virtual Size : 1D238

Virtual Offset : 6000

Raw Size : 1D238

Raw Offset : 6000

Pour les caractéristiques, la section a juste besoin d'être Readable, soit 40000000.


On en a terminé avec la Section Header, il reste certains champs du PeHeader à changer.

Déjà on vient de changer le nombre de sections, on est passé de 3 à 4, il va nous falloir changer le champ NumberOfSections, à l'offset 12.

Il va nous falloir aussi changer le champ SizeOfImage (offset 5C), qui correspond à la taille du fichier en mémoire. Ce qui équivaut à VirtualOffset de la dernière section + VirtualSize de la dernière section. Ici ça donne du 6000 + 1D238, soit 23238.

Et enfin, dernier petit changement....le DataDirectory :-)

On a vu que le DataDirectory était un tableau contenant des pointeurs vers certains élément du programme, la Ressource Table en fait partie. Il suffit d'indiquer le début de la section .rsrc, soit 6000, à l'offset 94. Mettez aussi la taille dans le champ d'après, c'est à dire 1D234.


Et voilà ! Vous pouvez lancer votre .exe maintenant, et oui, il se lance :-) Nous venons de terminer le.......Quoi ? Comment ça ça fonctionne pas ?

Ah oui....bon....

Si vous mattez la tronche de ce qui vient de se lancer (petite capture pour ceux qu'y n'ont rien branlé) :



On devine clairement qu'il y a un problème avec les ressources :) Oui mais quoi ? Je ne sais pas....Bon, à mon avis, si Kharneth nous a filé de la documentation (en anglais, bouh !) sur la section ressource, c'est qu'on va devoir se plonger un peu dedans :-).

Petit cours sur la section des ressources :

La section des ressources contient ce qu'on appelle un IMAGE_RESOURCE_DIRECTORY, c'est un peu comme une table des matières, un peu vide certes mais bon :-)

Voici la structure d'un IMAGE_RESOURCE_DIRECTORY


En principe, on trouve ce IMAGE_RESOURCE_DIRECTORY au début de la section ressource (même si je pense que c'est comme l'Import Table, on pourrait la caler n'importe où, mais je peux me tromper), et c'est le cas ici. Ce qui nous donne une fois rempli :


Ce 3 signifie qu'on aura trois catégories de ressources (je sais pas trop si c'est comme ça que ça se dit).

Juste après ce tableau viennent les 3 entrées. Ce que j'appelle entrée est en fait un IMAGE_RESOURCE_DIRECTORY_ENTRY (ouais bah c'est pareil hein) dont voici la structure :

On en a trois comme ça, voici la première entrée :



Pour le Name, on a 3. Ce 3 correspond à RT_ICON. Pour OffsetToData on a 8000 0028. Il nous faut prendre le dernier octet (28) pour savoir à combien d'octets à partir du début de la section se trouve notre ressource. Donc là c'est 28 octets après 6000, soit 6028 :-)

Rendons nous en 6028, et qu'est-ce qu'on trouve ? Et bien un deuxième IMAGE_RESOURCE_DIRECTORY ! Étudions un peu ses valeurs :


Nous aurons donc 3 ressources RT_ICON. RT_ICON représente en fait une icône. Ici on a trois RT_ICON, ce qui pourrait signifier que le programme contient trois icônes, mais ici ce n'est pas tout à fait vrai, il contient trois résolutions différentes, valà, merci Kharneth; Suite à ce IMAGE_RESOURCE_DIRECTORY nous retrouverons 3 IMAGE_RESOURCE_DIRECTORY_ENTRY, voici le premier (relatif à la première RT_ICON, je sais, on s'y perd facilement) :


Donc ici :