Annonce

Réduire
Aucune annonce.

Injection de processus sous Windows - contournement de pare-feu

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

  • Injection de processus sous Windows - contournement de pare-feu

    Bonjour,

    Dans un précèdent article, je m'étais attelé à la réalisation d'un keylogger. Nous avions vu que, de par la méthode utilisée (hook global sur WH_GETMESSAGE), il s'agissait d'injecter une DLL dans tous les processus, du moins là où c'était possible. L'injection de DLL est donc en quelque sorte un effet secondaire de cette façon de procéder. J'avais mentionné que cela permettait plein de choses dans la mesure où l'on parvient à faire exécuter son propre code par un autre processus.

    Ici, on va voir comment on peut injecter une DLL pour contourner un pare-feu. Le principe est très simple : on sélectionne une application qui est supposée avoir des droits suffisants dans le pare-feu et on y injecte notre DLL. A titre d'exemple, elle va contenir un serveur UDP, lequel va se mettre en attente de connexion extérieures.

    Comme il s'agit de cibler un processus (et un seul) désigné à l'avance, un hook global n'est pas adapté pour ce faire. En conséquence, on va en profiter pour étudier une autre technique d'injection, celle par l'API Windows CreateRemoteThread().




    Dans cet exemple, sous Windows 7, j'ai d'abord lancé le programme inj en ligne de commande (en haut à droite) pour injecter Firefox (visible en bas à droite). Ensuite, j'utilise la distribution GhostBSD (à gauche, en machine virtuelle) pour envoyer des données qui vont être lues par la DLL (donc Firefox) et finalement renvoyées à l'injecteur ("Coucou Windows !"). Le pare-feu de Windows n'y voit aucun inconvénient.

    Ainsi que le montre netstat, c'est bien Firefox qui écoute sur UDP 53 (utilisé normalement pour le protocole DNS).
    Code:
    C:\Windows\system32>netstat -ab -p UDP
    
    Connexions actives
      Proto  Adresse locale         Adresse distante       État
      UDP    0.0.0.0:53             *:*
     [firefox.exe]

    Concepts et principes
    • L'injection de DLL par CreateRemoteThread() est une technique ancienne que l'on trouve documentée (plus ou moins bien) un peu partout sur internet (1). Pour faire simple, cela permet de faire exécuter certaines API Windows par le processus cible. Ici, nous lui demandons simplement de charger notre DLL dans son espace mémoire (et de la décharger lorsqu'on en a fini).
    • De la même façon que nous l'avions vu pour le Keylogger, on se retrouve confronté au problème suivant : un processus 32 bits ne peut charger qu'une DLL 32 bits, idem pour un processus 64 bits qui ne peut charger qu'une DLL 64 bits. De plus, l'injection elle-même ne peut être réalisée que par un programme de même nombre de bits que la cible (cela bloque à CreateRemoteThread() : accès refusé). Ce projet va donc demander la compilation de deux injecteurs et de deux DLL, les uns en 32 bits et les autres en 64 bits, bien que le code soit exactement le même.
    • Ce dernier point amène à la réalisation d'un autre programme dit "frontal" qui va avoir en charge de trouver le processus cible et déterminer s'il s'agit d'un 32 bits ou d'un 64 bits. Sur la base de son résultat, il appelle la bonne version de l'injecteur qui lui-même fait charger la bonne version de la DLL.





    Compiler l'ensemble
    • 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 de l'injecteur, placer les fichiers dans un répertoire.
    • Ouvrir une invite de commande (cmd.exe), se placer là 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 -s -o inj.exe inj-frontal.cpp
    3. g++ -shared -static -s -o inj32.dll Injecteur_dll.cpp -lws2_32
    4. g++ -static -s -o inj32.exe Injecteur.cpp
    5. Set path=C:\Program Files\mingw-w64\x86_64-7.2.0-win32-seh-rt_v5-rev1\mingw64\bin
    6. g++ -shared -static -s -o inj64.dll Injecteur_dll.cpp -lws2_32
    7. g++ -static -s -o inj64.exe Injecteur.cpp

    Note : les commandes Set path font référence aux emplacements par défaut, avec un Windows 10 standard, des compilateurs 32 et 64 bits. A rectifier, selon la place où se trouvent vraiment les binaires de minGW-W64.


    Conclusions

    Alors, le crime est parfait ? Pas tout à fait, et loin s'en faut en vérité.

    Premièrement, ouvrir un port dans un pare-feu, c'est bien, mais il faut encore pouvoir passer la box ou le routeur qui est en première ligne sur internet. Ceci dit, ce n'est pas impossible.

    Deuxièmement, les applications qui s'octroient tous les droits dans le pare-feu de Windows se font rares. Firefox.exe, que j'ai pris comme exemple, fait telle chose lors de son installation. Cependant, ça ne fonctionnera pas du tout avec Chrome.exe dont les règles de paquets entrants semblent plutôt bien étudiées. Avec Edge, il n'y a aucun espoir car il est protégé contre ce genre d'amusement, comme le sont certains processus système de Windows (svchost.exe, ...). Maintenant, injecter un programme comme Explorer.exe ou OneDrive.exe va provoquer une demande d'autorisation du pare-feu ; cela peut éventuellement passer (mieux en tout cas que si c'est Inj.exe qui le demande ).

    Troisièmement, ainsi que je le disais, l'injection par CreateRemoteThread() est bien connue de tout le monde, y-compris des antivirus. Certains d'entre eux vont placer Inj.exe en "bac à sable", constater la tentative d'injection de processus et décider que ce n'est pas bien , puis, pouf, quarantaine. A cela, il y a encore des parades mais d'autres interceptent certains appels systèmes (WriteProcessMemory() notamment dont on a besoin) et les rendent inopérants. Là, je ne vois pas comment faire pour s'en sortir, enfin simplement du moins.
    Fichiers attachés
    Dernière modification par Icarus, 03 mars 2018, 12h49.

  • #2
    Voici quelques explications complémentaires




    Ecrire un logiciel, c'est avoir un objectif et utiliser ce qui paraît le plus approprié pour y parvenir. C'est aussi un chemin parsemé d’embûches qu'il va falloir surmonter. Dans la plupart des cas, ces obstacles sont générés par la méconnaissance du domaine auquel on s'attaque. On peut utiliser certaines techniques ; on peut croire les connaître, mais dans les faits, il y a toujours un coin obscur au détour d'une simple fonction C ou C++ pourtant mainte fois employée ou bien à l'intérieur d'un appel système. A mon sens, c'est ce qui fait tout le charme de la programmation : c'est une éternelle découverte mais aussi redécouverte.


    Injection via CreateRemoteThread()

    Le but ici va être de faire charger une DLL de notre cru par une application. Une DLL se charge grâce à l'API Windows LoadLibrary(). Elle est très simple d'utilisation, il suffit de donner le texte de l'emplacement du fichier DLL sous forme d'un char* (cet emplacement doit être le chemin complet, du moins dans le cas qui nous occupe). Le problème, c'est que cette API doit être appelée par le processus que nous voulons injecter.
    C'est là qu'entre en scène CreateRemoteThread() qui sert normalement à lancer un thread dans un processus distant. Un thread est une fonction comme une autre, à ceci près qu'elle va s'exécuter en parallèle du processus et de ses autres threads. Ce que l'on va faire, c'est lancer LoadLibrary() comme si c'était un thread. Peu importe qu'elle se déroule en concurrence du processus, ce n'est pas le but ; la finalité est juste de l'exécuter au nom de l'application que l'on va injecter. De la sorte, on va lui faire charger notre DLL dans son espace mémoire.

    Examinons cette fonction :
    Code:
    HANDLE WINAPI CreateRemoteThread(
    _In_  HANDLE                 hProcess,
    _In_  LPSECURITY_ATTRIBUTES  lpThreadAttributes,
    _In_  SIZE_T                 dwStackSize,
    _In_  LPTHREAD_START_ROUTINE lpStartAddress,
    _In_  LPVOID                 lpParameter,
    _In_  DWORD                  dwCreationFlags,
    _Out_ LPDWORD                lpThreadId
    );
    Nous avons besoin d'un HANDLE sur le processus cible. C'est facile à obtenir à partir de son PID. Disons qu'on dispose de ce PID. Ensuite, la fonction demande un LPSECURITY_ATTRIBUTES. Inutile d'épiloguer là-dessus, on peut s'en passer et lui donner NULL en lieu et place. Idem pour SIZE_T dwStackSize, il s'agit de la taille de la pile pour ce thread ; si on met 0, c'est la taille par défaut. Finissons les paramètres plus ou moins inutiles avec DWORD dwCreationFlags qui marche très bien si on lui met également 0, et LPDWORD lpThreadId qui va donner l'identifiant du thread créé dont on se fout éperdument. On peut lui mettre NULL aussi.
    Là où il va falloir travailler, c'est pour obtenir l'adresse de départ de la fonction que nous voulons lancer (LPTHREAD_START_ROUTINE lpStartAddress). Il convient également de lui fournir l'argument du thread (LPVOID lpParameter). Ici, comme nous l'avons vu plus haut, ce doit être le texte du chemin complet de notre DLL.

    Il faut donc retrouver l'adresse de la fonction LoadLibrary() dans le processus distant. Difficile a priori car les applications sont isolées entre elles... C'est là que Microsoft nous fait un cadeau inespéré. Cette fonction est incluse dans kernel32.dll et il se trouve que toutes les fonctions qu'elle contient sont localisées à la même adresse quel que soit le processus (2). Donc nous n'avons plus qu'à trouver cette adresse dans notre propre processus, celui de l'injecteur, et ce sera la bonne. A cette fin, on va employer GetProcAddress() :
    Code:
    LPVOID lpLLAf = (LPVOID)GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");
    En point de détail, on peut noter que le nom exact de la fonction est LoadLibraryA et non pas LoadLibrary. Ceci, car certaines fonctions ont une version ANSI et UNICODE selon comment sont encodés les caractères à transmettre (3).

    Maintenant qu'on a obtenu l'adresse de LoadLibrary(), on va voir comment lui transmettre son paramètre (le chemin complet de la DLL). Cela paraît simple de prime abord : on créé un char*, on y place le chemin en question et puis voilà ! Malheureusement, cela ne marche pas. Il faut que ce char* soit dans l'espace mémoire du processus qui appelle LoadLibrary(). C'est ici que les complications surviennent : il s'avère nécessaire de réserver de la mémoire dans le processus cible et d'y écrire le chemin de notre DLL. A cette fin, il convient d'employer les API Windows VirtualAllocEx() puis WriteProcessMemory(). Enfin, si on ne veut pas trop pourrir le processus distant, on libère la mémoire qu'on a prise en son nom par VirtualFreeEx().
    Code:
    LPVOID lpArg = (LPVOID) VirtualAllocEx(hP, NULL, st, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE);
    
    if (lpArg != NULL) { // La mémoire a effectivement été réservée
    
       BOOL b = WriteProcessMemory(hP, lpArg, param, st, NULL);
    
       if (b) { // WriteProcessMemory() a été exécuté sans erreur
    
          HANDLE hRemThread = CreateRemoteThread(hP, NULL, 0, (LPTHREAD_START_ROUTINE) lpLLAf, lpArg, 0, NULL);
    
          // Par souci de concision, on passe la vérification de la bonne exécution de CreateRemoteThread() -> hRemThread != NULL
    
          DWORD dwR = WaitForSingleObject(hRemThread, pc_TIME_OUT);
    
          if (dwR == WAIT_OBJECT_0) VirtualFreeEx(hP, lpArg, 0, MEM_RELEASE);
    
       }
    }
    lpArg est l'adresse de début de la zone de mémoire allouée, hP est le HANDLE du processus cible, st est la taille de mémoire désirée en octets = longueur de la chaîne de caractères (sans oublier le 0 terminal) et param est un char* qui pointe dans la zone mémoire de l'injecteur où se trouve le chemin de la DLL (WriteProcessMemory() va copier son contenu vers lpArg). Pour mémoire, lpLLAf est l'adresse de début de LoadLibrary().

    On constate l'apparition d'une autre API : WaitForSingleObject(). Elle permet d'attendre la fin de l'exécution du thread du processus cible, autrement dit de LoadLibrary(). Car si on libère la mémoire alors qu'elle est encore utilisée... Aïe ! Cette fonction est bloquante et ne va retourner que lorsque le thread de HANDLE hRemThread sera terminé ou lorsque la temporisation en millisecondes pc_TIME_OUT sera dépassée. Lorsque le thread est vraiment terminé, le code de retour est WAIT_OBJECT_0 et ce n'est qu'à cette condition qu'on peut libérer la mémoire sinon on risque de faire planter l'application hôte.


    Autres techniques utilisées dans ce projet
    • Recherche du PID d'un processus à partir de son nom d'exécutable.
    • Définition d'un gestionnaire de signal en console.
    • Déterminer si un processus donné est 32 ou 64 bits.
    • Lancement de processus par CreateProcess().
    • Utilisation d'un tube nommé (Named pipe) en mode bidirectionnel (communication DLL <--> Injecteur).
    • Utilisation d'objets de synchronisation : Mutex et Event.
    • Client / serveur socket UDP.
    • Accessoirement, écriture et compilation d'une DLL mais cela a déjà été abordé ici.
    Dernière modification par Icarus, 03 mars 2018, 08h44.

    Commentaire


    • #3
      J'ai oublié de dire que ces programmes fonctionnent de Windows XP à Windows 10 (64 ou 32 bits).

      Je tiens cependant à ajouter qu'au détour d'un test dont je n'attendais pas grand-chose, j'ai atteint la gloire absolue avec ReactOS :



      Pour le reste, je vous rassure, le Keylogger ne fonctionne quand même pas (il se lance mais n'injecte aucun processus).
      Dernière modification par Icarus, 03 mars 2018, 17h36.

      Commentaire


      • #4
        Excellent ton article Icarus, très instructif et d'une précision remarquable, Bravo

        Commentaire


        • #5
          Bonjour,

          Très intéressant. Tu tombe très bien car je travail en ce moment sur de la lecture de processus. En l'état actuel mon programme lis directement la mémoire du processus sans passer par de DLL. C'est un peux une galère car à la moindre MàJ tous les offsets sont à refaire...
          N'ayant jamais tenté de le faire par DLL, c'est l'occasion , merci à toi
          Dernière modification par bambish, 06 mars 2018, 06h00.

          Commentaire


          • #6
            Salut,

            Il existe bien des façons d'injecter un processus, avec ou sans DLL, plus ou moins compliquées à mettre en oeuvre, plus ou moins à même de faire réagir certains antivirus.

            La manière de procéder est étroitement liée aux objectifs poursuivis et aux contraintes qui en résultent. Qui plus est, dans ton cas, l'injection elle-même ne semble pas absolument nécessaire puisqu'il ne s'agit que de "lire".

            Peut-être devrais-tu créer un sujet pour en discuter ?

            Commentaire


            • #7
              Faut que j'étudie ça d'un peux plus près, ce n'est pas vraiment mon domaine de prédilection mais c'est intéressant. Pour mon projet j'ai également besoin d'écrire dans la mémoire des processus.
              Sachant que mon projet n'est encore très avancé je ne vais pas ouvrir un post tout de suite mais après pourquoi

              Commentaire

              Chargement...
              X