上一讲讲到的InlineHook,每次Hook的时候,都要读写两次内存(先Hook,再还原)这种Hook方式,性能比较低,今天我们讲的这种Hook方式,可以说是InlineHook的升级版本
HotFix(热补丁)
我们先来讲讲原理:
我们继续来看看目标程序反汇编:
cpp
770A8E19 | CC | int3 |
770A8E1A | CC | int3 |
770A8E1B | CC | int3 |
770A8E1C | CC | int3 |
770A8E1D | CC | int3 |
770A8E1E | CC | int3 |
770A8E1F | CC | int3 |
770A8E20 | 8BFF | mov edi,edi |
770A8E22 | 55 | push ebp |
770A8E23 | 8BEC | mov ebp,esp |
770A8E25 | 833D 8C8C0D77 00 | cmp dword ptr ds:[770D8C8C],0 |
770A8E2C | 74 22 | je user32.770A8E50 |
770A8E2E | 64:A1 18000000 | mov eax,dword ptr fs:[18] |
770A8E34 | BA 18930D77 | mov edx,user32.770D9318 | edx:"榍\f"
770A8E39 | 8B48 24 | mov ecx,dword ptr ds:[eax+24] | ecx:"榍\f"
770A8E3C | 33C0 | xor eax,eax |
770A8E3E | F0:0FB10A | lock cmpxchg dword ptr ds:[edx],ecx | edx:"榍\f", ecx:"榍\f"
770A8E42 | 85C0 | test eax,eax |
770A8E44 | 75 0A | jne user32.770A8E50 |
770A8E46 | C705 288D0D77 01000000 | mov dword ptr ds:[770D8D28],1 |
770A8E50 | 6A FF | push FFFFFFFF |
770A8E52 | 6A 00 | push 0 |
770A8E54 | FF75 14 | push dword ptr ss:[ebp+14] |
770A8E57 | FF75 10 | push dword ptr ss:[ebp+10] |
770A8E5A | FF75 0C | push dword ptr ss:[ebp+C] |
770A8E5D | FF75 08 | push dword ptr ss:[ebp+8] |
770A8E60 | E8 0BFEFFFF | call <user32.MessageBoxTimeoutW> |
770A8E65 | 5D | pop ebp |
770A8E66 | C2 1000 | ret 10 |
我们发现呢,在MessageBox API之前,还有一堆int3,而这些int3是没用的,我们是否能用这点空余的内存,来构造一个jmp指令,来跳转到我们HOOK的函数?
很明显,jmp(E9)指令占一个字节,32位环境下,地址也占4个字节,而这里int3的数目,足够让我们来构造一个指令了
-
思路:
MessageBox函数第一句指令,mov edi,edi 实际上在32位环境下并没有什么意义,我们利用这个指令的两个字节,跳转到他上面的5个字节,构造跳转到我们自己的函数地址
这样的话,我们就要修改7个字节的数据
-
实操演示:
我们还是利用动态库,注入的方式完成Hook:
dll:
cpp// dllmain.cpp : 定义 DLL 应用程序的入口点。 #include "pch.h" BOOL hotFixHook(const WCHAR* pszModuleName, const char* pszFuncName, PROC pHookFunc) { //构造段跳转指令: BYTE ShortJmp[2] = { 0xEB,0xF9 }; //替换5个int3的jmp指令: BYTE JmpCode[5] = { 0xE9,0, }; HMODULE hModule = GetModuleHandle(pszModuleName); if (hModule == INVALID_HANDLE_VALUE) { MessageBox(NULL, L"打开进程失败", L"错误", NULL); return FALSE; } //获取函数地址 FARPROC pFuncAddr = GetProcAddress(hModule, pszFuncName); //修改内存属性 DWORD dwOldProcAddrProtect = 0; VirtualProtectEx(GetCurrentProcess(), (LPVOID)((DWORD)pFuncAddr - 5), 7, PAGE_EXECUTE_READWRITE, &dwOldProcAddrProtect); DWORD JmpAddr = (DWORD)pHookFunc - (DWORD)pFuncAddr; *(DWORD*)(JmpCode + 1) = JmpAddr; //修改内存 memcpy((LPVOID)((DWORD)pFuncAddr - 5), JmpCode, 5); memcpy(pFuncAddr, ShortJmp, 2); //改回内存属性 VirtualProtectEx(GetCurrentProcess(), pFuncAddr, 7, dwOldProcAddrProtect, &dwOldProcAddrProtect); return TRUE; } BOOL UnHook(const WCHAR* pszModuleName, const char* pszFuncName) { //构造段跳转指令: BYTE ShortJmp[2] = { 0x8B,0xFF }; //替换5个int3的jmp指令: BYTE JmpCode[5] = { 0x90,0x90,0x90,0x90,0x90 }; HMODULE hModule = GetModuleHandle(pszModuleName); if (hModule == INVALID_HANDLE_VALUE) { MessageBox(NULL, L"打开进程失败", L"错误", NULL); return FALSE; } //获取函数地址 FARPROC pFuncAddr = GetProcAddress(hModule, pszFuncName); //修改内存属性 DWORD dwOldProcAddrProtect = 0; VirtualProtectEx(GetCurrentProcess(), (LPVOID)((DWORD)pFuncAddr - 5), 7, PAGE_READWRITE, &dwOldProcAddrProtect); //修改内存 memcpy((LPVOID)((DWORD)pFuncAddr - 7), JmpCode, 5); memcpy(pFuncAddr, ShortJmp, 2); //改回内存属性 VirtualProtectEx(GetCurrentProcess(), pFuncAddr, 7, dwOldProcAddrProtect, &dwOldProcAddrProtect); return TRUE; } typedef int (WINAPI *fnMyMessageBox)( _In_opt_ HWND hWnd, _In_opt_ LPCWSTR lpText, _In_opt_ LPCWSTR lpCaption, _In_ UINT uType); int WINAPI MyMessageBox( _In_opt_ HWND hWnd, _In_opt_ LPCWSTR lpText, _In_opt_ LPCWSTR lpCaption, _In_ UINT uType) { //如果我们还是通过正常流程调用MessageBOx,就会进入死循环,这里通过函数指针的方式调用,跳过前两字节(我们构造的短跳转指令) fnMyMessageBox Func = (fnMyMessageBox)((DWORD)MessageBox + 2); int nRet = Func(NULL, L"Hook", L"Hook", NULL); return nRet; } BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: hotFixHook(L"User32.dll", "MessageBoxW", (PROC)MyMessageBox); break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: break; } return TRUE; }
Hook测试:
运行目标程序:
注入dll:
Hook成功:
进程通信
1.文件映射
进程一:
cpp
// Process1.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <Windows.h>
#define BUF_SIZE 256
TCHAR szName[] = L"WdigObject";
int main()
{
//为指定文件创建或打开命名或未命名的文件映射对象
HANDLE hFileMap = CreateFileMapping(
INVALID_HANDLE_VALUE, //要从中创建文件映射对象的句柄
NULL, //安全属性
PAGE_READWRITE, //指定文件映射对象的页面保护
0, //文件映射对象的最大大小的高阶 DWORD
BUF_SIZE, //文件映射对象最大大小的低序 DWORD
szName //文件映射对象的名称
);
//将文件映射的驶入映射到调用进程的地址空间
LPVOID lpMapAddr = MapViewOfFile(
hFileMap, //文件映射对象的句柄
FILE_MAP_ALL_ACCESS, //对文件映射对象的访问类型,用于确定页面的页面保护
0, //视图开始位置的文件偏移量的高顺序 DWORD
0, //要开始视图的文件偏移量低序 DWORD
BUF_SIZE //要映射到视图的文件映射的字节数
);
CopyMemory(lpMapAddr, L"Hello FileMap", (wcslen(L"Hello FileMap")+1)*2);
getchar();
//从调用进程的地址空间中取消映射文件的映射视图。
UnmapViewOfFile(lpMapAddr);
CloseHandle(hFileMap);
}
进程二:
cpp
// Process2.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <Windows.h>
#define BUF_SIZE 256
TCHAR szName[] = L"WdigObject";
int main()
{
//打开文件映射
HANDLE hFileMap = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, szName);
LPVOID lpBuffer = MapViewOfFile(hFileMap, FILE_MAP_ALL_ACCESS, 0, 0, BUF_SIZE);
MessageBox(NULL, (LPCWSTR)lpBuffer, L"success", NULL);
UnmapViewOfFile(lpBuffer);
CloseHandle(hFileMap);
}
通信测试:
先运行进程一,在运行进程二:
通信成功:
我们来看看文件映射完成进程通信的原理:
就是生成一个文件映射,两个进程都能访问这些内存,就完成了进程通信
2.命名管道
进程一:
cpp
// Process1.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <Windows.h>
int main()
{
//创建命名管道的实例,并返回后续管道操作的句柄
HANDLE hPipe = CreateNamedPipe(
L"\\\\.\\pipe\\Communication", //管道的唯一名称
PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,//打开模式
PIPE_TYPE_BYTE, //管道模式
1, //可为此管道创建的最大实例数
1024, //要为输出缓冲区保留的字节数
1024, //要为输入缓冲区保留的字节数
0, //
NULL //安全属性
);
//创建一个事件
HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
//使命名管道服务进程能够等待客户端进程连接到命名管道的实例
OVERLAPPED ovlap;
ZeroMemory(&ovlap, sizeof(ovlap));
ovlap.hEvent = hEvent;
if (!ConnectNamedPipe(hPipe, &ovlap)) {
//这里需要注意,如果成功,也会报错
if (GetLastError() != ERROR_IO_PENDING) {
std::cout << "ConnectNamedPipe Failed Code:" << GetLastError() << std::endl;
CloseHandle(hPipe);
CloseHandle(hEvent);
return -1;
}
}
if (WaitForSingleObject(hEvent, INFINITE) == WAIT_FAILED) {
std::cout << "WaitForSingleObject Error" << GetLastError() << std::endl;
CloseHandle(hPipe);
CloseHandle(hEvent);
return -1;
}
CloseHandle(hEvent);
char szBuffer[0x100] = { 0 };
DWORD dwReadSize = 0;
ReadFile(hPipe, szBuffer, 0x100, &dwReadSize, NULL);
std::cout << szBuffer << std::endl;
char WriteBuffer[] = "Hello";
DWORD dwWriteSize = 0;
WriteFile(hPipe, WriteBuffer, strlen(WriteBuffer) + 1, &dwWriteSize, NULL);
system("pause");
return 0;
}
进程二:
cpp
// Process2.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <Windows.h>
int main()
{
//等待命名管道
if (!WaitNamedPipe(
L"\\\\.\\pipe\\Communication", //命名管道的名称
NMPWAIT_WAIT_FOREVER //函数等待命名管道实例可用的毫秒数
)) {
std::cout << "WaitNamedPipe Failed Code:" << GetLastError() << std::endl;
return -1;
}
//创建或打开文件或 I/O 设备
HANDLE hFile = CreateFile(
L"\\\\.\\pipe\\Communication", //要创建或打开的文件或设备的名称
GENERIC_READ | GENERIC_WRITE, //请求对文件或设备的访问权限
NULL, //请求的文件或设备的共享模式
NULL, //安全属性
OPEN_EXISTING, //要对存在或不存在的文件或设备执行的操作
FILE_ATTRIBUTE_NORMAL, //文件或设备属性和标志
NULL
);
char WriteBuffer[] = "Hello";
DWORD dwWrittenByte = 0;
WriteFile(hFile, WriteBuffer, strlen(WriteBuffer) + 1, &dwWrittenByte, NULL);
char lpBuffer[0x100] = { 0 };
DWORD dwReadBytes = 0;
ReadFile(hFile, lpBuffer, 0x100, &dwReadBytes, NULL);
std::cout << lpBuffer << std::endl;
system("pause");
return 0;
}
通信测试:
先运行进程一,在运行进程二
通信成功: