啰嗦啰嗦:
开始之前还是要啰嗦几句,看到自己博客粉丝增加,访问量也越来越多,感到非常开心,而且好评也是不少,指错也非常感谢,从错误中发现了很多问题,非常感谢,也高兴自己的文章能帮助到其它人。
就比如之前的比较火动态视频桌面代码,网络上几乎找不到可行的动态视频桌面代码,最后博主花费了一晚上时间去研究,然后把研究成果从头到位到理论,到Windows桌面结构,以及体系分析,都说的非常透彻,也让不少人实现了动态视频桌面这样的工具。
而且最近博主也因为项目原因比较忙,最近用到APIHOOK,写完之后,想写到博客里去,想看看互联网上有没有合适的文章如果有,那么博主就没必要重新去写了,但是发现APIHOOK文章很多,很杂,但是大部分代码都千篇一律,有的带汇编,但是这样并不合适,这样对一些初学者来说门槛就变高了,有的直接撂N行的代码,让人不知道从哪儿看,更不知道个因为所以然。
所以博主决定写一个不使用汇编,且理论知识足,对名词做通俗易懂的介绍,把自己的理解以最简单的方式表达出来,本篇文章没有使用较复杂的方式,代码也会一步一步给大家分析写,这才是博主最开始写博客的初衷!
最近也因为比较忙,不能经常更新博客,还请大家谅解,最近发现stm32的文章还是蛮受大家欢迎的,等博主有时间了在更新这方面的文章,因为最近项目比较忙,博主业余时间喜欢去写写机器人,所以会玩玩这块板子!
前言:
APIHOOK是一种基于HOOK技术为基础的一种钩子方式,HOOK在上一篇关于HOOK技术知识点的讲解里,HOOK是针对消息,并且只有在具备GUI图形的窗口才会有效(详细见HOOK详解),而这种HOOK可以无需消息,或必须具备GUI图形的窗口,因为此方式是挂钩API的,通过模块表里找到API的输出地址,并且通过修改此输出地址达到跳转到自己的API位置,也称为API拦截。
这是一种黑客手段,同时也是一种给应用程序打补丁的好手段,这一点在Windows上运用的淋漓尽致,众所周知,Windows太过于庞大,以至于微软的员工都不愿意去对内核和其它文件体系去重写和学习调试找漏洞,因为过于庞大出了问题会引起一连串的蝴蝶效应,或者修改其中一行代码导致异常问题。
为什么说这是一种打补丁的好手段呢?
答:当我们发布了一个大型的应用程序,当我们发现了严重的漏洞后,是应用程序里的某个API思路的问题,我们可以通过此方式拦截这个API然后执行我们补丁里新的API达到修复漏洞的方式,因为一个应用程序那么大,出了漏洞,如果让用户重新安装,过于麻烦,不如写一个小型补丁来修补应用程序上的问题,就好像一条渔网,漏网了,如果我们要换个网的话,需要去重新编网或者去买一个,然后在拿回来这个过程,成本就已经算在里了,不如直接用胶带之类的绑起来,然后在慢慢换新的!当然这只是一个比喻!
API HOOK理论:
API:
API即应用程序接口技术,这个名词相信大家都不会陌生,就是当我们在某个平台上(如Windows,Linux)进行开发时,这些平台为我们提供的相应驱动系统工作的接口,我们通过调用对应的接口来驱动系统执行不同的工作,但更低层来说,就是系统通过封装CPU的汇编指令,集成为不同的功能驱动硬件工作,我们通过调用系统封装好的接口来驱动GPU或相关硬件设备工作,但是系统更低层封装是很复杂的,只有参与过硬件开发的才明白,这里只是稍微概括说下!如果想学习硬件方面可以去看下博主硬件方面的资料!
HOOK:
HOOK即钩子,负责钩住消息和API,理论可以见博主上篇文章(详细见HOOK详解),当API工作时我们可以提前通过HOOK住这个API,让其拐弯执行我们自己的API,达到API拦截的效果,一般也可以用于修改发布的大型程序补丁!
简单概括一下就是鱼塘就像WIndows或Linux这些平台,而钩子就是鱼钩,我们去当鱼钩构到鱼儿时,鱼儿游动,跑来跑去,我们鱼竿都会有波动!
API HOOK:
APIHOOK是一种基于HOOK技术为基础的一种钩子方式,HOOK在上一篇关于HOOK技术知识点的讲解里,HOOK是针对消息,并且只有在具备GUI图形的窗口才会有效(详细见HOOK详解),而这种HOOK可以无需消息,或必须具备GUI图形的窗口,因为此方式是挂钩API的,通过模块表里找到API的输出地址,并且通过修改此输出地址达到跳转到自己的API位置,也称为API拦截。
其实现方法有很多,下面给大家说说几种常见的实现方法!
IAT-HOOK:
这种方式需要懂得PE文件格式的结构,这种方式就是通过IAT表的方式获取指定模块,那么问题来了!
什么是IAT表?
答:IAT表里存储的是,在程序中的函数代码(API)地址,但是又不属于程序,意思就是这是函数代码属于外部的,在程序启动的时候被加载到程序当中了,所以此时它属于程序中的一部分,但是并不是本来就是存在程序中的,最明显的例子就是DLL,DLL是在程序运行时被加载进来的,原本DLL是不属于程序的!
IAT表这个可以在PE文件格式里找到,它是一个链表,它的映射文件到内存映射地址可以通过PE文件格式分析得到,且每当新的DLL模块被加载进来时,它就会存储这个DLL里的所有API地址!
此方法Linux不可行,因为Linux的PE文件结构不同,关于Linux平台的HOOK后面在细说!
注册表-HOOK
注册表HOOK是比较简单的,但是Linux不可行,因为Linux不是使用注册表方式,在不同平台上,当我们运行程序,上面说过了API的理论,如果要使用这些,那么一定会加载平台依赖的库,这些库Windows7以下的系统是使用注册表的方式记录的,我们可以通过修改注册表的方式把我们自己的DLL名字路径放进去,这样每个程序运行时都会自动加载这些依赖库,我们的DLL也顺利被加载进去了,这样我们就可以通过一些手段在DLLMAIN事件里(DLL首次被加载执行得代码)做些事情。
其中我们可以通过这个方式去HOOK已经被加载得某个API修改地址即可,因为DLL被加载好了以后,它就属于这个进程,所以可以随意修改内存,但是代码段还是需要获取权限得,因为代码段得TXT区段是R只可读!修改方法可以参考博主写的游戏修改器的文章!
其还可以把一些系统的一些依赖库模仿一个,函数名都一样,然后就间接性的替换个某个API,但这种方法只能小部分,因为很麻烦,你需要实现所有的代码!
这种方法也不可行,同时也不建议,因为Win7以上这种方法已经不可行了!通用性不强!
API-修改地址方式
这种方式和IAT表的方式差不多,我们不通过PE文件方式得到IAT表的地址然后寻找特定API的位置,我们则是通过检索模块句柄,然后得到模块地址里的API地址,然后写入跳转地址,然后当CPU去跳转,和IAT表的方式几乎一样!
核心-注入DLL
上面说的每一种方式的前提是将DLL你的代码方式注入到指定进程下,且指定进程必须加载了你使用的DLL,也就是说APIHOOK建立在DLL这种方式之上,因为程序原本的代码函数不可被检索,已经在编译阶段被编译成地址了,不被记录,只有DLL这样的模块系统会记录到进程下!
其我们的HOOK代码也必须是DLL,因为在程序下想要把指定API或代码写入到进程下,只有通过DLL这种比较合法的方式,让程序加载,通过DLLMAIN方式让DLL去执行一段SHELLCODE代码,或者可以通过Win提供的进程内存操作函数写入大量代码,让CPU去执行,从而间接实现API注入,但是编写起来较为复杂,不如直接使用IDE编写!
DLL注入
DLL注入的方式很多如远线程,HOOK,注册表(这个方式在上面的理论知识里说过)等,今天我们使用之前使用过的方式远线程的方式,但是随着Windows的更新,平台安全性越来越高,所以我们实际开发的过程中需要很高的权限,user权限,这已经不是XP时代那个黑客档次较低的时代了,Win8以上的系统,虽然Windows有为我们提供一些进程令牌,但是仅限调试阶段有效,所以还是建议大家去使用驱动代码,内核驱动开发!
实战
说了这么多的理论知识,下面让我们进入实战吧!
开发平台:
操作系统:WIndows10 64bit(MAC OS虚拟机)
开发环境:VS2017
1.写一个测试用的DLL
我们打开VS创建一个Windows控制台程序
然后选择DLL,并且设置为空项目,也可以不设置,这里只是博主编写DLL的一个习惯
增加一个源文件
然后我们直接在源文件里写一个简单的API
int Add(int a, int b){
return a + b;
}
然后声明为导出
这里要以C的方式导出,因为VS默认使用C++导出,而C++的导出符号表名字会@加上参数,一般不好检索,我们使用默认C方式!
extern "C" _declspec(dllexport) int Add(int a, int b);
完整代码:
extern "C" _declspec(dllexport) int Add(int a, int b);
int Add(int a, int b) {
return a + b;
}
好了最后我们编译生成
提示这个错误,代表DLL顺利编译成功!
我们继续在打开一个VS,然后创建一个c/c++的控制台空项目,然后写上使用此DLL的一些代码!
不要忘记把刚刚的DLL文件放入到这个测试API工程的Debug文件夹下哦
#include
int main()
{
std::cout << "Hello World!\n";
}
然后写下加载dll的代码,我们去测试一下刚刚写的API接口
#include
#include
#include
int main()
{
HINSTANCE h = LoadLibraryA("Project3.dll"); //加载动态库
typedef int(*FunPtr)(int a, int b);//定义函数指针来指向Dll动态库里的函数
if (h == NULL)
{
FreeLibrary(h);
printf("load dll error\n");
}
else {
FunPtr funPtr = (FunPtr)GetProcAddress(h, "Add"); //获取动态库里指定函数文件偏移首地址
if (funPtr != NULL)
{
int result = funPtr(3, 3); //调用函数
printf("3 + 3 = %d \n", result);
}
else {
printf("error add");
}
}
getchar();
}
运行:
可以看到成功调用了DLL模块,并且使用OK,那么这里,我们在写一个DLL,注意这个DLL就是APIHOOK上面说过了,如果想要修改指定程序的API拦截,那么就要以DLL方式注入到这个进程下,然后修改这个模块的地址,跳转到我们自己的模块表里执行!
所以这个DLL里包含了检索这个进程下指定模块API地址的代码,然后在写入并修改它!
同时我们还需要写一个注入的EXE程序。
先编写一个注入的DLL
先创建一个dll,具备Windows消息事件,这里不要创建空项目的DLL,直接使用vs生成一个默认的DLL
#include "windows.h"
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
这里在DLL_PROCESS_ATTACH事件里写代码,这部分是当DLL被默认加载进来时Windows会传递进来的消息!
先定义几个全局的变量
HMODULE hDll = NULL; //模块句柄
typedef int(*FunPtr)(int a, int b);//入口地址
FunPtr func = NULL; //指向API地址
然后第一步从进程中得到要拦截API存在哪个模块下的句柄
GetModuleHandle函数可以从当前进程下得到指定DLL模块句柄
//第一步检索Project3模块句柄
hDll = GetModuleHandle("Project3");
if (hDll == NULL) {
MessageBox(NULL, "没有检索到Project3模块", "ERROR", 0);
return FALSE;
}
第二步通过句柄来检索API的输出库地址
注意func的原型要与输出库的原型一致
//第二步获取模块下的API首地址
func = (FunPtr)GetProcAddress(hDll, "Add");
if (func == NULL) {
MessageBox(NULL, "没有检索到Project3模块下Add API接口地址", "ERROR", 0);
return FALSE;
}
第三步,这一步我们已经得到注入进程下要拦截API的地址了,我们先做一个小demo:
我们先随意写入这个函数前五个字节,五个随机跳转指令,让其API无法正常执行崩溃
为什么是五个字节?为什么要写入首地址作为偏移量的五个字节呢?
答:库函数的首地址属于代码段,会被CPU执行,我们通过修改这部分的字节代码,让CPU执行我们的命令
为什么是五个字节呢?
答:因为写入时,只能写入汇编指令对应的机器码,因为在编辑期间是文本型的到编译时编译器帮我们翻译成了机器码,而我们程序已经在运行了,所以只能写入机器码!
是五个字节的原因是:jmp在汇编里属于跳转的意思,对应的机器码是0xe9,一个字节可以表示,一个完整的32位地址需要四个字节表示,所以我们写入前五个字节就可以了,也就是0x9e+你的地址
这里我们先随便写一个地址:
注意建议是0x00,因为在跳转时会加上段DS寄存器的地址来跳转,我们写入的只是偏移量,如果太大可能跳出程序范围导致越界中断!
char Ptr[] = { 0xe9,0x00,0x00,0x00,0x00 };
这样的话就跳转到代码段的首地址去了,ds:0x000000
开始写修改代码:
写之前需要说一点,代码段是不可写的,只可读,所以我们要使用API让代码段变得可写可读!
这里可以使用VirtualProtectEx函数,具体介绍可以参见:内存修改
这里将获取到的模块句柄与API地址放进去,将权限改为READWRITE可读可写,同时在申请一个变量用于保存之前的权限,方便恢复
DWORD dwOldProtect; //用于存储之前的权限
第一个参数是进程句柄,不是DLL句柄,可以使用GetCurrentProcess获取,DLL内存空间属于进程空间的一部分,不是进程空间!!,所以VirtualProtectEx第一个参数是进程句柄
VirtualProtectEx(GetCurrentProcess(), func, 5, PAGE_READWRITE, &dwOldProtect);
然后在内存copy一下就可以了,因为dll已经注入了,所以可以使用WriteProcessMemory来直接写入
//写入代码
WriteProcessMemory(GetCurrentProcess(), func, &Ptr, 5, &dwNumberOfBytesRead);
当然也需要定义一个全局变量存储之前的权限,给0也可以
DWORD dwNumberOfBytesRead; //用于存储之前的权限
最后弹出提示框告诉我们已经完成API拦截
MessageBox(NULL, "API拦截成功", "ERROR", 0);
完整代码:
#include "windows.h"
#include "stdio.h"
HMODULE hDll = NULL; //模块句柄
typedef int(*FunPtr)(int a, int b);//入口地址
FunPtr func = NULL; //指向API地址
char Ptr[] = { 0xe9,0x00,0x00,0x00,0x00 }; //跳转指令
char ps[1024] = { 0 };
DWORD dwOldProtect; //用于存储之前的权限
DWORD dwNumberOfBytesRead; //用于存储之前的权限
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
//第一步检索Project3模块句柄
hDll = GetModuleHandle("Project3");
if (hDll == NULL) {
printf("%d", GetLastError());
MessageBox(NULL, "没有检索到Project3模块", "ERROR", 0);
return FALSE;
}
//第二步获取模块下的API首地址
func = (FunPtr)GetProcAddress(hDll, "Add");
if (func == NULL) {
MessageBox(NULL, "没有检索到Project3模块下Add API接口地址", "ERROR", 0);
return FALSE;
}
//第三步修改API地址
//获取API地址内存区域的权限,变成可读写
VirtualProtectEx(GetCurrentProcess(), func, 5, PAGE_READWRITE, &dwOldProtect);
//写入代码
//memcpy(func, Ptr,5);
WriteProcessMemory(GetCurrentProcess(), func, &Ptr, 5, &dwNumberOfBytesRead);
MessageBox(NULL, "API拦截成功", "ERROR", 0);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
这样当目标进程运行时,我们在运行注入程序,将这个dll注入进去,通过消息会执行我们上面的拦截代码,从而拦截另外一个模块的API!
当目标进程在调用API时,就会发现已经跳转到main入口函数再次执行去了!
我们在编写一个注入程序,把我们的DLL注入到目标程序里去:
在新建一个c++程序:
这里我们可以直接用这里的代码:
Windows核心编程_远线程方式实现Dll注入
这里只是其中的方法之一,因为我之前有说过,所以这里使用这个方法:
#include
#include
#include
int main()
{
//程序的窗口句柄
HWND hWnd = FindWindow(NULL, "TestApiWin");
if (hWnd == NULL) {
MessageBox(NULL, "无法打开指定进程", "Error", 0);
}
//根据窗口句柄获取进程PID
DWORD Process_ID = (DWORD)0;
GetWindowThreadProcessId(hWnd, &Process_ID); //PID这里传址
if (Process_ID == (DWORD)0) { //判断是否与原值相同
MessageBox(NULL, "无法获取进程PID", "Error", 0);
}
//打开进程
HANDLE hPro = OpenProcess(PROCESS_CREATE_THREAD | //允许远程创建线程
PROCESS_VM_OPERATION | //允许远程VM操作
PROCESS_VM_WRITE,//允许远程VM写
FALSE, Process_ID);
if (hPro == NULL) {
MessageBox(NULL, "无法打开进程空间", "Error", 0);
}
// 在远程进程中为路径名称分配空间
char str[256] = { 0 };
strcpy_s(str, "C:\\Users\\WIN10\\source\\repos\\Dll1\\Debug\\Dll1.dll"); //你的dll路径
int dwSize = (lstrlenA(str) + 1); //路径大小+1是因为\0
//开辟空间
LPVOID pszLibFileRemote = (PWSTR)VirtualAllocEx(hPro, NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (pszLibFileRemote == NULL) {
MessageBox(NULL, "无法开辟内存!", "Error", 0);
return -1;
}
//写入数据
DWORD dwWritten; //写入字节数
DWORD n = WriteProcessMemory(hPro, (LPTHREAD_START_ROUTINE)pszLibFileRemote, "C:\\Users\\WIN10\\source\\repos\\Dll1\\Debug\\Dll1.dll", dwSize, &dwWritten);
if (n == NULL) {
MessageBox(NULL, "无法写入内存!", "Error", 0);
return -1;
}
//Kernel32模块已经被包含在依赖库里自动加载进来了,所以我们可以直接获取
PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA");
if (pfnThreadRtn == NULL) {
MessageBox(NULL, "无法获取dll库函数", "Error", 0);
return -1;
}
//创建远线程
DWORD dwID = NULL;
HANDLE hThread = CreateRemoteThread(hPro, 0, 0, pfnThreadRtn, pszLibFileRemote, 0, &dwID);
if (hThread == NULL) {
MessageBox(NULL, "无法创建远线程!", "Error", 0);
printf("%d", GetLastError());
}
//等待线程结束
WaitForSingleObject(hThread, INFINITE);
return 0;
}
这里我们修改一下目标进程里的代码:
为了方便FindeWindows获取控制台窗口句柄,我们增加修改一下标题,除此为了方便调试在写一个循环调用API的代码:
修改标题可以使用SetConsoleTitle
#include
#include
#include
int main()
{
SetConsoleTitle("TestApiWin");//修改标题
HINSTANCE h = LoadLibraryA("C:\\Users\\WIN10\\source\\repos\\Project3\\Debug\\Project3.dll"); //加载动态库
typedef int(*FunPtr)(int a, int b);//定义函数指针来指向Dll动态库里的函数
if (h == NULL)
{
FreeLibrary(h);
printf("load dll error\n");
}
else {
FunPtr funPtr = (FunPtr)GetProcAddress(h, "Add"); //获取动态库里指定函数文件偏移首地址
if (funPtr != NULL)
{
while (1) { //循环
getchar(); //按下空格在调用
int result = funPtr(3, 3); //调用函数
printf("3 + 3 = %d \n", result);
}
}
else {
printf("error add");
}
}
getchar();
}
核心在这部分,这样写方便我们调试
while (1) { //循环
getchar(); //按下空格在调用
int result = funPtr(3, 3); //调用函数
printf("3 + 3 = %d \n", result);
}
我们先运行目标程序:
按下空格后看下是否正确调用API:
正确执行
那么,我们在运行一下注入程序:
运行后提示API拦截成功,上面的ERROR请忽略~
然后我们在按一下回车看一下:
直接结束进程了~~,也就是说程序跳到起始位置0x000000的位置上去了,这段代码执行时异常终止了~
所以没人知道这段代码发生了什么,先把完整代码给大家:
测试API DLL代码:
extern "C" _declspec(dllexport) int Add(int a, int b);
int Add(int a, int b) {
return a + b;
}
目标进程代码,用于测试API DLL的程序:
// ConsoleApplication3.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include
#include
#include
int main()
{
SetConsoleTitle("TestApiWin");//修改标题
HINSTANCE h = LoadLibraryA("C:\\Users\\WIN10\\source\\repos\\Project3\\Debug\\Project3.dll"); //加载动态库
typedef int(*FunPtr)(int a, int b);//定义函数指针来指向Dll动态库里的函数
if (h == NULL)
{
FreeLibrary(h);
printf("load dll error\n");
}
else {
FunPtr funPtr = (FunPtr)GetProcAddress(h, "Add"); //获取动态库里指定函数文件偏移首地址
if (funPtr != NULL)
{
while (1) { //循环
getchar(); //按下空格在调用
int result = funPtr(3, 3); //调用函数
printf("3 + 3 = %d \n", result);
}
}
else {
printf("error add");
}
}
getchar();
}
API拦截的DLL代码:
#include "windows.h"
#include "stdio.h"
HMODULE hDll = NULL; //模块句柄
typedef int(*FunPtr)(int a, int b);//入口地址
FunPtr func = NULL; //指向API地址
char Ptr[] = { 0xe9,0x00,0x00,0x00,0x00 }; //跳转指令
char ps[1024] = { 0 };
DWORD dwOldProtect; //用于存储之前的权限
DWORD dwNumberOfBytesRead; //用于存储之前的权限
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
//第一步检索Project3模块句柄
hDll = GetModuleHandle("Project3");
if (hDll == NULL) {
printf("%d", GetLastError());
MessageBox(NULL, "没有检索到Project3模块", "ERROR", 0);
return FALSE;
}
//第二步获取模块下的API首地址
func = (FunPtr)GetProcAddress(hDll, "Add");
if (func == NULL) {
MessageBox(NULL, "没有检索到Project3模块下Add API接口地址", "ERROR", 0);
return FALSE;
}
//第三步修改API地址
//获取API地址内存区域的权限,变成可读写
VirtualProtectEx(GetCurrentProcess(), func, 5, PAGE_READWRITE, &dwOldProtect);
//写入代码
//memcpy(func, Ptr,5);
WriteProcessMemory(GetCurrentProcess(), func, &Ptr, 5, &dwNumberOfBytesRead);
MessageBox(NULL, "API拦截成功", "ERROR", 0);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
注入程序的代码:
// ConsoleApplication4.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include "pch.h"
#include
#include
#include
int main()
{
//程序的窗口句柄
HWND hWnd = FindWindow(NULL, "TestApiWin");
if (hWnd == NULL) {
MessageBox(NULL, "无法打开指定进程", "Error", 0);
}
//根据窗口句柄获取进程PID
DWORD Process_ID = (DWORD)0;
GetWindowThreadProcessId(hWnd, &Process_ID); //PID这里传址
if (Process_ID == (DWORD)0) { //判断是否与原值相同
MessageBox(NULL, "无法获取进程PID", "Error", 0);
}
//打开进程
HANDLE hPro = OpenProcess(PROCESS_CREATE_THREAD | //允许远程创建线程
PROCESS_VM_OPERATION | //允许远程VM操作
PROCESS_VM_WRITE,//允许远程VM写
FALSE, Process_ID);
if (hPro == NULL) {
MessageBox(NULL, "无法打开进程空间", "Error", 0);
}
// 在远程进程中为路径名称分配空间
char str[256] = { 0 };
strcpy_s(str, "C:\\Users\\WIN10\\source\\repos\\Dll1\\Debug\\Dll1.dll"); //你的dll路径
int dwSize = (lstrlenA(str) + 1); //路径大小+1是因为\0
//开辟空间
LPVOID pszLibFileRemote = (PWSTR)VirtualAllocEx(hPro, NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (pszLibFileRemote == NULL) {
MessageBox(NULL, "无法开辟内存!", "Error", 0);
return -1;
}
//写入数据
DWORD dwWritten; //写入字节数
DWORD n = WriteProcessMemory(hPro, (LPTHREAD_START_ROUTINE)pszLibFileRemote, "C:\\Users\\WIN10\\source\\repos\\Dll1\\Debug\\Dll1.dll", dwSize, &dwWritten);
if (n == NULL) {
MessageBox(NULL, "无法写入内存!", "Error", 0);
return -1;
}
//Kernel32模块已经被包含在依赖库里自动加载进来了,所以我们可以直接获取
PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA");
if (pfnThreadRtn == NULL) {
MessageBox(NULL, "无法获取dll库函数", "Error", 0);
return -1;
}
//创建远线程
DWORD dwID = NULL;
HANDLE hThread = CreateRemoteThread(hPro, 0, 0, pfnThreadRtn, pszLibFileRemote, 0, &dwID);
if (hThread == NULL) {
MessageBox(NULL, "无法创建远线程!", "Error", 0);
printf("%d", GetLastError());
}
//等待线程结束
WaitForSingleObject(hThread, INFINITE);
return 0;
}
注意:
VS2017上测试,会出现API拦截的DLL代码里hDll = GetModuleHandle("Project3");会出现返回NULL的问题,同时错误码为126,这个时候不要在VS里运行目标程序,直接在文件夹里运行,原因大概是VS2017的调试功能里把模块表设置为一次只能一个线程占用,不能被其他线程获取句柄之类的! VS2015上测试没有问题!
其次注入程序需要以管理员权限运行才能完成注入
小Demo写完了,那么我们根据上面的程序继续修改一下代码吧,将代码修改为API拦截将指定API跳转到我们的API里去,也就是把0x000000改成我们的API地址,同时在教大家如何获取传递进来的参数!
这里我们在实现API 拦截的DLL代码里修改,在第三步开始修改
第一步我们声明一个与要拦截API的原型一致的函数:
实现代码里直接return 10;
int Add(int a, int b) {
return 10;
}
在定义一个全局的数组,用于存储Add我们自己的函数地址
BYTE myFuncPtr[5];
在申请一个远指针,远指针是专门用于指向函数地址的,为了显得标准一点我们就用上它吧,也可以不用函数指针也是可以的
FARPROC pfOldAdd; //指向函数的远指针
将获取到的地址转换成远指针
//将函数首地址转换成远指针
pfOldAdd = (FARPROC)func;
获取函数首地址里的数据
//第三步获取原API里的内存数据
VirtualProtectEx(GetCurrentProcess(), pfOldAdd, 5, PAGE_READWRITE, &dwOldProtect);
ReadProcessMemory(GetCurrentProcess(), pfOldAdd, pc, 5, NULL);
这一步使用汇编来做,因为这一步会有类型转换问题,所以我们使用汇编来做这一步,可以略过类型转换!
我们的API实际地址转换公式是:
实际地址=我们的api地址-拦截的api地址-要写入的数据长度
这个公式是某位大牛分享的
_asm
{
lea eax, Add //获取我们的Add函数地址
mov ebx, pfOldAdd //原系统API函数地址
sub eax, ebx //int nAddr= UserFunAddr – SysFunAddr
sub eax, 5 //nAddr=nAddr-5
mov dword ptr[myFuncPtr + 1], eax //将算出的地址nAddr保存到NewCode后面4个字节
//注:一个函数地址占4个字节
}
写入:
myFuncPtr[0] = 0xe9 ;
WriteProcessMemory(GetCurrentProcess(), pfOldAdd, &myFuncPtr, 5, &dwNumberOfBytesRead);
最后权限恢复:
//权限恢复
VirtualProtectEx(GetCurrentProcess(), pfOldAdd, 5, dwOldProtect, &dwTemp);
MessageBox(NULL, "API拦截成功", "ERROR", 0);
运行效果:
完美HOOK
dll完整代码:
#include "windows.h"
#include "stdio.h"
HMODULE hDll = NULL; //模块句柄
typedef int(*FunPtr)(int a, int b);//入口地址
FunPtr func = NULL; //指向API地址
char Ptr[] = { 0x9E,0x00,0x00,0x00,0x00 }; //跳转指令
char ps[1024] = { 0 };
DWORD dwOldProtect; //用于存储之前的权限
DWORD dwNumberOfBytesRead; //用于存储之前的权限
char pc[5] = { 0 }; //保存原数据
FARPROC pfOldAdd; //指向函数的远指针
BYTE myFuncPtr[5]; //存储我们自己API的地址
DWORD dwTemp = 0; //保留
int Add(int a, int b) {
return 10;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
//第一步检索Project3模块句柄
myFuncPtr[0] = { 0xe9 };
hDll = GetModuleHandle("Project3");
if (hDll == NULL) {
printf("%d", GetLastError());
MessageBox(NULL, "没有检索到Project3模块", "ERROR", 0);
return FALSE;
}
//第二步获取模块下的API首地址
func = (FunPtr)GetProcAddress(hDll, "Add");
if (func == NULL) {
MessageBox(NULL, "没有检索到Project3模块下Add API接口地址", "ERROR", 0);
return FALSE;
}
//将函数首地址转换成远指针
pfOldAdd = (FARPROC)func;
//第三步获取原API里的内存数据
VirtualProtectEx(GetCurrentProcess(), pfOldAdd, 5, PAGE_READWRITE, &dwOldProtect);
ReadProcessMemory(GetCurrentProcess(), pfOldAdd, pc, 5, NULL);
_asm
{
lea eax, Add //获取我们的Add函数地址
mov ebx, func //原系统API函数地址
sub eax, ebx //int nAddr= UserFunAddr – SysFunAddr
sub eax, 5 //nAddr=nAddr-5
mov dword ptr[myFuncPtr + 1], eax //将算出的地址nAddr保存到NewCode后面4个字节
//注:一个函数地址占4个字节
}
//写入
myFuncPtr[0] = 0xe9;
WriteProcessMemory(GetCurrentProcess(), pfOldAdd, &myFuncPtr, 5, &dwNumberOfBytesRead);
//权限恢复
VirtualProtectEx(GetCurrentProcess(), pfOldAdd, 5, dwOldProtect, &dwTemp);
MessageBox(NULL, "API拦截成功", "ERROR", 0);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
如果要复原的话,只需要将pc里的数据写入到原API里就可以啦!
如何获取传递进来的参数呢?
答:只要原型一样就可以了,因为编译器会在编译阶段确定参数后,写入参数读取代码,入栈的代码!
因为参数是存放在通用寄存器里的,每次进入到指定函数时,函数开头会在编译阶段写入分配内存的代码,然后写入从通用寄存器里读取参数的代码写入到变量里去,这些工作编译器在编译阶段就已经智能的搞定了,所以我们只需要直接调用就可以了。
如修改API拦截DLL里的代码:
int Add(int a, int b) {
printf("传递进来的参数a:%d,b:%d", a,b);
getchar();
return 10;
}
运行:
好了,最后再给大家分享一下完整代码:
API DLL:
extern "C" _declspec(dllexport) int Add(int a, int b);
int Add(int a, int b) {
return a + b;
}
调用API DLL的程序代码:
#include
#include
#include
int main()
{
SetConsoleTitle("TestApiWin");//修改标题
HINSTANCE h = LoadLibraryA("C:\\Users\\WIN10\\source\\repos\\Project3\\Debug\\Project3.dll"); //加载动态库
typedef int(*FunPtr)(int a, int b);//定义函数指针来指向Dll动态库里的函数
if (h == NULL)
{
FreeLibrary(h);
printf("load dll error\n");
}
else {
FunPtr funPtr = (FunPtr)GetProcAddress(h, "Add"); //获取动态库里指定函数文件偏移首地址
if (funPtr != NULL)
{
while (1) { //循环
getchar(); //按下空格在调用
int result = funPtr(3, 3); //调用函数
printf("3 + 3 = %d \n", result);
}
}
else {
printf("error add");
}
}
getchar();
}
API HOOK DLL代码:
#include "windows.h"
#include "stdio.h"
HMODULE hDll = NULL; //模块句柄
typedef int(*FunPtr)(int a, int b);//入口地址
FunPtr func = NULL; //指向API地址
char Ptr[] = { 0x9E,0x00,0x00,0x00,0x00 }; //跳转指令
char ps[1024] = { 0 };
DWORD dwOldProtect; //用于存储之前的权限
DWORD dwNumberOfBytesRead; //用于存储之前的权限
char pc[5] = { 0 }; //保存原数据
FARPROC pfOldAdd; //指向函数的远指针
BYTE myFuncPtr[5]; //存储我们自己API的地址
DWORD dwTemp = 0; //保留
int Add(int a, int b) {
printf("传递进来的参数a:%d,b:%d", a,b);
getchar();
return 10;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
//第一步检索Project3模块句柄
myFuncPtr[0] = { 0xe9 };
hDll = GetModuleHandle("Project3");
if (hDll == NULL) {
printf("%d", GetLastError());
MessageBox(NULL, "没有检索到Project3模块", "ERROR", 0);
return FALSE;
}
//第二步获取模块下的API首地址
func = (FunPtr)GetProcAddress(hDll, "Add");
if (func == NULL) {
MessageBox(NULL, "没有检索到Project3模块下Add API接口地址", "ERROR", 0);
return FALSE;
}
//将函数首地址转换成远指针
pfOldAdd = (FARPROC)func;
//第三步获取原API里的内存数据
VirtualProtectEx(GetCurrentProcess(), pfOldAdd, 5, PAGE_READWRITE, &dwOldProtect);
ReadProcessMemory(GetCurrentProcess(), pfOldAdd, pc, 5, NULL);
_asm
{
lea eax, Add //获取我们的Add函数地址
mov ebx, func //原系统API函数地址
sub eax, ebx //int nAddr= UserFunAddr – SysFunAddr
sub eax, 5 //nAddr=nAddr-5
mov dword ptr[myFuncPtr + 1], eax //将算出的地址nAddr保存到NewCode后面4个字节
//注:一个函数地址占4个字节
}
//写入
myFuncPtr[0] = 0xe9;
WriteProcessMemory(GetCurrentProcess(), pfOldAdd, &myFuncPtr, 5, &dwNumberOfBytesRead);
//权限恢复
VirtualProtectEx(GetCurrentProcess(), pfOldAdd, 5, dwOldProtect, &dwTemp);
MessageBox(NULL, "API拦截成功", "ERROR", 0);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
注入程序代码:
// ConsoleApplication4.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include "pch.h"
#include
#include
#include
int main()
{
//程序的窗口句柄
HWND hWnd = FindWindow(NULL, "TestApiWin");
if (hWnd == NULL) {
MessageBox(NULL, "无法打开指定进程", "Error", 0);
}
//根据窗口句柄获取进程PID
DWORD Process_ID = (DWORD)0;
GetWindowThreadProcessId(hWnd, &Process_ID); //PID这里传址
if (Process_ID == (DWORD)0) { //判断是否与原值相同
MessageBox(NULL, "无法获取进程PID", "Error", 0);
}
//打开进程
HANDLE hPro = OpenProcess(PROCESS_CREATE_THREAD | //允许远程创建线程
PROCESS_VM_OPERATION | //允许远程VM操作
PROCESS_VM_WRITE,//允许远程VM写
FALSE, Process_ID);
if (hPro == NULL) {
MessageBox(NULL, "无法打开进程空间", "Error", 0);
}
// 在远程进程中为路径名称分配空间
char str[256] = { 0 };
strcpy_s(str, "C:\\Users\\WIN10\\source\\repos\\Dll1\\Debug\\Dll1.dll"); //你的dll路径
int dwSize = (lstrlenA(str) + 1); //路径大小+1是因为\0
//开辟空间
LPVOID pszLibFileRemote = (PWSTR)VirtualAllocEx(hPro, NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (pszLibFileRemote == NULL) {
MessageBox(NULL, "无法开辟内存!", "Error", 0);
return -1;
}
//写入数据
DWORD dwWritten; //写入字节数
DWORD n = WriteProcessMemory(hPro, (LPTHREAD_START_ROUTINE)pszLibFileRemote, "C:\\Users\\WIN10\\source\\repos\\Dll1\\Debug\\Dll1.dll", dwSize, &dwWritten);
if (n == NULL) {
MessageBox(NULL, "无法写入内存!", "Error", 0);
return -1;
}
//Kernel32模块已经被包含在依赖库里自动加载进来了,所以我们可以直接获取
PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA");
if (pfnThreadRtn == NULL) {
MessageBox(NULL, "无法获取dll库函数", "Error", 0);
return -1;
}
//创建远线程
DWORD dwID = NULL;
HANDLE hThread = CreateRemoteThread(hPro, 0, 0, pfnThreadRtn, pszLibFileRemote, 0, &dwID);
if (hThread == NULL) {
MessageBox(NULL, "无法创建远线程!", "Error", 0);
printf("%d", GetLastError());
}
//等待线程结束
WaitForSingleObject(hThread, INFINITE);
return 0;
}