前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >windows10 记事本进程 键盘消息钩子 dll注入

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

作者头像
全栈程序员站长
发布2022-09-17 11:58:33
1.7K1
发布2022-09-17 11:58:33
举报
文章被收录于专栏:全栈程序员必看

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

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

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

先上实现的图片:

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

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

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

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

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

一、DLL编写

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

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

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

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

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

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

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

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

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

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

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

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

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文件:

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

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

代码语言:javascript
复制
LIBRARY DLL

EXPORTS
KeyboardProc

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

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

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

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

二、CPP编写

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

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

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(‘\0’)结尾的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:\Windows\System32\notepad.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:\Windows\System32\notepad.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:

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

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
游戏安全
游戏安全领航者,基于10余年的经验沉淀,倾力打造一站式游戏安全解决方案。覆盖游戏反外挂、游戏加固、内容安全、经济安全等多种安全服务,专业构筑游戏安全防线。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档