Nous continuons sur le sujet , soit : 'de debug win32 API'. Dans ce tutorial, nous allons voir comment modifier le process du debuggee.
Downloadez l'exemple.
Dans le tutorial précédent, nous avons vu comment charger le debuggee et gérer les événements de debug qui arrivent dans son process. Pour être utile, notre programme doit être capable de modifier le process de debuggee. Il existe plusieurs APIs pour faire ça.
ReadProcessMemory proto hProcess:DWORD, lpBaseAddress:DWORD, lpBuffer:DWORD, nSize:DWORD, lpNumberOfBytesRead:DWORD
hProcess
est l'handle du process dans lequel vous souhaitez effectuer votre lecture.
lpBaseAddress est l'adresse de départ dans le process cible où vous voulez commencer votre lecture. Par exemple, si vous voulez lire 4 octets du process de debuggee commençant à l'offset 401000, la valeur dans ce paramètre doit être 401000.
lpBuffer est l'adresse du buffer qui recevra les octets qu'on va lire dans le process.
nSize est le nombre d'octets que vous souhaitez lire.
lpNumberOfBytesRead est l'adresse de la variable (de taille dword) qui reçoit le numéro d'octets qu'on aura réellement lus. Si vous vous en foutez, vous pouvez employer un NULL.
Les deux fonctions API suivantes ont besoin d'une petite explication quant à leur utilisation. Dans un OS multi-taches tel que Windows, on peut avoir plusieurs programmes tournant en même temps. Windows donne un temps d'activité restreint à chacun d'eux. Quand ce temps d'activité expire, Windows gèle le programme qui était en train de tourner et passe au programme suivant qui prend maintenant la priorité la plus haute. juste avant de commuter sur ce nouveau programme, Windows a sauvegardé les valeurs du premier dans les Registres de lien (du premier) pour qu'au moment où il est temps de reprendre le lien, Windows puisse rétablir l'ancien *environnement* de ce lien. Les valeurs des Registres sauvegardés, représentent un ensemble qu'on appelle un contexte.
Revenons à notre sujet. Lorsqu'un événement de debug arrive, Windows suspend le debuggee. Le contexte du debuggee est sauvegardé. Puisque le debuggee est suspendu, on est sûrs que les valeurs dans le contexte resteront inchangées. Nous pouvons obtenir les valeurs dans le contexte grâce à GetThreadContext
et nous pouvons les changer avec SetThreadContext.
Ces deux APIs sont extrêmement puissantes. Avec elles, on a à notre portée la puissance du VxD du debuggee : on peut changer les valeurs des registres sauvegardés, et juste avant que le debuggee ne reprenne son exécution, on récupère les anciennes valeurs du contexte de nouveau dans les Registres. Quelque soit les changements qu'on a fait au contexte est renvoyé en arrière au debuggee, il s'aperçoit de tout. Pensez-y : on peut même changer la valeur du Registre EIP et ainsi détourner le flux exécution n'importe où, où on le souhaite! C'est à dire, l'EIP représente à chaque instant la ligne du programme qui est en train d'être lue par l'ordinateur. De fil en aiguille on avance dans la lecture du programme ligne par ligne. Maintenant, si on est capable de changer la valeur du registre EIP, on peut sauté vraiment où on veut. Alors que normalement c'est l'ordinateur et personne d'autre qui contrôle l'EIP. C'est quelque chose qu'on serait bien incapable de faire en temps normal.
GetThreadContext proto hThread:DWORD, lpContext:DWORD
hThread
est l'handle du lien que vous voulez obtenir dans le context
lpContext est l'adresse de la structure CONTEXT qui sera remplie, lorsque cette fonction retourne avec succès.
SetThreadContext a exactement les mêmes paramètres. On va voir à quoi ressemble une structure de CONTEXT :
Comme vous pouvez le voir, les membres de ces structures sont des imitations des Registres réels du processeur. Avant que vous ne puissiez employer cet artifice, vous devez indiquer quels groupes de Registres vous souhaitez lire/écrire dans le membre ContextFlags.
Par exemple, si vous voulez lire/écrire tous les Registres, vous devez mettre le membre CONTEXT_FULL dans la structure ContextFlags.
Si vous voulez seulement lire/écrire regEbp, regEip, regCs, regFlag, regEsp ou regSs, vous devez mettre le membre CONTEXT_CONTROL dans
Encore une chose que vous devez vous rappeler, en employant la structure CONTEXT: elle doit absolument fonctionner avec des modèles dword sinon vous obtiendriez des résultats étranges sous NT. Vous devez mettre "align dword" juste au-dessus de la ligne qui la déclare, de cette façon :
align dword
MyContext CONTEXT <>
Ce premier exemple sert à montrer comment on utilise un Debugger de type un ActiveProcess. D'abord, on doit lancer une cible qui s'appelle win.exe, ce programme est une boucle infinie qui tourne en rond sans pouvoir en sortir et on ne peut donc pas arriver à son autre fonction (qui est juste après) qui affiche une fenêtre à l'écran. A partir de là, on lance notre exemple (appelé debug2.exe), il s'attachera à win.exe et modifiera son code tel que win.exe puisse sortir de sa boucle infinie et ainsi il affichera sa fenêtre.
Si ça ressemble pas à un Memory-Patch çà... :).386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib
.data
AppName db "Win32 Debug Example no.2",0
ClassName db "SimpleWinClass",0
SearchFail db "Cannot find the target process",0
TargetPatched db "Target patched!",0
buffer dw 9090h
.data?
DBEvent DEBUG_EVENT <>
ProcessId dd ?
ThreadId dd ?
align dword
context CONTEXT <>
.code
start:
invoke FindWindow, addr ClassName, NULL
.if eax!=NULL
invoke GetWindowThreadProcessId, eax, addr ProcessId
mov ThreadId, eax
invoke DebugActiveProcess, ProcessId
.while TRUE
invoke WaitForDebugEvent, addr DBEvent,
INFINITE
.break .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags,
CONTEXT_CONTROL
invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread,
addr context
invoke WriteProcessMemory,
DBEvent.u.CreateProcessInfo.hProcess, context.regEip ,addr buffer, 2, NULL
invoke MessageBox, 0,
addr TargetPatched, addr AppName, MB_OK+MB_ICONINFORMATION
.elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
.if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
invoke
ContinueDebugEvent, DBEvent.dwProcessId,DBEvent.dwThreadId, DBG_CONTINUE
.continue
.endif
.endif
invoke ContinueDebugEvent, DBEvent.dwProcessId,
DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw
.else
invoke MessageBox, 0, addr SearchFail, addr AppName,MB_OK+MB_ICONERROR
.endif
invoke ExitProcess, 0
end start
;--------------------------------------------------------------------
; Le code source de win.asm est en réalité une simple fenêtre comme
; celle de l'exemple du tutorial 2 avec une boucle infinie insérée
; juste avant qu'on ne puisse arriver jusqu'à elle (la fenêtre).
;----------------------------------------------------------------------
......
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ hInst,NULL
mov hwnd,eax
jmp $ ;<---- Voici notre boucle infinie. Elle boucle sur elle-même.
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.break .if (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp
invoke FindWindow, addr ClassName, NULL
Notre programme a besoin de s'attacher au debuggee avec DebugActiveProcess lequel a besoin de l'ID du process du debuggee. Nous pouvons obtenir l'ID du process en appelant GetWindowThreadProcessId qui a à son tour à besoin de l'handle de la fenêtre en tant que paramètre. Donc nous avons d'abord besoin d'obtenir cet handle.
Avec FindWindow, nous pouvons spécifier le nom de la Window Class (classe de fenêtre) dont nous avons besoin. Il renvoie l'handle de la fenêtre créée par cette Window Class. S'il renvoie un NULL, aucune fenêtre de cette classe n'est présente.
.if eax!=NULL
invoke GetWindowThreadProcessId, eax, addr ProcessId
mov ThreadId, eax
invoke DebugActiveProcess, ProcessId
Après avoir obtenu l'ID du process, nous pouvons appeler DebugActiveProcess. Comme ça, nous passons à la boucle de debug attendant les événements de debug.
.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags,
CONTEXT_CONTROL
invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread,
addr context
Quand on arrive à la partie du programme qui contient la ligne CREATE_PROCESS_debug_INFO, ça signifie que le debuggee est suspendu, disponible pour que nous puissions faire l'acte de chirurgie en son process. Dans cet exemple, nous remplaçons l'instruction de la boucle infinie dans le debuggee (qui est un Jmp codé en langage machine par 0EBh 0FEh) par deux NOP (90h 90h).
D'abord, nous avons besoin d'obtenir l'adresse où se situe cette instruction (ce Jmp). Puisque le debuggee est déjà dans la boucle au moment où notre programme est attaché à lui, alors EIP indique forcément l'adresse de cette instruction. Tout ce que nous avons besoin de faire, c'est de récupérer la valeur de EIP. On utilise GetThreadContext pour faire ça.
Nous plaçons le membre ContextFlags dans le CONTEXT_CONTROL pour indiquer à GetThreadContext que nous souhaitons qu'il remplisse les Registres de "control" avec les membres de la structure CONTEXT. Ainsi par exemple EIP sera récupéré dans notre Registre de Control appelé regEip.
invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, context.regEip ,addr buffer, 2, NULL
Maintenant que nous avons la valeur de l'EIP, nous pouvons appeler WriteProcessMemory pour récrire par dessus l'instruction "jmp $" avec NOP, ainsi on aura aidé efficacement le debuggee à sortir de sa boucle infinie. Après ça, nous affichons un message destiné à l'utilisateur et appelons ensuite ContinueDebugEvent pour reprendre le debuggee. Maintenant que l'instruction "jmp $" est remplacée par deux instructions NOP, le debuggee est capable de continuer jusqu'à atteindre l'affichage de sa fenêtre et du message. La preuve, c'est que nous verrons apparaître cette fenêtre à l'écran.
L'autre exemple (Debug3.exe) utilise une approche un peu différente pour casser la boucle infinie.
.......
.......
.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
mov context.ContextFlags, CONTEXT_CONTROL
invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread,
addr context
add context.regEip,2
invoke SetThreadContext,DBEvent.u.CreateProcessInfo.hThread,
addr context
invoke MessageBox, 0, addr LoopSkipped, addr AppName, MB_OK+MB_ICONINFORMATION
.......
.......
Comme pour l'exemple précédent on appelle toujours GetThreadContext pour obtenir la valeur actuelle de l'EIP, mais au lieu de l'action : 'effacer la boucle du "jmp$" avec deux instructions NOP (pour briser la boucle), ici on incrémente la valeur de regEip par 2 (regEip-->EIP+2) grâce à l'instruction "skip over". Le résultat, c'est que lorsque le debuggee reprend le contrôle, il reprend son exécution à l'instruction suivante, juste après le "jmp$". Donc on ne détruit pas la boucle mais on passe par dessus. Vous allez me dire pourquoi +2 ! le saut 'jmp' est codé en langage machine sur deux octets (EB et FE ou 0EBh 0FEh) donc quand l'EIP est sur le 'jmp' on est sur EB si on ajoute 2 à l'EIP, on passe sur EB puis sur FE et donc on arrive à ce qui suit le jmp donc à l'instuction suivante. On vient de sauter par dessus !
Maintenant vous comprenez la puissance de 'Get & SetThreadContext'. Vous pouvez aussi modifier d'autres Registres et leurs valeurs seront ensuite rétablies après le passage du debuggee. Vous pouvez même insérer l'instruction 'int 3h' lequel sert à placer des Break Points dans le programme qu'on est en train de debugger.