mardi 22 juillet 2008

Inject your DLL

Pour continuer mon apprentissage du fabuleux (aheum) monde de Windows, je me suis lancé dans l'implémentation d'un grand classique : l'injection de DLL.

Mon but est donc simple : injecter une bibliothèque dynamique dans un/des process distant(s) ( sans m'occuper de l'utilisation qu'on pourrait en faire ). Pour celà j'ai implémenté différentes techniques "de base" dans un petit tool. J'ai également tripé en essayant de rendre le truc le plus furtif possible, c'est à dire en libérant au maximum la mémoire allouée pour l'injection quand il y en a. Let's start :


1- "Classical method" :

Archi-connue, cette technique consiste à utiliser l'API CreateRemoteThread() qui créé donc un thread dans un process distant (jsuis totally bilingual et oué) en lui donnant l'adresse de LoadLibrary() à laquelle on fournit le nom de la DLL à injecter, qui aura été précédemment écris dans la mémoire du process cible ( VirtualAllocEx() + WriteProcessMemory() ).

La seule difficulté c'était de savoir quand je pouvais libérer cette mémoire allouée pour l'appel ( et gagner un petit peu de furtivité plutot que de laisser le nom de la DLL se balader dans la mémoire ) : il me fallait détecter que l'injection était terminée donc que LoadLibrary() à finie son boulot. Comme j'étais chaud bouillant j'ai commencé à dumper le EIP en boucle et à m'exciter sur les valeurs qu'il pouvait prendre une fois l'injection terminée... Puis j'ai découvert WaitForSingleObject(), qui dixit la doc "Waits until the specified object is in the signaled state or the time-out interval elapses." et pour un thread le "signaled state" c'est "when the thread terminates." c'est dans la poche :-)

A noter qu'il ne faut pas oublier de s'allouer les droits de debug pour pouvoir ouvrir un HANDLE sur n'importe quels process et pouvoir ainsi s'injecter dans tout ce qui bouge.
De plus, cette méthode ne fonctionne que sous Windows NT alors par acquis de conscience j'ai rajouté une fonction isWindowsNT() qui utilise l'API GetVersionEx() pour vérifier que la version de l'OS est ok...

2- "Cave code method"

La technique précédente c'est du tout bon, ca marche bien, mais ça fait appel à l'API surpuissante CreateRemoteThread() qui finalement sert "juste" à faire un appel à LoadLibrary(), et qui peut être facilement repéré par des outils anti-injection (enfin, j'imagine :p ).
Donc une autre idée est de se coder un loader en assembleur qui remplacerait l'appel à CreateRemoteThread(), ce qui nous permettrait de nous injecter en chargeant ce loader dans la mémoire du process distant puis en redirigeant le flux d'exécution dessus.

La principale difficulté en ce qui concerne le loader c'est qu'il contient plusieurs adresses qu'on ne connait pas au moment de la compilation : l'adresse de retour où on retournera après que notre loader ait été exécuté, l'adresse de l'argument de LoadLibrary() (= le nom de la dll qu'on veut mapper) et l'adresse de LoadLibrary() ( même si en pratique on a envie d'hardcodé celle là, il faut pas forget que même entre service pack 2 et 3 sous XP il y a des différences au niveau des adresses de mapping des DLLs, donc si on veut faire un truc un minimum portable c'est pas top).

Cela nous donne :

push 0xFAFAFEFE // return address

pushfd // save the eflags register
pushad // save the registers

push 0xFAFAFEFE // argument of LoadLibrary()
mov eax,0xFAFAFEFE // @ of LoadLibrary()
call eax

popad
popfd

ret

Je fous des 0xFAFAFEFE à la place des trucs que je vais devoir patché et j'oublie pas de sauvegarder les registres avant et de les rétablir après histoire de pas avoir de souci :-)

Le code du loader se trouve donc dans mon process "injecteur" et premier petit problème, comment localiser ce code pour pouvoir le patcher ? Sous GCC y a pas de souci, tu utilises la balise __asm__ et tu fous ton code ASM direct à l'intérieur d'une fonction, l'adresse de la fonction te donnera l'adresse de ton loader et en cadeau bonus, tu peux même déclarer une autre fonction "vide" derrière ce qui te permettra de connaitre la taille de ton loader avec la différence d'adresse. Sauf que voilà, dans le but de m'introduire plus profondément dans la communauté des t4pz je mange des chocapicz et je compile désormais avec VC++ 2008. Et sous ce magnifique compilateur que trouve t'on à l'adresse de la fonction dans laquelle on a mis le code ASM du loader ? Je te le donne en mille émile :

00401005 JMP main.00401060

Oui dude, un JMP vers le "vrai" code de la fonction, ce qui est emmerdant. Donc la seule solution c'est de transformer mon loader en opcodes et de mettre tout ça dans une variable globale qui me donnera l'adresse dont j'ai besoin.

Donc une fois que l'adresse du loader est connue, on commence par patcher l'adresse de l'argument de LoadLibrary() qu'on obtient en allouant de la mémoire dans le process cible avec VirtualAllocEx() puis celle de LoadLibrary() qu'on obtient avec GetProcAddress().

Reste à mettre l'adresse de retour, pour celà on va d'abord suspendre le main thread du process distant, à cet effet je me suis codé une fonction qui récupère le TID de ce thread à partir du PID du process distant, rien de bien compliqué : on fait un snapshot de tous les threads du système avec CreateToolhelp32Snapshot(), on récupère ceux dont le PID owner est le même que le PID de notre process cible. Une fois le TID du main thread récupéré, on créer un HANDLE dessus avec OpenThread() et on suspend le thread avec SuspendThread().

On récupère alors l'EIP avec GetThreadContext(), on l'écrit dans notre loader.. et hop ! notre loader est patché, il ne reste plus qu'à le copier dans la mémoire de notre cible. Une fois que c'est fait on modifie l'EIP pour qu'il pointe vers lui avec SetThreadContext(), on relance le thread et on attend ! Si tout va bien, le process cible va exécuter notre loader, charger la DLL puis revenir là où il était...

Oui mais voilà, on attend un peu trop longtemps... Tout est en place, l'EIP pointe bien sur notre loader mais pourtant le code ne s'exécute pas ou du moins pas tout de suite.. à moins que je "passe la souris dessus" ( pour un process graphique j'entend, pour un process console c'est une autre histoire ) . Ce qui n'est pas très pratique, je voudrai pouvoir être sûr que mon code va être exécuté directement une fois que j'ai relancé le thread sans devoir intervenir sur la cible et ainsi pouvoir libérer la mémoire occupé par le loader histoire d'être caché inside the bosquet.

Ma première idée c'était que le problème se situe au niveau de l'ordonnancement : le système ne donne pas de temps processeur à mon process cible tant qu'il n'y pas "quelque chose" qui lui laisse croire qu'il va se passer un truc important dans ce process ( d'où le coup de souris ) ou qu'il n'a rien de mieux à faire. Ce qui est confirmé par le fait que "de temps en temps" l'injection va avoir lieu au bout de 10s et dans d'autres cas, après une minute toujours rien...

Donc j'ai commencé à faire le fou avec les fonctions SetPriorityClass() et SetThreadPriority() histoire de "forcer" le processeur à exécuter mon process cible. Mais j'ai eu beau foutre la priorité max, j'ai vu aucune différence notable... Et j'ai donc abandonné l'idée en me disant que de toute façon l'ordonnanceur Windows doit être un beau bordel et que faudrait être maso ( ou plus fort que moi :p ) pour jouer avec. J'ai quand même retenu cette phrase de la doc qui m'a bien fait rire "Threads are scheduled in a round-robin fashion at each priority level". On y croit.

Finalement, ce que je veux c'est un moyen de simuler mon "coup de souris" sur la cible puisque apparament il n'y a que ça qui force le process à être exécuté ca$h par le processeur à coup sûr. "Me dis pas que dans ces API de malades que Windows possèdent en ce qui concerne les manipulations de processus en user-land, je trouverai pas mon bonheur." Et effectivement, je l'ai trouvé, il suffit d'envoyer un "message" au process qui, comme tout bon GUI process possède une "message queue" et va réagir au quart de tour pour traiter ce message. Pour cela j'utilise PostThreadMessage() qui, dixit la doc, "posts a message to the message queue of the specified thread" (sympa la doc non?). L'effet est immédiat : mon code est exécuté dans la seconde, et je peux enchainer en libérant la mémoire du loader, celle de l'argument de LoadLibrary() et ni vu ni connu jt'injecte :-)

Reste que pour un process console "basique" y a pas de message queue et que donc mon message va tomber dans le vide inter-sidéral et ça ne va rien accélèrer, donc pour ces process là ( que je détecte suivant le code de retour de PostThreadMessage() ) je libère pas la mémoire allouée dans la cible et j'attend sagement que l'injection se fasse. Si quelqu'un connait une fucking way de forcer un process console à être exécuté ca$h par le processeur je lui serai gré de m'en faire part.

3- "SetWindowsHookEx()" :

Là encore, une méthode très connue : tout se joue avec l'API SetWindowsHookEx() qui permet de poser un hook (en gros, une fonction) pour un certain type d'event : si cet event se produit, notre fonction sera appelée. Pour que la fonction soit appelée dans un process distant (= la cible de l'injection) elle dois être définie dans une DLL, lorsque l'event se produit, le process va vouloir exécuter la fonction définie comme étant le hook et pour cela il va charger la DLL qui la contient :-) De plus, on peut définir si on veut poser le hook pour un thread particulier ou pour tous les threads qui sont dans le "same desktop" donc ça nous donne une possibilité d'injection massive ! Intéressons nous d'abord à une attaque "ciblé".

La théorie ça rox, mais en bidouillant un peu on s'aperçoit que cette API est un peu plus fourbe que ça :

1- Première remarque : cette technique ne marche que sur des process "graphiques" et pas pour des process consoles ( décidément... ), en trifouillant sur le net on trouve une explication "Hooks don't work on console processes. The process wich runs consoles (csrss) is considered to be too important to the system so it is designed this way." Pour rendre les choses plus propres on a un moyen de distinguer les process consoles/GUI et éviter ainsi des appels inutiles :

if(WaitForInputIdle(targetHandle,0)==WAIT_FAILED)
{
// console process
}
else
{
// GUI process
}

2- Sur quel évènement poser ce hook ? Il nous en faut un qui nous garantisse une injection le plus rapidement possible une fois le hook mis en place. Je comprend pas vraiment pourquoi la majorité des exemples sur le net se borne à mettre WH_CBT en argument de SetWindowsHookEx() c'est à dire un évènement qui correspond à "activating, creating, destroying, minimizing, maximizing, moving, or sizing a window", en gros un truc qui nous pousse à devoir intervenir sur le process cible avec la souris. En mettant un WH_GETMESSAGE qui lui "Installs a hook procedure that monitors messages posted to a message queue. ", il suffit ensuite de poster un message avec PostThreadMessage() et on est sûr que le hook va avoir lieu...

3- Le hook n'existe que tant que le thread qui le pose est vivant, dès qu'il meurt non seulement le hook n'existe plus ( et donc l'injection ne peut plus avoir lieu ) mais de plus, il va y avoir un FreeLibrary() sur notre DLL dans le thread qui a utilisé ce hook. Là j'avoue ne pas vraiment comprendre pourquoi il y a cet appel, après une petite enquète il est déclenché par un GetMessageW(), j'aurai donc tendance à penser que c'est bien déclenché à distance mais il n'est pas à exclure que ça soit plutôt une erreur dans mon implémentation ;-)
Ca pose en tout cas un léger problème : une fois que le thread qui a posé le hook est mort, le FreeLibrary() qui va avoir lieu dans la cible va décrémenter le compteur de référence de la DLL qui va arriver à 0 (=plus personne n'a besoin de cette bibliothèque) et elle va donc être déchargé de la cible... Un peu emmerdant c'était justement le but de la manoeuvre de la charger :-D
Donc pour bypass ce ptit souci j'ai rajouté dans la fonction de hook dans la DLL un appel à LoadLibrary() sur elle-même, ce qui incrémente le compteur de référence et évite qu'elle soit déchargé lors de la mort du thread injecteur. "C'est moche mais ça marche !"
Vous trouverez le code de la DLL en question ici. Remarquez le très subtil chemin de la DLL rentrer en dur pour l'appel à LoadLibrary()...

4- "The IvanOv m4l4ri4"
Ainsi nommée d'après la personne bien intentionné qui m'en a donné l'idée.

Comme dis précédemment, SetWindowsHookEx() permet de poser un hook sur tous les threads du bureau, ce qui nous donne envie de s'en servir pour injecter tout le monde !
Déjà, dans l'idée de l"IvanOv m4l4ri4", l'évènement qui va déclencher le hook ne sera pas "controlé" par l'injecteur : on va laisser le WH_CBT et dès qu'un process "bougera" il sera infecté (c'est plus rigolol, non ?).

Rappelons nous que le hook n'existe que tant que le thread qui l'a posé est vivant. Or, on a pas envie de laisser tourner notre process injecteur en tache de fond le temps que tous les autres process soient infectés. Pour celà il serait plus judicieux de faire une première injection dans un process dont on est sûr qu'il sera "toujours là" et on laisse ce process poser le hook.

En fait tout va se jouer dans la DLL, on va l'injecter dans notre process qui va servir de pivot à l'infection, puis on va poser le hook sur tout le système dans le DllMain(). Une fois le hook posé, tous les process qui vont recevoir un event graphique vont charger la DLL. Ca nous amène au principal problème de cette technique : les hooks qui concerne tous les threads du bureau sont extrèmement couteux en performance, donc si à chaque chargement de la DLL, le hook est de nouveau posé ça va rapidement faire ramer la machine, pas très discret. Il nous faut donc un moyen de savoir si le hook est déjà poser quand on charge la DLL, histoire de savoir si c'est à nous de le faire ou pas. Ce qu'on veut c'est donc une sorte de "variable globale" au système qui serait en gros mise à 1 si le hook est déjà en place et nous permettrait ainsi une communication inter-processus. Et ça porte un nom : le mutex. Pour faire simple un mutex est lié à un thread qui le "possède", et n'importe quel thread du système peut essayer de prendre possession du mutex en appelant WaitForSingleObject(), il réussira si personne ne l'a pris avant lui (ou si il l'a libéré). Pour le reconnaitre le mutex sera dans notre cas "nommé" c'est à dire qu'on lui donne un nom particulier. Donc tout ce qu'il y a faire lors du chargement de la DLL c'est de faire appel à CreateMutex() avec le nom de notre mutex, si c'est le premier appel le mutex va être créé, puis le thread va faire un WaitForSingleObject() pour en prendre possession, et ainsi poser le hook. Le prochain thread qui "bouge" va utiliser le hook et charger la DLL, ce coup-ci l'appel à CreateMutex() lui retournera juste un HANDLE sur le mutex ( qui existe déjà ) et l'appel à WaitForSingleObject() lui signalera que le mutex est déjà pris par un autre thread, donc que ce n'est pas à lui de poser le hook.
Remarquons que pour faire la première injection, il ne faut pas utiliser la technique 1 qui consiste à créer un thread dans le process cible, car une fois que ce thread va se terminer, le hook va être enlevé et le mutex se retrouvera esseulé (= dans l'état WAIT_ABANDONED, qui est décrit à tord comme un "success state" dans la doc).. La "cave method" est bien plus adapté car elle utilise le main thread ( je savais bien que j'avais pas fait ça pour rien !).
Donc pour cette technique il suffit de lancer l'injection de la DLL dont le code source est ici en "cave method" puis de laisser faire la nature, le premier qui bouge, BAM dans sa geule.

Il faut quand même remarquer que j'ai un souci au niveau de la libération du mutex, qui doit se faire lorsque le process "pivot" de l'injection meurt, donc quand il décharge la DLL. J'ai pas réussi à faire marcher un ReleaseMutex() dans la clause DLL_PROCESS_DETACH de ma DLL. C'est pas vraiment grave dans le sens où ça empeche juste la technique de fonctionner une deuxième fois de suite avec un mutex de même nom ( le mutex est en WAIT_ABANDONED : le thread qui le possèdait est mort et ne l'a pas libéré ). Mais si votre infection a bien fonctionné, y a normalement pas besoin de la relancer :-)

Le code final est ici.

Aucun commentaire: