函数挂钩实际上是如何工作的? WinAPI, C++
How does function hooking actually work? WinAPI, C++
我正在将我的 DLL 注入到一个进程中,然后像这样挂接一个函数:(recv)
BOOL HookFunction(LPCWSTR moduleName, LPCSTR funcName, LPVOID funcProxy,
unsigned char* lpBackup)
{
BYTE jmp[6] = { 0xe9,0x00,0x00,0x00,0x00,0xc3 };
DWORD funcAddr = (DWORD)GetProcAddress(GetModuleHandle(moduleName), funcName);
DWORD prev;
VirtualProtect((LPVOID)funcAddr, 6, PAGE_EXECUTE_READWRITE, &prev);
ReadProcessMemory(GetCurrentProcess(), (LPVOID)funcAddr, lpBackup, 6, NULL);
DWORD proxy = ((DWORD)funcProxy - funcAddr) - 5;
memcpy(&jmp[1], &proxy, 4);
memcpy((LPVOID)funcAddr, jmp, 6);
VirtualProtect((LPVOID)funcAddr, 6, prev, &prev);
FlushInstructionCache(GetCurrentProcess(), NULL, NULL);
return funcAddr;
}
// Hook
HookFunction(L"ws2_32.dll", "recv", (LPVOID*)nRecv, hookR);
然后我附加了一个调试器,结果如下:
挂钩前:
挂钩后:
虽然有几件事我不明白,因为我仍在尝试理解和想象堆栈、堆等是如何在调试器中一起工作的。
BYTE jmp[6] = { 0xe9,0x00,0x00,0x00,0x00,0xc3 };
我这里是不是要替换指令,比如把原来函数的"move, edi, edi"(recv)换成0xe9?然后是带有 0x00 的下一条指令...或者它究竟是如何工作的?
任何详细的解释将不胜感激。
BOOL HookFunction(LPCWSTR moduleName, LPCSTR funcName, LPVOID funcProxy,
unsigned char* lpBackup)
{
BYTE jmp[6] =
{
0xe9,0x00,0x00,0x00,0x00, /*JMP and 4 bytes of offset*/
0xc3 /*RET*/
};
/*
JMP (e9) is relative, its 32-bit signed immediate operand encodes the
number of bytes to jump forward relative to the NEXT instruction.
*/
/* Get the target address of the function to hook */
DWORD funcAddr = (DWORD)GetProcAddress(GetModuleHandle(moduleName), funcName);
/* Code is not necessarily mapped as writable, we remap it */
DWORD prev;
VirtualProtect((LPVOID)funcAddr, 6, PAGE_EXECUTE_READWRITE, &prev);
/* Read the original 6 bytes we are going to overwrite */
ReadProcessMemory(GetCurrentProcess(), (LPVOID)funcAddr, lpBackup, 6, NULL);
/*
Compute the offset: target - source
target = funcProxy
source = funcAddr + 5 (length of JMP)
target - source = funcProxy - funcAddr - 5
*/
DWORD proxy = ((DWORD)funcProxy - funcAddr) - 5;
/*
Create the JMP instruction: set the offset
*/
memcpy(&jmp[1], &proxy, 4);
/* Overwrite the first 6 bytes of the target function */
memcpy((LPVOID)funcAddr, jmp, 6);
/* Reset the memory protection to its original value*/
VirtualProtect((LPVOID)funcAddr, 6, prev, &prev);
/* Since we write to a code section with DS, flush the L1 I cache */
FlushInstructionCache(GetCurrentProcess(), NULL, NULL);
return funcAddr;
}
HookFunction
在内存中创建一小段 x86 代码 (a trampoline)
jmp <0> ;e9 00 00 00 00
ret ;c3
其中<0>
被跳转的编码目标(见代码中的注释)连续覆盖-钩子函数。
蹦床一旦制作完成,就变成了
jmp funcProxy ;e9 .. .. .. ..
ret ;c3
然后将这段代码直接写在钩子函数的开头,从而覆盖其原始代码。
代码是多语言的 - 它适用于 x86 和 x86-64。
钩子函数原代码复制到lpBackup
.
这需要重新调用原函数,钩子函数必须先恢复才能调用。
由于这很昂贵且不可重入,因此更简洁的方法是修改 Import Address Table - 但是此解决方案的有效性取决于您的要求。
我正在将我的 DLL 注入到一个进程中,然后像这样挂接一个函数:(recv)
BOOL HookFunction(LPCWSTR moduleName, LPCSTR funcName, LPVOID funcProxy,
unsigned char* lpBackup)
{
BYTE jmp[6] = { 0xe9,0x00,0x00,0x00,0x00,0xc3 };
DWORD funcAddr = (DWORD)GetProcAddress(GetModuleHandle(moduleName), funcName);
DWORD prev;
VirtualProtect((LPVOID)funcAddr, 6, PAGE_EXECUTE_READWRITE, &prev);
ReadProcessMemory(GetCurrentProcess(), (LPVOID)funcAddr, lpBackup, 6, NULL);
DWORD proxy = ((DWORD)funcProxy - funcAddr) - 5;
memcpy(&jmp[1], &proxy, 4);
memcpy((LPVOID)funcAddr, jmp, 6);
VirtualProtect((LPVOID)funcAddr, 6, prev, &prev);
FlushInstructionCache(GetCurrentProcess(), NULL, NULL);
return funcAddr;
}
// Hook
HookFunction(L"ws2_32.dll", "recv", (LPVOID*)nRecv, hookR);
然后我附加了一个调试器,结果如下:
挂钩前:
挂钩后:
虽然有几件事我不明白,因为我仍在尝试理解和想象堆栈、堆等是如何在调试器中一起工作的。
BYTE jmp[6] = { 0xe9,0x00,0x00,0x00,0x00,0xc3 };
我这里是不是要替换指令,比如把原来函数的"move, edi, edi"(recv)换成0xe9?然后是带有 0x00 的下一条指令...或者它究竟是如何工作的?
任何详细的解释将不胜感激。
BOOL HookFunction(LPCWSTR moduleName, LPCSTR funcName, LPVOID funcProxy,
unsigned char* lpBackup)
{
BYTE jmp[6] =
{
0xe9,0x00,0x00,0x00,0x00, /*JMP and 4 bytes of offset*/
0xc3 /*RET*/
};
/*
JMP (e9) is relative, its 32-bit signed immediate operand encodes the
number of bytes to jump forward relative to the NEXT instruction.
*/
/* Get the target address of the function to hook */
DWORD funcAddr = (DWORD)GetProcAddress(GetModuleHandle(moduleName), funcName);
/* Code is not necessarily mapped as writable, we remap it */
DWORD prev;
VirtualProtect((LPVOID)funcAddr, 6, PAGE_EXECUTE_READWRITE, &prev);
/* Read the original 6 bytes we are going to overwrite */
ReadProcessMemory(GetCurrentProcess(), (LPVOID)funcAddr, lpBackup, 6, NULL);
/*
Compute the offset: target - source
target = funcProxy
source = funcAddr + 5 (length of JMP)
target - source = funcProxy - funcAddr - 5
*/
DWORD proxy = ((DWORD)funcProxy - funcAddr) - 5;
/*
Create the JMP instruction: set the offset
*/
memcpy(&jmp[1], &proxy, 4);
/* Overwrite the first 6 bytes of the target function */
memcpy((LPVOID)funcAddr, jmp, 6);
/* Reset the memory protection to its original value*/
VirtualProtect((LPVOID)funcAddr, 6, prev, &prev);
/* Since we write to a code section with DS, flush the L1 I cache */
FlushInstructionCache(GetCurrentProcess(), NULL, NULL);
return funcAddr;
}
HookFunction
在内存中创建一小段 x86 代码 (a trampoline)
jmp <0> ;e9 00 00 00 00
ret ;c3
其中<0>
被跳转的编码目标(见代码中的注释)连续覆盖-钩子函数。
蹦床一旦制作完成,就变成了
jmp funcProxy ;e9 .. .. .. ..
ret ;c3
然后将这段代码直接写在钩子函数的开头,从而覆盖其原始代码。
代码是多语言的 - 它适用于 x86 和 x86-64。
钩子函数原代码复制到lpBackup
.
这需要重新调用原函数,钩子函数必须先恢复才能调用。
由于这很昂贵且不可重入,因此更简洁的方法是修改 Import Address Table - 但是此解决方案的有效性取决于您的要求。