|
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
|