Sous-classement de fenêtre d'un autre process par injection dll

5/5 (11 avis)

Vue 7 675 fois - Téléchargée 612 fois

Description

Voici un code source montrant comment sous-classer une fenêtre appartenant à un autre process. La technique de l'injection de code par dll est utilisée. Le but de ce sous-classement est de pouvoir intercepter les messages destinés à n'importe quelle fenêtre cible afin de les traiter de manière personnalisée.
L'injecteur est sous forme de boite de dialogue disposant d'un combobox qui contiendra les noms de toutes les fenêtres visibles à l'écran. Le bouton "Injecter" permet d'injecter et exécuter le code de notre dll dans le process de la fenêtre choisie. Le HWND de cette dernière est transmis via le presse-papier au DllMain de notre dll en utilisant un format privé. Un bouton "Actualiser" permet de rafraichir le contenu du combobox.
La dll contient deux fonctions: Le DllMain et notre procédure de sous-classement. Dans le DllMain, l'adresse de la procédure originale de la fenêtre à sous-classer est sauvegardée dans une propriété assignée pour l'occasion à cette fenêtre grâce à SetProp(). GetProp() permet de retrouver cette adresse originale dans la procédure de sous-classement afin d'y aiguiller les messages qu'on ne désire pas traiter. Ainsi, chaque fenêtre retrouve sa propre procédure originale si le message qui lui parvient n'est pas capturé par notre procédure de sous-classement. Dans cet exemple, le message WM_SYSCOMMAND dont wParam vaut SC_CLOSE est intercepté pour afficher un petit message prouvant que le sous-classement est effectif chaque fois qu'on décide de fermer la fenêtre sous-classée. Libre à vous de modifier le code pour traiter les messages que vous voulez.
Ce code source se compose de deux projets: celui de l'injecteur et celui de la dll. Cette dernière est incluse en ressource binaire de l'exécutable injecteur qui l'extrait dans son dossier s'il ne la trouve pas.
Les deux petits projets ont été faits sous Visual C/C++ 2005 et sont facilement adaptables. Celui de la dll est en pur C.
Il est à noter que par souci de clarté du code source, généralement les valeurs de retour des fonctions ne sont pas traitées.
Pour tester l'injecteur, renommez-le en Injecteur.exe. Des tests ont été faits sans aucun problème sur Windows XP, Vista et 7.
Voilà, j'espère n'avoir rien oublié.
Toutes les remarques, questions et commentaires sont les bienvenus.

Source / Exemple :


//********************** Code de l'injecteur **********************
#include <windows.h>

DWORD Inject(HWND hwndclipboard, HWND hwndcible)
{
	char dllpath[MAX_PATH];
	// Obtenir le chemin complet de notre exécutable:
	GetModuleFileName(0,dllpath,MAX_PATH);
	// Remplacer le nom de l'exécutable par le nom de la dll:
	lstrcpy(strrchr(dllpath,'\\')+1,"wndprocdll.dll");
	// Vérifier l'existence de la dll dans le dossier de l'exe:
	DWORD attrib=GetFileAttributes(dllpath);
	// Extraire la dll depuis les ressouces de l'exe si elle n'existe pas:
	if(attrib==INVALID_FILE_ATTRIBUTES)
	{
		// Trouver la ressource:
		HRSRC hbinresource=FindResource(0,"IDB_DLL","BINARY");
		// Déterminer sa taille:
		DWORD dwtaille=SizeofResource(0,hbinresource);
		// Charger la ressource en mémoire:
		HGLOBAL hResource=LoadResource(0,hbinresource);
		// Obtenir un pointeur sur cette zone mémoire:
		char* pbuf=(char*)LockResource(hResource);
		// Créer le fichier destination:
		HANDLE hFichier=CreateFile(dllpath,GENERIC_WRITE,0,0,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0);
		// S'assurer que le fichier a bien été créé:
		if(hFichier!=INVALID_HANDLE_VALUE)
		{
			// Copier la ressource dans le fichier:
			DWORD dwecrits;
			WriteFile(hFichier,pbuf,dwtaille,&dwecrits,0);
			// Fermer le fichier:
			CloseHandle(hFichier);
		}
		// Libérer la ressource:
		UnlockResource(hResource);
		FreeResource (hResource);
	}
	// Obtenir l'identificateur du process auquel appartient la fenêtre:
	DWORD dwpid;
	DWORD threadid=GetWindowThreadProcessId(hwndcible,&dwpid);
	// Ouvrir ce process:
	HANDLE hProcess=OpenProcess( PROCESS_ALL_ACCESS, 0,dwpid);
	// Obtenir la longueur du chemin complet de notre dll:
	DWORD dwpathlen=lstrlen(dllpath);
	// Allouer de la mémoire dans ce process pour y stocker le chemin de notre dll:
	char* pmem=(char*)VirtualAllocEx(hProcess,0,dwpathlen+1,MEM_COMMIT,PAGE_READWRITE);
	// Copier le chemin de notre dll vers la mémoire allouée dans le process cible:
	BOOL bret=WriteProcessMemory(hProcess,(LPVOID) pmem,(LPVOID) dllpath, dwpathlen+1, NULL);
	// Obtenir le HMODULE de kernel32.dll:
	HMODULE hkernel32=GetModuleHandle("Kernel32");
	// Obtenir l'adresse de la fonction LoadLibraryA de kernel32.dll:
	PTHREAD_START_ROUTINE pfnThreadRtn=(PTHREAD_START_ROUTINE)GetProcAddress(hkernel32, "LoadLibraryA");
	// Ouvrir le presse-papier:
	OpenClipboard(hwndclipboard);
	// Mettre le HWND de la fenêtre cible dans le presse-papier:
	SetClipboardData(CF_PRIVATEFIRST ,hwndcible);// Format privé
	// Fermer le presse-papier:
	CloseClipboard();
	// Créer et lancer un thread distant dans le process cible:
	HANDLE hThread= CreateRemoteThread(hProcess, NULL, 0,pfnThreadRtn, pmem, 0, NULL);
	// Attendre que le thread soit terminé:
	WaitForSingleObject(hThread, INFINITE);
	// Libérer la mémoire allouée précédemment:
	VirtualFreeEx(hProcess, pmem, 0, MEM_RELEASE);
	// Fermer les handles:
	CloseHandle(hThread);
	CloseHandle(hProcess);
	return 0;
}

// Procédure d'énumération des fenêtres et de remplissage du combobox:
BOOL CALLBACK EnumWindowsProc(HWND hwnd,LPARAM lParam)
{
	static		char buffer[MAX_PATH];
	// S'assurer que la fenêtre est visible:
	if(IsWindowVisible(hwnd))
	{
		// Récupérer le HWND de notre combobox depuis lParam:
		HWND hCombo=(HWND)lParam;
		// S'assurer que la fenêtre a un titre et qu'il ne s'agit pas de celle de notre injecteur:
		if(GetWindowTextLength(hwnd) && hwnd!=GetParent(hCombo))
		{
			// Obtenir le nom de la classe de fenêtre:
			GetClassName(hwnd,buffer,MAX_PATH);
			// S'assurer que le nom de classe est différent de "progman":
			if(lstrcmpi(buffer,"progman"))
			{
				// Obtenir le texte du titre de la fenêtre:
				GetWindowText(hwnd,buffer,MAX_PATH);
				// Ajouter ce texte comme élément au combobox:
				SendMessage(hCombo,CB_INSERTSTRING,0,(LPARAM)buffer);
				// Mettre le HWND de la fenêtre comme valeur associée à l'élément:
				SendMessage(hCombo,CB_SETITEMDATA,0,(LPARAM)hwnd);
			}
		}
	}
	return TRUE;
}

// Procédure de notre boite de dialogue:
BOOL CALLBACK DlgProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	static HWND hCombo,hFermer,hInjecter,hActualiser;
	switch(message)
	{

	case WM_INITDIALOG:
		{
			// Définir le titre de notre boite de dialogue:
			SetWindowText(hwnd,"Injecteur");
			// Créer les contrôles:
			CreateWindowEx(0,"static","Liste des fenêtres :",WS_CHILD | WS_VISIBLE,20,10,360,20,hwnd,0,0,0);
			hCombo=CreateWindowEx(0,"combobox",0,WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_VSCROLL,20,28,360,150,hwnd,0,0,0);
			hActualiser=CreateWindowEx(0,"button","Actualiser",WS_CHILD | WS_VISIBLE ,20,170,80,20,hwnd,0,0,0);
			hInjecter=CreateWindowEx(0,"button","Injecter",WS_CHILD | WS_VISIBLE | WS_DISABLED ,160,170,80,20,hwnd,0,0,0);
			hFermer=CreateWindowEx(0,"button","Fermer",WS_CHILD | WS_VISIBLE ,300,170,80,20,hwnd,0,0,0);
			// Enumérer les fenêtres en remplissant le combobox:
			EnumWindows(EnumWindowsProc,(LPARAM)hCombo);
			// Obtenir la police par défaut des boites de dialogue:
			HFONT hguifont=(HFONT)GetStockObject(DEFAULT_GUI_FONT);
			// Appliquer cette police à nos contrôles:
			HWND child=0;
			while(child=FindWindowEx(hwnd,child,0,0))SendMessage(child,WM_SETFONT,(WPARAM)hguifont,0);
		}
		break;

	case WM_COMMAND:
		// Dégriser le bouton "Injecter" si un élément du combobox a été sélectionné:
		if(HIWORD(wParam)==CBN_SELENDOK) EnableWindow(hInjecter,1);
		// Clic sur "Injecter":
		if((HWND)lParam==hInjecter)
		{
			// Obtenir l'index de la séléction courante du combobox:
			int cursel=(int)SendMessage(hCombo,CB_GETCURSEL,0,0);
			// Récupérer le HWND associé comme valeur à l'élément sélectionné:
			HWND hwndcible=(HWND)SendMessage(hCombo,CB_GETITEMDATA,cursel,0);
			// S'assurer que la fenêtre n'a pas déjà été sous-classée:
			if(GetProp(hwndcible,"PROP_WNDPROC"))
			{
				MessageBox(hwnd,"La fenêtre choisie a déjà été sous-classée.","Injecteur",0);
				return 0;
			}
			// Lancer la fonction d'injection si la fenêtre est valide:
			if(IsWindow(hwndcible))Inject(hwnd,hwndcible);
			// Sinon afficher un message indiquant que le HWND n'est pas valide:
			else MessageBox(hwnd,"Le HWND de la fenêtre choisie n'est plus valide.","Injecteur",0);	
			return 0;
		}
		// Clic sur "Actualier":
		if((HWND)lParam==hActualiser)
		{
			// Vider le combobox:
			SendMessage(hCombo,CB_RESETCONTENT,0,0);
			// Griser le bouton "Injecter":
			EnableWindow(hInjecter,0);
			// Enumérer les fenêtres en remplissant le combobox:
			EnumWindows(EnumWindowsProc,(LPARAM)hCombo);
			return 0;
		}
		// Fermer la boite de dialogue si clic sur "Fermer":
		if((HWND)lParam==hFermer)SendMessage(hwnd,WM_CLOSE,0,0);
		break;

	case WM_CLOSE:
		// Détruire la boite de dialogue:
		EndDialog(hwnd,0);
		break;
	default:
		break;
	}
	return 0;
}
int APIENTRY WinMain (HINSTANCE hinst, HINSTANCE hprev, LPSTR szcmd, int ishow)
{
	// Allouer de la mémoire pour notre DIALOG TEMPLATE:
	LPDLGTEMPLATE lpdt=(LPDLGTEMPLATE)GlobalAlloc(GPTR,512);
	// Définir les dimensions de notre boite dedialogue:
	lpdt->cx=200; lpdt->cy=100;
	// Définir les styles de notre boite de dialogue:
	lpdt->style=DS_CENTER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
	// Lancer notre boite de dialogue:
	DialogBoxIndirectParam(GetModuleHandle(0),lpdt,0,(DLGPROC)DlgProc,0);
	// Libérer la mémoire allouée pour notre DIALOG TEMPLATE:
	GlobalFree(lpdt);
	return 0;
}
//***********************************************************************

//*********************** Code de la DLL ********************************
#include <windows.h>

// Notre procédure de sous-classement:
LRESULT CALLBACK MyWndProc(HWND hwnd,UINT message, WPARAM wParam, LPARAM lParam)
{
	WNDPROC OldWndProc;
	// Récupérer l'adresse de la procédure originale depuis la propriété "PROP_WNDPROC" assignée à la fenêtre:
	OldWndProc=(WNDPROC)GetProp(hwnd,"PROP_WNDPROC");
	switch(message)
	{
	case WM_SYSCOMMAND:
		if(wParam==SC_CLOSE)
		{
			// Afficher le message prouvant le sous-classement de la fenêtre:
			MessageBox(hwnd,"Ce message prouve que la fenêtre a bien été sous-classée.","Sous-Classement par injection de code.",0);
		}
		break;
	case WM_NCDESTROY:
		// Retirer la propriété "PROP_WNDPROC" préalablement assignée à la fenêtre sous-classée:
		RemoveProp(hwnd,"PROP_WNDPROC");
		break;
	default:
		break;
	}
	// Appeler la procédure originale:
	return CallWindowProc(OldWndProc,hwnd,message,wParam, lParam);
}

// La fonction d'entrée DllMain:
int APIENTRY DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
	if (dwReason == DLL_PROCESS_ATTACH)
	{
		HWND hwnd;
		// Ouvrir le presse-papier:
		OpenClipboard(0);
		// récupérer le HWND de la fenêtre cible transmis par le programme injetceur:
		hwnd=(HWND)GetClipboardData(CF_PRIVATEFIRST);
		// Fermer le presse-papier:
		CloseClipboard();
		// S'assurer que le HWND est valide et que la propriété "PROP_WNDPROC" ne lui est pas assignée:
		if(IsWindow(hwnd) && !GetProp(hwnd,"PROP_WNDPROC"))
		{
			WNDPROC OldWndProc;
			// Récupérer l'adresse de la procédure originale de la fenêtre cible:
			OldWndProc=(WNDPROC)GetWindowLong(hwnd,GWL_WNDPROC);
			//Sauvegarder cette adresse dans la propriété "PROP_WNDPROC" à assigner à la fenêtre: 
			SetProp(hwnd,"PROP_WNDPROC",(HANDLE)OldWndProc);
			// Remplacer la procédure originale par la notre:
			SetWindowLong(hwnd, GWLP_WNDPROC, (LONG)MyWndProc);
		}
	}
	return 1; 
}
//**********************************************************************

Conclusion :


Téléchargez le zip pour voir les deux projets.

Codes Sources

A voir également

Ajouter un commentaire Commentaires
thenaoh Messages postés 111 Date d'inscription dimanche 28 septembre 2003 Statut Membre Dernière intervention 24 juin 2015
15 juin 2010 à 17:08
Merci beaucoup pour ce code !

Sauf que j'ai un petit souci, car chez moi, ça ne marche pas ... J'ai créé 2 projets sous Dev C++ (un pour l'exécutable, un pour la DLL), et j'ai copié pour chacun des 2 projets le code que tu as mis en ligne. Mon premier souci a été que "PTHREAD_START_ROUTINE" me générait une erreur de compilation. Après investigation, il semblerait que ce soit un autre nom pour "LPTHREAD_START_ROUTINE", et j'ai donc fait le changement dans mon code. Maintenant, ça compile bien, mais lorsque je lance ton code et que clique sur "Injecter", rien ne semble injecté (je n'ai pas la popup "Ce message prouve que ..." quand je ferme la fenêtre cible), alors que l'exécutable que tu as fourni dans le zip marche.

Je cherche d'où ça peut venir, mais pour l'heure, je ne trouve pas. J'ai également essayé avec Visual Studio 2008 (et donc sans changer "PTHREAD_START_ROUTINE" cette fois), mais pareil, rien ne se passe.

Saurais-tu m'éclairer ?
thenaoh Messages postés 111 Date d'inscription dimanche 28 septembre 2003 Statut Membre Dernière intervention 24 juin 2015
15 juin 2010 à 17:49
Précisions concernant le problème rencontré : Ton code fonctionne lors de la 1ère utilisation ! Si je ferme et que je relance ton appli pour reproduire le test, là, plus rien ne se passe. N'y aurait-il pas un problème de ressource quelconque non libérée ? Car après avoir lancé puis quitté ton appli, je ne peux pas supprimer la DLL "wndprocdll.dll" (par contre je peux la renommer, ainsi que renommer le répertoire parent ... Comprenne qui pourra ...).

Merci !
racpp Messages postés 1910 Date d'inscription vendredi 18 juin 2004 Statut Modérateur Dernière intervention 14 novembre 2014 15
15 juin 2010 à 21:54
Tu dois voir du coté de la dll. Comme précisé dans la présentation, le code de la dll est en pur C. Il ne suffit donc pas de le coller dans un nouveau projet C++. La seule modification à apporter est d'ajouter extern "C" devant la fonction DllMain:
extern "C" int APIENTRY DllMain(...
Le programme Injecteur n'utilise la dll que pour l'injecter dans le processus cible. Elle se trouve donc chargée par ce dernier et tant que ce processus n'est pas fermé la dll ne sera pas libérée donc impossible de la supprimer ou la modifier. C'est tout à fait normal. Tu n'as qu'à fermer toutes les fenêtres que tu as sous-classées pour libérer la dll. L'injecteur n'est pas responsable de sa libération. J'avais pensé ajouter un autre bouton qui appelle une autre fonction qui forcera le processus, ou tous les processus, cible à libérer la dll mais cela comporte le risque de les faire planter. Faute de temps, je ne l'ai finalement pas jugé nécessaire. Je vais revoir cette option plus tard.
Même après fermeture de l'injecteur, les fenêtres déjà choisies restent sous-classées et ont donc encore besoin de la dll. Quand tu relances l'injecteur puis tentes de re-sous-classer l'une de ces fenêtres, un message d'erreur te dira que cette fenêtre a déjà été sous-classée. C'est encore tout à fait normal. D'après les tests que j'ai faits je n'ai remarqué aucune anomalie.
J'espère que les choses sont plus claires maintenant.
thenaoh Messages postés 111 Date d'inscription dimanche 28 septembre 2003 Statut Membre Dernière intervention 24 juin 2015
16 juin 2010 à 10:31
Merci pour ces précisions !

Alors j'ai refait de nouveaux tests, et il apparaît la chose suivante : si on teste ton code sur un Notepad, ça fonctionne sans problème, on peut refaire 15 fois la même manip, ça marchera les 15 fois (et la DLL est bien libérée comme il faut). Si je teste par contre avec le visualisateur d'images Windows, ça ne marchera qu'une fois, mais pas les suivantes (et la DLL n'est pas libérée), même quand je ferme le visualisateur d'images entre chaque test (et que je fais "Actualiser" dans ton appli entre chaque fois, bien sûr).

Mais pour en revenir à mon souci de fenêtre sans focus qui ne veut pas bouger avec ma souris, j'ai fait la chose suivante : dans le code de la DLL, sous le if(wParam==SC_CLOSE), j'ai ajouté le code suivant, pour forcer le déplacement de ma fenêtre :

else if(wParam == SC_MOVE)
{
RECT *pRect = (RECT *)lParam;
SetWindowPos(hwnd, 0, pRect->left, pRect->top, pRect->right - pRect->left, pRect->bottom - pRect->top, 0);
}

mais rien ne se passe. Aurais-tu une idée à ce sujet ?
Merci !!
thenaoh Messages postés 111 Date d'inscription dimanche 28 septembre 2003 Statut Membre Dernière intervention 24 juin 2015
16 juin 2010 à 13:34
YEEESSSSSSSSSSSSSSSSSSSSSSSSS !!!!!!!!!!
ça y'est ça marche ! Merci infiniment !!!! J'avais mal compris où il fallait que je place ce code, mais maintenant, j'arrive sans problème à déplacer une fenêtre d'une autre application même si elle est sans focus :-)

Concernant le problème du visualisateur d'images, peut-être que ça peut intéresser d'autres personnes, mais ne te casse pas la tête pour moi, je n'en aurai pas l'utilité, et tu en as déjà fait beaucoup ! Cependant, si ça peut t'aider, lorsque j'avais fait le test avec le visualisateur d'images, j'ai pu finalement libérer la DLL en killant le processus explorer.exe . Je sais pas s'il y a un lien, m'enfin bon...

Encore une fois, merci beaucoup pour ton aide ! Me reste plus qu'à savoir comment faire pour connaître la forme du curseur de souris, notamment quand il prend la forme I-beam, mais ça fera l'objet d'un autre post ! (parce que oui, y'a aucun lien avec le sujet courant, évidemment ;-) !!)

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.