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.

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.

Interception. KeithJJ @ pixabay

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.

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 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 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 auront une utilité) :

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

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

Déviation. photoheuristic.info @ flickr

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 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
    
    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é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 LowLevelKeyboardProc(
  _In_ int    nCode,
  _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 :

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

alseeger @ pixabay

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. Free-Photos @ pixabay

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.