Débogage sous Windows
Introduction
Dans ce chapitre, nous allons utiliser PyDbg inspiré du livre Gray Hat Python de Justin Seitz, chapitre 4. Nous sommes dans la limite du Hacking et du Forensic. PyDbg va nous aider à pister les données dans les programmes, à débugger mais aussi à effectuer du fuzzing, du hook d’application...
Nous travaillerons donc ici sous Windows et pour ma part, sous Linux, j’utiliserai une machine virtuelle VirtualBox pour écrire et tester les exercices et exemples de ce chapitre.
Une connaissance préalable des systèmes Windows, de l’assembleur et bien sûr de Python est requise.
Nous devons savoir associer le debugger à un processus soit en ouvrant l’exécutable et en le lançant ensuite, soit en l’attachant (l’exécutable est déjà lancé).
Nous allons attacher le processus quand cela nous permettra de ne pas prendre en compte le démarrage de l’application et de nous focaliser sur une partie du code spécifique.
Lorsque nous ouvrirons le programme dans le debugger, nous contrôlerons le processus dès le démarrage, nous pourrons donc voir ce qui est chargé par exemple, ce qui est très utile lors d’analyse de malwares ou virus.
Le module ctypes de Python
Le module ctypes permet d’interagir avec des librairies écrites en langage C.
Nous utiliserons dans la suite le module ctypes pour notre debugger.
Le module ctypes fournit des méthodes pour charger des librairies écrites en C et appeler les fonctions de ces librairies, des types de données compatibles avec le langage C pour passer et récupérer des variables avec ces fonctions.
Le module ctypes est disponible à partir de Python 2.5 et supérieur.
Le module ctypes exporte un objet cdll qui permet de charger une librairie. Dans l’environnement Windows, nous disposons en plus des objets windll et oledll.
L’objet cdll fournit une méthode LoadLibrary() qui permet de charger une librairie dynamique.
From ctypes import *
cdll.LoadLibrary("libc6.so.6") # Linux
print windll.kernel32 # windows
print cdll.msvcrt # windows
libc=cdll.msvcrt
Une fois que nous avons chargé une librairie via la méthode LoadLibrary(), nous l’affectons à un objet qui va permettre d’appeler les fonctions définies par la librairie.
Cet objet agit en wrapper en permettant d’accéder aux fonctions de la librairie en passant par des pointeurs.
print windll.kernel32.GetModuleHandleA # Affiche <_FuncPtr object
at 0x...>
print hex(windll.kernel32.GetModuleHandleA(None)) # appel de la
fonction GetModule HandleA de la windll
Le module...
Première approche
Comment créer un processus sous Windows dans un debugger ?
La fonction s’appelle CreateProcessA(), à laquelle nous pouvons transmettre des paramètres.
Nous pourrons trouver sur Internet, et en particulier sur le site de MSDN, des détails sur cette fonction si nous le souhaitons.
Nous ne verrons ici que les paramètres à transmettre qui nous seront utiles pour la suite.
Les paramètres utilisés sont :
-
lpApplicationName
-
lpCommandLine
-
dwCreationFlags
-
lpStartupInfo
-
lpProcessInformation
Les autres paramètres pourront être mis à NULL.
Les deux premiers paramètres vont nous permettre de donner le chemin de l’application et les commandes à lui passer (en ligne de commande) si nécessaire.
dwCreationFlags nous servira à indiquer au processus qu’il devra démarrer avec le debugger (en mode debug donc) et les deux derniers paramètres sont des pointeurs sur des structures (STARTUPINFO et PROCESS_INFORMA-TION) qui détermineront la façon dont le processus devra démarrer et nous donnera des informations après le démarrage du processus.
Nous allons donc créer en Python trois programmes, un script qui va définir les deux structures dont nous avons parlé ci-dessus (mes_definitions_debugger.py), un script qui sera le debugger (mon_debugger.py) et un script de test pour vérifier le bon fonctionnement des deux scripts précédents.
mes_definitions_debugger.py
from ctypes import *
WORD = c_ushort
DWORD = c_ulong
LPBYTE = POINTER(c_ubyte)
LPTSTR = POINTER(c_char)
HANDLE = c_void_p
DEBUG_PROCESS = 0x00000001
CREATE_NEW_CONSOLE = 0x00000010
class STARTUPINFO(Structure):
_fields_ = [
("cb", DWORD),
("lpReserved",LPTSTR),
("lpDesktop",LPTSTR),
("lpTitle",LPTSTR),
("dwX",DWORD),
("dwY",DWORD),
("dwXSize",DWORD), ...
État des registres
Un des principaux atouts d’un débogueur est de pouvoir visualiser le contenu des registres. Quand une exception apparaît, nous devons être capables de déterminer l’état de la pile à n’importe quel endroit et à n’importe quel moment.
Pour ce faire, nous devons donc obtenir les informations du thread courant.
La fonction OpenThread() va nous y aider en lui indiquant les paramètres adéquats. Cette fonction est très semblable à OpenProcess() mais au lieu de lui transmettre le PID, nous devrons lui transmettre le TID (Thread Identifier).
1. Énumération des threads
Nous devons donc être capables d’énumérer tous les threads du processus. Nous avons pour cela à notre disposition la fonction CreateToolhelp32Snapshot() de la DLL kernel32.dll. Nous pourrons donc obtenir par exemple la liste des processus, des threads et les DLL chargés.
Le paramètre dwFlags indiquera à la fonction ce que nous désirons regarder (threads, DLL, processus, heap).
Nous mettrons donc ce paramètre à TH32CS_SNAPTHREAD (valeur 0x00000004) pour voir les threads.
L’autre paramètre, th32ProcessID, est simplement le PID du processus.
Quand la fonction réussit, elle nous retourne le handle de l’objet snapshot.
Une fois que nous avons la liste des threads, nous pouvons...
Les événements du debugger
Nous allons revenir sur la fonction WaitForDebugEvent() qui nous retourne la structure DEBUG_EVENT.
Cette structure contient beaucoup d’information dont dwDebugEventCode qui va particulièrement nous intéresser, elle va nous indiquer quel type d’événement a eu lieu.
En regardant la valeur de dwDebugEventCode, nous pourrons ainsi déterminer l’événement.
Code d’événement |
Valeur du code |
Valeur de l’union u |
0x1 |
EXCEPTION_DEBUG_EVENT |
u.Exception |
0x2 |
CREATE_THREAD_DEBUG_EVENT |
u.CreateThread |
0x3 |
CREATE_PROCESS_DEBUG_EVENT |
u.CreateProcessInfo |
0x4 |
EXIT_THREAD_DEBUG_EVENT |
u.ExitThread |
0x5 |
EXIT_PROCESS_DEBUG_EVENT |
u.ExitProcess |
0x6 |
LOAD_DLL_DEBUG_EVENT |
u.LoadDll |
0x7 |
UNLOAD_DLL_DEBUG_EVENT |
u.UnloadDll |
0x8 |
OUTPUT_DEBUG_STRING_EVENT |
u.DebugString |
0x9 |
RIP_EVENT |
u.RipInfo |
Ajoutons donc maintenant quelques définitions à mon_debugger3.py qui devient donc mon_debugger_final.py
Nous utiliserons mon_debugger_final.py jusqu’à la fin de ce chapitre, des fonctions que nous verrons ultérieurement y ont déjà été placées.
mon_debugger_final.py
from ctypes import *
from mes_definitions_debugger_final import *
import sys
import time
kernel32 = windll.kernel32
class debugger():
def __init__(self):
self.h_process = None
self.pid = None
self.debugger_active = False
self.h_thread = None
self.context = None
self.breakpoints = {}
self.first_breakpoint= True
self.hardware_breakpoints = {}
system_info = SYSTEM_INFO()
kernel32.GetSystemInfo(byref(system_info))
self.page_size = system_info.dwPageSize
self.guarded_pages...
Les points d’arrêt (breakpoints)
1. Points d’arrêt logiciel
Pour placer des points d’arrêt, nous devons être capables d’écrire et de lire dans la mémoire.
La fonction ReadProcessMemory() va nous y aider ainsi que WriteProcessMemory().
Grâce à ces deux fonctions, nous allons pouvoir inspecter la mémoire.
Les paramètres à leur fournir sont lpBaseAddress (adresse où nous voulons commencer à lire ou écrire), lpBuffer (pointeur vers la donnée que nous voulons lire ou écrire), nSize (nombre total d’octets que nous voulons lire ou écrire).
En utilisant ces deux fonctions, nous pourrons utiliser aisément dans notre debugger les points d’arrêt.
Généralement, les points d’arrêt sont placés sur un appel de fonction. Pour l’exercice, nous utiliserons l’appel à printf().
Pour déterminer l’adresse virtuelle d’une fonction, nous utiliserons GetProcessAddress() qui sera exporté de kernel32.dll.
Nous aurons besoin de l’en-tête de la fonction sur l’appel de laquelle nous désirons placer le point d’arrêt, GetModuleHandle() nous y aidera.
Vous pouvez aller voir les définitions de read_process_memory(), write_process_memory(), bp_set() et func_resolve() dans mon_debugger_final.py si vous souhaitez plus de détails là-dessus.
Pour nous permettre de tester le script qui va boucler...
La bibliothèque PyDbg
Nous avons vu dans les sections précédentes comment placer des points d’arrêt, mais comment faire avec PyDbg ?
Une fonction bp_set(address, descriptor= ’’ ’’, restore= True,handler=None) va nous y aider.
L’argument address est l’adresse où nous voulons placer le point d’arrêt logiciel.
Le paramètre descriptor est optionnel et peut être utilisé pour donner un nom au point d’arrêt.
Le paramètre restore détermine si le point d’arrêt doit automatiquement être remis à zéro après avoir récupéré l’en-tête et le paramètre handler spécifie quelle fonction appeler.
Toutes les informations du contexte, des threads et processus vont être renseignées par cette classe lors de l’appel à la fonction.
Nous allons utiliser le script boucle_printf.py des sections précédentes et grâce à un nouveau script, nous allons lire les valeurs du compteur et les remplacer par une valeur aléatoire comprise entre 1 et 100.
Nous allons donc observer, lire et manipuler des données du processus cible en temps réel.
printf_random.py
from pydbg import *
from pydbg.defines import *
import struct
import random
def printf_randomizer(dbg):
# Lecture de la valeur du compteur en ESP + 0x8 comme un DWORD
parameter_addr = dbg.context.Esp + 0x8
counter = dbg.read_process_memory(parameter_addr,4)
counter = struct.unpack("L",counter)[0]
print "Counter: %d" % int(counter)
random_counter = random.randint(1,100)
random_counter = struct.pack("L",random_counter)[0]
dbg.write_process_memory(parameter_addr,random_counter)
return DBG_CONTINUE
dbg = pydbg()
pid = raw_input("Enter the printf_loop.py PID: ")
dbg.attach(int(pid))
printf_address = dbg.func_resolve("msvcrt","printf")
dbg.bp_set(printf_address,description="printf_address",
handler=pri ntf_randomizer)
dbg.run()
Résultat obtenu pour boucle_printf.py
C:\Documents and Settings\fasm\Mes documents\livre_python\ ...
Mise en pratique : Hooking
Énoncé
Prérequis : PyDbg
But : créer un hook d’Internet Explorer.
Énoncé :
Un hook (littéralement crochet ou hameçon) permet à l’utilisateur d’un logiciel de personnaliser le fonctionnement de ce dernier, en lui faisant réaliser des actions supplémentaires à des moments déterminés.
Nous voudrions donc « sniffer » les connexions SSL d’Internet Explorer qui utilise CryptoAPI de Microsoft.
Solution
hook_ssl_pydbg.py
from pydbg import *
from pydbg.defines import *
import utils
import sys
import re
import struct
def ssl_sniff_encryptmessage( dbg, args ):
buffer = ""
addr_bufflen=dbg.context.Esp + 0x2C
bufflen=struct.unpack('L',dbg.read_process_memory(addr_bufflen , 4))[0]
addr=dbg.context.Esp + 0x34
addr_buffer = struct.unpack('L',dbg.read_process_memory(addr, 4))[0]
try:
buffer=dbg.read_process_memory(addr_buffer,bufflen)
buffer=re.sub('.*gzip.*\r\n','',buffer)
dbg.write_process_memory(addr_buffer, buffer)
except:
pass
print "Requete: \n\n%s"...