0/5 (1 avis)
Vue 12 289 fois - Téléchargée 656 fois
#### Introduction : Vous partez en vacance à la campagne chez tante Martine. Tante Martine a un PC sous XP qui lui sert de machine à écrire, mais n'a pas le net. Vous prenez donc vos précautions en mettant votre environement de développement préféré sur votre clé USB, histoire de pouvoir l'installer pèpère chez elle. Manque de chance, à peine arrivé, vous perdez votre clé USB en batifolant dans un champ. Le soir, désespéré devant le PC de tante Martine, vous vous demandez ce que vous allez bien pouvoir faire... Un PC sortie d'usine ne propose que peu d'options pour faire du développement. On peut faire de l'assembleur DOS 16 bits avec debug. On est coincé dans une console, et on ne peut utiliser que les interruption DOS et BIOS, dont vous avez forcément oublié les numéros (Et oui, il n'y a pas le net chez tantine). On peut faire du DOS. Cette fois, la liste des commande (Plus complète que celle de help) est présente dans l'aide et support, et les commandes s'autodocumentent avec /?. Manque de chance vous connaissez déjà bien le DOS, et vous ne voyez pas grand chose d'intéressant à faire avec... Le navigateur est capable de dessiner des pages web à partir de code HTML. Mais vous vous êtes toujours dit qu'appelez le HTML du code, c'était un peu exagéré. On peut faire des scripts VB et JS, qui seront executés par wscript, cscript, ou encore les mêler à de l'HTML pour les exécuter sous IE. Si office est présent on peut faire aussi du VBA. Mais le problème c'est que ces langages vous donnent de l'urticaire (Et vi, ce tuto est pas sur usineàgazfr.com). Ne commencez pas une partie de démineur : il reste une option ! Le vieux Bill a en effet décidé de nous livrer en standard sous XP un débugueur 32 bits. Vivi, même à tante Martine. Celui-ci répond au doux nom de ntsd.exe et se trouve fort peu originalement dans system32. #### Présentation de ntsd : Pour ceux qui connaissent WinDbg, la prise en main de ntsd ne posera pas de problème. ntsd est comme WinDbg à plusieurs différences près : 1 Il ne permet que de déboguer en ring 3. 2 Il est en invite de commande. 3 Des dlls d'extension proposant des commandes supplémentaires fournies avec WinDbg ne sont pas présentes en standard. 4 Il lui arrive de planter si on fait une faute de frappe... 5 Une bonne partie des commandes des extensions fonctionnent mal ou plantent. 6 Il vaut mieux oublier les pseudo registres ($ra, $ea, $exp...). Les débogueurs Microsoft (WinDbg, kd, ntsd, cdb...) se différencient de certains débugueurs/désassembleurs classiques (IDA, TD32, W32DASM...) par le fait qu'ils se manipulent en entrant des commandes en invite, comme avec le bon vieux debug.exe. Ils proposent toutes les fonctionnalités nécessaires en matière de pas à pas, points d'arrêts... mais ne propose pas forcément autant d'outils d'extraction d'information. Par exemple, IDA est capable d'éssayer de déterminer quel compilo à généré un exe, ou W32DASM affiche les tables d'import et d'export directement. #### Démarrage de ntsd : Si on lance ntsd sans lui passer de paramètre, il affiche son aide et se ferme. Voici quelques possibilités de lancement : 1 On peut passer un fichier .exe en paramètre de ntsd pour qu'il démarre son débogage. 2 On peut s'attacher à un processus existant, via son pid et l'option -p. 3 On peut s'attacher à un processus existant, via son nom de module, mais il faut qu'il y en ai un seul qui ait ce nom. Exemple : ntsd -pn notepad.exe 4 On peut le mettre en remplacement du docteur Watson, pour qu'il s'attache automatiquement à un processus qui plante. Pour cela, il faut changer la clé : HKLM\Software\Microsoft\Windows NT\CurrentVersion\AeDebug La valeur auto permet de définir la présence d'une MessageBox qui demande si on veut déboguer le processus. Dans la valeur Debugger, on peut par exemple mettre : ntsd -p %ld -g #### Prise en main : ? permet d'afficher une partie des commandes, et .help une autre partie. Au début, le débugueur affiche des erreurs ou des warnings en parlant de symboles. Les symboles de débogages facilitent celui-ci. Cependant, on ne dispose généralement pas des symboles d'une application que l'on a pas compilé. Quand le débugueur laisse tourner le processus (On ne peut plus rentrer de commandes et le processus à un fonctionnement normal), il faut utiliser ctrl + c. q permet de quitter ntsd, et tuera l'application débuguée. Alt + entrée est le raccourcis qui permet de mettre une application DOS en mode plein écran. Mais je n'ai pas trouver d'instruction more. ctrl + b permet de quitter plus violemment que que q. Pour se détacher d'une application sans la tuer, il faut commencer par faire .detach, puis q. .restart permet de redémarrer l'application, mais ne fonctionne pas si on s'est attaché à un processus. Le processus peut avoir plusieurs threads. On affiche la liste des threads avec ~*. On peut executer une commande sur un seul thread en la préfixant : ~NumeroThreadCommande Un ? suivie d'une expression provoque l'évaluation de celle-ci. echap permet d'effacer la ligne. Les registres doivent parfois être préfixés par @. #### S'attacher à un processus en cours de fonctionnement : On a vu que l'on pouvait utiliser l'option -pn. ntsd crée alors un thread pour mettre un point d'arrêt. On met alors souvent en place un point d'arrêt sur une fonction de l'API Win32 probablement utilisée dans le code que l'on veut voire. Par exemple : bp BitBlt Puis on utilise la commande g pour reprendre l'execution. Si la fonction est appelée par le programme, celui-ci va être arrêter par le point d'arrêt. On se trouve alors au tout début de la fonction BitBlt. On peut examiner les paramètres passés à cette fonction de Windows, mais son code nous intéresse moins que le code de l'appelant. L'adresse de l'appelant est empilée sur la pile. Pour faire un step out il faut donc faire (La commande gu et le pseudo registre $ra de WinDbg semblent mal supportés par ntsd) : g poi(esp) poi() permet de déférencer un pointeur. #### Les dlls d'extension : Ces dlls permettent de rajouter des commandes à ntsd. Ces commandes commencent par un ! et certaines d'entres-elles fonctionnent mal sous ntsd. Pour afficher la liste des dlls dans lesquelles ntsd cherche les commandes, on utilise la commande .chain. Elle affiche le chemin de recherche des dlls, ainsi que les dlls chargées et non chargées. Les dlls non chargées peuvent provoquer l'affichage de messages d'erreurs génants. On peut les décharger avec la commande .unload. Les dlls présentes en standard sur mon XP sont : dbghelp.dll exts.dll ntsdexts.dll Vous pouvez charger une dll d'extension avec la commande .load. Pour lister les commandes d'une dll, on utilise la syntaxe suivante : !module.help Elle ne fonctionne pas sur dbghelp.dll. #### Format PE et loader de windows : Chez tantine, on souhaite pouvoir rédiger et tester du code assembleur, et pourquoi pas s'arranger pour obtenir des .exe valides. Un .exe est au format PE. Il est composé de headers et de sections. Le loader de windows charge les headers dans la mémoire à une certaine adresse qui peut théoriquement être arbitraire. En réalité, la plupart des .exe exigent d'être chargés à une adresse bien précise (Les dlls ont par contre une base de relocation qui leurs permet d'être chargées à n'importe quelle adresse). Les headers précisent à quelles adresses doivent être chargées les sections, relativement à l'adresse des headers (L'adresse du module quoi). Les PE disposent d'une table d'importation qui définit quelles fonctions de l'API Win32 sont nécessaires à l'application. Le loader de Windows complète cette table avec les adresses qui vont bien lors du chargement de l'exe. Dans le code machine, lors de l'appel d'une fonction de l'API, le call récupère l'adresse dans cette table. Il connait l'adresse de cette table car elle se trouve toujours au même endroit, à un certains offset depuis l'adresse du module qui elle même est fixe. La construction d'un exe valide au format PE à la main et sans documentation est du pur suicide. Il est bien plus simple d'utiliser un .exe éjà existant sur le PC de tantine. On souhaite pouvoir executer n'importe quelle fonction de l'API Win32... Il faut donc disposer des fonctions LoadLibrary et GetProcAddress. notepad.exe importe ces deux fonctions, et servirat donc de base de travail. #### Récupération des informations sur un executable : Voyons voir ce que notepad a dans le ventre : ntsd notepad.exe ntsd charge le programme et s'arrête sur un point d'arrêt. La commande lm affiche les modules (.dll, .exe, .ocx...) chargés par l'appli, ainsi que leurs adresses de début et fin. Notons l'adresse du module notepad : 01000000. La command !lmi, suivie d'un nom de module, donne un peu plus d'informations sur celui-ci. Mais la commande !dh est nettement plus efficace : elle affiche tout un tas d'infos sur les headers et les sections. On remarquera par exemple dans les caractéristiques "Relocations stripped", qui signifie que ce module ne peut être chargée qu'à une seule adresse. On peut vérifier que le champ "Image Base" a bien la valeure qu'on a noté tout à l'heure : 01000000. On prend note du point d'entrée (entry point), 739D. Sous le "optional header", il y a les data directories. Ce sont des pointeurs vers des données particulières, elles même se trouvant généralement dans les sections. Ces données sont par exemple les tables d'import et d'export, les tables de relocation qui permettent aux dlls d'être chargées à n'importe quelle adresse, les ressources... Notons l'adresse de la table d'importation, 7604. Finalement, on a les informations sur les sections. notepad à 3 sections : 1 .text : Contient le code. Dans le fichier, cette section se trouve à l'adresse 400 à partir du bébut de fichier, pour une taille de 7800 octets (Informations "raw data"). Dans la mémoire elle se trouve à l'adresse du module (01000000) + 1000, pour une taille de 7748 (Informations "virtual") On remarque que le point d'entrée et la table d'importation sont dans cette section. 2 .data : Contient les données initialisées ou non. On constate que la taille dans le fichier (raw data) est très inferieure à la taille dans la mémoire (virtual). Les données non initialisée seront placés dans cet espace supplémentaire. Les données initialisées ou non sont par exemple les variables globales. 3 .rsrc : Les ressources. Menu, bitmap, chaînes... Jettons un coup d'oeil à la table d'importation. Elle se trouve en 01000000 + 7604. Pour l'afficher, on utilise la commande db. db prend une adresse ou une plage. db notepad + 7604 L1000 affiche les 0x1000 premiers octets de la table. Cela affiche les noms des fonctions importées, même si le formatage n'est pas terrible... On peut préciser la base d'un nombre en le préfixant : 0x prefix (hexadecimal) 0n prefix (decimal) 0t prefix (octal) 0y prefix (binary) Les commandes classiques d'affichage du contenu de la mémoire sont : dd : dword da : ASCII du : Unicode db : byte dw : 16 bits dq : 64 bits dyd : Binaire Toujours concernant la récupération d'informations sur les modules, on peut récupérer la liste des fonctions exportées par une dll. Par exemple : x kernel32!Get* Affiche toutes les fonctions exportées par kernel32 qui commencent par Get. Certaines API sont en deux versions, une suffixée A pour l'ASCII, une autre suffixée W pour l'Unicode. Pour se simplifier la vie, on peut utiliser la commande ss, suivie de w(Unicode), a(ASCII) ou n(Aucun). De cette manière, lorsque l'on entre un symbole que ntsd ne trouve pas, il ajoute le suffixe au symbole, et recommence la recherche. La commande ln nous donne les symboles les plus proches d'une adresse. Dans notre cas où l'on a pas de symboles autres que les fonctions exportées, cela nous permet de savoir à quelle fonction appartient une adresse facilement. Enfin, la commande !handle liste les handles sur des objets systèmes et leurs types. #### Détourner le comportement d'un executable : A présent, on se propose de modifier notepad pendant son execution. On commence par repartir sur une base propre : ntsd notepad.exe On se souvient de l'adresse du point d'entré : c'est là que l'on va assembler notre code. Jettons un coup d'oeil au code actuel : u notepad + 739D On va appeler MessageBoxA, puis ExitProcess. On va commencer par mettre deux chaînes dans la mémoire, une pour le contenu de la MessageBox, une autre pour le titre. Ou mettre ces chaînes ? On a beaucoup de place, mais ce n'est pas une raison pour écraser quelque-chose. On va donc écrire dans les données non initialisées. !dh notepad Section .data. 1BA8 virtual size 9000 virtual address 800 size of raw data 7C00 file pointer to raw data On ecrira donc en notepad + 9000 + 800. Les commandes pour écrire dans la mémoire sont les suivantes : ea : Ecrire une chaîne de caractère dans la mémoire, sans caractère terminal. eb : Ecrire des octets dans la mémoire. ew, ed, eq : Ecrire de la taille spécifiée dans la mémoire. La commande d'écriture de chaînes à zéro terminal eza de WinDbg n'est pas utilisable... On écrit donc : eb notepad + 9800 'H' 'e' 'l' 'l' 'o' ' ' 'w' 'o' 'r' 'l' 'd' 0 on vérifie : db notepad + 9800 On écrit le titre un peu plus loin : eb notepad + 9810 'Y' 'e' 'a' 'h' '!' 0 A présent, il ne reste plus qu'à coder !! (Enfin !) Pour assembler, c'est a : a notepad + 739D Pour arrêter d'assembler, il suffit de ne rien entrer sur la ligne et de presser entrée. Il faut pousser les arguments de MessageBoxA, de la droite vers la gauche : xor @eax, @eax push @eax lea @ebx, [notepad + 9810] push @ebx lea @ebx, [notepad + 9800] push @ebx push @eax call MessageBoxA xor @eax, @eax push @eax call ExitProcess On désassemble pour vérifier : u notepad + 739D Et go ! : g #### Travailler depuis un fichier : Bon, le code que l'on a écrit, on préfèrerait le conserver... Avec debug, on pouvait envoyer des commandes contenues dans un fichier avec une redirection <, ou encore en utilisant un pipe et la commande type. Avec ntsd, on peut utiliser l'option -c et la commande $< comme je l'ai fait dans le zip. Si on souhaite mettre des lignes vides dans le fichier en question, il faut mettre celles-ci en commentaire, car une pression sur entrée demande à ntsd de réexecuter la dernière commande. Pour mettre en commentaire une ligne, on utilise $. On garderat la commande * pour mettre des vrais commentaires. Cela peut nous permettre de faciliter l'imortation du code dans un assembleur classique. Ces caractères provoqueront une erreur mineure au milieu du code (Par contre cela ne facilite pas vérification que l'assemblage s'est bien passé), lors de l'assemblage, sans poser plus de problèmes. =============================== eb notepad + 9800 'H' 'e' 'l' 'l' 'o' ' ' 'w' 'o' 'r' 'l' 'd' 0 eb notepad + 9810 'Y' 'e' 'a' 'h' '!' 0 $ a notepad + 739D $ xor @eax, @eax push @eax lea @ebx, [notepad + 9810] push @ebx lea @ebx, [notepad + 9800] push @ebx push @eax call MessageBoxA xor @eax, @eax push @eax call ExitProcess $ g q =============================== On copie ça, on lance ntsd notepad.exe, puis clique gauche sur l'icône en haut à gauche de la console, coller. #### Principe de la sauvegarde de l'exe sur le disque : Devoir faire un copier coller lors du développement, c'est déjà bof, mais alors être obligé de le faire sur l'application finale, ce serait horrible. Qui plus est, si on pouvait éviter de réassembler l'application à chaque lancement, ce ne serait pas plus mal... On va donc écrire notre notepad depuis la RAM vers le disque. Le programme de compilation sera placé dans les données non initialisées pour pouvoir disparaître. Les chaînes de notre application vont devoir se trouver dans les données initialisées. Notre code devra se trouver dans la section .text. On exploiterat donc les zones suivantes : Données non initialisées : 9800 -> ABA8 Données initialisées (2 milles caractères) : 9000 -> 9800 Code : 3000 -> 7604 Le point d'entrée est en 739D. La valeur de 3000 comme début du code est arbitraire mais assure une bonne marge de sécurité tout en nous donnant bien assez de place. Le point d'entrée en 739D était un peu près de la table d'import en 7604. On va donc faire un saut à l'adresse 3000 dès le début du programme. Le code de compilation est théoriquement simple : il faut claquer la mémoire dans un fichier. Mais pas n'importe comment car il faut penser aux section. L'écriture se fait en 4 phase : 1 Copie des headers et des bits qui le suive (On dirait que la Bound Import Table n'est pas dans une section...) : raw data : 0 -> 400 virtual : 0 taille : 400 2 Copie de la section .text (On remplit jusqu'à tomber sur l'offset de la section .data : raw data : 400 -> 7C00 virtual : 1000 taille : 7800 3 Copie de la section .data raw data : 7C00 -> 8400 virtual : 9000 taille : 800 4 Copie de la section .rsrc raw data : 8400 -> 11400 virtual : B000 taille : 9000 0x11400 = 70656 octets = la taille de notepad.exe ! #### L'utilisation des fonctions de l'API Win32 : Les fonctions de l'API Win32 sont dans des dlls, et sont donc suceptibles de ne pas être chargées aux mêmes adresse d'une execution sur l'autre. En fait si, on fait des éssais, on s'aperçoit que les principale dlls (kernel32...) sont toujours chargées au même endroit. Mais essayons de travailler proprement. On sait que le loader va mettre les adresses de LoadLibrary et GetProcAddress à une adresse précise en mémoire car ces fonctions sont dans la table d'import de notepad. La première étape consiste à déterminer les emplacement de ses adresses. On pourrait décrypter la table d'import. Mais on va plutôt laisser faire le travail à ntsd. La dernière section est à l'adresse virtuelle notepad + B000, avec une taille qu'on arrondit à 9000. On va donc bourriner en cherchant de notepad à notepad + 14000. La commande s permet de rechercher dans une plage mémoire : s -b : 8 bits s -w : 16 bits s -d : 32 bits s -q : 64 bits s -a : ASCII s -u : Unicode Les commandes sa et su permettent de cherher des chaînes repectivement ASCII et Unicode sans préciser de pattern. s -d notepad L14000 LoadLibraryA s -d notepad L14000 GetProcAddress Les résultats nous donnent une adresse et une série de valeur. On peut vérifier que c'est bien la première valeur en évaluant le symbole : ? LoadLibraryA On obtient : LoadLibraryA -> 10c8 GetProcAddress -> 1110 On peut essayer de vérifier tout ça en utilisant une fonction intéressante de ntsd. On va placer un point d'arrêt qui ne va pas se déclencher lorsque eip pointera dessus, mais lorsque qu'un accès mémoire sera effectué à cette adresse. ntsd peut refuse la mise en place de ce type de breakpoint lorsque l'on est pas passé par l'entry point, ce qui arrive lorsque l'on lance ntsd en même temps que le processus (Il suffit alors de faire tourner celui-ci puis de le stopper avec ctrl+c), ou, plus grave, lorsque l'on s'attache à un processus. Dans notre cas, lOadLibraryA est suceptible de n'être appeler qu'en début de prorgamme, on va donc mettre un point d'arrêt sur l'entry point. ntsd notepad.exe bp notepad + 739D g ba r4 notepad + 10c8 ba r4 notepad + 1110 Ces breaks point seront déclenchés lors de la lecture ou écriture de 4 octets à l'adresse spécifiée. bl liste les breakpoints, et bc permet d'en supprimer un à partir de son id. Pas de chance, LoadLibrary est appelée avant l'entry point (On voit bien les ModLoad)... Mais on va quand même se stopper sur un GetProcAddress. #### Le code de l'application : L'application serat identique à la précédente, à trois différences près : 1 MessageBox et ExitProcess seront appelés quelles que soient leurs adresses. 2 Les chaînes seront stockées dans la mémoire initialisée de manière à être sauvegardée sur le disque quand on compilera. 3 On mettra seulement un saut vers notre code à l'entry point. ========================================
15 juin 2010 à 14:52
Vous n'êtes pas encore membre ?
inscrivez-vous, c'est gratuit et ça prend moins d'une minute !
Les membres obtiennent plus de réponses que les utilisateurs anonymes.
Le fait d'être membre vous permet d'avoir un suivi détaillé de vos demandes et codes sources.
Le fait d'être membre vous permet d'avoir des options supplémentaires.