Bonjour,
Il y a quelques temps, Kayzekk a demandé comment faire un keylogger en Python dans cette discussion. Je connais très mal Python mais, en revanche, faire cela en C++ est dans mes cordes. Je m'y suis donc intéressé de près. La technique la plus évidente est de mettre en place un hook quelque part dans le cheminement des frappes clavier car c'est une fonctionnalité de l'API Windows. J'ai donc réalisé un programme, deux exactement (nous verrons plus loin pourquoi), que l'on voit ci-dessous en action :
Cette idée n'a rien de nouveau ni de révolutionnaire. Il existe une multitude de programmes utilisant cette technique. Certains sont des keyloggers et d'autres, des choses plus sérieuses... Ce à quoi je me suis heurté dans ce développement, c'est qu'on ne trouve nulle part tous les concepts et les pièges inhérents à l'utilisation des hooks. Tout est éparpillé entre la documentation de Microsoft (pas très claire) et les expériences des programmeurs qui butent sur un point ou un autre dans ce domaine.
Concepts généraux
Concepts du keylogger présenté ici
Comment compiler ce keylogger
La compilation avec minGW-W64 rend ce Keylogger incompatible avec Windows 98SE et Windows NT4. Ce n'est pas la faute de son code mais des appels système que le compilateur fait faire au programme. Il suffit probablement de compiler les sources avec un logiciel plus ancien pour que ça fonctionne (cela dit, qui utilise encore ces Windows ?). Il a été testé avec succès pour les versions suivantes :
Git : https://git.hackademics.fr/Icarus/Ke...ws/tree/master
Il y a quelques temps, Kayzekk a demandé comment faire un keylogger en Python dans cette discussion. Je connais très mal Python mais, en revanche, faire cela en C++ est dans mes cordes. Je m'y suis donc intéressé de près. La technique la plus évidente est de mettre en place un hook quelque part dans le cheminement des frappes clavier car c'est une fonctionnalité de l'API Windows. J'ai donc réalisé un programme, deux exactement (nous verrons plus loin pourquoi), que l'on voit ci-dessous en action :
Cette idée n'a rien de nouveau ni de révolutionnaire. Il existe une multitude de programmes utilisant cette technique. Certains sont des keyloggers et d'autres, des choses plus sérieuses... Ce à quoi je me suis heurté dans ce développement, c'est qu'on ne trouve nulle part tous les concepts et les pièges inhérents à l'utilisation des hooks. Tout est éparpillé entre la documentation de Microsoft (pas très claire) et les expériences des programmeurs qui butent sur un point ou un autre dans ce domaine.
Concepts généraux
- Un hook consiste à implanter un code qui va être appelé pour un événement donné (il en existe 14, sous la forme WH_xxxxxxx) et parmi ceux-ci, on trouve la frappe d'une touche de clavier. C'est ce qui va, de prime abord, nous intéresser. La mise en place d'un hook se fait grâce à l'API Windows SetWindowsHookEx().
- Tout d'abord, il faut comprendre la différence entre hook de portée thread et hook global. Un hook de portée thread ne pourra être effectif que dans le thread dans lequel réside la fonction de hook, c'est-à-dire, en l’occurrence dans le keylogger lui-même. Ce qui n'a aucun intérêt. Il faut installer un hook de portée globale qui va intercepter les événements clavier pour tous les programmes s'exécutant sur la machine. Jusque-là, tout est relativement clair. C'est maintenant que ça se gâte... Lorsqu'on consulte la documentation de Microsoft sur le sujet, on a d'abord l'impression, pour ne pas dire la certitude, que réaliser un hook global, implique que le code contenant la fonction de hook soit dans une dll (SetWindowsHookEx()).
lpfn [in] Type: HOOKPROC
A pointer to the hook procedure. If the dwThreadId parameter is zero or specifies the identifier of a thread created by a different process, the lpfn parameter must point to a hook procedure in a DLL. Otherwise, lpfn can point to a hook procedure in the code associated with the current process.
Be aware that the WH_MOUSE, WH_KEYBOARD, WH_JOURNAL*, WH_SHELL, and low-level hooks can be called on the thread that installed the hook rather than the thread processing the hook. For these hooks, it is possible that both the 32-bit and 64-bit hooks will be called if a 32-bit hook is ahead of a 64-bit hook in the hook chain.
- C'est ainsi que les Keylogger les plus répandus sur la toile utilisent le type de hook WH_KEYBOARD_LL (1) car cela offre deux grands avantages : pas besoin d'une dll, comme nous l'avons vu et surtout, sur un système 64 bits, le hook est effectif sur les processus 32 bits comme 64 bits (nous verrons que ce n'est pas le cas lorsqu'on utilise une dll). Ils présentent toutefois plusieurs inconvénients. Tout d'abord, ils doivent disposer d'une boucle de lecture de messages Windows (sinon ça ne marche pas du tout). Alors, comme dans l'exemple donné, le plus simple est d'afficher une MessageBox qui dispose de sa propre boucle. J'ignore comment Windows fait fonctionner cela en coulisse mais il est clair que cette boucle y est impliquée. Ensuite, il ne paraît pas possible de savoir de quelle application viennent les frappes enregistrées. Mais surtout, ce dont on va disposer en guise d'information ne sont pas vraiment les caractères tapés mais les touches du clavier qui ont été pressées. Par exemple, on va apprendre que l'utilisateur a appuyé sur la touche " mais on ne saura pas quel caractère en a résulté (3, " ou #). C'est ce que l'on appelle le code de touche virtuelle (virtual key code). Après de multiples recherches, je n'ai pas pu trouver un moyen fiable de transformer le code de touche virtuelle en caractère réellement obtenu. Il existe bien des fonctions pour cela (ToASCII() entre autres) mais pas moyen de les faire fonctionner sans des tests laborieux sur les touches pressées en même temps (Shift, Ctrl et autres).
- Si, de notre côté, on choisit un hook de portée globale dont la fonction est contenue dans une dll, on va être confronté au problème suivant : une dll compilée en 32 bits ne peut être chargée et injectée que dans des applications 32 bits. C'est exactement la même chose pour les dll et processus 64 bits (2, voir "Remarks"). Cela signifie que sur un système d'exploitation Windows 64 bits, pour injecter un maximum de processus, il faut que le Keylogger soit présent en deux exemplaires : l'un en 32 bits et l'autre en 64 bits.
- Enfin, il faut savoir qu'il existe des processus qui sont protégés contre le hook (certains appartenant au système Windows et d'autres, tels que les antivirus) ; ils ne peuvent donc pas être atteint par cette seule technique (3).
Concepts du keylogger présenté ici
- Concernant le problème d'obtenir les vrais caractères tapés, il se trouve que Windows fait cette conversion de lui-même pour les programmes. Lorsqu'il a pris connaissance des touches, il va en déduire le caractère et envoyer son code à l'application via le message WM_CHAR. On trouve ici (voir "Processing Character Messages") l'implémentation de la lecture de ce message. Donc, il faut poser le hook non au niveau des événements clavier, mais sur la boucle de lecture des messages Windows et attendre qu'un WM_CHAR se présente. A cette fin, on utilise le type de hook WH_GETMESSAGE dont on trouve l'implémentation de lecture ici. En pratique, cela va quelque peu se compliquer, car, pour des raisons inconnues, certains programmes (Outlook par exemple) reçoivent plusieurs WM_CHAR pour un seul caractère tapé. La parade consiste à scruter en premier lieu chaque message WM_KEYDOWN et ne permettre la lecture que d'un seul WM_CHAR par WM_KEYDOWN.
- Une fois le code de la touche obtenu, il faut le rapatrier quelque part. On va utiliser le keylogger lui-même (appelé client) pour recevoir ces caractères et les afficher. Se pose donc le problème de faire communiquer les dll qui ont été injectées dans diverses applications avec le client. A la base, les processus sont isolés entre eux, ils ne peuvent dialoguer sans le recours de techniques particulières qu'on regroupe sous le terme de "communication inter-processus" (InterProcesses Communication, IPC). Parmi les diverses formes, trois ont été testées : Socket (UDP), Pipe (named) et MailSlot, le reste n'étant pas approprié pour la problématique. On notera le File Mapping qui est très utile lorsque deux processus doivent se partager des variables (cette technique va être employée mais pour d'autres buts).
- C'est le MailSlot qui a été finalement retenu pour ce programme car il présente les avantage suivants : c'est le plus simple à implémenter, il est plutôt discret. Par ailleurs, tout comme les Named Pipes, il ne fait pas réagir le pare-feu de Windows alors que les sockets oui, quand bien même il s'agit de communications sur la même machine (127.0.0.1 --> 127.0.0.1).
Quant au Named Pipe, il y a un détail qui fait assez peur dans la documentation Microsoft (4) :
Windows 10, version 1709: Pipes are only supported within an app-container; ie, from one UWP process to another UWP process that's part of the same app. Also, named pipes must use the syntax "\\.\pipe\LOCAL" for the pipe name.
- Suite à la remarque sur les dll et processus 32/64 bits, il va donc y avoir besoin de compiler ce programme et sa dll dans ces deux versions. A cette fin, j'ai utilisé minGW-W64 qui est libre et gratuit. Ci-après un mini-tutoriel pour compiler les sources du Keylogger.
Comment compiler ce keylogger
- Télécharger l'installateur de minGW-W64. Il faudra le lancer deux fois, pour rapatrier les deux versions 32 et 64 bits.
- Pour le compilateur 32 bits choisir les paramètres : Version = laisser le défaut, Architecture = i686, Thread = win32, Exception = dwarf, Buil Revison = laisser le défaut. Par défaut, il va s'installer dans C:\Program Files (x86)\mingw-w64\i686-7.2.0-win32-dwarf-rt_v5-rev1\.
- Relancer l'installateur. Pour le compilateur 64 bits choisir les paramètres : Version = laisser le défaut, Architecture = X86_64, Thread = win32, Exception = seh. Buil Revison = laisser le défaut. Par défaut, il va s'installer dans C:\Program Files\mingw-w64\x86_64-7.2.0-win32-seh-rt_v5-rev1.
- Télécharger les sources du Keylogger qui se limitent à deux fichiers à dézipper dans un même répertoire: keylog.cpp et keylog_dll.cpp.
- Ouvrir une invite de commande (cmd.exe), se placer dans le répertoire où se trouvent les sources et entrer :
- Set path=C:\Program Files (x86)\mingw-w64\i686-7.2.0-win32-dwarf-rt_v5-rev1\mingw32\bin
- g++ -static -Wl,--kill-at -s -shared -o keylog32.dll keylog_dll.cpp
- g++ -static -s -o keylog32.exe keylog.cpp
- Set path=C:\Program Files\mingw-w64\x86_64-7.2.0-win32-seh-rt_v5-rev1\mingw64\bin
- g++ -static -Wl,--kill-at -s -shared -o keylog64.dll keylog_dll.cpp
- g++ -static -s -o keylog64.exe keylog.cpp
- Toutes ces commandes ne doivent donner aucun message d'erreur (ni autres messages). Si ce n'est pas le cas, vérifier la valeur de PATH par rapport à l'emplacement des compilateurs. Les chemins donnés ici sont ceux par défaut mais ils peuvent varier (ne serait-ce que si le numéro de version évolue). Pour éviter des fautes de frappes, faire des copier-coller entre ici et la ligne de commande.
- Taper keylog32 dans la ligne de commande puis en ouvrir une autre, se placer dans le répertoire des sources, et lancer Keylog64.
La compilation avec minGW-W64 rend ce Keylogger incompatible avec Windows 98SE et Windows NT4. Ce n'est pas la faute de son code mais des appels système que le compilateur fait faire au programme. Il suffit probablement de compiler les sources avec un logiciel plus ancien pour que ça fonctionne (cela dit, qui utilise encore ces Windows ?). Il a été testé avec succès pour les versions suivantes :
- XP 32 bits (SP3)
- 7 32 bits
- 7 64 bits.
- 8 64 bits
- 10 64 bits (version antérieure à 1709)
- 10 64 bits (version 1709)
Git : https://git.hackademics.fr/Icarus/Ke...ws/tree/master
Commentaire