Intercepter le clavier sous Windows
Divulgâchage : Initialement conçu pour éviter qu’un enfant n’utilise des raccourcis clavier, voici comment détourner les événements sous Windows pour en bloquer certains.
Il y a quelques années, j’ai découvert, comme de nombreux jeunes parents, qu’il est plus efficace de laisser mes enfants jouer sur mon ordinateur 5 minutes sur mes genoux (après, ils se lassent et vont jouer à autre chose) que de leur dire « non » pendant un quart d’heure…
Malheureusement, j’ai aussi découvert, comme de nombreux jeunes parents, que les enfants n’ont pas leur pareil pour découvrir des raccourcis claviers improbables, pénibles et surtout impossibles à anticiper dans leur frénésie créative. Tout comme avec les autres jouets, on passe alors ensuite un temps bête pour tout ranger. Deux exemples.
- Sur Windows, avec certaines cartes graphiques,
CTRL
+ALT
+←
(ou↑
,→
,↓
) qui permettent de tourner l’écran, très amusant pour faire une blague, mais pas forcément pratique lorsqu’on en est victime la première fois… - Ou encore les touches rémanentes (sous Windows), activée
par 5 appuis successifs sur la touche
MAJ
, très pratiques pour ceux qui en ont besoin mais franchement pénible pour les autres (ne serait-ce que ce pop-up qui vampirise le focus). Vous pouvez désactiver cette fonctionnalité via le Panneau de Configuration, options d’ergonomie, onglet « clavier », dans la section « Touches rémanentes », passez le sélecteur à « Désactivé ».
C’est donc initialement pour bloquer le clavier le temps qu’ils s’amusent et éviter les accidents que je me suis penché sur la gestion du clavier par Windows. Et rapidement, après très peu de lignes de code, j’avais une petite application désactivant des touches clavier choisies ; plus de stress lorsque mes enfants faisaient n’importe quoi, plus besoin d’être aux aguets.
Comme vous vous en doutez, si on peut bloquer certaines touches, on peut aussi savoir lesquelles sont utilisées. On peut alors faire des statistiques ou carrément enregistrer les séquences à la recherche de motifs fréquents (c’est à dire des identifiants).
Alors sans me faire d’illusions sur l’intérêt que vous porterez à la chose – scientifique et professionnel, je n’en doute pas – voici comment intercepter globalement le clavier sur Windows.
Comprendre le clavier
Avant d’entrer dans le vif du sujet, j’aimerais prendre un peu de temps pour vous décrire le trajet suivi par les informations du clavier jusqu’aux applications. Ça n’est pas long, promis (pour les détails, vous pouvez voir la documentation officielle) et ça permettra de mieux comprendre le pourquoi du comment.
Tout commence donc pas une manipulation du clavier. Une touche est enfoncée, relâchée et dans certains cas, un interrupteur peut être basculé (i.e. cas des touches Verr. Maj.). C’est très technique. Trop technique en fait car chaque clavier a son agencement, ses conventions, ses touches additionnelles. Le scan code fourni par le clavier pour nous indiquer de quelle touche il s’agit lui est propre.
D’où l’intérêt des pilotes (ou drivers en anglais) qui se chargent d’interpréter ce scan code idiomatique et le traduire dans une version plus commune dans un message d’événement qui contient tout le nécessaire pour savoir qu’en faire pour les composants suivants.
L’événement est d’abord inséré dans la file des événements systèmes qui, comme son nom l’indique, contient tous les événements dont le système a eu vent et qu’il va pouvoir gérer ou transférer à qui de droit.
Dans ce deuxième cas, l’événement est inséré dans la file des événements du thread destinataire. Il sera ensuite traité dans la boucle de gestion des événements du thread (ou de l’application si elle n’a qu’un thread).
Thread… En français, on parle aussi de tâche, qui porte encore plus a confusion, ou de processus léger que je trouve un peu plus adapté. Si vous ne savez pas ce que c’est, considérez que c’est un bout de programme autonome. Pas besoin d’entrer dans des détails anatomiques.
Pour ce dont nous avons besoin, il faut donc intercepter l’événement assez tôt pour être global et rater le moins d’événements possibles, mais tout en étant assez tard quand même pour rester simple et éviter un code trop bas niveau. Ça tombe bien, Windows met justement à disposition des fonctions natives pour ça.
Exemple de mauvaise idée : la fonction BlockInput qui permet de désactiver les entrées claviers. Car elle intervient au niveau du thread, donc trop tard. Impossible de bloquer le clavier des autre applications et certains raccourcis globaux.
Installer un hook
L’installation d’un hook d’événement se fait via la fonction SetWindowsHookExA() dont voici la signature :
(
HHOOK SetWindowsHookExAint idHook,
,
HOOKPROC lpfn,
HINSTANCE hmod
DWORD dwThreadId);
Les 4 paramètres ont bien sûr chacun leur intérêt (bien que pour nous, seuls les deux premiers auront une utilité) :
- idHook spécifie le type d’interception (quel événements sera capturé et quand ça aura lieu), il y a toute une liste, je vous donne des précisions juste après.
- lpfn, un pointeur vers la fonction qui sera appelée lors de la capture de l’événement, sa signature est toujours la même mais le sens des paramètres peut changer.
- hmod identifie la DLL contenant la
fonction et qui devra être injectée dans les processus pour que la
fonction puisse s’y exécuter. Ce paramètre est inutile (passé à
NULL
) quand aucune injection n’est nécessaire. - dwThreadId identifie le thread pour lequel on
souhaite intercepter les événements, il peut être omis (passé à
0
) si l’interception est globale.
Avertissements : L’injection de DLL dans les threads pose habituellement quelques limitations…
- Votre DLL ne sera injectée que dans les processus dont l’architecture est compatible (32 vs 64 bits),
- Si votre application vient du Windows Store, elle ne sera pas injectée dans les autres applications du Windows Store et le Runtime Broker (qui gère les permissions).
Si vous faite une application de bureau (Desktop app en langage officiel), elle peut, par contre, injecter sa DLL dans les autres applis, y compris du Windows Store (testé en capturant le clavier dans Minecraft). Je n’ai pas regardé si une app du Windows store pouvait s’injecter dans une app normale…
Lorsque le hook est installé, la fonction nous retourne un
identifiant (qu’on utilisera ensuite). En cas d’échec, elle retourne
NULL
(et des informations sur l’erreur sont disponibles via
GetLastError).
Concernant le clavier, nous pouvons intercepter les événements avant et après leur passage dans la file des événements du thread :
- Avant (
idHook = WH_KEYBOARD_LL
), l’interception est alors dite bas niveau et forcément globale puisqu’elle a lieu avant que l’événement ne soit transféré à sa destination, dans ce cas, le code est exécuté dans notre application et il n’y a pas besoin d’injecter de DLL (les paramètreshmod
etdwThreadId
peuvent être omis). - Après (
idHook = WH_KEYBOARD
), l’interception se fait alors dans le processus qui gère l’événement et peut concerner un thread ou être globale, dans ce cas, le code est exécuté dans le processus ciblé et la DLL devra être injectée (les paramètreshmod
etdwThreadId
prennent ici tout leur sens).
Vu qu’on veut capturer tous les événements, y compris les raccourcis
claviers système (comme ALT
+ TAB
),
on va placer notre hook avant la file via la constante
WH_KEYBOARD_LL
. Notre code va donc ressembler à quelque
chose de ce genre :
#include "windows.h"
#include "Winuser.h"
(int nCode, WPARAM wParam,LPARAM lParam) ;
LRESULT CALLBACK MyKeyboardCB
int main(int argc, char *argv[])
{
= SetWindowsHookExA(WH_KEYBOARD_LL, MyKeyboardKB, NULL,0) ;
HHOOK h if (h == NULL) {
return 1 ;
}
// Do Stuffs
Désinstaller le hook
Lorsqu’on n’en a plus besoin, ou avant que votre application ne soit terminée (ce qui revient alors au même), il est poli de désinstaller le hook. Rien n’est documenté sur ce qu’il se passerait si vous ne le désinstallez pas, on ne peut donc qu’imaginer des choses horribles.
Pour désinstaller votre hook, vous disposez de la fonction UnhookWindowsHookEx() qui prend en paramètre l’identifiant du hook précédement installé. Pour reprendre le code précédent, voici comment il se terminerait :
// Do Stuffs
(h) ;
UnhookWindowsHookExreturn 0 ;
}
La valeur de retour (booléen) nous renseigne sur la réussite du processus, dans mon cas, vu que je quitte l’application, je ne l’ai pas traitée.
Bloquer l’événement
Nous l’avions déclarée dans l’exemple de code, il est maintenant temps de la définir, la fonction qui va gérer les événements claviers. Pour info, voici sa signature :
(
LRESULT CALLBACK LowLevelKeyboardProcint nCode,
_In_ ,
_In_ WPARAM wParam
_In_ LPARAM lParam);
En captant l’événement de bas niveau (puisque nous avons
utilisé le drapeau WH_KEYBOARD_LL
), les paramètres ont un
sens particulier (les détails sont fournis dans la doc de LowLevelKeyboardProc())
dont voici le sens général :
code
nous renseigne si nous pouvons gérer l’événement (il vaut0
) ou si nous devons l’ignorer et le passer aux hooks suivants (il est négatif). C’est bien sûr une erreur de conception mais c’est souvent comme ça avec les systèmes qui ont pris de l’âge.wParam
nous renseigne sur le type d’événement clavier (touche pressée ou relâchée).lParam
pointe vers une structure contenant les détails de l’événement.
Lorsqu’on a fini de traiter l’événement et/ou qu’on veut le passer aux hooks suivants, il est demandé d’appeler la fonction CallNextHookEx() – avec les mêmes paramètres que ceux qui nous ont été fournis et retourner le résultat qu’elle fourni, comme si nous n’existions pas. Si vous ne le faites pas, d’autres choses horribles pourraient arriver (pour les autres applications qui n’auront pas accès aux événements).
Nous pouvons aussi choisir notre propre valeur de retour.
0
signifiant que l’événement peut continuer son trajet vers
les applications. N’importe quelle autre valeur (i.e.
1
) signifiant que le trajet doit s’arrêter ici, l’événement
ne pouvant pas aller plus loin.
Nous avons donc tout ce qu’il nous faut pour bloquer le clavier avec le hook suivant :
(int nCode, WPARAM wParam,LPARAM lParam)
LRESULT CALLBACK MyKeyboardCB{
if (nCode >= 0) {
// Block every keyboard event
return 1 ;
}
return CallNextHookEx(NULL, nCode, wParam, lParam) ;
}
Limitations : Comme ce hook a lieu autour de la file
des threads (et non à l’intérieur du noyau système de Windows), il ne
peut pas bloquer CTRL
+ ALT
+
DELETE
et sera inactif sur l’écran de login.
Filtrer l’événement
Plutôt que tout bloquer bêtement, ce qui n’est pas très utile je vous l’accorde, je vous proposes d’ajouter un peu de filtrage. On laisse passer certaines touches, et on en bloque d’autres.
Le problème, si on bloque toutes les entrées clavier, c’est que ça se voit vite et les enfants qui étaient tout content de pouvoir « travailler comme papa » sont frustrés de ne voir aucune réaction sur l’écran (à cet âge, notepad++ est un jeux très prenant).
Pour ça, on va lire le troisième paramètre (lparam
), un
pointeur vers la structure KBDLLHOOKSTRUCT,
qui contient un champ vkCode
avec le code virtuel
de la touche (un identifiant commun à tous les claviers). La liste
officielle vous les donnera tous, vous pouvez aussi les écrire sur
la console pour voir quelle touche donne quelle valeur…
Dans mon cas, je me suis surtout concentré sur certaines touches
spéciales qui peuvent avoir des conséquences (TAB
,
ALT
,…). À vous d’adapter le code suivant vos besoins…
(int nCode, WPARAM wParam, LPARAM lParam)
LRESULT CALLBACK MyKeyboardCB{
if (nCode == 0) {
// Cast du paramètre vers le bon type
* event = (KBDLLHOOKSTRUCT *) lParam ;
KBDLLHOOKSTRUCT
switch (event->vkCode) {
case VK_TAB:
case VK_MENU:
case VK_SNAPSHOT:
case VK_HELP:
case VK_LWIN:
case VK_RWIN:
case VK_APPS:
case VK_SLEEP:
return 1 ;
}
}
return CallNextHookEx(NULL, nCode, wParam, lParam) ;
}
Et Voilà !
En quelques 40 lignes de code, vous avez un petit programme qui bloque des touches arbitraires du clavier. À vous d’imaginer la suite 😉.
De notre côté, ça fait bien longtemps qu’on n’a plus besoin de bloquer les touches. Les enfants ont grandis et nos jeux ont évolué ; on est passé de notepad++ à minetest puis à CS GO…
En attendant d’autres articles sur ce thème, voici quelques articles qui peuvent vous intéresser…
- Shellcode pour Windows 10
-
6 janvier 2020 Parce qu’il n’y a pas que Linux dans la vie, on va se tourner vers Windows pour un nouvel article sur la fabrication de shellcodes.
- Écrire des keylogger pour le noyau linux
-
Phrack 59 - 0x0e Cet article est divise en deux parties. La premiere donne un apercu general de la facon dont fonctionne un driver clavier sous linux. La seconde partie presente en details Vlogger, un petit keylogger linux basé kernel.