Dossier n°9 : Les Debug APIs


Ce " dossier " sur les Debug API's a pu être réalisé grâce à la participation directe ou indirecte de +Spath, +Frog's Print, Alsindor, Pulsar et
Morgatte, en s'appuyant sur des écrits de Iceman (WIN32 - Inside Debug API), Matt Pietrek (Window 95 Programming Secrets dont vous trouverez deux extraits : Under the Hood et Dirty Little Secrets about Windows 95), Iczelion (Win32 Assembly Tutorial 29 Win32 Debug API part I, II & III traduit par Morgatte) et Jeremy Gordon (Win32 Exception handling for assembler programmers).

Ce dossier est accompagné de quatre petits programmes:

Un programme de Pulsar : Patch Mémory d'un Process via les Debug APIs

Cible.exe et B-crk475.exe qui sont les cibles à atteindre.
Au passage, Cible.exe a été créé pour être un exemple significatif, donc facile, mais rien ne vous empèche d'en réaliser le KEYGenerateur.

Un programme de démonstration.


Handle

Le terme 'handle' dans ce texte fait référence aux handles des objets gérés par le kernel32.
A la base, chaque handle peut être fermé par CloseHandle(). Ceci n'inclus PAS les objets graphiques (GDI) comme HPEN, HDC, HBITMAP etc, qui n'ont rien à voir avec le kernel32.
Un handle, dans le contexte d'un process, est supposé être un index dans une table dont l'entrée contiendrait les informations concernant différents objets.

DialogBox1

handle

 
 

Champ d’édition 1

handle

 

Champ d’édition 2

handle

Prenons l'exemple d'un CrackMe se présentant sous la forme d'une Boite de dialogue à deux champs, l'un pour le nom, l'autre pour le serial.
L'application, dés son démarrage, va se faire attribuer un handle (identificateur hInstance) :

invoke GetModuleHandle, NULL
       mov hInstance, eax

Puis la fenêtre-objet (ici notre boite de dialogue) se voit à son tour attribuer un handle (identificateur hWnd):

       INVOKE CreateWindowExA,WS_EX_WINDOWEDGE,\ 
       ADDR ClassName,ADDR NameApp,\ 
       WS_SYSMENU or WS_MINIMIZEBOX or WS_DLGFRAME or WS_BORDER\ 
       or WS_CLIPCHILDREN or WS_CLIPSIBLINGS or WS_VISIBLE,\ 
       280,200,385,162,NULL,NULL,hInst,NULL 
       mov hWnd,eax   

Le handle de la DialogBox est en seconde position sur la pile, c'est la fenêtre_objet-propriétaire.
Si cette boite de Dialogue dépendait d'une autre fenêtre_objet, son handle serait celui d'une fenêtre_objet-fille.

Fenêtre 1

handle

       
   

DialogBox1

handle

   
       

Champ d’édition 1

handle

       

Champ d’édition 2

handle

Puis c'est au tour du premier champ d'édition (identificateur hwnedit1)

                Invoke CreateWindowExA,WS_EX_CLIENTEDGE, ADDR EditClass, NULL,\ 
       WS_CHILDWINDOW or ES_READONLY or WS_VISIBLE or\ 
       ES_AUTOHSCROLL,12,80,261,23,hWnd,NULL,hInstance,NULL 
       Mov hwndEdit1,eax
HWND CreateWindowEx
DWORD dwExStyle,                      // style de fenêtre étendue 
LPCTSTR lpClassName,                 // pointe vers le nom de la class enregistrée 
LPCTSTR lpWindowName,               // pointe vers le nom de la fenêtre 
DWORD dwStyle,                     // style de fenêtre 
int x,                            // cordonnée horizontale de la fenêtre 
int y,                           // cordonnée verticale de la fenêtre 
int nWidth,                     // largeur de la fenêtre 
int nHeight,                   // hauteur de la fenêtre 
HWND hWndParent,              // handle de la fenêtre parent ou propriétaire
HMENU hMenu,                 // handle du menu ou de l'identifiant de la 
                            //fenêtre fille (NULL dans ce cas précis)
HINSTANCE hInstance,       // handle de l'application 
LPVOID lpParam            // pointe vers les données de la fenêtre de création

Etc…

La table des handles est un DWORD contenant le nombre de handles différents, suivi par un tableau [1] contenant les identificateurs-handle des objets en question. Les identificateurs-handle sont des DWORD contenant le drapeau d'accès à un pointeur vers l'objet situé en mémoire.

typedef struct _HANDLE_TABLE
{
  DWORD   cEntries;               // Nombre de handles dans la table
  HANDLE_TABLE_ENTRY array[1];   // Tableau dont la taille est fixée par cEntries
} HANDLE_TABLE, *PHANDLE_TABLE;

typedef struct _HANDLE_TABLE_ENTRY
{
  DWORD   flags;           // flags dépend du type d'objet concerné
  PVOID   pObject;        // Pointeur vers l'objet auquel se réfère le handle
} HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY;

Un processID est un pointeur vers la structure du process contenant les informations de celui ci XORé avec un 'obsfucator'. Il y a plusieurs algorithmes possibles pour récupérer cette valeur. L'une des méthodes les plus courantes est de passer par l'appel à la fonction GetCurrentProcessId() en Ring3, qui passe cette valeur à un VxD qui appelle VWIN32_GetCurrentProcessHandle, et de XORer les deux valeurs retournées pour obtenir l'Obsfucator.

Obsfucator


Mais qu'est ce que cet Obsfucator ?

Dans les premières versions de Windows 95, GetCurrentProcessId et GetCurrentThreadId retournaient des pointeurs vers le processus approprié et les structures Threads DataBase. Peu de temps après, ces fonctions ont commencé à retourner des valeurs qui n'étaient plus des pointeurs. La valeur de retour était la valeur du pointeur original, mais XORé avec une valeur apparemment aléatoire.

D'où vient cette valeur aléatoire ?

A chaque fois que le système démarre, il emploie l'horloge pour calculer cette valeur aléatoire. Elle est appelée "Obsfucator", et permet d'accéder aux informations contenues dans la thread DataBase.

La seule raison du XOR est d'empêcher quiconque d'arriver aux structures de données du système...

Une autre méthode pour obtenir cette valeur, sans utiliser de VxD, est de faire:

DWORD _dw = 0, _dwCurProcId = GetCurrentProcessId();
__asm {
	mov eax,dword ptr fs:[18h]  
	mov eax,dword ptr [eax+30h] 
	mov ebx,eax
	xor ebx,dword ptr [_dwCurProcId]
	mov dword ptr [_dw],ebx  };

Unobsfucator = _dw;

Ou encore

KERNEL32!GetCurrentProcessId: 
mov eax, [PTR_CURRENT_PROCESS_ID]
push dword ptr [eax] 
call OBFUSCATOR
ret 
OBFUSCATOR:
mov eax, [MAGIC_XOR_PATTERN] ; e.g., 7EAE7049h 
test eax, eax 
jnz do_xor
xor eax, eax 
jmp do_ret 
do_xor: 
xor eax, [esp+4] 
do_ret: 
ret 4 

Ce module utilise TIDTOTDB () et PIDTOPDB () pour récupérer les valeurs retournées par ces fonctions dans les adresses des structures de données (Datas). Le code est basé sur le fait que si un XOR b EQU c, alors un XOR c EQU b.

Thread DataBase(TDB) et Thread Information Block (TIB)


La structure DataBase d'un process (PDB) contient toute les informations sur le process actif :

typedef struct _PROCESS_DATABASE
{
    DWORD   Type;                      // 00h KERNEL32 object type (5)
    DWORD   cReference;                // 04h Number of references to process
    DWORD   un1;                       // 08h
    DWORD   someEvent;                 // 0Ch An event object 
    DWORD   TerminationStatus;         // 10h Returned by GetExitCodeProcess
    DWORD   un2;                       // 14h
    DWORD   DefaultHeap;               // 18h Address of the process heap
    DWORD   MemoryContext;             // 1Ch pointer to the process's context
    DWORD   flags;                     // 20h
    DWORD   pPSP;                      // 24h Linear address of PSP?
     WORD   PSPSelector;               // 28h
     WORD   MTEIndex;                  // 2Ah
     WORD   cThreads;                  // 2Ch
     WORD   cNotTermThreads;           // 2Eh
     WORD   un3;                       // 30h
     WORD   cRing0Threads;             // 32h number of ring 0 threads
    HANDLE  HeapHandle;                // 34h Heap to allocate handle tables out of
                                       // This seems to always be the KERNEL32 heap
    DWORD   W16TDB;                    // 38h Win16 Task Database selector
    DWORD   MemMapFiles;               // 3Ch memory mapped file list (?)
    PENVIRONMENT_DATABASE pEDB;        // 40h Pointer to Environment Database
    PHANDLE_TABLE pHandleTable;        // 44h Pointer to process handle table
 struct _PROCESS_DATABASE * ParentPDB; // 48h Parent process database
    PMODREF MODREFlist;                // 4Ch Module reference list
    DWORD   ThreadList;                // 50h Threads in this process
    DWORD   DebuggeeCB;                // 54h Debuggee Context block?
    DWORD   LocalHeapFreeHead;         // 58h Head of free list in process heap
    DWORD   InitialRing0ID;            // 5Ch
    CRITICAL_SECTION    crst;          // 60h
    DWORD   un4[2];                    // 78h
    DWORD   pConsole;                  // 84h Pointer to console for process
    DWORD   tlsInUseBits1;             // 88h  // Represents TLS indices 0 - 31
    DWORD   tlsInUseBits2;             // 8Ch  // Represents TLS indices 32 - 63
    DWORD   ProcessDWORD;              // 90h 
    PROCESS_DATABASE * ProcessGroup;   // 94h
    PMODREF pExeMODREF;                // 98h pointer to EXE's MODREF
    DWORD   TopExcFilter;              // 9Ch Top Exception Filter?
    DWORD   BasePriority;              // A0h Base scheduling priority for process
    DWORD   HeapOwnList;               // A4h Head of the list of process heaps
    DWORD   HeapHandleBlockList;       // A8h Pointer to head of heap handle block list
    DWORD   pSomeHeapPtr;              // ACh normally zero, but can a pointer to a
                                       // moveable handle block in the heap
    DWORD   pConsoleProvider;          // B0h Process that owns the console we're using?
     WORD   EnvironSelector;           // B4h Selector containing process environment
     WORD   ErrorMode;                 // B6H SetErrorMode value (also thunks to Win16)
    DWORD   pevtLoadFinished;          // B8h Pointer to event LoadFinished?
     WORD   UTState;                   // BCh
} PDB, *PPDB;

Avec la Thread DataBase, certains champs sont indispensables pour exécuter du code. Si indispensable, que l'architecture WIN32 les rends immédiatement accessibles sans avoir à passer par la structure Thread DataBase. Ces champs sont stockés dans une structure appelée TIB (Thread Information Block) et correspondant aux offets 10h à 3Ch dans la Thread DataBase. Il suffit de soustraire 10h à l'adresse d'entrée du TIB pour avoir celle de la thread DataBase (TDB), parfois appelées Thread Control Block (suivant votre niveau de privilège : Ring 0 ou Ring 3).

Les champs du TIB sont les suivants :

00h   DWORD   pvExcept
04h   DWORD   TopOfStack
08h   DWORD   StackLow
0Ch    WORD   W16TDB
0Eh    WORD   StackSelector16
10h   DWORD   Selmanlist
14h   DWORD   UserPointer
18h    PTIB   pTIB
1Ch    WORD   TIBFlag
1Eh    WORD   Win16MutexCount
20h   DWORD   CebugContext
24h  PDWORD   pCurrentProcess
28h   DWORD   MessageQueue
2Ch  PDWORD   PTLSArray
30h   DWORD   pProcess (Process DataBase Pointer)  

Quand une thread est créée, Windows charge dans FS une valeur dont l'adresse de base pointe vers le TIB correspondant.
L'intérêt est qu'ainsi chaque thread peut utiliser le même code pour les SEH, par exemple, alors que dans chaque cas FS:0 pointe vers une zone mémoire différente. Maintenant il est beaucoup plus pratique pour le système d'utiliser une adresse linéaire ("flat") pour accéder aux TIBs, d'ou l'intérêt de cette valeur en FS:18.
Autrement dit, FS:0 pointe vers la même zone mémoire que l'adresse linéaire en FS:18.

Instruction :  MOV EAX, FS : [18] 
Le but :          indicateur linéaire sur le TEB 
Exemples: 
MOV EAX, FS : [18] 
MOV EAX, [EAX+24]

La description :
le TEB (Thread Environnement Block) est toujours pointé par le registre FS. Pour accéder au code, il est utile d'employer une adresse linéaire pour accéder au TEB. L'adresse linéaire du TEB peut être trouvée à l'offset 0x18 dans celui ci. Le pointeur qui est lu dans FS:[18] permet de lire d'autres valeurs du TEB.

Un TEB est créé par Windows a chaque fois qu'une nouvelle thread est créée.
C'est une structure qui contient des informations sur la thread, et dont Windows se sert, par exemple a chaque fois qu'il doit passer d'une thread a l'autre 'dans un environnement multitâches.

A l'aide de l'Obsfucator, il est possible d'utiliser cette valeur pour obtenir la position de n'importe quelle TIB, à partir du thread ID correspondant (avec un XOR).

Chaque objet du kernel32 (en fait tous ce qui peut être représenté par un handle) utilise une entête commune : le premier DWORD concerne le type de l'objet, et le second DWORD concerne le nombre de handles référencés pour cet objet. Les types des objets sont définis ainsi :

K32OBJ_SEMAPHORE            0x1
K32OBJ_EVENT                0x2
K32OBJ_MUTEX                0x3
K32OBJ_CRITICAL_SECTION     0x4
K32OBJ_PROCESS              0x5
K32OBJ_THREAD               0x6
K32OBJ_FILE                 0x7
K32OBJ_CHANGE               0x8
K32OBJ_CONSOLE              0x9
K32OBJ_SCREEN_BUFFER        0xA
K32OBJ_MEM_MAPPED_FILE      0xB
K32OBJ_SERIAL               0xC
K32OBJ_DEVICE_IOCTL         0xD
K32OBJ_PIPE                 0xE
K32OBJ_MAILSLOT             0xF
K32OBJ_TOOLHELP_SNAPSHOT    0x10
K32OBJ_SOCKET               0x11

Pour obtenir le process ID d'un process référencé par un process handle, il est possible d'utiliser GetCurrentProcessId() ^ Obsfucator pour récupérer le pointeur vers la structure DataBase du process en cours.
Cette structure contient un pointeur vers l'offset 44h qui lui même pointe vers la table des handles. Tous les objets commencent par un DWORD indiquant le type de cet objet, suivi par un DWORD contenant le nombre de handles référencés par cet objet. Le handle pourra être un index dans ce tableau, et le process ID concerné par le handle sera un pointeur vers l'objet, XORé avec l'Obsfucator.

Dans la philosophe Win32, un process est un objet qui a un espace mémoire de réservé, du code, des datas, et une thread primaire. Chaque process n'a au début qu'une seule thread. A partir de la Thread primaire, il est possible de créer d'autres Threads, par la suite, qui fonctionneront dans le même espace mémoire.
Contrairement à ce que beaucoup pensent, un processus n'exécute PAS du code. Ce sont les Threads qui s'en chargent. Les objets des Threads partagent le même espace d'adresse et les mêmes ressources mais elles ont des contextes individuels.

Qu'est ce que ca veut dire ?

Windows95 et WindowsNT sont des systèmes multitâches ET multiThreads.
L'OS semble gérer tous les threads en même temps, mais ce n'est pas exacte. Chaque Thread individuel est prévue s'exécuter pendant un temps court (de l'ordre de 20 millisecondes) et l'OS sauve l'état de la Thread dans une structure appelée CONTEXT_structure, puis passe à la thread suivante.
L'information sauvée dans cette structure représente le Thread Context et est structuré ainsi :

- threads machine registers (CPU registers)
- la pile du kernel et les adresses utilisées par celle ci
- l'adresse du thread environment block (TEB)

Quand l'OS rencontre de nouveau notre thread, il la rétablit à partir de la structure CONTEXT comme si rien n'était arrivé.

Debug API's

(Pour plus d'information sur ces Fonctions, reportez vous à la traduction d'Iczelion, faite par Morgatte)
Voyons comment il serait possible d'utiliser ces informations, maintenant, et commençons par modifier un programme cible.
La méthode est de sauver une page de codes du processus cible, et de la recopier avec le nouveau code, exécuter ce nouveau code, et rétablir la page de code.

Voyons cela point par point dans un premier exemple (Courtesy of Iceman) :
1. Employer CreateProcess pour créer un processus pour la mise au point.
2. Créer "la Boucle Principale" WaitForDebugEvent - ContinueDebugEvent"
3. Arrêter la thread de la cible en utilisant SuspendThread.
4. Employez VirtualProtectEx pour mettre la lecture-écriture dans la page de la cible
5. Employer ReadProcessMemory pour sauver la page de la cible.
6. Employer GetThreadContext pour sauver le context de la thread.
7. Employer WriteProcessMemory pour écrire le nouveau code.
8. S'assurer que la dernière instruction dans le nouveau code est une INT 3. Nous avons besoin pour cela de prendre le contrôle quand notre code est fini. L'INT 3 sera pris au piège par EXCEPTION_DEBUG_EVENT.
Assurez vous que vous avez bien un EXCEPTION_BREAKPOINT et que vous êtes arrivé à l'adresse où réside notre INT 3.
9. Faites une copie provisoire de la structure CONTEXT sauvée.
10. Mettez un nouvel EIP dans la structure CONTEXT provisoire
11. Reprenez l'exécution de la thread. Observez le nouveau code s'exécuter. Quand l'INT 3 s'exécute, notre loader va prendre le relais. La Thread de la cible va être stoppée.
12. Rétablissez la page du code originale en employant WriteProcessMemory.
13. Rétablissez les attributs de protection de la page cible.
14. Employez SetThreadContext pour mettre le context de la thread de la première structure CONTEXT.
15. Relancez la thread.

A l'aide de ce second exemple, vous allez peut être comprendre plus facilement comment tout cela peut s'articuler, et tout particulièrement le rôle que peut jouer l'Obsfucator :

L'objectif de la routine qui suit est de provoquer un saut (en jouant sur le registre EIP) vers une autre partie de l'application.
Imaginez la situation :

Vous entrez un serial
Celui ci est en fait une adresse vers laquelle le programme se rendra via les Debug Apis par remplacement de l'adresse contenue dans le registre EIP:

La fonction CreateProcess va créer un nouveau process et sa thread primaire. Le nouveau process exécute le fichier exécutable spécifié.

BOOL CreateProcess(

    LPCTSTR lpApplicationName, 
    LPTSTR lpCommandLine,
    LPSECURITY_ATTRIBUTES lpProcessAttributes, 
    LPSECURITY_ATTRIBUTES lpThreadAttributes,  
    BOOL bInheritHandles, 
    DWORD dwCreationFlags, 
    LPVOID lpEnvironment,  
    LPCTSTR lpCurrentDirectory, 
    LPSTARTUPINFO lpStartupInfo, 
    LPPROCESS_INFORMATION lpProcessInformation   );

lpApplicationName,

string dans laquelle se trouve placée le nom de l'exécutable

  lpCommandLine,	// pointeur vers CommandLine  

que l'on récupère ainsi:

invoke GetCommandLine
mov CommandLine, eax

  lpProcessAttributes,     // pointer to process security attributes 
  lpThreadAttributes,       // pointer to thread security attributes 

La structure SECURITY_ATTRIBUTES contient le descriptor des attributs de sécurité d'un objet

typedef struct _SECURITY_ATTRIBUTES { // sa 
    DWORD  nLength                     ; taille en octets de la structure
    LPVOID lpSecurityDescriptor        ; pointe vers le security descriptor de l'objet
    BOOL   bInheritHandle              
} SECURITY_ATTRIBUTES;   
  InheritHandles,	// handle inheritance flag 
  dwCreationFlags,	// creation flags 

Les flags peuvent être l'un des cas suivants:

CREATE_DEFAULT_ERROR_MODE	
CREATE_NEW_CONSOLE	
DETACHED_PROCESS flag.
CREATE_NEW_PROCESS_GROUP	
CREATE_SEPARATE_WOW_VDM	
CREATE_SHARED_WOW_VDM	
CREATE_SUSPENDED	
CREATE_UNICODE_ENVIRONMENT	
DEBUG_PROCESS	

Si ce drapeau est mis, le process appelant est traité comme étant un debuggeur, et le nouveau process comme étant le process débuggé. Le system notifie au débuggeur tous les Debug_Events qui surviennent dans le process débuggé.
En créant un process avec ce flag, seul le thread appelant pourra utiliser un Call WaitForDebugEvent.

DEBUG_ONLY_THIS_PROCESS 
DETACHED_PROCESS

lpEnvironment, // pointeur vers le new environment block

Pointe vers un "environment block" pour le nouveau process.
Si ce paramètre est NULL, le nouveau process utilise l'environnement du process appelant.
Un "environment block" consiste en un null-terminated block ou une null-terminated string.

lpCurrentDirectory, // pointeur vers le nom du directory en cours
Précise le path d'accès au programme cible. Si aucun paramètre n'est passé, le répertoire de la cible est concidéré comme étant le même que celui du process appelant.

lpStartupInfo, // pointeur vers STARTUPINFO

Cette structure précise les propriétés de l'objet créé:

typedef struct _STARTUPINFO { // si 
    DWORD   cb; 
    LPTSTR  lpReserved; 
    LPTSTR  lpDesktop; 
    LPTSTR  lpTitle; 
    DWORD   dwX; 
    DWORD   dwY; 
    DWORD   dwXSize; 
    DWORD   dwYSize; 
    DWORD   dwXCountChars; 
    DWORD   dwYCountChars; 
    DWORD   dwFillAttribute; 
    DWORD   dwFlags; 
    WORD    wShowWindow; 
    WORD    cbReserved2; 
    LPBYTE  lpReserved2; 
    HANDLE  hStdInput; 
    HANDLE  hStdOutput; 
    HANDLE  hStdError; 

} STARTUPINFO, *LPSTARTUPINFO; 

lpProcessInformation // pointer to PROCESS_INFORMATION

La structure PROCESS_INFORMATION est remplie par la fonction CreateProcess avec les informations concernant le nouveau process créé et sa Thread primaire.

typedef struct _PROCESS_INFORMATION { // pi 
    HANDLE hProcess           ; handle du nouveau process créé 
    HANDLE hThread            ; handle du Thread primaire de ce process
    DWORD dwProcessId         ; identificateur du process créé 
    DWORD dwThreadId          ; identificateur du Thread du process crée
} PROCESS_INFORMATION; 

Dans le cas de CIBLE.exe,
les informations rapportées par la structure Process_Information, relevées dans la fenêtre des datas, sont les suivantes:

00403ABE:  00 00 00 08   ; hProcess
00403AC2:  00 00 00 0C   ; hThread
00403AC6:  FF E3 7E 5D   ; dwProcessID
00403ACA:  FF E3 65 51   ; dwThreadID  

Et pour la fonction WaitForDebugEvent:

00403FBE:  00 00 00 01   ; dwDebugEvent
00403FC2:  FF E3 7E 5D   ; dwProcessID
00403FC6:  FF E3 65 51   ; dwThreadID

Il y a donc plusieurs adresses qui peuvent contenir dwThreadId...

Mais revenons au Dead Listing :

Un nouveau Process est créé, destiné à être l'application débuggée.

:004011AA  PUSH      lpProcessInformation 
:004011AF  PUSH      lpStartupInfo 
:004011B4  PUSH      00 (lpCurrentDirectory) 
:004011B6  PUSH      00 (lpEnvironment)
:004011B8  PUSH      01 (dwCreationFlags)	
:004011BA  PUSH      01 (bInheritHandles) 	
:004011BC  PUSH      00 (lpThreadAttributes)
:004011BE  PUSH      00 (lpProcessAttributes)
:004011C0  PUSH      00 (lpCommandLine) 
:004011C2  PUSH      lpApplicationName 
:004011C7  CALL      CreateProcessA

:004011CC  TEST      EAX,EAX
:004011CE  JZ        erreur               le process n'est pas crée -> Exitprocess
:004011D4  MOV       [hInstance],EAX      récupère le handle du process

lpApplicationName contient le nom du programme. L'application crée donc une " copie " du process en cours...

:004011D9  PUSH      0000FFFF             dwMilliseconds (6 secondes)
:004011DE  PUSH      lpDebugEvent
:004011E3  CALL      KERNEL32!WaitForDebugEvent

La fonction WaitForDebugEvent attends qu'un debugging event survienne dans le process en cours de débuggage. C'est l'entrée d'une boucle que l'on ne peut quitter que si le temps alloué est dépassé (à moins qu'il ne soit mis à FFFFFFFF = INFINITY), ou si un DEBUG_EVENT est survenu.

BOOL WaitForDebugEvent

   LPDEBUG_EVENT lpDebugEvent, // adresse de la structure "event information"  
   DWORD dwMilliseconds       // nombre de millisecondes d'attente  

lpDebugEvent
Pointe vers une structure DEBUG_EVENT qui est remplie avec des informations concernant le debugging event.

dwMilliseconds
Spécifie le temps alloué pour attendre un debugging event.

La structure DEBUG_EVENT est définie ainsi:

typedef struct _DEBUG_EVENT { // de 
    DWORD dwDebugEventCode 
    DWORD dwProcessId              ;debug_event + 04  
    DWORD dwThreadId               ;debug_event + 08 
    union { 
        EXCEPTION_DEBUG_INFO Exception                
        CREATE_THREAD_DEBUG_INFO CreateThread          
        CREATE_PROCESS_DEBUG_INFO CreateProcessInfo  
        EXIT_THREAD_DEBUG_INFO ExitThread            
        EXIT_PROCESS_DEBUG_INFO ExitProcess          
        LOAD_DLL_DEBUG_INFO LoadDll                  
        UNLOAD_DLL_DEBUG_INFO UnloadDll              
        OUTPUT_DEBUG_STRING_INFO DebugString         
        RIP_INFO RipInfo; 
    } u; 
} DEBUG_EVENT;

En fonction du type d'événement qui survient, un code sera attribué au debugging event :

01  EXCEPTION_DEBUG_EVENT	
02  CREATE_THREAD_DEBUG_EVENT	
03  CREATE_PROCESS_DEBUG_EVENT	
04  EXIT_THREAD_DEBUG_EVENT	
05  EXIT_PROCESS_DEBUG_EVENT	
06  LOAD_DLL_DEBUG_EVENT	
07  UNLOAD_DLL_DEBUG_EVENT	
08  OUTPUT_DEBUG_STRING_EVENT	
09  RIP_EVENT
:004011E8  CMP       DWORD PTR [lpDebugEvent],05  EXIT_PROCESS_DEBUG_EVENT
:004011EF  JZ        ExitProcess
:004011F5  CMP       DWORD PTR [lpDebugEvent],01  EXCEPTION_DEBUG_EVENT
:004011FC  JNZ       continue si pas d'exception
:004011FE  CMP       [Flag_Obsfucator],01         Flag qui fait sauter par dessus
:00401205  JZ        0040127A                     la récup de l'obfuscator

La fonction GetCurrentThreadId retourne l'identificateur du "Thread Information Block" (TIB):

:00401207 CALL      GetCurrentThreadId 
:0040120C MOV       [ThreadID],EAX                pseudo-handle du thread

La récupération de l'Obsfucator se fait à l'aide d'une variante du MOV EAX,FS[18], sur le principe suivante :

:00401211 MOV       AX,FS                         passe le segment FS dans AX
:00401214 MOV       BX,ES                         idem pour ES dans BX
:00401217 MOV       ES,AX                         passe FS dans ES
:0040121A MOV       EAX,00000018                  pousse 18
:0040121F MOV       EAX,ES:[EAX]                  MOV EAX, FS[18]
:00401222 SUB       EAX,10                        petite subtilité que l'on va étudier
:00401225 MOV       ES,BX                         restore ES
:00401228 XOR       EAX,[ThreadID]                @ donnée par GetCurrentThreadID

En agissant ainsi, le programme obtient le pointeur vers la Thread DataBase:

ThreadID XOR Pointeur Thread DataBase = Obsfucator

Petit Rappel :
La TIB est toujours pointée par le segment FS. A l'offset 18h du TIB, vous trouvez une adresse linéaire qui en permet un accès plus rapide.
Les champs du TIB qui vont nous intéresser le plus sont les suivants :

00h DWORD pvEXCEPT          entrée de la chaîne SEH (voir le dossier SEH)
04h DWORD TopOfStack        la soustraction de l'offset 04h et 08h,
08h DWORD StackLow          permet de connaître la taille de la pile
18h  PTIB pTIB              pointeur vers le TIB
14h DWORD UserPointer
1Ch  WORD TIBFlags
20h DWORD DebugContext
30h DWORD pProcess          pointeur vers la process DataBase 
40h DWORD pProcessDataBase  pointeur vers le process auquel appartient la thread

La relation entre le TIB et la Thread DataBase est étroite.
Le début du TDB est le suivant :

00h DWORD                   contient 7 pour Win 98, 6 pour Win 95
04h DWORD                   nb d'objets gérés par la Thread
08h pProcessDataBase        on retrouve un double de ce pointeur à l'offset 40h
0Ch DWORD  pSomeEvent
10h DWORD  txExcept         début du TIB
74h DWORD  DebbugerCB       pointeur vers un bloc d'informations rempli par WaitFordebuEvent
7Ch pCONTEXT                pointe vers la structure CONTEXT

A partir de l'offset 10h, on retrouve les mêmes informations que celles du TIB…
Et pour cause ! le TIB commence à cet offset...
Au total, la TDB contient 200h (512d) offsets dont 30h sont occupés par le TIB.

le SUB EAX, 10h (EAX contenant le pointeur TIB) revient donc à dire que :

pThreadDataBase = pTIB -10h -> pTDB va s'appeler l'UnObsfucator.

:0040122E XOR EAX,[dwThreadID]        Identifie la thread

On a déjà vu comment dwThreadID est obtenu, dans la structure DEBUG_EVENT, et lors de l'appel à CreateProcess.
Eax va maintenant pointer sur le début de la Thread DataBase

:00401234 ADD EAX,7C

Va faire pointer EAX sur le pointeur de la structure CONTEXT

Tout ce cirque n'avait pour objectif que de récupérer le pointeur vers la structure CONTEXT. Un façon beaucoup plus discrète que de faire un :

push dword ptr [hThread]              identifie le handle de la thread
push dword ptr [lpContext]            pointe vers l'@ de la structure CONTEXT
Call GetThreadContext 

Avant de lancer GetThreadContext, lpContext doit recevoir une ou plusieurs des valeurs suivantes

CONTEXT_i486             equ 00010000h
CONTEXT_CONTROL          equ CONTEXT_i386 OR 00000001h
CONTEXT_INTEGER          equ CONTEXT_i386 OR 00000002h
CONTEXT_SEGMENTS         equ CONTEXT_i386 OR 00000004h
CONTEXT_FLOATING_POINT   equ CONTEXT_i386 OR 00000008h
CONTEXT_DEBUG_REGISTERS  equ CONTEXT_i386 OR 00000010h

L'intérêt de la structure CONTEXT est qu'elle garde en mémoire pour l'OS, le temps qu'il gère un autre process, l'ensemble des informations qui seront nécessaires à l'Operating System pour relancer le process qui nous intéresse Y COMPRIS l'état des registres !
Il suffit alors de remplacer, par exemple, l'adresse contenue dans le registre EIP par l'adresse ou vous souhaitez que le programme se rende à la sortie de la boucle WaitForDebugEvent/ContinueDebugEvent.

WaitForDebugEvent étant l'entrée d'un boucle, vous trouverez à la sortie de celle ci :

BOOL ContinueDebugEvent (idProcess, idThread, fdwContinueStatus)

DWORD dwProcessID        // indique quel process doit être repris
DWORD dwThreadID        // indique quelle thread doit être reprise 
DWORD dwContinueStatus // indique de quelle façon reprendre le process

La fonction ContinueDebugEvent permet à un debuggeur de reprendre la Thread qu'il avait précédemment suspendu dans le programme cible quand celui-ci avait rencontré un 'Debugging événement', et de reprendre le cours normal de l'application.

:0040127A PUSH DBG_CONTINUE         Spécifie comment continue la thread
:0040127F PUSH [dwThreadID]         Identifie la thread 
:00401285 PUSH [dwProcessId]        Identifie le process 
:0040128B CALL ContinueDebugEvent

DBG_CONTINUE
Si le lien indiqué par dwThreadID a précédemment annoncé un événement du type EXCEPTION_DEBUG_EVENT (equ 01), la fonction arrête tout le traitement d'exception et reprend le cours de la Thread. Pour un autre événement, ce Flag permet au programme de continuer normalement.
Les Informations récupérées par WaitForEventDebug (un gros bloc stocké par la fonction à l'adresse lpDebugEvent) vont contenir tout un ensemble de pointeurs dont dwThreadID et dwProcessId (voir la structure DEBUG_EVENT):

Petit rappel :
lpDebug_event + 08 = dwThreadId
lpDebug_event + 04 = dwProcessId

CONTEXT

La structure CONTEXT se décline ainsi

+00 context flags (utilisé lors d'un call GetThreadContext)

DEBUG REGISTERS
+04 debug register #0
+08 debug register #1
+0C debug register #2
+10 debug register #3
+14 debug register #6
+18 debug register #7
SEGMENT REGISTERS
+8C gs register
+90 fs register
+94 es register
+98 ds register
CONTROL REGISTERS
+B4 ebp register
+B8 eip register
+BC cs register
+C0 eflags register
+C4 esp register
+C8 ss register
FLOATING POINT / MMX registers
+1C ControlWord
+20 StatusWord
+24 TagWord
+28 ErrorOffset
+2C ErrorSelector
+30 DataOffset
+34 DataSelector
+38 FP registers x 8 (10 bytes each)
+88 Cr0NpxState
ORDINARY REGISTERS
+9C edi register
+A0 esi register
+A4 ebx register
+A8 edx register
+AC ecx register
+B0 eax register
 

Troisième exemple :

=========================================================================
 Ce source ASM est reconstitué à partir d'un programme de Pulsar
=========================================================================

debug_patch proc 
    push      offset StartupInfo    ; adresse définie par le programmeur
    Call      GetStartupInfoA       ; récupère propriétés du process créé

    invoke CreateProcess, addr Name_of_exe,  addr Name_of_exe, NULL, NULL,\
    FALSE, DEBUG_PROCESS + DEBUG_ONLY_THIS_PROCESS , NULL,\ 
    NULL, ADDR StartupInfo, ADDR lp_process_info ; lance le nouveau process
    mov     esi, dword ptr [lp_process_info+04]  ; mémorise hThread
    mov     dword ptr [hThread], esi  
    xor     esi,esi                              ; ESI permettra de sortir de la boucle
    jmp     continue2                            ; saute l'entrée dans la boucle
loop_debug:
    push    esi                                  ; récupère ESI
    invoke  WaitForDebugEvent, addr debug_event, INFINITE ; début de la boucle
          
    .if  debug_event.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT 
                                                 ; process à débugger créé?
       call   contexte                           ; pose le BreakPoint       
       invoke ContinueDebugEvent, debug_event.dwProcessId,\
       debug_event.dwThreadId, DBG_CONTINUE      ; continue le debuggage
       jmp    do_continue

    .elseif  debug_event.dwDebugEventCode==EXCEPTION_DEBUG_EVENT ; BreakPoint ? 
       mov    dword  ptr [context],CONTEXT_i486\
       or CONTEXT_CONTROL or CONTEXT_INTEGER or CONTEXT_SEGMENTS
       ; rempli la structure déclarée context par les infos de CONTEXT_structure
       invoke  GetThreadContext, dword ptr [hThread], addr context
       ; le process debuggé est il rendu à l'adresse 004014E1 ? 
       cmp    dword  ptr [context+B8hd], 004014E1h ; RegEIP = 004014E1 
       jne    continue4                            ; adresse non atteinte
              
       call   contexte2                            ; modifie les registres
       call   contexte3                            ; supprime le breakpoint
continue4:
       invoke ContinueDebugEvent, debug_event.dwProcessId,\
       debug_event.dwThreadId, DBG_CONTINUE 

    .elseif  debug_event.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
       pop   esi
       jmp   Go_out_process                        ; abandonne le process
    .endif

       invoke ContinueDebugEvent, debug_event.dwProcessId, debug_event.dwThreadId,\
       DBG_EXCEPTION_NOT_HANDLED
          
do_continue:
          pop esi
continue2:
          or  esi, esi
          je  loop_debug
 
Go_out_process:
          ret

debug_patch endp

;=========================================================================
;            pose un breakpoint si le process débug l'@ 004014E1
;========================================================================= 

contexte proc
        pushad
        mov    dword ptr [context], CONTEXT_i486 \
        or CONTEXT_DEBUG_REGISTERS
        invoke GetThreadContext, dword ptr [hThread], addr context
        mov    dword ptr [context+B8h], 004014E1h      ; regEIP
        or     dword ptr [context+18h], 00000003h      ; DR7
        and    dword ptr [context+18h], FFF0FFFFh      ; DR7
        invoke SetThreadContext, dword ptr [hThread], addr context
        popad
ret
contexte endp

;=========================================================================
;                   remplace le registre EDX par EAX
;========================================================================= 
contexte2 proc 
        pushad
        mov    dword ptr [context], CONTEXT_i486 \
        or CONTEXT_CONTROL or CONTEXT_INTEGER or CONTEXT_SEGMENTS
        invoke GetThreadContext, dword ptr [hThread], addr context
        mov    eax, dword ptr [context+B0h]         ; regEDX
        mov    dword ptr [context+A8h], eax         ; regEAX
        incoke SetThreadContext, dword ptr [hThread], addr context
        popad
ret
contexte2 endp

;=========================================================================
;                         supprime le BreakPoint posé
;========================================================================= 

contexte3 proc
        pushad
        mov    dword ptr [context], CONTEXT_i486 \
        or CONTEXT_DEBUG_REGISTERS
        invoke GetThreadContext, dword ptr [hThread], addr context
        mov    dword ptr [context+04h], 00000000h      ; DR0
        and    dword ptr [context+18h], FFFFFFF0h      ; DR7
        invoke SetThreadContext, dword ptr [hThread], addr context
        popad
ret
contexte3 endp

Debug Registers

Pour compléter un peu ce texte, voici un morceau de voile à lever sur les DebugRegister.

Vous vous êtes déjà demandé pourquoi les BPM (BreakPoint on Memory) n'étaient limités qu'à quatre?

Tout simplement parce qu'il n'existe, dans l'architecture Win32, que quatre endroit où stocker les adresses lineaires de ces breakpoint: DR0, DR1, DR2, DR3. Les DR4 et DR5 sont réservés, le DR7 "configure" le type de Break à exécuter à une adresse donnée, le DR6 sert aux debuggeurs.

Reprennont l'exemple du source ci dessus: activation du BreakPoint

        mov    dword ptr [context+B8h], 004014E1h      ; regEIP
        or     dword ptr [context+18h], 00000003h      ; DR7
        and    dword ptr [context+18h], FFF0FFFFh      ; DR7

Pour être exact, il faudrait l'écrire ainsi:

        mov    dword ptr [context+B8h], 004014E1h                         ; regEIP
        or     dword ptr [context+18h], 00000000000000000000000000000011b ; DR7
        and    dword ptr [context+18h], 11111111111100001111111111111111b ; DR7

La représentation en binaire va permettre de mieux comprendre le fonctionnement global.

Dans une architecte 32 bytes, le tableau général des Debug Registers est le suivant:

      31  29  27  25  23  21  19  17  15    12    9 8 7 6 5 4 3 2 1 0
     |---+---+---+---+---+---+---+---+---+-+-----+-+-+-+-+-+-+-+-+-+-|
     |LEN|R/W|LEN|R/W|LEN|R/W|LEN|R/W|   | |     |G|L|G|L|G|L|G|L|G|L|
     |   |   |   |   |   |   |   |   |0 0|0|0 0 0| | | | | | | | | | | DR7
     | 3 | 3 | 2 | 2 | 1 | 1 | 0 | 0 |   | |     |E|E|3|3|2|2|1|1|0|0|
     |              (10 à 12 et 14 à 15 sont reserved)               |
     |---+---+---+---+---+---+---+---+-+-+-+-----+-+-+-+-+-+-+-+-+-+-|
     |                               |B|B|B|                 |B|B|B|B|
     |0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0| | | |0 0 0 0 0 0 0 0 0| | | | | DR6
     |                               |T|S|D|                 |3|2|1|0|
     |---------------+---------------+-+-+-+---------+-------+-+-+-+-|
     |                            RESERVED                           | DR5
     |---------------+---------------+---------------+---------------|
     |                            RESERVED                           | DR4
     |---------------+---------------+---------------+---------------|
     |                 BREAKPOINT 3 LINEAR ADDRESS                   | DR3
     |---------------+---------------+---------------+---------------|
     |                 BREAKPOINT 2 LINEAR ADDRESS                   | DR2
     |---------------+---------------+---------------+---------------|
     |                 BREAKPOINT 1 LINEAR ADDRESS                   | DR1
     |---------------+---------------+---------------+---------------|
     |                 BREAKPOINT 0 LINEAR ADDRESS                   | DR0
     |---------------+---------------+----------------+--------------|

Les bytes 10 à 15 (reserved):
31                  15 14 13 12 11 10         0
+------------------+--+--+--+--+--+--+--------+
|                  | T| T| G| I|  |  |        |
|                  | 2| R| D| R|  |  |        |
+------------------+--+--+--+--+--+--+--------+
                     |  |  |  |
                     |  |  |  +- IceBp    1=INT01 causes emulator
                     |  |  |              to break emulation
                     |  |  |              0=CPU handles INT01
                     |  |  +---- Global   Debug 
                     |  +------- Trace1   1=Generate special address
                     |                    cycles after code dis-
                     |                    continuities.  On Pentium,
                     |                    these cycles are called
                     |                    Branch Trace Messages.
                     +---------- Trace2   1=Unknown.

Dans l'exemple utilisé, les OR et AND vont servir à conserver les informations du DR7 (d'éventuels autres BPM), et à ne modifier que celles qui vont directement concerner le DR0 dans lequel le BreakPoint attendu (en 004014E1) va être posé. Si aucun BreakPoint nest posé, les bits sont tous à 0.

      31  29  27  25  23  21  19  17  15    12    9 8 7 6 5 4 3 2 1 0
     |---+---+---+---+---+---+---+---+---+-+-----+-+-+-+-+-+-+-+-+-+-|
     |LEN|R/W|LEN|R/W|LEN|R/W|LEN|R/W|   | |     |G|L|G|L|G|L|G|L|G|L|
       00  00  00  00  00  00  00  00  00 0  0 00 0 0 0 0 0 0 0 0 1 1
 +     11  11  11  11  11  11  00  00  11 1  1 11 1 1 1 1 1 1 1 1 1 1
 =    -----------------------------------------------------------------
       00  00  00  00   00  00 11  11  00 0  0 00 0 0 0 0 0 0 0 0 1 1
                               ------                             ---
                                  |                                | 
                                  +---------------DR0--------------+

Quelques explications:

Debug Address Registers (DR0-DR3)

Chacun de ces registres contient l'adresse lineaire (par exemple 00401000) associée avec l'une des quatre conditions possibles. Ces conditions sont définies dans le DR7.

Debug Control Register (DR7)

A chaque adresse lineaire des registres DR0 à DR3, correspond un champ R/W. Le processeur interprète ces bits ainsi:

00 -- Break on instruction execution only
01 -- Break on data writes only
10 -- undefined
11 -- Break on data reads or writes but not instruction fetches

Les champs LEN0 à LEN3 précisent, quant à eux, la longeur des items à monitorer.
Les valeurs des champs sont interprétés de cette façon:

00 -- one-byte length
01 -- two-byte length
10 -- undefined
11 -- four-byte length

Si RWn est egal à 00 (instruction execution), alors LENn doit aussi être égal à 00. Toute autre valeur est ignorée.

Les champs L0 à L3 (Local) et G0 à G3 (Global), également lié à l'un des quatre DR0-DR3, activent (ou désactivent suivant l'état) l'adresse du breakpoint selectionné.

Debug Status Register (DR6)

Le DR6 sert principalement aux débuggeurs pour déterminer quelle "debug condition" est arrivée. Dans le source ci dessus, il n'est pas nécessaire de s'en occuper.

Liste des Debug Api's

ContinueDebugEvent
DebugActiveProcess
DebugBreak
FatalExit
FlushInstructionCache
GetThreadContext
GetThreadSelectorEntry

IsDebuggerPresent
OutputDebugString
ReadProcessMemory
IsDebuggerPresent
SetThreadContext
WaitForDebugEvent
WriteProcessMemory

Bonne Journée

Christal