[winnt] intercepter les appels aux api win32

Description

Voilà, je suis tombé il y a pas longtemps sur une fonction qui permettait d'intercepter un appel à une fonction de Windows, mais seulement dans le même processus. Je m'expliqe:
Chaque processus (programme) en cours d'exécution a en mémoire plusieus modules (DLL) comme Kernel32.dll, User32.dll etc. Cette fonction permettait d'intercepter tous les appels à une fonction précise d'un module (par exemple CreateFile dans Kernel32.dll) et de la remplacer par une fonction personelle, mais seulement dans le même processus. C'est à dire qu'on ne pouvait pas s'en servir pour intercepter les appels d'un autre processus, juste de son propre processus.

Comme je voulais pouvoir espionner les appels à certaines fonctions (comme la création, suppression de fichiers, ou l'accès au registre) de certains programmes, j'ai créé plusieurs DLL pour y arriver:

-SharpSpyHelper.dll, en C avec une partie d'ASM inline, qui est injectée dans le processus à espionner (pour l'exemple notepad) et qui contient tout ce qu'il faut pour hooker une fonction (la remplacer par une autre)

-SharpSpy.dll, en C# (mais qui peut être fait en C/C++, c'est pas le problème) qui injecte l'autre DLL dans le processus (notepad) et qui communique avec elle quand les fonctions considérées sont appellées (là pour l'exemple, j'intercepte les appels de notepad à LoadIconW, et je lui fait charger un icône personalisé)

Le seul problème c'est qu'injecter une DLL dans un processus nécessite d'y créer un thread avec la fonction CreateRemoteThread avec pour point d'entrée la fonction LoadLibrary (Kernel32.dll), c'est à dire que dès que le thread est créé il va charger dans le processus ma DLL. Et comme cette fonction n'est disponible que sur Windows NT/2000/XP, on ne pas intercepter des fonctions d'autres processus avec Windows 95/98/ME, désolé!

Aussi, comme la mémoire de chaque processus est isolée, si on veut faire exécuter à un autre processus une fonction qui a pour arguments des pointeurs ou des chaînes, il faut allouer de la mémoire dans ce processus avec VirtualAllocEx pour y mettre le contenu des arguments, mais cette fonction n'est aussi disponible que sous NT/2000/XP...

Source / Exemple :


Pour utiliser SharpSpyHelper.dll et/ou SharpSpy.dll:

[C#]
//Ajouter la référence 'SharpSpy.dll'
//Définir un délégué pour la fonction à appeller:
delegate IntPtr LoadIconWHandler( IntPtr hInstance, IntPtr lpIconName );
//Et écrire la fonction qui doit être appellée à la place de l'API
IntPtr MyLoadIconW( IntPtr hInstance, IntPtr lpIconName )
{
   <...>
}

//puis ajouter ce code quelque part:

//INITIALISATION
//processus dans lequel intercepter des API
Process processus = Process.GetProcessesByName( "notepad" )[0];
SystemProcess proc = new SystemProcess( processus );
//tas de mémoire du processus
MiniHeap heap = proc.Heap;
SharpSpyHelper helper;
HookTable hook;
uint error = 0;
//on injecte SharpSpyHelper.dll dans le processus
uint helperAddress = proc.InjectLibraryEx( @"<Chemin de la DLL SharpSpyHelper.dll>", out error );

if( (helperAddress != 0) && (error == 0) )
{
    helper = new SharpSpyHelper( proc, @"<Chemin de la DLL SharpSpyHelper.dll>", (IntPtr)address );
    //on récupère la table des hooks
    table = helper.Table;
    helper.InitMsgWindow();	//helper.InitMsgWindow( this.Handle ); si le code s'exécute dans une Form    
}

//CREATION DU HOOK
//on récupère un handle du processus
uint hProcess = helper.GetProcessHandle(); 
//on crée un hook: <DLL>, <Fonction à hooker>
//ici HICON LoadIconW( HINSTANCE hInstance, LPCTSTR lpIconName );
HookParams hParams = new HookParams( "User32.dll", "LoadIconW", heap );
hParams.HookName = "NativeCallSinkProxy";
hParams.Module = hProcess;
hParams.HookedAddress = helper.GetFunctionAddress( "CallSinkProxy" );
//Taille des arguments en octets
 hParams.ArgumentsSize = 8;
//Taille de la valeur de retour en octets
hParams.ReturnValueSize = 4;
//on définit la fonction a appeller à la place de l'originale
hParams.Callback = new LoadIconWHandler( MyLoadIconW ); 
//on ajoute le hook à la table des hooks
table.Add( hParams );
//on initialise la table
table.Init();

//on effectue le hook
if( helper.HookByID( hParams.HookID ) )
{
    //Le hook a réussi!
    //tous les appels à LoadIconW dans notepad seront remplacés par un appel à MyLoadIconW
    //rafraîchit les infos de la table des hooks, comme l'adresse originale de la fonction
    table.Resfresh();
}

[C++ Managé]
Pas de différence avec la version C# si ce n'est dans la syntaxe... (pointeurs __gc, etc)

[C/C++]
//inclure les headers de Windows
#include <windows.h>
#include <winnt.h>
//etc
#include "SharpSpyHelper.h"
//récupérer un handle du processus d'abord
HANDLE hProcess; 

LPWSTR lpModule = L"<Chemin de la DLL SharpSpyHelper.dll>";
DWORD cbModule = (wcslen( lpModule ) + 1) * 2;
//on alloue de la mémoire dans le processus
LPVOID hMem = VirtualAllocEx( hProcess,  NULL,  cbModule,  MEM_COMMIT,  PAGE_READWRITE );
DWORD dwWritten;
//puis on écrit dans la mémoire du processus le chemin de SharpSpyHelper.dll
WriteProcessMemory( hProcess, hMem, lpModule, cbModule, &dwWritten );

//on récupère l'addresse de LoadLibraryW dans Kernel32.dll, commune à tous les procesus
DWORD hKernel = GetModuleHandle( _T("Kernel32.dll") );
DWORD hLL = GetProcAddress( _T("LoadLibraryW") );
		
//on va injecter SharpSpyHelper.dll dans le processus
//c'est à dire y créer un thread qui va exécuter LoadLibraryW( "SharpSpyHelper.dll" )
//qui contient tous les fonctions de hook.

HANDLE hThread = NULL;
DWORD dwThreadId;
DWORD hModuleAddress;
hThread = CreateRemoteThread( hProcess, NULL, hLL, hMem, 0, &dwThreadId );

if( hThread != NULL )
{
    //On attend que le thread se termine
    WaitForSingleObjectEx( hThread, TIMEOUT_INFINITE, TRUE );
    //on récupère la valeur de retour du thread: soit l'addresse de la DLL chargée,
    //soit un code d'erreur si elle n'a pas été trouvée
    GetExitCodeThread( hThread, &hModuleAddress );
}
//on libère la mémoire allouée pour le chemin de la DLL
VirtualFreeEx( hProcess, hMem, cbModule, MEM_RELEASE );

//à partir de la SharpSpyHelper.dll est chargé dans le processus (notepad par exemple)
//il reste plus qu'à obtenir l'addresse de la fonction GetMyProcAddress dans SharpSpyHelper.dll
//avec CreateRemoteThread mais en passant l'addresse de GetProcAddress (toujours Kernel32.dll)
//et comme paramètre la chaîne _T("GetMyProcAddress")
//une fois qu'on a l'addresse de GetMyProcAddress, il suffit de l'appeller (toujours avec CreateRemoteThread)
//en passant un des paramètre suivant: (pour la liste complete voir SharpSpyHelper.h)

#define PROCADDRESS_DLLMAIN			1
#define PROCADDRESS_GETPROCESSHANDLE	                2
#define PROCADDRESS_INDIRECTCALLEX		3
#define PROCADDRESS_HOOK			4

//pour avoir l'addresse d'une fonction
//par exemple en appelant GetMyProcAddress( PROCADDRESS_INDIRECTCALLEX )
//on obtient l'addresse de la fonction IndirectCallEx dans SharpSpyHelper.dll
hThread = CreateRemoteThread( hProcess, NULL, <addresse de GetMyProcAddress>, PROCADDRESS_INDIRECTCALLEX, 0, &dwThreadId );
//etc...

//On récupère l'addresse de la fonction IndirectCallEx que voila:
void __stdcall IndirectCallEx( NCallParams* lpCallParams );

struct NCallParams
{
	DWORD address;				
	DWORD* lpStack;				
	DWORD cbStack;				
	void* lpReturn;				
	DWORD cbReturn;				
	BOOL cdeclCall;				
};
//Cette fonction permet d'appeller avec CreateRemoteThread une fonction
//qui a plus d'un argument. Par exemple, pour appeller

void InitHookList( void* lpList, DWORD cbList );

void* lpList;
DWORD cbList;
//un pointeur 32 bits et un DWORD = 8 octets
BYTE* lpStack = (BYTE*)malloc( 8 );
//on copie lpList et cbList dans lpStack
memcpy( lpStack, &lpList, 4 );
memcpy( lpStack + 4, &cbList, 4 );

//il faut d'abord créer une structure NCallParams
NCallParams nc;
//à récupérer avec GetMyprocAddress (addresse de InitHookList)
nc.address = lpInitHookList;
//pile de 8 octets
nc.lpStack = (DWORD*)lpStack;
nc.cbStack = 8;
//pas de valeur de retour
nc.lpReturn = NULL; 		
nc.cbReturn = 0;
//convention d'appel __stdcall
nc.cdeclCall = FALSE; 		

//on appelle enfin la fonction avec CreateRemoteThread
DWORD dwWritten;
LPVOID hParams = VirtualAllocEx( hProcess, NULL, sizeof(NCallParams), MEM_COMMIT, PAGE_READWRITE );
WriteProcessMemory( hProcess, hParams, &nc, sizeof(NCallParams), &dwWritten );
hThread = CreateRemoteThread( hProcess, NULL, <addresse de IndirectCallEx>, hParams, 0, &dwThreadId );
//idem qu'avant avec WaitForSingleObjectEx, GetExitCodeThread et VirtualFreeEx
//on libère aussi la mémoire de la pile
free( lpStack );

//Pour faire le hook:
//Créer une structure HookParams, comme dans l'exemple C#
struct HookParams
{
	DWORD dwHookID;				
	HMODULE hModule;			
	LPSTR library;				
	LPSTR function;				
	DWORD originalAddress;		
	DWORD hookedAddress;		
	DWORD thunkAddress;			
	DWORD argsSize;				
	DWORD returnSize;			
	DWORD jumpAddress;			
	BOOL  hooked;				
};

HookParams hook;
hook.dwHookID = 0;
//à récupérer avec GetProcessHandle( 0 ) dans SharpSPyHelper.dll
hook.hModule = <hModule du processus>;
hook.library = "User32.dll";
hook.function = "LoadIconW";
//à récupérer avec GetMyProcAddress( PROCADDRESS_CALLSINKPROXY )
hook.originalAddress = NULL;
hook.hookedAddress = <Adresse de NativeCallSinkProxy>;
hook.thunkAddress = NULL;
hook.argsSize = 8;
hook.returnSize = 4;
hook.jumpAddress = NULL;
hook.hooked = FALSE;

//on copie le contenu de la structure HookParams dans la mémoire du processus,
//avec VirtualAllocEx et WriteProcessMemory
LPVOID lpHook = <VirtualAllocEx( ... ) >

//On exécute InitHookList avec GetMyProcAddres, CreateRemoteThread et IndirectCallEx
InitHookList( lpHook, sizeof(HookParams) );
//Puis on active le hook (ici, 0 est l'ID du hook - voir plus haut) ici pas besoin d'IndirectCallEx (un seul argument)
if( HookByID( 0 ) ) 
{
     // le hook est réussi!
}

//après ca il reste juste à gérer dans la boucle des messages de la fenêtre de l'application
//gérer les messages WM_COPYDATA:
//un message WM_COPYDATA est envoyé à chaque fois qu'une fonction hookée est appellée
//avec les paramètres de l'appel (ID du hook, arguments...)
InitMsgWindow( <hWnd de la fênetre> );

//Dans WndProc
LRESULT WndProc( HWND, UINT, WPARAM, LPARAM );
//dans le switch des messages de la fenêtre
switch( uMsg )
{
     case WM_COPYDATA:
     {
          	LPCOPYDATA lpcd = (LPCOPYDATA)LPARAM;
          	void* pData = malloc( lpcd.cbData ); 
          
          	memcpy( pData, lpcd.lpData, lpcd.cbData );
          	//traiter les données reçues: (voir GetSendCallBuffer dans SharpSpyHelper.cpp)
          	//struct
	//{
	//    BYTE bType = COMM_CALL;
	//    DWORD dwSize;		//taille du message
	//    DWORD dwCallID;		//ID d'appel - ignorer
	//    DWORD dwHookID;		//ID du hook appellé
	//    DWORD cbStack;		//taille de la pile
	//    //si cbStack > 0
	//    BYTE stack[cbStack];	//données de la pile (paramètres de la fonction)
	//};
	//Comme HICON LoadIconW( HINSTANCE hInstance, LPCTSTR lpIconName );
	HINSTANCE hInstance;
	LPCTSTR lpIconName;
	//on extrait les paramètres de la pile (stack)
	memcpy( &hInstance, &stack, 4 );
	memcpy( &lpIconName, &stack + 4, 4 );

	//appeller la fonction qui remplace la fonction hookée
	//elle doit retourner un HICON
	HICON hIcon;
	
	//renvoyer un pointeur vers une structure, 
	//pour la valeur de retour
	//struct
	//{
	//    BYTE bType = COMM_ANSWER;
	//    DWORD dwSize;		//taille du message
	//    DWORD dwCallID;		//ID d'appel - mettre le même
	//    DWORD dwHookID;		//ID du hook - idem
	//    DWORD cbReturn;		//taille de la valeur de retour
	//    //si cbReturn > 0
	//    BYTE returnVal[cbReturn];	//valeur de retour
	//};
	cbReturn = 4; 		//sizeof(HICON)
	bType = COMM_ANSWER;
	//on alloue une BYTE, 4 DWORDs plus de la place pour returnVal
	dwSize = 17 + cbReturn;
	BYTE* pAns = (BYTE*)malloc( dwSize );
	//on remplit la 'structure'
	memcpy( pAns, &bType, 1 );
	memcpy( pAns + 1, &dwSize, 4 );	
	memcpy( pAns + 5, &dwCallID, 4 );
	memcpy( pAns + 9, &dwHookID, 4 );
	memcpy( pAns + 13, &cbReturn, 4 );
	memcpy( pAns + 17, &hIcon, 4 );	//valeur de retour
	//on libère la mémoire allouée pour les données reçues
	free( pData );
	//on retourne la 'structure' de retour
	return pAns;
     }
}

Conclusion :


Les DLL SharpSpy et SharpSpyHelper que j'ai écrites sont surtout axées sur du code .NET (C++ Managé, C# ou VB.NET) mais peuvent être utilisées avec du code C/C++ comme j'ai essayé de le montrer avec le pseudo-code de l'exemple en C/C++.
Ce qu'il faut surtout retenir c'est que les fonctions qu'on intercepte ne s'exécutent pas dans le processus en cours, donc que tous les handles et pointeurs (HWND, mémoire...) ne sont pas valide et qu'il faut les copier dans l'autre processus avec WriteProcessMemory ou les récupérer après un appel avec ReadProcessMemory. Pour les handles il faut les faires créer en faisant exécuter les bonnes fonctions sur l'autre processus avec CreateRemoteThread (Par exemple il faut faire exécuter CreateFile sur l'autre processus pour accéder à un fichier).

Voilà, j'espère que ça servira à quelqu'un, malgré la complexité (mais je me suis tapé le plus gros avec SharpSpyHelper et l'ASM inline...)

Codes Sources

A voir également

Vous n'êtes pas encore membre ?

inscrivez-vous, c'est gratuit et ça prend moins d'une minute !

Les membres obtiennent plus de réponses que les utilisateurs anonymes.

Le fait d'être membre vous permet d'avoir un suivi détaillé de vos demandes et codes sources.

Le fait d'être membre vous permet d'avoir des options supplémentaires.