lundi 14 juillet 2008

Debug me !

Dans la série des useless tools...

Après l'avoir vu utilisé dans différents articles (notamment le très bon post de YoLeJedi sur nibbles ou encore par Ivan dans son implémentation de PaX) j'ai décidé de me coder un petit débuggeur tranquillou mon pépère.
Mon idée est classique : c'est de permettre deux modes : l'un intrusif et l'autre pas. Késako ?

I- Be bad

Le mode intrusif c'est du classique, on créé un processus en mode debug et on va donc recevoir tous les debugEvent qui vont se produire ( chargement des DLLs, exceptions...) ce qui peut être bien pratique pour fuzzer : on lance une application en boucle avec différentes entrées et on récupère les exceptions pour voir où ça merde. Côté réalisation y a rien de bien compliqué, surtout que la doc est assez explicite sur ce point: après la création du process ( avec l'argument DEBUG_PROCESS ) on rentre dans une debug loop qui va catcher tout les debugEvent et c'est du tout bon.

II- Be bad ok, but without pain please


Le mode non-intrusif est plus fallacieux. Déjà, quel est son intéret ? Il est simple : un process peut avoir des comportements différents suivant qu'il est sous le contrôle d'un débuggeur ou non. Sans rentrer dans des techniques anti-debugging complexes on peut citer deux exemples "naturels" : le gestionnaire d'exception final ( aka UnhandledExceptionFilter ) qu'on peut mettre en place dans un programme avec SetUnhandledExceptionFilter() n'est pas appelé en mode debug, ce qui n'est pas très pratique quand on veut tester une exploitation qui consiste justement à réécrire son adresse. On peut aussi parler des chunks dans le tas qui sont différents en mode débug (16 bytes de plus), ce qui facilite pas les choses lors du test d'un heap overflow ;-)

L'idée du mode non-intrusif consiste à s'attacher au process une fois qu'il est lancé avec l'API DebugActiveProcess(). J'ai alors crié "This is just so fuking easy !" : je créé mon process avec un CreateProcess() en mode normal, j'apelle cette API pour m'attacher puis je lance ma debug loop et hop.

Je me suis lancé, j'ai codé, j'ai compilé, j'ai executé et jme suis pris un comportement bien bizarre dans la gueule : pour la majorité des process auquels je m'attache ça plante complètement, pour d'autres ça marchent, mais pas à tous les coups et dans certains cas, chose bizarre, j'arrive à récupérer le nom des DLLs qui sont chargés (après l'avoir lancé une première fois) alors que ça ne devrait pas être le cas...

Après avoir consulté l'oracle (aka YoLeJedi :p), j'ai compris ce qui se passait. Pour celà, intéressons nous au processus de création d'un process sous Windows (par l'appel à CreateProcess()) : il se divise en 6 étapes (extrait de Windows Internals) :

  1. Open the image file (.exe) to be executed inside the process.

  2. Create the Windows executive process object.

  3. Create the initial thread (stack, context, and Windows executive thread object).

  4. Notify the Windows subsystem of the new process so that it can set up for the new process and thread.

  5. Start execution of the initial thread (unless the CREATE_ SUSPENDED flag was specified).

  6. In the context of the new process and thread, complete the initialization of the address space (such as load required DLLs) and begin execution of the program


Comme on peut le voir c'est assez complexe, et c'est là que se situe le problème, si juste après l'appel à CreateProcess() j'apelle DebugActiveProcess(), je risque de m'attacher avant que la fin de l'initialisation réalisé à l'étape 6 ne soit terminée. Or DebugActiveProcess() est une API qui est faite pour s'attacher à des process "en cours" donc complètement initialisé. Ca explique aussi pourquoi desfois je reçois les noms des DLLs : si je m'attache avant l'étape 6 alors je serai "notifié" du chargement des DLLs comme si le process avait été lancé en mode DEBUG ( m'enfin je m'avance peut être un peu là ).
Tout ce qu'il faut retenir c'est qu'il faut laisser le processus d'initialisation se terminer avant de s'attacher.

Une première idée pourrait être de caler un ptit Sleep(X) avant de faire appel à DebugActiveProcess() : ça fonctionne mais c'est moche : si la durée d'endormissement est trop longue le process qu'on débugge aura déjà attaquer d'exécuter son code et donc on risque de louper des debugEvents ( et c'est bien sûr pas portable du tout, si un process charge beaucoup de DLLs il mettra plus de temps à s'initialiser et hop il faudra changer la valeur ).

Il existe une meilleure façon de faire, in 6 steps :

1- On créé le process en mode SUSPENDED : il est suspendu (jure?)...
2- On modifie l'Entry Point (EP) du programme pour y mettre une boucle infinie
3- On relance le process et on attend que le main thread (créé à l'étape 5) du programme atteigne l'EP ( avant d'y arriver il réalise l'étape 6 de l'initialisation, c'est à dire le chargement des DLLs et tout le bordel) : le process est alors complétement initialisé et se met à boucler.
4- On s'attache au process avec DebugActiveProcess() .
5- On restaure l'EP initial, le main thread commence l'execution du code et on n'en perd pas une miette :-)

Décrivons plus en détail les étapes les plus compliquées (pas trop non plus :p) :

2- Il nous faut récupérer l'EP, pour celà jme fait un petit parcours du header PE. Les lecteurs assidus de ce blog (et ils sont nombreux, aheum), auront remarqué que mon premier useless tool était justement un PE reader, cool, je peux réutiliser mon code ?! En fait pas du tout, pour faire plaisir au ch3f je mappe le fichier en mémoire avec MapViewOfFile() et je le parcours directement en déréférençant des pointeurs.
2-1 Une fois l'adresse de l'EP récupérée, on utilise VirtualProtectEx() pour changer la protection de la mémoire où il se trouve et pour pouvoir y écrire.
2-2 On oublie pas de faire une sauvegarde des deux premiers octets se trouvant à l'EP
2-3 On écrit EB FE à la place de ces octets ce qui correspond aux opcodes d'un JMP à la même addresse, donc une boucle infinie :). On cale un ptit appel à FlushInstructionCache() derrière pour s'assure que nos modifs se propageront bien en mémoire centrale et ne resteront pas dans le cache du processeur.

3- On relance le processus avec ResumeThread(), et on attend qu'il atteigne l'EP en dumpant la valeur de l'EIP avec GetThreadContext().

4- Appel à DebugActiveProcess()..

5- On réécrit les anciens opcodes à la place de notre boucle infinie, on oublie pas de flusher et de restaurer les anciens droits de la mémoire.

Let's launch the debug loop, et c'est good :-)


III- Et comment tu t'apelles ?

Comme YoLeJedi l'explique dans son post, dans le monde non intrusif, on n'a pas accès aux noms des DLLs loadés en mémoire directement dans les structures de debug qu'on récupère. Pour trouver ces noms il faut se casser un peu le c..
YoLeJedi donne 3 techniques pour le faire, perso j'ai implémenté la première qui, dixit le maitre, est la plus rapide :-)
"Le principe consiste à mapper le premier octet du fichier en mémoire à partir de son Handle (CreateFileMapping + MapViewOfFile). Ceci crée indirectement un objet section qui contient le chemin du fichier. Celui-ci est alors récupéré avec l’API GetMappedFileName. La lettre du lecteur est retrouvée avec le couple GetLogicalDriveStrings + QueryDosDevice."
Et on trouve un code source qui nous fait tout ça ici.

Mais comme ce code est un peu trop propre et que le copier/coller favorise pas l'apprentissage, je me suis retapé un codage "maison" que vous trouverez dans la fonction getTheNameOfTheFile(), c'est pas vraiment joli (voire carrément dégeulasse) et sûrement pas assez commenté mais ca reste du parcours de string, donc pas très complexe à refaire :)

Vous trouverez donc la source du bouzin ici ( pas oublié de linker avec Psapi.lib lors du build de l'executable pour GetMappedFileName() ) et le binaire compilé sous XP SP2 .

Une prochaine amélioration pourrait être un mode interactif pour pouvoir afficher la mémoire du process avec des commandes à la gdb...

Thanks to YoLeJedi pour son aide et ses commentaires utiles ;-)

Aucun commentaire: