Création d'une DLL de proxy pour les vérifications des opérations de DLL de piratage

Lorsque j'examine la sécurité des logiciels, l'un des points à vérifier est de travailler avec des bibliothèques dynamiques. Les attaques comme la DLL de piratage («usurpation de DLL» ou «interception de DLL») sont très rares. Très probablement, cela est dû au fait que les développeurs Windows ajoutent des mécanismes de sécurité pour empêcher les attaques, et les développeurs de logiciels sont plus attentifs à la sécurité. Mais la situation est d'autant plus intéressante que le logiciel cible est vulnérable.

Décrivant brièvement l'attaque, la DLL de piratage crée une situation dans laquelle un exécutable essaie de charger la DLL, mais l'attaquant intervient dans ce processus, et au lieu de la bibliothèque attendue, une DLL spécialement préparée est chargée avec la charge utile de l'attaquant. En conséquence, le code de la DLL sera exécuté avec les droits de l'application lancée, par conséquent, les applications avec des droits plus élevés sont généralement sélectionnées comme cible.

Pour que la bibliothèque se charge correctement, un certain nombre de conditions doivent être remplies: la taille en bits du fichier exécutable et la bibliothèque doivent correspondre, et si la bibliothèque est chargée au démarrage de l'application, la DLL doit exporter toutes les fonctions que cette application s'attend à importer. Souvent, une seule importation ne suffit pas - il est très souhaitable que l'application continue son travail après le chargement de la DLL. Pour cela, il est nécessaire que les fonctions de la bibliothèque préparée fonctionnent de la même manière que l'original. La façon la plus simple de procéder consiste à simplement passer les appels de fonction d'une bibliothèque à une autre. Ce sont les DLL appelées DLL proxy.



Sous la coupe, il y aura plusieurs options pour créer de telles bibliothèques - à la fois sous forme de code et d'utilitaires.

Une petite revue théorique


Les bibliothèques sont plus souvent chargées à l'aide de la fonction LoadLibrary, dans laquelle le nom de la bibliothèque est transmis. Si, au lieu du nom, vous passez le chemin complet, l'application essaiera de charger la bibliothèque spécifiée. Par exemple, l'appel à LoadLibrary («C: \ Windows \ system32 \ version.dll») chargera la DLL spécifiée. Ou, si la bibliothèque n'existe pas, elle ne sera pas chargée.

Un peu d'ennui
Si une DLL est déjà chargée dans l'application, elle ne sera pas chargée à nouveau. Étant donné que version.dll est chargé au début de presque n'importe quel fichier exe, en fait, l'appel ci-dessus ne chargera rien. Mais nous considérons toujours le cas général, considérons l'exemple comme un appel à une bibliothèque abstraite.

C'est tout autre chose si vous écrivez LoadLibrary («version.dll»). Dans une situation normale, le résultat sera exactement le même que dans le cas précédent - C: \ Windows \ system32 \ version.dll se chargera, mais pas si simple.

Tout d'abord, une bibliothèque sera recherchée, qui ira dans l' ordre suivant:

  1. Dossier exécutable
  2. Dossier C: \ Windows \ System32
  3. Dossier C: \ Windows \ System
  4. Dossier C: \ Windows
  5. Le dossier défini comme courant pour l'application
  6. Dossiers de la variable d'environnement PATH

Un peu plus d'ennui
Lors du démarrage d'applications 32 bits sur un système 64 bits, tous les appels à C: \ Windows \ system32 seront transférés vers C: \ Windows \ SysWOW64. C'est juste pour la précision de la description, du point de vue de l'attaquant, la différence n'est pas particulièrement importante.

Lorsque vous exécutez le fichier exe, le système d'exploitation charge toutes les bibliothèques à partir de la section d'importation de fichiers. Dans un sens général, nous pouvons supposer que le système d'exploitation force le fichier à appeler LoadLibrary, en passant tous les noms de bibliothèque qui sont écrits dans la section d'importation. Étant donné que dans 99,9% des cas, il existe des noms et non des chemins, au démarrage de l'application, toutes les bibliothèques chargées seront recherchées dans le système.

Dans la liste des emplacements de recherche de DLL, deux points sont vraiment importants pour nous - 1 et 6. Si nous mettons version.dll dans le même dossier à partir duquel le fichier est lancé, alors au lieu du système, celui chargé sera chargé. Cette situation n'est presque jamais rencontrée, car s'il est possible de mettre une bibliothèque, il est très probable qu'il soit possible de remplacer le fichier exécutable lui-même. Mais encore, de telles situations sont possibles. Par exemple, si le fichier exécutable se trouve dans un dossier accessible en écriture et est un service avec un démarrage automatique, il ne peut pas être modifié pendant que le service lui-même est en cours d'exécution. Ou le fichier lancé est vérifié en externe par somme de contrôle avant de commencer, puis le remplacement du fichier n'est toujours pas une option. Mais mettre la bibliothèque à côté sera bien réel.

Vous ne pourrez peut-être pas créer de fichiers à côté de fichiers exécutables, mais vous pouvez créer des dossiers. Dans cette situation, le mécanisme de redirection WinSxS (alias «DotLocal») peut fonctionner.

En bref sur DotLocal
Le manifeste du fichier peut contenir une dépendance à la bibliothèque d'une version spécifique. Dans ce cas, lors du démarrage du fichier exécutable (par exemple, que ce soit application.exe), le système d'exploitation vérifie l'existence d'un dossier nommé application.exe.local dans le même dossier que le fichier lui-même. Ce dossier doit avoir un sous-dossier avec un nom complexe comme amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.9600.19291_none_6248a9f3ecb5e89b à l'intérieur duquel se trouve déjà une bibliothèque comctl32.dll. Le nom de la bibliothèque et les informations pour le nom du dossier doivent être indiqués dans le manifeste, voici juste un exemple du premier processus rencontré. S'il n'y a pas de dossiers ou de fichiers, la bibliothèque sera extraite de C: \ Windows \ WinSxS. Dans l'exemple, C: \ Windows \ WinSxS \ amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.9600.19291_none_6248a9f3ecb5e89b \ comctl32.dll.

Mais c'est plus l'exception que la règle. Mais les situations où la recherche de DLL atteint le 6e numéro de la liste sont bien réelles. Si l'application essaie de charger une DLL qui ne se trouve pas sur le système ou à côté du fichier, toutes les recherches iront jusqu'à 6 points, qui pourraient être des dossiers accessibles en écriture.

Par exemple, une installation Python typique se produit le plus souvent dans le dossier C: \ Python (ou fermer). Le programme d'installation de python lui-même suggère d'ajouter ses dossiers à la variable système PATH. En conséquence, nous avons un bon tremplin pour démarrer une attaque - le dossier est accessible en écriture par tous les utilisateurs et toute tentative de chargement d'une bibliothèque inexistante ira à la recherche de chemin à partir de PATH.

Maintenant que la théorie est terminée, considérez la création de la charge utile - les bibliothèques proxy elles-mêmes.

La première option. Bibliothèque de proxy honnête


Commençons par une solution relativement simple - nous créerons une bibliothèque de proxy honnête. L'honnêteté dans ce cas implique que toutes les fonctions de la dll seront explicitement enregistrées, et pour chaque fonction un appel de fonction avec le même nom de la bibliothèque d'origine sera écrit. Travailler avec une telle bibliothèque sera complètement transparent pour le code appelé: s'il appelle une fonction, il recevra la bonne réponse, le résultat et tout ce qui devrait arriver côte à côte.

Voici un lien vers l'exemple fini ( github ) de la bibliothèque version.dll.

Points saillants du code:

  • Tous les prototypes de fonctions de la table d'exportation de la bibliothèque d'origine sont honnêtement décrits.
  • La bibliothèque d'origine est chargée et tous les appels à nos fonctions y sont lancés.

De façon pratique , l'application continue de fonctionner correctement, sans aucun "effet spécial". Il n'est pas pratique que j'ai dû écrire un tas de code uniforme pour chacune des fonctions, en plus, en vérifiant soigneusement la coïncidence des prototypes.

La deuxième option. Simplifiez l'écriture de code


Lorsqu'il s'agit d'une bibliothèque comme version.dll, où la table d'importation est petite, il n'y a que 17 fonctions et les prototypes sont simples, alors une bibliothèque proxy honnête est un bon choix.



Mais si le proxy de la bibliothèque, par exemple, bcrypt, alors tout est plus compliqué. Voici sa table d'importation:



57 fonctionnalités! Et voici quelques exemples de prototypes:




Disons simplement que rien n'est impossible, mais faire un proxy honnête pour une telle bibliothèque n'est pas très agréable.

Vous pouvez simplifier le code si vous trichez un peu avec les fonctions. Nous déclarerons toutes les fonctions de la bibliothèque comme __declspec (nu), et dans le corps, nous utiliserons du code assembleur qui crée simplement jmp sur la fonction de la bibliothèque d'origine. Cela nous permettra de ne pas utiliser de longs prototypes, mais de mettre des annonces simples partout sans paramètres d'affichage:

void foo ()

Lorsque l'application appelle notre fonction, la bibliothèque proxy n'effectuera aucune manipulation avec le registre et la pile, permettant à la fonction d'origine de faire tout le travail comme il se doit.

Un exemple ( github ) de la bibliothèque version.dll avec cette approche.

Faits saillants:

  • La bibliothèque d'origine est chargée et tous les appels à nos fonctions y sont lancés. Les corps de fonction et le chargement sont enveloppés dans des macros.

Fonctionnement pratique et correct de l'application et le fait que même un grand nombre de fonctions sont facilement décrites, grâce aux macros. Il est gênant ce râteau plutôt inattendu en x64. Visual Studio (quelque part depuis 2012, si je me souviens bien) interdit d'utiliser des insertions nues et asm dans du code 64 bits. Lors de l'écriture d'un proxy à partir de zéro, il est nécessaire que chaque fonction vérifie qu'elle est décrite dans le fichier def, que l'original est chargé et que le corps de la fonction est décrit.

La troisième option. On jette le corps en général


Utiliser nu suggère une autre option. Vous pouvez créer une table d'importation qui, pour toutes les fonctions, fera référence à une vraie ligne de code:

void nop () {}

Une telle bibliothèque sera chargée par l'application, mais ne fonctionnera pas. Lors de l'appel de l'une des fonctions, la pile sera très probablement déchirée ou une autre boue se produira. Mais ce n'est pas toujours mauvais - si, par exemple, le but d'une injection de DLL est d'exécuter simplement le code avec les droits nécessaires, alors il suffit d'exécuter la charge utile à partir de la bibliothèque proxy DllMain et de terminer l'application immédiatement en silence. Dans ce cas, il n'y aura pas de véritable appel aux fonctions et il n'y aura pas de plantage d'erreur.

Un exemple sur un github , encore pour version.dll.

Points saillants du code:

  • Toutes les fonctions du fichier def se réfèrent à une fonction nop.

De manière pratique , une telle bibliothèque proxy est écrite juste pour quelques minutes. Il n'est pas pratique que l'application appelée cesse de fonctionner.

La quatrième option. Prenez des utilitaires prêts à l'emploi


L'écriture d'une DLL est bonne, mais pas toujours pratique et pas très rapide, vous devriez donc envisager des options automatisées.

Vous pouvez suivre le chemin des anciens virus - prenez la bibliothèque dont nous voulons créer les proxys, créez-y une section exécutable de code, notez-y la charge utile et modifiez le point d'entrée dans cette section. Ce n'est pas le moyen le plus simple, car vous pouvez accidentellement casser quelque chose, vous devez écrire dans l'assembleur, n'oubliez pas le périphérique du fichier PE. Ce n'est pas notre chemin.

Pour faire fonctionner le détournement de DLL, nous ajouterons un autre détournement de DLL.



C'est relativement facile à faire. Nous copions la bibliothèque dont nous voulons créer le proxy et ajoutons une DLL avec une fonction arbitraire à la table d'importation de cette copie. Maintenant, le téléchargement suivra la chaîne - au début du fichier exécutable, la DLL du proxy sera chargée, qui chargera la bibliothèque spécifiée elle-même.

«Hé, tu as remplacé le chargement d'une bibliothèque par une autre. À quoi ça sert? Il faudra tout de même coder dll! ". Tout est correct, mais il y a toujours un sens. Il y aura désormais moins d'exigences pour une bibliothèque avec une charge utile. Vous pouvez spécifier n'importe quel nom, l'essentiel est d'exporter une seule fonction, qui peut avoir n'importe quel prototype. Entrez le nom principal de la bibliothèque et de la fonction dans la table d'importation.

Une bibliothèque avec une charge utile peut être une pour toutes les occasions.

Vous pouvez modifier la table d'importation avec de nombreux éditeurs PE, par exemple CFF explorer ou pe-bear. Pour ma part, j'ai écrit un petit utilitaire en C # qui corrige une table sans gestes inutiles. Sources sur github , binar dans la section Release .

Conclusion


Dans l'article, j'ai essayé de divulguer les méthodes de base pour créer une DLL proxy, que j'ai moi-même utilisées. Il ne reste plus qu'à dire comment se défendre.

Il n'y a pas beaucoup de recommandations universelles:

  • Ne stockez pas de fichiers exécutables, en particulier ceux exécutés avec des autorisations élevées, dans des dossiers accessibles en écriture aux utilisateurs.
  • Il est préférable de rechercher et de vérifier d'abord l'existence de la bibliothèque avant d'effectuer LoadLibrary.
  • Regardez les méthodes de protection existantes disponibles dans le système d'exploitation. Par exemple, dans Windows 10, vous pouvez définir l'indicateur PreferSystem32 afin que la recherche dll ne commence pas par le dossier de fichiers exécutables, mais par system32.

Merci de votre attention, je serai heureux d'entendre des questions, des souhaits, des suggestions et des commentaires.

UPD: Sur les conseils des commentateurs, je vous rappelle que vous devez choisir soigneusement et soigneusement une bibliothèque. Si la bibliothèque est incluse dans la liste KnownDlls ou si le nom est similaire à MinWin (ApiSetSchema, api-ms-win-core-console-l1-1-0.dll - c'est tout), il est très probable qu'il ne sera pas possible de l'intercepter en raison des fonctionnalités de traitement ces DLL dans le système d'exploitation.

Source: https://habr.com/ru/post/fr449904/


All Articles