windows10 记事本进程 键盘消息钩子 dll注入

2022-09-17 11:58:33 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

看了很多文档,垮了很多坎,终于完成了这个demo;

有很多个人理解,可能不完全正确,见谅;

先上实现的图片:

如图,我通过SetWindowsHookEx()函数向记事本进程中当前窗口线程注入了自己写的dll,dll中设置的回调函数使,当键盘按了1,那么就会触发一个MessageBox。

工具:VS 2015, PCHunter(用于查看是否成功注入了dll,其实看能否实现功能就信,非必须的)

思路:先写一个dll(就是要被注入的dll),再写一个windows控制台程序(用于将dll注入到我们想要注入的进程)

接下来我们一步步实现看看:

一、DLL编写

1、打开VS新建一个名为DLL的Win32 项目:

2、在应用程序向导中选中DLL、空项目(空项目比较干净,没有多余的东西):

3、创完了项目,先别急着写代码,还有很多必要的动西要改,右键点击项目->属性。将MFC的使用改为“在共享DLL中使用MFC”,原因是dll中会用到CString类型,要加入#include <afx.h>这个头文件,如果不设置MFC的话,之后编译会报错;将字符集改为“使用多字节字符集”,及ANSI,原因是在ANSI和Unicode下,CSting的存储结构是不同的,前者是char *,后者是wchar_t *,而且字符集不同,有些函数的参数也会跟着变,这个后面会说。

4、如图点击配置管理器:

5、将Debug配置的平台改为64位,原因是:我的windows是64位的,记事本软件也是64位的(虽然它的执行文件在System32文件夹下,但是用PCHunter可以看到它是64位的程序),而我们最重要的注入函数SetWindowsHookEx()的官网文档说了,这个函数只能用于64位程序将64位dll注入64位程序,或32位程序将32位dll注入32位程序,如果我们编写的dll是32位的,那么到时候注入时程序就会卡死(别问我为什么知道),也就是注入失败了,再给个官方文档地址点击打开链接。

6、在源文件目录下新建一个名为DLL的cpp文件:

7、现在我们可以写代码了:

代码语言:javascript复制
#include <afx.h> //CString的头文件
#include "stdio.h"
#include "windows.h" //要调用的很多windows api函数的头文件

HHOOK g_hHook = NULL; //HHOOK是钩子句柄,如果想搭建钩子链,也可把下一个需要传给的钩子句柄放在这。

CString IsNumber(WPARAM wParam)
{
	CString message;
	switch (wParam) {
	case 0x30: message.Format("按了0"); break;
	case 0x31: message.Format("按了1"); break;
	case 0x32: message.Format("按了2"); break;
	case 0x33: message.Format("按了3"); break;
	case 0x34: message.Format("按了4"); break;
	case 0x35: message.Format("按了5"); break;
	case 0x36: message.Format("按了6"); break;
	case 0x37: message.Format("按了7"); break;
	case 0x38: message.Format("按了8"); break;
	case 0x39: message.Format("按了9"); break;
	default: message.Format("未定义的按键"); break;
	}
	return message;
}
//获取到的wparam是16位的int(也可能是long,这个无所谓),用于标识键盘截取到的消息是哪个键,我简单的
//识别了键盘上的数组键(不是小键盘的数组键),返回CString对象。

LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
//这是一个键盘钩子消息的回调函数,当设置钩子成功,dll被注入到目标线程,该回调函数会在每次有键盘消息
//传递给目标线程时被调用,第二个参数在这个类型的钩子中放回的是虚拟键盘的信息,其他两个参数我不太清楚
{
	MessageBox(NULL, IsNumber(wParam), _T("Message"), 0);
	return CallNextHookEx(g_hHook, nCode, wParam, lParam);
	//我理解这句代码意思是,如果g_hHook非空,就把消息传给这个句柄,否则就传给应用程序。
}

KeyboardProc,官方文档有解释:点击打开链接,关于KeyboardProc中wparam参数返回的信息:点击打开链接

我可能也有很多地方没理解对,有能力尽量看官方文档。

8、在源文件目录下新建一个名为DLL的def文件:

9、添加如下代码,可以将KeyboardProc函数导出:

代码语言:javascript复制
LIBRARY DLL

EXPORTS
KeyboardProc

整个项目下只有“源文件”下的两个文件:

10、点击最上方的生成->生成解决方案,成功的话,找到DLL->x64->Debug这个文件夹,看下有没有DLL.dll这个文件,注意:不是DLL->DLL->x64->Debug这个文件夹,不要问我为什么会知道。

至此第一部分就算完成了,我们得到了DLL.dll这个文件。

二、CPP编写

1、打开VS新建一个名为CPP的Win 32控制台应用程序:

2、之后的设置都是默认的(之前写dll选了空项目,写cpp就不用了)。

3、4、5、全部参考第一部分。

6、在源文件目录下的CPP.cpp文件添加代码:

代码语言:javascript复制
#include "stdafx.h"
#include "windows.h"
#include "Psapi.h" //连接了库后引用头文件,EnumProcesses及GetModuleFileNameEx都需要引入这个头文件
#pragma comment(lib,"Psapi.lib") //预编译指令,连接psapi.lib库

DWORD FindProcessByEnumProcess(CString TargetProcessName)
//参数是目标程序名,如notepad.exe
//返回值类型DWORD,是32位的long型,值是找到的目标进程的进程id, 如果打开了多个同名程序,找到的是最后打开的那个进程的进程id
{
	DWORD TargetProcessId = 0; //目标进程初始值是0,没找到时就返回0
	DWORD ProcessesId[1024] = { 0 }; //进程id数组,在之后EnumProcesses函数调用会将当前所有进程id放入数组
	DWORD NeededProcessesId = 0; //在之后EnumProcesses函数调用后会将实际需要的进程数组的大小赋值给它
	LPSTR ProcessName = (LPSTR)malloc((sizeof(char)) * 1024);
	//LPSTR定义是typedef LPSTR char * ,LPSTR被定义成是一个指向以NULL(‘’)结尾的32位ANSI字符数组指针
	//用于存储返回到的进程名

	EnumProcesses(ProcessesId, sizeof(ProcessesId), &NeededProcessesId);
	//查询所有当前进程
	//第一个参数是输出参数,返回进程数组存储到ProcessesId[1024]中
	//第二个参数的输入参数,输入需要返回的进程数组的存储大小
	//第三个参数的输出参数,返回实际需要的进程数组的存储大小
	DWORD ProcessNumber = NeededProcessesId / sizeof(DWORD);
	//得到进程个数,用于遍历

	for (unsigned int i = 0; i < ProcessNumber; i  )
	{
		if (ProcessesId[i] != 0)
		{
			//对每个进程id执行下面操作

			HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, ProcessesId[i]);
			//HANDLE是进程句柄类型,hProcess存储的就是进程句柄了
			//OpenProcess函数通过进程id获取进程句柄
			//第一个参数是输入参数,标识需要获取的权限,这里我们获取PROCESS_QUERY_INFORMATION和PROCESS_VM_READ权限
			//因为下面的GetModuleFileNameEx函数指定要这两个权限
			//第二个参数是输入参数,用来标识该句柄是否希望被子进程继承,不过不考虑子进程的继承权限则直接赋值为FALSE
			//第三个参数是输入参数,输入需要打开进程的进程id
			//返回值就是得到的句柄了
			if (hProcess != NULL)
			{
				GetModuleFileNameEx(hProcess, NULL, ProcessName, 1024);
				//根据进程句柄获取到进程完整的名称,如C:WindowsSystem32notepad.exe
				//第一个参数是输入参数,输入需要获取进程名的进程句柄
				//第二个参数是输入参数,输入需要获取的模块的模块句柄,为NULL表示获取进程主模块
				//第三个参数是输出参数,输出进程模块完整的名称
				//第四个参数是输入参数,表明ProcessName的存储大小
				CString ProcessFullPathName = (CString)ProcessName;
				//把LPSTR类型转为CString类型,便于进行字符处理
				//CString在ANSI字符集下以存储char数组,在Unicode字符集下以存储wchar_t数组,后者的长度是前者的两倍
				//CString a,则a可作为指向存储的char数组的头部的指针,和LPSTR类型是一样的,所以我用了强转
				//我百度到的转化方法是这么写的:CString ProcessFullPathName(ProcessName); 也可以
				CString ProcessBaseName = ProcessFullPathName.Right(ProcessFullPathName.GetLength() - ProcessFullPathName.ReverseFind('\') - 1);
				//把路径去掉,留下一个基础名称及C:WindowsSystem32notepad.exe转为notepad.exe

				if (ProcessBaseName == TargetProcessName)
				//如果该进程名与目标进程名相同,那么该进程id就是目标进程id
				{
					TargetProcessId = ProcessesId[i];
				}
				CloseHandle(hProcess);//关闭句柄
				hProcess = NULL;
			}
		}
	}

	return TargetProcessId;
}

void DoInject(DWORD TargetWindowThreadId)
{
	HMODULE hDll = LoadLibrary(_T("DLL.dll"));
	//HMODULE是模块句柄类型
	//LoadLibrary可以显示加载dll
	//这里我没有加路径,所有执行前要将dll放到exe文件同目录下
	if (hDll == NULL) {
		printf("将dll加载到自身进程失败n");
		exit(0);
	}
	else {
		printf("将dll加载到自身进程成功n");
	}

	FARPROC KeyboardProc = (FARPROC)GetProcAddress(hDll, "KeyboardProc");
	//通过GetProcAddress函数获取到hDll句柄中的KeyboardProc函数的地址
	if (KeyboardProc == NULL) {
		printf("获取到回调函数地址失败n");
		exit(0);
	}
	else {
		printf("获取到回调函数地址成功n");
	}

	HHOOK g_hHook = SetWindowsHookEx(WH_KEYBOARD, (HOOKPROC)KeyboardProc, hDll, TargetWindowThreadId);
	//将dll注入目标线程,设置函数指针指向写好的键盘消息回调函数
	//第一个参数输入钩子类型
	//第二个参数根据不同的钩子类型,要输入不同类型的回调函数地址
	//第三个参数输入dll句柄
	//第四个参数输入目标线程id

	if (g_hHook) {
		printf("向目标线程添加钩子并注入dll成功n");
	}

	printf("输入q卸载钩子:");
	while (getchar() != 'q');

	if (g_hHook)
	{
		UnhookWindowsHookEx(g_hHook);
		g_hHook = NULL;
	}
	//卸载钩子

	FreeLibrary(hDll);//释放dll
}

BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam)
//EnumWindows设置的回调函数,系统每发现一个窗口都会调用该回调函数
//HWND是窗口句柄类型
//第一个参数返回的是当前窗口句柄,第二个参数类型可以自己定,我传入的是目标进程id
{
	DWORD CurrentWindowProcessId; //当前窗口进程id
	DWORD CurrentWindowThreadId; //当前窗口线程id

	CurrentWindowThreadId = GetWindowThreadProcessId(hwnd, &CurrentWindowProcessId);
	//GetWindowThreadProcessId()可以通过窗口句柄,获取该窗口的所在的进程及线程
	//第一个参数是输入参数,输入目标窗口句柄
	//第二个参数是输出参数,类型是LPDWORD,及指向DWORD的指针,所以要取地址,函数执行成功后CurrentWindowProcessId值就是返回的当前窗口进程id
	//返回值是值传递的,DWORD类型,直接赋值给DWORD类型就行了,值是当前窗口线程id
	if (CurrentWindowProcessId == lParam) {
		//如果当前窗口进程id等于目标进程的进程id
		//则得到的当前窗口线程id就是目标窗口线程id
		DoInject(CurrentWindowThreadId);
		//得到了线程id后就可以注入了
		return false; //当找到后就返回false,这样才会终止遍历
	}

	return true; //不是当前窗口,返回true,继续遍历
}

int main()
{
	CString TargetProcessName(_T("notepad.exe"));
	DWORD TargetProcessId = FindProcessByEnumProcess(TargetProcessName); //先找到目标进程id
	EnumWindows(EnumWindowsProc, TargetProcessId); //根据目标进程id进行遍历,找到目标线程,并注入dll
}

我把cpp的框架写出来

FindProcessByEnumProcess()函数是输入进程名,返回进程id

DoInject()函数是执行注入的过程,需要知道被注入的线程的id

EnumWindowsProc()函数是回调函数,对于每个已存在的窗口,判断其进程id是否与目标进程id相同,如果是,就锁定了目标线程id,再调用DoInject()函数执行注入的过程

int main()

{

1、得到目标进程id

2、设置回调函数,等待其执行

}

再来说说我的思路:我们目标是要找到计算本程序线程id,因为注入函数SetWindowsHookEx的最后一个参数是目标线程id,进程id是 不行的,其实有两种实现方法:

思路1:找到记事本进程id,根据进程id找到其所有的线程id,但是一个记事本进程有很多子线程,我不知道是否都要注入还是只要注入一个,而且列出所有子线程那个方法我没弄懂,于是没这么做;

思路2:找到找到记事本进程id,枚举当前所有窗口参看窗口的进程id以及线程id,对比记事本进程id,相同的话就锁定了记事本窗口所在线程id;

思路3:其实最开始我们的源头就是记事本的进程名notepad.exe,我们有没有办法绕过进程id,找到线程id呢,FindWindow()这个函数可以通过窗口名找到窗口句柄,再GetWindowThreadProcessId()根据窗口句柄找到窗口线程id不就行了吗,但是可惜的是这个窗口名并不是notepad.exe,而是“新建文本文档.txt – 记事本”,根本不好锁定,所以此路不通。

7、在stdafx.h这个头文件中添加代码:

#include <afx.h> //因为我们cpp建的不是空项目,项目是有结构的,引入头文件一定要放在stdafx.h中

8、点击最上方的生成->生成解决方案,将DLL->x64->Debug目录下的DLL.dll文件复制到CPP->x64->Debug目录下

9、先打开一个记事本程序(如果你打开两个,前面也说到了,只能锁定后打开的窗口),点击上方调试->开始执行,这时再打开回到记事本窗口,试试摁下键盘看看有没有效果。

还可以通过PCHunter查看被注入的dll,方法是右击进程->查看进程模块,如下图被标记为红色的dll:

参考文章:1、点击打开链接(腾讯 游戏安全实验室,这个demo只是其中的一个作业)/2、点击打开链接

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/159477.html原文链接:https://javaforall.cn

0 人点赞