Intercepter le clavier sous Windows

tbowan

23 janvier 2020

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 improbables1, pénibles2 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.

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. Avec succès puisqu’en très peu de lignes, 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.

Interception, illustration de KeithJJ

Comme vous vous en doutez, si on peut bloquer certaines touches, on peut aussi savoir lesquelles sont utilisées, faire des statistiques ou carrément les enregistrer à la recherche de mots fréquents (et donc d’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.

Trajet d’un événement clavier

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 drivers (pilote en français) 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 thread3 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).

Pour la petite histoire, j’avais initialement regardé la fonction BlockInput qui permet de désactiver les entrées claviers. Le problème, c’est qu’elle n’intervient qu’au niveau du thread qui appelle la fonction…

  • Impossible de bloquer le clavier des autre applications,
  • Impossible de bloquer certains raccourcis globaux.

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.

Installer un hook

L’installation d’un hook d’événement se fait via la fonction SetWindowsHookExA() dont voici la signature :

HHOOK SetWindowsHookExA(
  int       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 seront utilisés) :

  1. 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.
  2. 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.
  3. 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. L’identifiant de la DLL est celui retourné par la fonction LoadLibraryA.
  4. 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…

  1. Votre DLL ne sera injectée que dans les processus dont l’architecture est compatible (32 vs 64 bits),
  2. 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)4.

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

Déviation, illustration de photoheuristic.info

Concernant le clavier, nous pouvons intercepter les événements avant et après leur passage dans la file des événements du thread :

  1. 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ètres hmod et dwThreadId peuvent être omis).
  2. 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ètres hmod et dwThreadId 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"

LRESULT CALLBACK MyKeyboardCB(int nCode, WPARAM wParam,LPARAM lParam) ;

int main(int argc, char *argv[])
{
    HHOOK h = SetWindowsHookExA(WH_KEYBOARD_LL, MyKeyboardKB, NULL,0) ;
    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 de bon ton 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 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
    
    UnhookWindowsHookEx(h) ;
    return 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é.

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 rappel, voici sa signature :

LRESULT CALLBACK LowLevelKeyboardProc(
  _In_ int    nCode,
  _In_ WPARAM wParam,
  _In_ LPARAM lParam
);

En captant l’événement de bas niveau (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 :

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

Illustration de alseeger

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 :

LRESULT CALLBACK MyKeyboardCB(int nCode, WPARAM wParam,LPARAM lParam)
{
    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…

C’est mieux de filtrer. Illustration de Free-Photos

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…


LRESULT CALLBACK MyKeyboardCB(int nCode, WPARAM wParam, LPARAM lParam)
{
    if (nCode == 0) {
        // Cast du paramètre vers le bon type
        KBDLLHOOKSTRUCT * event = (KBDLLHOOKSTRUCT *) lParam ;
        
        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.


  1. 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…↩︎

  2. Sur Windows, les touches rémanentes, 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é ».↩︎

  3. 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 qui est autonome. Pas besoin d’entrer dans des détails anatomiques.↩︎

  4. 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…↩︎

  5. D’après moi, c’est une erreur de conception de Windows. Pourquoi nous passer un truc en nous disant « touches-y pas, c’est super important » ? C’est une erreur dans la répartition des responsabilités. Mais vu l’âge de la bête, c’est normal qu’elle ait quelques maladies.↩︎