Techniques d'interception d'appels de fonction sous Windows avec Detours

L'interception d'appels de fonctions, communément appelée "API hooking", est une technique puissante permettant de rediriger et de modifier le comportement des fonctions existantes sans altérer le code source original. Elle trouve des applications dans le débogage, l'ingénierie inverse et même le développement de solutions de contournement ou de modifications dans des applications existantes.

Principes fondamentaux de l'interception

Historiquement, l'interception reposait sur des mécanismes tels que les trampolines. Un trampoline est un petit bloc de code (shellcode) qui, inséré au début d'une fonction cible, modifie le flux d'exécution. Il utilise une instruction de saut (comme jmp) pour rediriger l'exécution vers une fonction personnalisée. Après l'exécution de la fonction personnalisée, le contrôle peut être renvoyé à la fonction originale ou à une autre destination.

Une approche plus sophistiquée est l'inline hooking. Contrairement aux trampolines qui peuvent simplement détourner l'exécution, l'inline hooking modifie directement les premières instructions de la fonction cible pour y insérer un saut vers la fonction de remplacement. L'astuce réside dans le fait de pouvoir ensuite rétablir l'exécution normale de la fonction d'origine après l'exécution de la fonction de remplacement, ce qui le rend plus efficace mais aussi plus complexe à implémenter et à maintenir.

Utilisation de la bibliothèque Detours de Microsoft

Microsoft Research a développé la bibliothèque Detours, un outil robuste pour intercepter et rediriger les appels de fonctions sous Windows. Elle permet de substituer une fonction cible par une fonction définie par l'utilisateur, offrant ainsi la possibilité d'exécuter du code avant, après, ou à la place de la fonction originale. Detours est particulièrement adaptée aux applications C/C++ et supporte à la fois les architectures 32 bits et 64 bits.

Le mécanisme de base de Detours consiste à remplacer les premières instructions d'une fonction cible par une instruction de saut inconditionnel pointant vers la fonction de remplacement ("detour function").

Prérequis et compilation

Pour utiliser Detours, il est nécessaire de télécharger le code source depuis le dépôt GitHub officiel (https://github.com/microsoft/Detours/releases) et de compiler la bibliothèque pour obtenir les fichiers statiques (.lib) requis. Les en-têtes (comme detours.h) doivent également être inclus dans le projet.

La compilation peut être effectuée à l'aide de la commande nmake dans une invite de commandes développeur Visual Studio appropriée (par exemple, pour x64 ou x86). Des erreurs mineures lors de la compilation des exemples peuvent survenir et ne doivent pas empêcher l'obtention des bibliothèques dans le dossier bin\lib.

Il faut ensuite configurer les chemins d'inclusion et de bibliothèque de votre projet pour pointer vers les artefacts compilés de Detours.

API Detours clés

Avant d'intercepter une fonction, il est crucial d'obtenir son adresse mémoire. Cette adresse est le point d'ancrage pour insérer les instructions de détournement.

Les fonctions principales de l'API Detours sont les suivantes :

  • DetourTransactionBegin() : Initialise une nouvelle transaction pour l'ajout ou la suppression de crochets. Elle doit être appelée avant toute opération de crochetage ou de dé-crochetage.
  • DetourUpdateThread(GetCurrentThread()) : Enregistre le thread courant dans la transaction active.
  • DetourAttach(target_function_ptr, detour_function) : Ajoute un crochet à la fonction cible spécifiée dans la transaction courante. L'application effective du crochet a lieu lors de la validation de la transaction.
  • DetourDetach(target_function_ptr, detour_function) : Retire un crochet de la fonction cible dans la transaction courante.
  • DetourTransactionCommit() : Valide la transaction, appliquant ainsi les ajouts ou suppressions de crochets demandés.

Ces fonctions retournetn une valeur LONG. NO_ERROR (qui est 0) indique le succès, tandis qu'une valeur non nulle signale un échec et peut servir de code d'erreur.

Création de la fonction de remplacement

Une fois le crochet posé, une fonction de remplacement doit être définie. Cette fonction doit idéalement avoir la même signature (type de retour et paramètres) que la fonction originale pour permettre une inspectoin ou une modification des arguments et du résultat. Par exemple, voici une ébauche de fonction pour remplacer MessageBoxA :

INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
  // Ici, vous pouvez examiner ou modifier les paramètres :
  // hWnd, lpText, lpCaption, uType
  // ...
  // return ...;
}

Il est important de noter que la fonction de remplacement ne doit pas avoir plus de paramètres que la fonction originale, sous peine de provoquer une violation d'accès mémoire.

Le piège de la boucle infinie

Un problème courant lors de l'interception est le risque de créer une boucle infinie. Si la fonction de remplacement appelle directement la fonction originale (qui est maintenant crochetée), cela déclenchera à nouveau la fonction de remplacement, menant à une récursion sans fin. Ce n'est pas un défaut de Detours, mais une conséquence logique du mécanisme d'interception.

Le code suivant illustre ce problème : appeler MessageBoxA depuis MyMessageBoxA entraînera une boucle infinie, car MessageBoxA pointera de nouveau vers MyMessageBoxA.

INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
  printf("Original lpText Parameter : %s\n", lpText);
  printf("Original lpCaption Parameter : %s\n", lpCaption);
  
  // ATTENTION : Ceci crée une boucle infinie !
  // return MessageBoxA(hWnd, "different lpText", "different lpCaption", uType); // Appel de MessageBoxA (qui est crochetée)
}

Solution 1 : Pointeur vers la fonction originale

Pour éviter la boucle infinie, Detours permet de sauvegarder un pointeur vers la fonction originale avant de poser le crochet. Ce pointeur peut être stocké dans une variable globale et utilisé dans la fonction de remplacement pour appeler la version non crochetée de la fonction.

// Pointeur global vers la fonction MessageBoxA originale (non crochetée)
fnMessageBox g_pOriginalMessageBoxA = MessageBoxA;

INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
  printf("Original lpText Parameter : %s\n", lpText);
  printf("Original lpCaption Parameter : %s\n", lpCaption);
  
  // Appel à la fonction originale via le pointeur sauvegardé
  return g_pOriginalMessageBoxA(hWnd, "different lpText", "different lpCaption", uType);
}

Solution 2 : Utilisation d'une fonction alternative

Une autre approche générale consiste à appeler une fonction différente mais fonctionnellement similaire, qui n'est pas soumise au même crochet. Par exemple, pour MessageBoxA, on pourrait utiliser MessageBoxW (la version Unicode). Ou pour des allocations mémoire, on pourrait passer de VirtualAlloc à VirtualAllocEx.

INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
  printf("Original lpText Parameter : %s\n", lpText);
  printf("Original lpCaption Parameter : %s\n", lpCaption);
  
  // Appel à une fonction alternative (MessageBoxW)
  return MessageBoxW(hWnd, L"different lpText", L"different lpCaption", uType);
}

Mise en œuvre d'un crochet avec Detours

Detours gère les opérations de crochetage via des transactions. Le processus typique implique la création d'une transaction, l'ajout de l'opération de crochetage (ou de dé-crochetage), puis la validation de la transaction.

// Pointeur global pour la fonction MessageBoxA originale
fnMessageBox g_pOriginalMessageBoxA = MessageBoxA;

// Fonction de remplacement pour MessageBoxA
INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
    printf("[+] Paramètres originaux :\n");
    printf("\t - lpText: %s\n", lpText);
    printf("\t - lpCaption: %s\n", lpCaption);

    // Appel à la version originale non crochetée
    return g_pOriginalMessageBoxA(hWnd, "different lpText", "different lpCaption", uType);
}

// Fonction pour installer le crochet
BOOL InstallHook() {
    DWORD dwDetoursErr = NO_ERROR;

    // Démarrer une transaction
    if ((dwDetoursErr = DetourTransactionBegin()) != NO_ERROR) {
        printf("[!] Erreur DetourTransactionBegin : %d\n", dwDetoursErr);
        return FALSE;
    }
    
    // Mettre à jour le thread courant pour la transaction
    if ((dwDetoursErr = DetourUpdateThread(GetCurrentThread())) != NO_ERROR) {
        printf("[!] Erreur DetourUpdateThread : %d\n", dwDetoursErr);
        return FALSE;
    }
    
    // Attacher le crochet : remplacer MessageBoxA par MyMessageBoxA via le pointeur global
    if ((dwDetoursErr = DetourAttach((PVOID*)&g_pOriginalMessageBoxA, MyMessageBoxA)) != NO_ERROR) {
        printf("[!] Erreur DetourAttach : %d\n", dwDetoursErr);
        return FALSE;
    }

    // Valider la transaction pour appliquer le crochet
    if ((dwDetoursErr = DetourTransactionCommit()) != NO_ERROR) {
        printf("[!] Erreur DetourTransactionCommit : %d\n", dwDetoursErr);
        return FALSE;
    }

    return TRUE; // Crochet installé avec succès
}

Désinstallation du crochet avec Detours

La désinstallation suit un processus similaire à l'installation, en utilisant DetourDetach dans une transaction.

// Pointeur global utilisé pour le dé-crochetage
fnMessageBox g_pOriginalMessageBoxA = MessageBoxA;

// Fonction de remplacement (identique à celle de l'installation)
INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
	printf("[+] Paramètres originaux : \n");
	printf("\t - lpText: %s\n", lpText);
	printf("\t - lpCaption: %s\n", lpCaption);
	return g_pOriginalMessageBoxA(hWnd, "different lpText", "different lpCaption", uType);
}

// Fonction pour désinstaller le crochet
BOOL Unhook() {
	DWORD dwDetoursErr = NO_ERROR;

  	// Démarrer une transaction
	if ((dwDetoursErr = DetourTransactionBegin()) != NO_ERROR) {
		printf("[!] Erreur DetourTransactionBegin : %d \n", dwDetoursErr);
		return FALSE;
	}

	// Mettre à jour le thread courant
	if ((dwDetoursErr = DetourUpdateThread(GetCurrentThread())) != NO_ERROR) {
		printf("[!] Erreur DetourUpdateThread : %d \n", dwDetoursErr);
		return FALSE;
	}

  	// Détacher le crochet de MessageBoxA
	if ((dwDetoursErr = DetourDetach((PVOID*)&g_pOriginalMessageBoxA, MyMessageBoxA)) != NO_ERROR) {
		printf("[!] Erreur DetourDetach : %d \n", dwDetoursErr);
		return FALSE;
	}

  	// Valider la transaction pour retirer le crochet
	if ((dwDetoursErr = DetourTransactionCommit()) != NO_ERROR) {
		printf("[!] Erreur DetourTransactionCommit : %d \n", dwDetoursErr);
		return FALSE;
	}

	return TRUE; // Crochet désinstallé avec succès
}

// Fonction principale démontrant l'utilisation
int main() {
    // Appel initial sans crochet
	MessageBoxA(NULL, "What Do You Think About Malware Development ?", "Original MsgBox", MB_OK | MB_ICONQUESTION);

    // Installation du crochet
	if (!InstallHook()) return -1;

    // Appel pendant que le crochet est actif (déclenche MyMessageBoxA)
	MessageBoxA(NULL, "Malware Development Is Bad", "Original MsgBox", MB_OK | MB_ICONWARNING);
	MessageBoxA(NULL, "Is Bad", " MsgBox", MB_OK | MB_ICONWARNING);

    // Désinstallation du crochet
	if (!Unhook()) return -1;
		
    // Appel après désinstallation (fonctionnement normal rétabli)
	MessageBoxA(NULL, "Normal MsgBox Again", "Original MsgBox", MB_OK | MB_ICONINFORMATION);
  
  	return 0;
}

Code complet

#include <stdio.h>
#include <windows.h>
#include "detours.h"

// Lier la bibliothèque Detours appropriée en fonction de l'architecture
#ifdef _M_X64
#pragma comment(lib, "detours.lib")
#else
#pragma comment(lib, "detours.lib") // Assurez-vous que c'est le bon nom de fichier .lib
#endif 

// Définition d'un type de pointeur de fonction pour MessageBoxA
typedef INT(WINAPI* fnMessageBoxA_t)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);

// Pointeur global pour la fonction MessageBoxA originale
fnMessageBoxA_t g_pOriginalMessageBoxA = (fnMessageBoxA_t)MessageBoxA;

// Fonction de remplacement pour MessageBoxA
INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
    printf("[+] Paramètres originaux reçus :\n");
    printf("\t - lpText: %s\n", lpText);
    printf("\t - lpCaption: %s\n", lpCaption);

    // Appel à la fonction originale non crochetée avec des textes modifiés
    return g_pOriginalMessageBoxA(hWnd, "different lpText", "different lpCaption", uType);
}

// Fonction pour installer le crochet sur MessageBoxA
BOOL InstallHook() {
    DWORD dwDetoursErr = NO_ERROR;

    // Initialiser la transaction de crochetage
    if ((dwDetoursErr = DetourTransactionBegin()) != NO_ERROR) {
        fprintf(stderr, "[!] Erreur lors de l'initialisation de la transaction : %lu\n", dwDetoursErr);
        return FALSE;
    }

    // Ajouter le thread courant à la transaction
    if ((dwDetoursErr = DetourUpdateThread(GetCurrentThread())) != NO_ERROR) {
        fprintf(stderr, "[!] Erreur lors de la mise à jour du thread : %lu\n", dwDetoursErr);
        DetourTransactionAbort(); // Annuler la transaction en cas d'échec
        return FALSE;
    }

    // Attacher le crochet : remplacer l'adresse de MessageBoxA par MyMessageBoxA
    // Note: Le premier argument est un PVOID*, donc on passe l'adresse du pointeur global
    if ((dwDetoursErr = DetourAttach((PVOID*)&g_pOriginalMessageBoxA, MyMessageBoxA)) != NO_ERROR) {
        fprintf(stderr, "[!] Erreur lors de l'attachement du crochet : %lu\n", dwDetoursErr);
        DetourTransactionAbort(); // Annuler la transaction en cas d'échec
        return FALSE;
    }

    // Valider la transaction pour appliquer les modifications
    if ((dwDetoursErr = DetourTransactionCommit()) != NO_ERROR) {
        fprintf(stderr, "[!] Erreur lors de la validation de la transaction : %lu\n", dwDetoursErr);
        // La fonction DetourAttach pourrait avoir déjà partiellement appliqué le crochet,
        // donc un DetourTransactionAbort() ici pourrait être nécessaire si on veut garantir un état propre.
        return FALSE;
    }

    printf("[+] Crochet sur MessageBoxA installé avec succès.\n");
    return TRUE; // Succès
}

// Fonction pour désinstaller le crochet de MessageBoxA
BOOL Unhook() {
    DWORD dwDetoursErr = NO_ERROR;

    // Initialiser la transaction de dé-crochetage
    if ((dwDetoursErr = DetourTransactionBegin()) != NO_ERROR) {
        fprintf(stderr, "[!] Erreur lors de l'initialisation de la transaction (Unhook) : %lu\n", dwDetoursErr);
        return FALSE;
    }

    // Ajouter le thread courant à la transaction
    if ((dwDetoursErr = DetourUpdateThread(GetCurrentThread())) != NO_ERROR) {
        fprintf(stderr, "[!] Erreur lors de la mise à jour du thread (Unhook) : %lu\n", dwDetoursErr);
        DetourTransactionAbort();
        return FALSE;
    }

    // Détacher le crochet : retirer MyMessageBoxA de l'interception de MessageBoxA
    if ((dwDetoursErr = DetourDetach((PVOID*)&g_pOriginalMessageBoxA, MyMessageBoxA)) != NO_ERROR) {
        fprintf(stderr, "[!] Erreur lors du détachement du crochet : %lu\n", dwDetoursErr);
        DetourTransactionAbort();
        return FALSE;
    }

    // Valider la transaction pour appliquer le retrait du crochet
    if ((dwDetoursErr = DetourTransactionCommit()) != NO_ERROR) {
        fprintf(stderr, "[!] Erreur lors de la validation de la transaction (Unhook) : %lu\n", dwDetoursErr);
        return FALSE;
    }

    printf("[+] Crochet sur MessageBoxA désinstallé avec succès.\n");
    return TRUE; // Succès
}

int main() {
    // 1. Appel original à MessageBoxA avant toute modification
    printf("Appel 1: MessageBoxA original.\n");
    MessageBoxA(NULL, "Première boîte de message.", "Test original", MB_OK | MB_ICONINFORMATION);

    printf("\n");

    // 2. Installer le crochet
    if (!InstallHook()) {
        fprintf(stderr, "Échec de l'installation du crochet. Sortie.\n");
        return 1;
    }

    // 3. Appel à MessageBoxA après installation du crochet
    // Ceci devrait déclencher MyMessageBoxA au lieu de MessageBoxA directement.
    printf("Appel 2: MessageBoxA après installation du crochet.\n");
    MessageBoxA(NULL, "Message intercepté.", "Test intercepté", MB_OK | MB_ICONWARNING);
    // Un deuxième appel pour montrer que ça fonctionne toujours
    printf("Appel 3: Autre MessageBoxA intercepté.\n");
    MessageBoxA(NULL, "Encore intercepté.", "Test 2 intercepté", MB_OK | MB_ICONQUESTION);


    printf("\n");

    // 4. Désinstaller le crochet
    if (!Unhook()) {
        fprintf(stderr, "Échec de la désinstallation du crochet. Sortie.\n");
        return 1;
    }

    // 5. Appel à MessageBoxA après désinstallation du crochet
    // Le comportement original devrait être restauré.
    printf("Appel 4: MessageBoxA après désinstallation du crochet.\n");
    MessageBoxA(NULL, "Dernier message, comportement restauré.", "Test restauré", MB_OK | MB_ICONASTERISK);
  
    printf("\nFin du programme.\n");
    return 0; // Succès
}

Étiquettes: API hooking Detours Windows Interception reverse engineering

Publié le 23 août à 17h38