Et kéketufais toi ?Toujours dans mes histoires de debuggeur, j'ai tenté d'améliorer la "non-intrusivité" du code que j'avais présenté dans
ce post. Je me suis donc intéressé aux diverses techniques qui permettent à un processus de détecter qu'il est sous la surveillance d'un débuggeur, c'est classique mais ça mange pas de pain au nutella de le faire.
Pour cela, j'ai fait un développement en parallèle : à ma gauche... venu tout droit de sa contrée lointaine de l'user-land... le programme
dontTryToDebugMe qui va faire appel à différentes techniques ninja pour checker la présence d'un débuggeur, et à ma droite... le JoE d3bugg3ur qui va tenter l'entourloupe de débugger son adversaire sans se faire repérer. La tension est à son comble, la foule est en délire... Avant de débuter ce qui sera sûrement un très grand match, rappelons que la principale contrainte est que je reste en user-land et que j'implémente les checks de la présence d'un débuggeur comme de simples appels de fonctions, c'est pas forcément très réaliste (pour certains d'entre eux) mais ça permet de se faire une bonne idée de leur fonctionnement.
1-PEB!IsDebugged et kernel32!IsDebuggerPresentLa première vérification de la présence d'un débuggeur peut se faire dans la structure Process Executive Block ( accessible en user-land et décrivant le process ). Si on la dump on trouve au troisième octet un champ nommé "IsDebugged" et qui (sans surprise) sera mis à 1 par le système si le process est débuggé ( y compris quand c'est par DebugActiveProcess() ). De plus, la fonction IsDebuggerPresent() va lire directement ce champ. Donc notre programme
dontTryToDebugMe va faire un appel à la fonction sus-nommée et aussi checker directement la valeur de ce champ en récupérant l'adresse du PEB avec l'API RtlGetCurrentPeb() (histoire d'éviter les hooks sur IsDebuggerPresent()).
Ce double-check on the byte ne pose aucune problème a être éviter, il suffit bien entendu d'aller modifier en dur le champ avant que la cible n'ait pu le checker ( ce qui se fait sans souci, j'y reviens de suite ).
2- PEB!NtGlobalFlags and Heap FlagsLa deuxième protection se trouve également en partie dans le Process Executiv Block. Elle se site au niveau du champ NtGlobalFlags qui va contenir des valeurs indiquant au programme comment gérer son tas ( cette gestion étant différente suivant que le process est débuggé ou non ). Après quelques tests, on se convainc assez facilement que ce champ prend la valeur 0x70 si le process est sous le contrôle d'un débuggeur. Il suffit alors de checker la valeur de ce champ, mais encore une fois le débuggeur n'a qu'a modifier la valeur pour y mettre 0x0 ( valeur de base ) avant que sa cible n'ait pu vérifier le champ.
Mais modifier en dur le PEB ne suffit pas en ce qui concerne le tas qui possède un comportement vraiment différent suivant que le process a été créé en mode debug ou non ( flags dans la structure de management du tas qui permettent aussi de checker le débuggage, taille des chunks différente..). Bref comme dirait l'ami zantrop "c'est le bowdel", et y a une solution qui permet de tout résoudre d'un coup : ne pas créer le process en mode debug et utiliser le mode non intrusif ( la boucle infinie à la place de l'EP et tout le bouzin, je te renvoie à
ce post ), ce qui nous permet de bypasser tous les checks sur le tas !
Ca a aussi comme avantage qu'on maitrise le moment où notre cible commence à exécuter son code ( puisqu'on on le fait boucler sur son EP ) et donc on peut tranquillement modifier les champs du PEB avant de le laisser le process cible continuer et faire ses vérifications.
3- ntdll!NtQueryInformationProcess et kernel32!CheckRemoteDebuggerPresentPlus profondément cachée, la fonction NtQueryInformationProcess() permet de récupérer tout un tas d'infos sur un processus. C'est un simple wrapper vers ZwQueryInformationProcess() qui débouche sur un appel système. Son prototype est :
NTSTATUS WINAPI NtQueryInformationProcess(
__in HANDLE ProcessHandle,
__in PROCESSINFOCLASS ProcessInformationClass,
__out PVOID ProcessInformation,
__in ULONG ProcessInformationLength,
__out_opt PULONG ReturnLength
);
En fixant la valeur de l'argument
ProcessInformationClass, on indique quels types d'info on veut regarder. Ces informations "résultantes" seront placés dans le buffer pointé par
ProcessInformation (3eme argument). Ce qui nous intéresse c'est que en mettant 7 en
ProcessInformationClass, la fonction nous retourne "a DWORD_PTR value that is the port number of the debugger for the process. A nonzero value indicates that the process is being run under the control of a ring 3 debugger." C'est easy donc, il suffit pour
dontTryToDebugMe d'appeler cette fonction et de checker la nullité de ProcessInformation :-D
Pour l'appel, la doc nous indique comment faire : "This function has no associated import library. You must use the LoadLibrary and GetProcAddress functions to dynamically link to Ntdll.dll."
Maintenant pour notre debuggeur user-land il faut réagir, la fonction étant appelé directement par son addresse, je ne vois qu'une seule manière de ne pas se faire repérer : il faut la hooker "in-line", c'est à dire modifier directement son code.
Notre objectif est simple : dans le cas où cette fonction est appelé avec 7 en
ProcessInformationClass, on doit mettre 0 dans le
ProcessInformation pour ne pas être repéré. De plus, comme la fonction NtQueryInformationProcess() est souvent appelée pour tout un tas de trucs (c'est précis comme description, hein ?), on doit modifier son résultat seulement dans le cas qui nous intéresse et la laisser s'exécuter normalement dans les autres, sinon on va foutre un bon gros bordel.
Donc pour le hook, je vais mettre en place à l'adresse de NtQueryInformationProcess() un jump vers un shellcode que j'aurai placé en mémoire qui me permettra de gérer tranquillement la fonction. Pour celà je regarde d'abord la geule du code au début de cette fameuse fonction (en fait ZwQueryInformationProcess(), mais on va les confondre :-) :
MOV EAX,9A
MOV EDX,7FFE0300
CALL DWORD PTR DS:[EDX]
RETN 14
Il s'agit d'un classique appel système, donc c'est là que je dois intervenir et mettre mon JMP vers un shellcode qui va tout pwned. Mon loader que je vais mettre à cet endroit aura la tête suivante :
MOV EAX,0xFAFAFEFE
JMP EAX
Où 0xFAFAFEFE est l'adresse où se trouve le shellcode et qui sera patché au moment de l'exécution.
Ce loader va ainsi écraser les deux premières instructions de NtQueryInformationProcess() en laissant trois opcodes inutilisés (rappelle toi en pour la suite cher lecteur), il me faudra donc les rétablir dans les cas où je veux laisser la fonction s'exécuter normalement.
Maintenant intéressons nous au shellcode, gardons à l'esprit que viennent d'être mis en place dans la pile les arguments de NtQueryInformationProcess():
//Est ce que ProcessInformationClass == 7 ?
CMP DWORD PTR SS:[ESP+8],7
// Si c'est pas le cas, on ne dois pas intervenir
JNZ SHORT Normal
// Time to hook !
MOV EAX,0 // on met en place la valeur de retour
MOV EDX,DWORD PTR SS:[ESP+C]
MOV DWORD PTR SS:[EDX],0 // on met 0 dans la valeur résultat =>
check pwned !CMP DWORD PTR DS:[ESP+14],0 // on teste si ReturnLength est NULL
JE SHORT FinDuHook // si c'est le cas, on n'a pas à la modifier
MOV EDX,DWORD PTR SS:[ESP+14]
MOV DWORD PTR SS:[EDX],4 // on met 4 dans ReturnLength
FinDuHook:
RETN 14
Normal: // ici on doit rediriger vers le flux normal d'exécution
MOV EAX,9A // rétablissement des deux premières instructions que le
MOV EDX,7FFE0300 // loader a écrasé
PUSH EBX // sauvegarde de EBX qui sera utilisé pour le JMP suivant
MOV EBX,NTDLL.7C91D7E9
JMP EBX // reprendre le cours normal de la fonction ( appel système )
Quelques remarques :
1- D'après la doc "The function returns an NTSTATUS success or error code." et on se convainc assez facilement que 0 est le code de succès, donc à mettre dans EAX :-)
2- Dans le cas du hook, il ne faut pas oublier de positionner ReturnLength (5ème argument c'est à dire ESP+14) à 4 car d'après la doc, c'est "A pointer to a variable in which the function returns the size of the requested information." et donc, même si il est optionnel, on pourrait imaginer que
dontTryToDebugMe l'utilise et teste la valeur retournée pour voir qu'elle n'est pas nulle (dans le cas d'un test de debug on retourne un DWORD_PTR donc c'est 4 bytes ). D'ailleurs c'est ce qu'il fait le petit salopio.
3- Dans le cas où on ne veut rediriger vers le flux normal d'exécution, y a pas de mystères, on exécute les deux instructions que le loader a écrasé et on saute sur l'adresse hardcodé qui était l'instruction suivante ( que j'apellerai instruction Y pour la suite de ce merdier ). En fait, comme pour ce JMP j'utilise EBX, je vais d'abord le sauvegarder sur la pile et je vais mettre juste avant l'instruction Y un POP EBX pour rétablir la valeur de ce registre et m'éviter de faire foirer le programme qui s'attend à trouver une "certaine" valeur dedans :-) Et tout ça tombe très bien puisque j'avais de la place à la fin de mon loader ( 3 opcodes précisément, rapelle toi ! ). Bon bien sûr c'est pas beau caca d'avoir hardcodé cette adresse, on pourrait tout à fait la patcher à l'éxecution puisqu'elle est située à distance fixe du début de NtQueryInformationProcess(). Mais j'avais la flemme, donc c'est l'adresse "kivabien" pour XP SP3.
Là où c'est tout bon c'est que en hookant cette fonction, on pwne aussi CheckRemoteDebuggerPresent() qui l'utilise :-)
4- La MSDN c'est plus fort que toi...Si on s'intéresse un peu à la doc sur les exceptions dans la MSDN, on trouve la fonction suivante :
BOOL CheckForDebugger()
{
__try
{
DebugBreak();
}
__except(GetExceptionCode() == EXCEPTION_BREAKPOINT ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
// No debugger is attached, so return FALSE
// and continue.
return FALSE;
}
return TRUE;
}
Rien de bien folichon, on déclenche une breakpoint exception dans le process appelant ( donc
dontTryToDebugMe dans notre cas ), et on regarde si effectivement cette exception s'est produite ( par l'appel à GetExceptionCode() ), si ce n'est pas le cas ça veut dire qu'elle a été "catché" par un débuggeur. Pour bypasser ça, il suffit de faire en sorte que le débuggeur ne gère pas la breakpoint exception et la laisse arriver jusqu'au process debuggé, cela se règle dans la fonction ContinueDebugEvent() en mettant le troisième argument à DBG_EXCEPTION_NOT_HANDLED, ce qui laisse le programme responsable de l'exception la traiter ( et la détecter :-).
En pratique ça veut dire que toutes les breakpoints exceptions doivent pouvoir être gérés par le programme cible, ce qui posera sans doute des problèmes quand il s'agit d'exceptions "non-naturelles" (mises en place par le débuggeur), mais dans mon cas, mon but étant plutôt de tracer le comportement du programme de façon furtive ça n'a pas de conséquences.
5- debug me !Pour l'instant le JoE d3bugg3r s'en sort bien, tous les checks d'avant sont assez facilement bypassables... C'est maintenant que les choses se compliquent :-)
Un autre trick consiste à se débugger soit même : on créé un nouveau processus et on lui fait appeler DebugActiveProcess() sur son processus parent, dans le cas où il on a déjà un débuggeur sur le dos, l'appel de cette fonction va échouer et hop, débuggeur spooted !
En pratique j'ai implémenté ça dans
dontTryToDebugMe par une création d'un process "classique" ( = notepad.exe, pour être le plus "portable" possible ) dans lequel je vais créer un thread distant. Mais on ne peut pas juste lui faire exécuter DebugActiveProcess() car on va prendre dans la geule le comportement par défaut de cette fonction qui est "Exiting the debugger also exits the process unless you use the
DebugSetProcessKillOnExit() function." c'est à dire que lorsque notre thread distant va terminer, il va tuer
dontTryToDebugMe...
Donc il faut travailler un peu plus et se faire un petit shellcode qui va nous faire l'appel à DebugSetProcessKillOnExit() pour changer le comportement par défaut lors de la fin du thread distant.
En pratique, ca ressemble à ça :
push 0xFAFAFEFE // the PID of the process to debug
mov edx,kernel32.DebugActiveProcess
call edx
test eax,eax
jz fin // if it works we dont jump
mov edx,kernel32.DebugSetProcessKillOnEx
push 0
call edx
fin:
retn
0xFAFAFEFE est le PID de
dontTryToDebugMe que je patche à l'exécution avant de copier le shellcode dans le process distant ( pas oublier de le faire en little-endian :). Il faut aussi éviter de faire l'appel à DebugSetProcessKillOnEx() dans le cas où DebugActiveProcess() à échoué car la valeur de retour de notre shellcode ( celle qui nous permet de dire si ça a marché ou pas ) sera celle du dernier appel, donc de DebugSetProcessKillOnEx() et cette fonction semble "marcher" même si DebugActiveProcess() a échoué.
Une fois le shellcode patché, on l'écrit dans notre process notepad.exe, on le fait s'exécuter en créant un thread à son adresse avec CreateRemoteThread() et on récupère la valeur de retour avec GetExitCodeThread()... Si c'est 0 ça veut dire que ça a échoué donc qu'un débuggeur est là :-)
Pour bypasser ça, c'est la galère... le débuggeur ne controlant pas la création du processus "fils" notepad.exe, il ne peut pas venir hooker à temps DebugActiveProcess() dans celui-ci. Et même si on y arrivait (= l'appel à DebugActiveProcess() "marcherait" tout le temps)
dontTryToDebugMe pourrait venir vérifier qu'on reçoit effectivement les exceptions ( par exemple celle de création du process qui est toujours reçue en premier ).
6- Tic tac, tic tac...
Une technique bien connue consiste à utiliser les différents compteurs que le système maintient à jour : l'idée consiste simplement à remarquer que certaines opérations prennent beaucoup plus de temps selon qu'un débuggeur est présent ou non. Quel genre d'opérations ? Sur le net on trouve pas mal d'exemples avec de simples boucles sur des printf(). Perso j'ai aucune différences, les printfs prennent autant de temps que le débuggeur soit là ou pas (ce qui semble assez logique, l'affichage d'une chaine par le biais d'un printf() n'est pas un évènement de débug, non?)... Il faut donc mieux utiliser une opération véritablement couteuse dans le cas d'un débuggage, par exemple OutputDebugString() :-)
Le code qu'on met dans
dontTryToDebugMe est donc du genre :
firstTick=GetTickCount();
for(loop=0;loop<10000;loop++)
{
OutputDebugString("MDR");
}
secondTick=GetTickCount();
if(secondTick-firstTick > NORMAL_TIME_COUNT)
{
// too long, debugger spooted xD
}
else
{
// no debugger
}
NORMAL_TIME_COUNT est une constante définie à partir de mesures des exécutions sans débuggeur ! Dans mon cas j'ai utilisé GetTickCount() mais on peut aussi faire avec QueryPerformanceCounter()...
Là encore pour le bypasser, c'est pas du gateau mon salop : on pourrait hooker GetTickCount() et lui faire retourner une valeur constante ( dans ce cas là le test secondTick-firstTick>NORMAL_TIME_COUNT serait toujours faux..) mais il suffit de modifier le test de notre précédent code et de mettre : if((secondTick-firstTick>NORMAL_TIME_COUNT)
||
(secondTick==firstTick))
et on détecte le hook :)
7- C'est cadeau !Pour terminer ce tour non-exhaustif, une technique un peu plus aggressive...
A force de m'attacher à des process pour essayer de leur démonter la tête, j'ai finis par m'apercevoir ( bien ouerj oeil de lynx ) que tous les débuggeurs ring3 ( que ça soit OllyDbg ou le JoE d3bugg3r ) créent un nouveau thread dès qu'ils s'attachent et que ce nouveau thread démarre toujours sur la fonction DbgUiRemoteBreakIn() dont le code est de la forme suivante :
VOID
NTAPI
DbgUiRemoteBreakin(VOID)
{
/* Make sure a debugger is enabled; if so, breakpoint */
if (NtCurrentPeb()->BeingDebugged) DbgBreakPoint();
/* Exit the thread */
RtlExitUserThread(STATUS_SUCCESS);
}
Mais le code n'est pas le plus important, ce qui rox, c'est que cette fonction soit toujours appelée au moment de l'attachement, pour empêcher un débuggeur de s'attacher, il nous suffit de remplacer le code de cette API par un code qui va l'emmerder ! Pour faire simple,
dontTryToDebugMe va écrire un appel à TerminateProcess() à l'emplacement de cette fonction, et ainsi quand le débuggeur s'attache au process, BAM il se termine.
La parade est simple, il suffit au débuggeur de repatcher à chaud le code de cette API avant de s'y attacher. Bon, a noter que dans notre cas j'ai même pas eu besoin de le faire puisque le JoE d3bugg3r s'attache au moment où la cible est à son EP, donc elle n'a pas pu encore patché son DbgUiRemoteBreakin(). Disons que cette protection est utile pour empêcher l'attachement à un process en cours, ou alors il faudrait utiliser un version modifiée de la DLL qui contient cette fonction (ntdll) mais là j'imagine qu'on touche du doigt des domaines un peu plus compliqué ( packers ? ) et j'en suis pas encore là :-)
A noter un effet de bord intéressant : ce fameux DbgUiRemoteBreakin() explique l'exception qui est toujours levé lorsqu'on commence de débugger un process en mode non intrusif ( il y en a aussi une en mode intrusif mais cela ne nous regarde pas ;). J'avais pris en compte ce fait en laissant toujours de côté la première exception dans le code de mon débuggeur, mais là on s'aperçoit que si on modifie le byte IsDebugged du PEB, l'exception ne sera plus levée, donc la première exception qu'on cathera sera une "vraie", à prendre en compte donc :)
Pour conclure, on peut remarquer que dans les conditions dans lesquelles je me suis plaçé ( c'est à dire en avantageant le débuggeur en lui laissant la main en premier ), il est facile de bypasser les APIs fournie par Microsoft ( IsDebuggerPresent(), CheckRemoteDebugger() ), mais d'un autre côté il y a d'autres checks très faciles à mettre en place ( le self-debug ou les timers ) qui semblent difficiles (impossibles?) à pwned... Donc le JoE d3bugg3r a perdu, mais il reviendra, plus fort, plus beau :D
Je suis bien sûr loin d'avoir étudier tous les anti-debug ( j'en ai même laisser certains importants de côté ), je me suis cantonné à des techniques "non-agressive" (ormis la dernière, mais c'est du caca celle là), je continuerai ce travail d'ici peu de temps pour voir où ça mène ;-)
Le code du debuggeur se trouve
ici, le mode non-intrusif implémente tous les anti-anti-debug que j'ai décrit au dessus. Le code source de
dontTryToDebugMe est
là ainsi que le binaire compilé sous XP SP3
ici ( vous pouvez checker votre débuggeur maison, voir si il fait mieux que moi :p ).
Coté bibliographie je me suis en grande partie basé sur l'article référence de N. Fallière que vous pouvez trouver
ici.