Packons (ou pas) avec miasm et elfesteem

Comme beaucoup le savent à présent MIASM est un framework d'ingénierie inverse écrit en Python par Fabrice Desclaux. Pour ma part j'avais joué un petit peu avec il y a un an, mais j'étais finalement assez rapidement passé à autre chose devant mon incapacité à porter un "packeur" maison de pefile jusqu'à miasm. Aujourd'hui, je re-tente la même tâche !

Engine - Creative Common by "cbowns" on Flickr
Il y a un an donc j'avais eu des soucis quand j'avais voulu porter un concept simple de packeur sur le framework miasm. En effet la base de tout packeur c'est d'ouvrir un fichier à traiter (dans mon cas un fichier PE), lui apporter des modifications, puis le sauvegarder pour obtenir un clone fonctionnel de notre fichier à traiter (clone dont la signature binaire sera pourtant différente). Le problème c'est que, même en n'apportant aucune modification à mon fichier cible, elfesteem semblait incapable de générer un clone fonctionnel. Par exemple le code suivant, qui est censé n'apporter aucune modification à l'exécutable passé en argument, me retournait systématiquement un clone que Windows refusait de démarrer :

import sys
from elfesteem.pe_init import PE

e = PE( open(sys.argv[1]).read() )

open( sys.argv[1]+'_modified.exe', 'wb').write(str(e))

De mémoire j'avais testé sur "winmine.exe", sur "calc.exe", et sur "notepad.exe" : à chaque fois l'exécutable modifié ne fonctionnait plus :-(

Un an après, re-motivé par la super conf de serpi au SSTIC, je réinstalle smiasm sur ma machine[1] et je re-tente. Tristesse : le bilan est le même. Les exécutables que j'ouvre puis que je sauvegarde avec elfesteem ne fonctionnent plus (enfin en tout cas "calc.exe", qui est ma cible de test préférée, donne systématiquement des clones "cassés"). Mais cette année, j'ai décidé d'investiguer un petit peu plus ! Retroussons nous les manches et voyons voir ça de plus près. D'abord assurons nous que "calc.exe" et "calc.exe_modified.exe" sont bien différents (et que nous n'avons donc pas à faire à un simple saute d'humeur de windows) : diff calc.exe calc.exe_modified.exe nous confirme que les fichiers sont différents.

Pour y voir de plus près on va regarder ces deux spécimens en hexa (xxd calc.exe > calc.exe.XXD && xxd calc.exe_modified.exe > calc.modified.XXD && diff *.XXD) :

5,14c5,14
< 0000040: 0e1f ba0e 00b4 09cd 21b8 014c cd21 5468  ........!..L.!Th
< 0000050: 6973 2070 726f 6772 616d 2063 616e 6e6f  is program canno
< 0000060: 7420 6265 2072 756e 2069 6e20 444f 5320  t be run in DOS 
< 0000070: 6d6f 6465 2e0d 0d0a 2400 0000 0000 0000  mode....$.......
< 0000080: 8745 1664 c324 7837 c324 7837 c324 7837  .E.d.$x7.$x7.$x7
< 0000090: 3907 3837 c624 7837 1907 6437 c824 7837  9.87.$x7..d7.$x7
< 00000a0: c324 7837 c224 7837 c324 7937 4424 7837  .$x7.$x7.$y7D$x7
< 00000b0: 3907 6137 ce24 7837 5407 3d37 c224 7837  9.a7.$x7T.=7.$x7
< 00000c0: 1907 6537 df24 7837 3907 4537 c224 7837  ..e7.$x79.E7.$x7
< 00000d0: 5269 6368 c324 7837 0000 0000 0000 0000  Rich.$x7........
---
> 0000040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 0000050: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 0000060: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 0000070: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 0000080: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 0000090: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 00000a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 00000b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 00000c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 00000d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
21c21
< 0000140: 00f0 0100 0004 0000 fcd7 0100 0200 0080  ................
---
> 0000140: 00f0 0100 0004 0000 b16f 0200 0200 0080  .........o......
39,46c39,46
< 0000260: 0ffe 7d3b 3800 0000 0efe 7d3b 4400 0000  ..};8.....};D...
< 0000270: 0efe 7d3b 4f00 0000 0efe 7d3b 5c00 0000  ..};O.....};\...
< 0000280: 0efe 7d3b 6900 0000 0efe 7d3b 7300 0000  ..};i.....};s...
< 0000290: 0000 0000 0000 0000 5348 454c 4c33 322e  ........SHELL32.
< 00002a0: 646c 6c00 6d73 7663 7274 2e64 6c6c 0041  dll.msvcrt.dll.A
< 00002b0: 4456 4150 4933 322e 646c 6c00 4b45 524e  DVAPI32.dll.KERN
< 00002c0: 454c 3332 2e64 6c6c 0047 4449 3332 2e64  EL32.dll.GDI32.d
< 00002d0: 6c6c 0055 5345 5233 322e 646c 6c00 0000  ll.USER32.dll...
---
> 0000260: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 0000270: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 0000280: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 0000290: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 00002a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 00002b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 00002c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> 00002d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

Voilà donc l'intégralité des changements entre les deux fichiers. Sur 112ko ça fait peu, on devrait pouvoir être capable d'identifier précisément ce qui ne fonctionne plus :)

Le premier bloc de différence se situe entre les octets 0x0000040 et 0x00000d0. La position (en tout début de fichier) et le contenu ("...This program cannot be run in DOS mode...") de ce bloc me font penser qu'il s'agit uniquement du "DOS stub"[2]. Bref : pas intéressant.

Le second bloc de différence est beaucoup plus petit puisqu'il n'impacte que la ligne de notre dump commençant à l'octet 0x0000140. Sur toute cette ligne on a 3 octets de différence. Si jamais ces octets participent à une indirection quelconque on a peut-être trouvé notre coupable :) ! Après un petit tour dans "Lord PE" je réalise que, malheureusement, ce n'est probablement pas là que se situe le coupable. En effet la valeur du checksum de "calc.exe" est "0001D7FC" et celle de "calc.exe_modified.exe" est de "00026FB1". Cette suite d'octets correspond donc probablement au checksum du fichier contenu dans l'en-tête NT_HEADER. Comme le DOS-stub a été supprimé il est normal que la valeur de checksum soit différente; de plus il me semble qu'un checksum erroné n'empèche pas le lancement de programme simples (pour les drivers c'est une autre histoire, mais pour un soft user-land je ne crois pas que Windows vérifie). Pour avoir la conscience tranquille je modifie quand même la valeur du checksum dans "calc.exe.XXD" puis je reconstruit un binaire (xxd -r calc.exe.XXD > calc.exe.badchecksum.exe) et je peux confirmer que cette copie de calc.exe dont j'ai uniquement modifié le checksum pour en mettre un invalide fonctionne :)

Nous arrivons ainsi au troisième bloc, qui est devenu notre suspect numéro 1. En plus, les quelques chaines de caractères qui ont été remplacées par des octets nuls ont bien la tête d'informations provenant d'une table d'importation ("msvcrt.dll" , "KERNEL3.dll", etc.) ce qui pourrait expliquer le non-fonctionnement du programme si ces informations ont été altérées. Malheureusemnt "Lord PE" me donne des tables d'importations identiques entre les deux fichiers "calc.exe" et "calc.exe_modified.exe", et je ne trouve pas d'outil sympa pour disséquer simplement un PE :-( ...Qu'à celà ne tienne : on va en pondre un maison, à la porc bien entendu ! L'idée est simple : visualiser les structures du fichier PE simplement afin d'identifier à quelle structure appartient la zone d'octets modifiés.

Pour réaliser cet outil, quick&dirty rappelons-le, je vais faire appel à une poignée de fidèles compagnons :

  • VIM/gedit : parce que ce sont d'excellents éditeurs de code :)
  • C : parce que pour les manipulation "bas niveau" on n'a pas encore fait mieux que le bon vieux C.
  • Python : parce que pour les prototypages rapide c'est le must.
  • Python Imaging Library (PIL) : parce qu'on veut "visualiser les structures du fichier PE" et que c'est la meilleure librairie de manipulation d'image en python que je connaisse.

Enfin, en guise d'aide mémoire, on utilisera aussi les excellents tutos d'Iczelions sur les entrailles du format PE et quelques URL de références de chez Krosoft. Au final j'ai obtenu en un aprem de jeu :

  • un prog en C qui analyse un fichier PE et dump ses caractéristiques sur la sortie standard (en prime j'ai rajouté un .h contenant les définitions des structures Windows, bien que j'aurai pu utiliser winnt.h qui doit se trouver quelque part sur ma machine étant donné que j'ai une gentoo avec de quoi compiler pour des centaines de systèmes exotiques, mais j'avais la flemme de le chercher :-D)
  • un prog en python qui lance le prog en C sur un exécutable passé en argument, parse la sortie obtenue, et génère deux jolies images permettant de visualiser quelle partie du fichier appartient à quelle structure.

Toutes les sources sont en pièce-jointe de ce billet, donc n'hésitez pas à jouez vous aussi (mais n'oubliez pas : j'ai codé ça comme un goret donc si ça ne marche pas chez vous "c'est normal", et si vous le lancez sur des PE de plusieurs Mo il est probable que votre RAM soit intégralement vampirisée, vous êtes prévenus).

La première image est une représentation à l'échelle des différentes structures du fichier PE sur disque (pas tel qu'il sera mappé en mémoire donc). La seconde image n'est qu'une représentation "compressée" de l'image précédente où j'ai inséré des séries de "petits points" pour remplacer visuellement les énormes applats de couleurs unis représentant des zones de fichier où "rien de spécial ne se passe" et qui rendent le premier type d'image carrément gigantesque (1200x14688 pour représenter "calc.exe" dans mon environnement de test ;) ). Voilà comment on s'en sert :

$ gcc -o a.out oz_pedissector.c
$ python oz_pedissector3.py calc.exe
INFO:root:PE zone discovered [ZONE : IMAGE_DOS_HEADER	En-tete DOS	0	64]
INFO:root:PE zone discovered [ZONE : IMAGE_NT_HEADERS	En-tete NT	240	488]
INFO:root:PE zone discovered [ZONE : SECTION_HEADERS[0]	En tete de section ".text"	488	528]
INFO:root:PE zone discovered [ZONE : SECTION[0]	Contenu de section ".text"	1024	76800]
(...)
INFO:root:Compression second pass 98%
INFO:root:Compression second pass 99%
INFO:root:Compression second pass finished.

Nous voilà à présent avec deux fichiers ".png", l'un étant la représentation à l'échelle, l'autre étant la représentation compressée. Voyons voir ce que donne l'image "compressée" générée automatiquement à partir du "calc.exe" de mon environnement de test (je vous invite à l'avoir sous les yeux en taille normale lorsque vous lirez le paragraphe suivant) :

Oz PE dissector (Compressed Example file) - Creative Common CC-BY-SA by Ozwald

En la regardant de haut en bas on retrouve bien :

  • L'en-tête MS-DOS commençant à l'adresse 0x0 et allant jusqu'à 0x40
  • Une zone noire (représentation graphique de "on ne sait pas ce que c'est") juste en dessous. C'est le DOS Stub.
  • l'en-tête NT commençant à 0xF0 et allant jusqu'à 0x1E8 (on se rappelle à ce moment là que le second bloc de différence était situé aux alentours de l'adresse 0x140 et que nous avions supposé qu'il s'agissait du checksum contenu dans l'en-tête NT, nous avons à présent confirmation que cette adresse est bien dans l'en-tête NT ce qui conforte notre hypothèse s'il en était besoin)
  • Les trois en-têtes de sections qui suivent l'en-tête NT
  • Une seconde zone noir juste en dessous...on en reparlera.
  • La première section (".text") qui va de 0x400 à 0x12C0. On notera (sur la droite de l'image) que cette section contient les zones mémoires suivantes appartenant au DATA_DIRECTORY : Import Address Table, Debug, Import Symbols.
  • La seconde section (".data") qui ne contient rien de spécial.
  • La troisième section (".rsrc") qui contient, on s'en doutai, la zone spéciale "Resources" du DATA_DIRECTORY.

Bref : tout va bien :) Tout va tellement bien d'ailleurs que je tombe d'accord avec elfesteem : il ne devrait rien y avoir d'important entre l'adresse 0x260 et 0x400[3]...Le mystère reste donc entier[4].

Edit du lundi 9 Juillet 2012, 14h53 : je remplace les 3 fichiers sources joints au billet par une archive unique contenant le tout, ça évitera les 403 imposés par mon hébergeurs sur les fichiers d'extension ".py" -_-

Edit du mardi 10 Juillet 2012, 17h30 : après vérification sur d'autres versions de "calc.exe" (provenant de diverses versions de WinXP) on retrouve toujours ce bloc non nul entre la fin des en-tête de section et le début de la première section... Etrange... Par contre je viens seulement maintenant de vérifier un exécutable qui serait plus typiquement ce que j'aurai besoin de packer[5], à savoir gsecdump, et il ne contient bien que des octets nuls entre la fin de ses en-tête de section et le début de sa première section. Donc à priori il devrait pouvoir être ouvert/enregistré par elfesteem sans être "cassé". Je teste ce soir :) ! Accessoirement je rajoute en PJ la version 4 du script dégueu programme permettant la visualisation des structures de fichiers PE. Il est maintenant infiniment moins gourmand en mémoire (forcément: il génère du svg directement compressé au lieu de générer un bitmap "taille réelle" puis de le compresser...); je l'ai testé sans douleur sur un binaire de 13Mo (en lui demandant cependant de n'afficher que les structures de plus de 40 octets).

Edit du mardi 10 Juillet 2012, 20h30 : A force d'avoir l'évidence sous les yeux (à savoir que je ne vois aucune raison pour lesquelles elfesteem ne pourrait pas légitimement écrire des bytes nuls sur ces zones du fichier) j'ai re-testé l'ouverture/sauvegarde simple mais sur plusieurs binaires ne provenant pas de Microsoft cette fois (à savoir : gsecdump-v2b5.exe, ftpserv.exe, notepad++.exe, 7zFM.exe, python.exe) et ils ont tous fonctionné à merveille après leur passage dans elfesteem. La conclusion du premier chapitre de ce mystère s'écrit donc d'elle même : elfesteem fonctionne très bien, c'est le compilateur utilisé par microsoft qui produit des résultats "exotiques" (et je suis un gros c*uillon de ne pas m'en être rendu compte il y a un an -_- ...).

Edit du mercredi 11 Juillet 2012 : Serpi a trouvé la solution (lisez les commentaires du billet pour les détails). Le problème venait de bound imports..

Notes

[1] L'installation n'est d'ailleurs toujours pas plus facile, je vous recommande la lecture de mon billet de l'époque, ça peut éventuellement aider

[2] Le DOS stub est,en résumé, un programme MSDOS censé être lancé à la place du programme PE lorsqu'il est exécuté sous DOS. Le but de ce mini programme DOS est principalement d'avertir l'utilisateur que le programme ne peut pas fonctionner sous DOS.C'est une zone totalement facultative de nos jours.

[3] Ce genre de zone "poubelle" pouvant apparaitre naturellement dans le fichier suite à des jeux de padding puisque les structures PE imposent des tailles multiples de certaines constantes renseignées dans les en-tête

[4] En attendant un autre après-midi de libre (où je pourrait me re-pencher sur le problème), ou que quelqu'un poste un commentaire expliquant ce bien étrange phénomène

[5] Pour rappel : je suis pentesteur pro, si je pack des binaires c'est pour mon taff et pour sensibiliser mes clients au fait qu'un AV ce n'est pas l'outil miracle qui les protègera de tout et n'importe quoi; ce n'est pas pour faire du pr0n, monter un botnet, ou pire : boire dans votre YOP.

Commentaires

1. Le lundi, juillet 9 2012, 10:53 par Gorgonite

Allez, je tente ma chance :
open(sys.argv[1],'rb')
au lieu de
open(sys.argv[1])

nb: je ne peux pas tester au boulot ^^

2. Le lundi, juillet 9 2012, 12:02 par Ozwald

La vache tu es rapide à répondre :-D !
Alors l'idée était intéressante mais je viens de tester et, malheureusement, j'obtiens le même résultats avec et sans le 'rb' :

$ md5sum calc.exe*
829e4805b0e12b383ee09abdc9e2dc3c calc.exe
bab1aecd69fa7e93d03da67b8601faf4 calc.exe_modified.exe
bab1aecd69fa7e93d03da67b8601faf4 calc.exe_gorgonite.exe

Décidément plus j'y réfléchi plus je me dit que ça doit avoir à faire avec la table d'importation. Vivement que j'ai un peu de temps pour me re-pencher sur le souci :) !

3. Le lundi, juillet 9 2012, 13:48 par Baboon

File les binaires ! (les 2 calcs)
J'ai jamais réussit à installer miasm chez moi et le SDK de win veux même plus s'installer non plus...)
(vais regarder ton précédent post tient)

4. Le mardi, juillet 10 2012, 15:38 par serpilliere

Yop!
C'est peut être normal. Sous windows 7:
* tu copies calc.exe de c:\windows\system32
* tu la colles sur le bureau
* tu lances celle sur le bureau
* *surprise* => segfault.

il a un problème pour retrouver ses petits dans les ressource de LANG FR s'il n'est pas lancé depuis c:\windows\system32

Après si tu es sous XP, ca devrait fonctionner :/

5. Le mardi, juillet 10 2012, 20:51 par Ozwald

Merci à tous pour vos réactions !

@baboon> Tu as les deux binaires dans tes mails (si l'adresse que j'ai utilisé est la bonne).

@serpi> Je testais sous WinXP :-/ En fait (je répète un peu mon dernier edit, mais s'pas grave) je ne vois toujours aucune raison pour lesquelles les binaires générés ne marchent pas. Du coup j'ai testé avec pas mal de PE non-Microsoft et ils fonctionnent tous à merveille. On peut donc plus ou moins dire que mon problème est résolu :) Mais niveau curiosité intellectuelle le mystère reste quand même entier : toutes les versions de "calc.exe" ou de "winmine.exe" sur lesquels j'ai pu mettre la main (WinXP, et 7), avaient cette caractéristique étrange d'embarquer, entre la fin des headers de section et le début de la première section, une partie binaire non-nulle qui semble essentielle au fonctionnement de l'appli (et qui, entre autre, répète la liste ASCII des DLL utilisées dans la table d'importation). Bref on peut renommer le mystère de "pourquoi elfesteem ne parvient pas à ouvrir/sauvegarder mes binaires de tests ?" en "pourquoi le compilateur/linkeur de microsoft fait des choses étranges ?" ^_^

6. Le mercredi, juillet 11 2012, 08:53 par serpilliere

Ok. c'est les bound imports.
En fait de base, elfesteem ne parse pas (encore) les bound import, du coup quand il rebuild, il les met à 0, par contre, il laisse l'entrée dans l'import directory.
résultat: quand tu lances, certaines fonctions ne sont pas fixée dans l'iat.

petit fix rapide: il faut transformer ton code en :
import sys
from elfesteem.pe_init import PE, pe

fname = sys.argv[1]
e = PE(open(fname).read())
e.NThdr.optentries[pe.DIRECTORY_ENTRY_BOUND_IMPORT].rva = 0
e.NThdr.optentries[pe.DIRECTORY_ENTRY_BOUND_IMPORT].size = 0
open(fname+".mod", "w").write(str(e))

bon ok: je vais fixer le tripou soit pour qu'il les parse, soit qu'il supprime l'entrée dans le header.
note: je n'ai pas remarqué au premier coup d'oeil, parce que *tout* mes scripts d'unpacking ou autre ont ce fix... oui, j'avoue c'est *painfull*.

du coup corolaire: Avec miasm, on apprend plein de truc dans la bonne humeur... Désolé pour le temps perdu!

7. Le mercredi, juillet 11 2012, 09:09 par serpilliere

Au passage, pour se marrer, la fameuse ligne de code est dans la doc (les slides du repository) de miasm: slide 9/105. (comme quoi je ne suis pas totalement un escroc :p)

Concernant le packer, j'en ai également un basé sur miasm développé pour les pentest, et j'avoue il y a encore 2/3 "blagues" du même style, du coup tu me donnes presque envie de le releaser (ce qui diminuera peut être son efficacité). Mais l'exercice reste intéressant.
Après j'avais une amie juriste qui m'avait glissé à l'oreille que la publication de code de ce genre reste illégale. Dieu ait pitié de l'âme des juristes.

8. Le mercredi, juillet 11 2012, 11:56 par Ozwald

Bount Import, nice catch !! Je ne maitrise pas du tout cette structure là des PE...je vais y jeter un oeil du coup (comme quoi tu as raison : c'est un exercice intéressant et instructif).

Pour la petite histoire : wine, lui, réussi à lancer les versions modifiées de "calc.exe" dont les bount imports sont "pourris" :-)

"Désolé pour le temps perdu!" <= c'est une blague ^_^ ? Tu ponds un framework super, tu le mets à dispo en open source, et tu t'excuse qu'il ne soit pas parfait ? C'est plutôt à moi de te payer une bière si on se croise un jour pour te remercier de l'opportunité d'en apprendre plus sur les bount imports :-p

Pour ce qui est de releaser un packeur il y a en effet un problème légal (une sombre histoire de "motif légitime" en rapport avec l'article 323-3-1 du code pénal si j'ai tout suivi...http://www.legifrance.gouv.fr/affic...).En tout cas c'est pour ça que je n'ai pas non plus releasé le mien :-/

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.

La discussion continue ailleurs

URL de rétrolien : http://www.ozwald.fr/index.php?trackback/57

Fil des commentaires de ce billet