Tutorial 29: Win32 Debug API Partie 2/3

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
.

Théorie:

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.

Exemple:

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

Analyse:

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.



[Iczelion's Win32 Assembly Homepage]


Traduit par Morgatte