大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。
Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺
看了很多文档,垮了很多坎,终于完成了这个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、现在我们可以写代码了:
#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函数导出:
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文件添加代码:
#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:
参考文章:1、点击打开链接(腾讯 游戏安全实验室,这个demo只是其中的一个作业)/2、点击打开链接
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/159477.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...