什么是Inline Hook
Inline Hook,即内联钩子。
系统提供的API都保存在DLL文件中,当程序运行需要调用某个API时,它会隐式地将此API所在的DLL文件加载到其进程中。那么,如果我们使用一种方法,直接修改DLL中的API函数在内存中的映像,使API函数的首条代码为jmp MyFuncAddress
这样就可以执行自己的函数了。
由于这种方法是直接在程序中嵌入jmp
指令来实现Hook的,所以把它称为内联钩子。
使用DLL实现对其他进程的Inline Hook
思路:写一个DLL,DLL内写好钩子函数,然后进行DLL注入。其在首次被Load入进程时,在进程中查找特定函数的地址,然后将其入口处前五字节替换为一条跳转指令,跳转到钩子函数的地址,这个过程叫挂钩。钩子函数中,执行完自定义操作后,如果想继续执行原程序的操作,则可以在钩子函数中调用原来的函数,但是要注意的是:调用被钩住的原函数,又会再次调用钩子函数,这样会死循环。所以如果想在钩子函数中调用原函数,必须先解钩,再调用,调用完了再挂钩上去。
32位和64位下的内联钩子
实现32位和64位系统的Inline Hook
要注意:一定保证位数的一致,不然执行jmp xxxxxxxx
时,由于偏移量不合法,很可能出现
源码
说明:全部文件均采用32位方式编译
文件结构如下:
钩子类与DLL源文件在同一项目下
被挂钩进程是独立的一个项目。
DLL注入器是一个独立的项目。
钩子类实现
inline_hook.h
//
// Created by 23028 on 2021/3/13.
//
#ifndef INLINE_HOOK_INLINE_HOOK_H
#define INLINE_HOOK_INLINE_HOOK_H
#include <windows.h>
class InlineHook{
private:
// 被钩函数地址
FARPROC hookedFuncAddress;
// 原函数入口处5字节的指令
BYTE origFuncInstruction[5];
// 跳转至钩子函数入口的指令
BYTE jmpToHookInstruction[5];
public:
InlineHook();
~InlineHook();
// 钩
// 参数为:被钩函数所在DLL名、被钩函数名、钩子函数地址
BOOL Hook(LPCTSTR pszModuleName, LPCTSTR pszHookedFuncName, FARPROC pfnHookFuncAddress);
// 解钩
VOID UnHook();
// 重钩
BOOL ReHook();
};
#endif //INLINE_HOOK_INLINE_HOOK_H
inline_hook.cpp
关于ReadProcessMemory的第二个参数用了一次(void*)
强转的解释见:
invalid conversion from FARPROC to LPCVOID
//
// Created by 23028 on 2021/3/13.
//
#include "inline_hook.h"
InlineHook::InlineHook() {
this->hookedFuncAddress = NULL;
ZeroMemory(this->origFuncInstruction, 5);
ZeroMemory(this->jmpToHookInstruction, 5);
}
InlineHook::~InlineHook() {
// 解钩
InlineHook::UnHook();
this->hookedFuncAddress = NULL;
ZeroMemory(this->origFuncInstruction, 5);
ZeroMemory(this->jmpToHookInstruction, 5);
}
BOOL InlineHook::Hook(LPCTSTR pszModuleName, LPCTSTR pszHookedFuncName, FARPROC pfnHookFuncAddress) {
// 钩子状态
BOOL hookStatus = FALSE;
// 拿到被钩函数的地址
this->hookedFuncAddress = (FARPROC)GetProcAddress(GetModuleHandle(pszModuleName), pszHookedFuncName);
if (this->hookedFuncAddress != NULL){
// 保存被钩函数入口前五字节的指令到origFuncInstruction
ReadProcessMemory(GetCurrentProcess(), (void*)this->hookedFuncAddress, this->origFuncInstruction, 5, NULL);
// 构造跳转指令jmpToHookInstruction为jmp xxxxxxxx,后4B为偏移量
// E9即:jmp操作
this->jmpToHookInstruction[0] = '\xE9';
// 计算被钩函数入口地址到要跳转地址的偏移量:目标地址 - 入口地址 - 本指令长度(jmp xxxxxxxx共5B)
// 指针后移一字节,强转为DWORD型指针,其内容赋为DWORD型的偏移量
*(DWORD*)(this->jmpToHookInstruction + 1) = (DWORD)pfnHookFuncAddress - (DWORD)this->hookedFuncAddress - 5;
// 写入构造好的跳转指令
if(WriteProcessMemory(GetCurrentProcess(), (void*)this->hookedFuncAddress, this->jmpToHookInstruction, 5, NULL)){
hookStatus = TRUE;
}
}
return hookStatus;
}
VOID InlineHook::UnHook() {
if (this->hookedFuncAddress != NULL && this->origFuncInstruction[0] != NULL) {
// 修改为原来的指令
WriteProcessMemory(GetCurrentProcess(), (void*)this->hookedFuncAddress, this->origFuncInstruction, 5, NULL);
}
}
BOOL InlineHook::ReHook() {
BOOL reHookStatus = FALSE;
if (this->hookedFuncAddress != 0) {
// 修改为跳转指令
WriteProcessMemory(GetCurrentProcess(), (void*)this->hookedFuncAddress, this->jmpToHookInstruction, 5, NULL);
reHookStatus = TRUE;
}
return reHookStatus;
}
DLL编写
参考:Windows开发:CLion下编写及动/ 静态调用DLL
library.h
#ifndef INLINE_HOOK_LIBRARY_H
#define INLINE_HOOK_LIBRARY_H
#include "inline_hook.h"
// 钩子函数,传入原函数所有参数
BOOL WINAPI MyCreateProcessA(LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation);
extern "C" BOOL APIENTRY DllMain(HANDLE hModule, DWORD reason_for_call, LPVOID lpReserved);
#endif //INLINE_HOOK_LIBRARY_H
library.cpp
#include "library.h"
// 声明钩子对象
InlineHook myHook;
BOOL WINAPI MyCreateProcessA(LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation){
BOOL createStatus = FALSE;
// 若钩住,则跳转到此钩子函数,此时先解钩,不然下面调用被钩函数,又跳转回来,死循环
myHook.UnHook();
// 弹窗
MessageBoxA(NULL, "in fact, call MyCreateProcessA ", "call CreateProcessA", MB_YESNO);
// 创建进程
createStatus = CreateProcessA(lpApplicationName,
lpCommandLine,
lpProcessAttributes,
lpThreadAttributes,
bInheritHandles,
dwCreationFlags,
lpEnvironment,
lpCurrentDirectory,
lpStartupInfo,
lpProcessInformation
);
// 重钩
myHook.ReHook();
return createStatus;
}
extern "C" BOOL APIENTRY DllMain(HANDLE hModule, DWORD reason_for_call, LPVOID lpReserved){
switch (reason_for_call){
// dll首次被Load时,钩住Kernel32.dll中的CreateProcessA,钩子函数为MyCreateProcessA
case DLL_PROCESS_ATTACH: {
if(myHook.Hook("Kernel32.dll", "CreateProcessA", (FARPROC)MyCreateProcessA)){
// 成功挂钩
MessageBoxA(NULL, "hook success", "success", MB_OK);
}
else{
MessageBoxA(NULL, "hook failed", "failed", MB_OK);
}
break;
}
// dll被卸载时解钩
case DLL_PROCESS_DETACH: {
myHook.UnHook();
break;
}
}
}
被挂钩进程
编译出delete.exe
主要做的工作就是调用CreateProcessA
创建进程。
#include <stdio.h>
#include <windows.h>
int main(void){
Sleep(20 * 1000);
PROCESS_INFORMATION pi = {0};
STARTUPINFO si = {0};
LPCTSTR lpApplicationName = "D:\\Notepad++\\notepad++.exe"; // 可执行文件名
LPTSTR lpCommandLine = "./readme.txt"; // 命令行参数
LPSECURITY_ATTRIBUTES lpProcessAttributes = NULL; // 进程安全属性
LPSECURITY_ATTRIBUTES lpThreadAttributes = NULL; // 线程安全属性
BOOL bInheritHandles = FALSE; // 可继承句柄是否可被新进程继承
DWORD dwCreationFlags = 0; // 进程优先级、创建标志
LPVOID lpEnvironment = NULL; // 新进程的环境变量
LPCTSTR lpCurrentDirectory = "D:\\Notepad++\\"; // 新进程的当前目录
LPSTARTUPINFO lpStartupInfo = &si; // 新进程的启动信息
LPPROCESS_INFORMATION lpProcessInformation = π // 新进程和主线程的相关信息(两个句柄两个ID)
CreateProcessA(lpApplicationName, lpCommandLine, lpProcessAttributes,
lpThreadAttributes, bInheritHandles, dwCreationFlags,
lpEnvironment, lpCurrentDirectory, lpStartupInfo,
lpProcessInformation);
Sleep(10 * 1000);
return 0;
}
DLL注入器
#include <iostream>
#include <windows.h>
#include <tchar.h>
#include <tlhelp32.h>
// 参数:dll路径,被注入进程的PID
bool ModuleInject(LPCTSTR szModule, DWORD dwPid){
// 参数:权限、是否有子进程继承权限、PID
HANDLE hProcess = OpenProcess(
PROCESS_QUERY_INFORMATION | PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE,
FALSE,
dwPid
);
if (hProcess){
// 成功获取到进程句柄
// dll路径字符串的大小
int cByte = (_tcslen(szModule) + 1) * sizeof(TCHAR);
// 在被注入进程中申请分配内存,返回首地址
// 参数:进程句柄、要开始分配内存的位置、分配大小、分配内存类型、内存管理方式
LPVOID pAddr = VirtualAllocEx(hProcess, NULL, cByte, MEM_COMMIT, PAGE_READWRITE);
if (pAddr){
// 分配成功则尝试在被注入进程中写入dll路径
// 参数:进程句柄、分配好的内存首址、数据内容指针、数据长度、传出(写入了多少字节)
if (WriteProcessMemory(hProcess, pAddr, szModule, cByte, NULL)){
// 如果定义了UNICODE
#ifdef _UNICODE
// 使用GMH获得Kernel32.dll的句柄,用GPA在其中找到宽字节的LoadLibraryW函数的地址,此地址在所有进程中相同
// 强转为PSR类型指针,作为CreateRemoteThread的参数
PTHREAD_START_ROUTINE pfnStartAddr = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(_T("Kernel32")), "LoadLibraryW");
#else
PTHREAD_START_ROUTINE pfnStartAddr = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(_T("Kernel32")), "LoadLibraryA");
#endif
if (pfnStartAddr){
// 远程线程ID
DWORD dwThreadID = 0;
// 远程线程句柄
// 参数:进程句柄、安全属性指针、堆栈大小(0默认)、线程函数(LoadLibrary)地址、线程函数参数(dll路径)、创建标志、传出远程线程ID
HANDLE hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, pfnStartAddr, pAddr, 0, &dwThreadID);
if (hRemoteThread){
// 若成功注入,则dll被load到进程中,其DllMain被执行
std::cout << "inject success." << std::endl;
// 等待远程线程退出再退出
WaitForSingleObject(hRemoteThread, INFINITE);
CloseHandle(hRemoteThread);
CloseHandle(hProcess);
}
else{
std::cout << "create remote thread failed." << std::endl;
}
}
else{
std::cout << "cannot find func LoadLibrary" << std::endl;
}
}
else{
std::cout << "write mem failed." << std::endl;
}
}
else{
std::cout << "alloc mem failed." << std::endl;
return FALSE;
}
}
else{
std::cout << "failed to open process." << std::endl;
return FALSE;
}
}
VOID EnumProcessModule(int myPid) {
MODULEENTRY32 me32 = {0};
me32.dwSize = sizeof(MODULEENTRY32);
// 创建DLL型快照,参数为进程ID
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, myPid);
if (hSnap == INVALID_HANDLE_VALUE){
std::cout << "failed to create module snapshot." << std::endl;
}
else{
BOOL enumStatus = Module32First(hSnap, &me32);
int i = 0;
while (enumStatus){
std::cout << me32.szModule << ", " << me32.szExePath << std::endl;
i++;
enumStatus = Module32Next(hSnap, &me32);
}
}
CloseHandle(hSnap);
}
int main() {
int myPid = 0;
std::cin >> myPid;
ModuleInject(_T("D:\\CLion_workspace\\win32_programing\\dll_programing\\dll_inject\\lib\\libdll_generate.dll"), myPid);
Sleep(5000);
EnumProcessModule(myPid);
return 0;
}
演示
首先编译DLL,然后编译DLL注入器,路径写生成的DLL路径,编译被注入的delete.exe。
启动DLL注入器
然后启动delete.exe
在delete.exe的源码中,我预留了20秒的时间
这20秒的时间是用来打开任务管理器,查看delete.exe进程的PID的
dll_inject.exe中输入此PID,回车,然后杀软放行
提示注入成功
首次load此DLL时,会调用myHook.Hook
进行挂钩,被钩函数为Kernel32.CreateProcessA
,钩子函数为自定义的MyCreateProcessA
先来看一下挂钩是怎么挂的
首先拿到Kernel32.dll的句柄,在其中查找CreateProcessA
的地址,然后保存此地址的前5字节的指令,计算此地址到要跳转的MyCreateProcessA
的地址的偏移量,构造跳转指令jmp xxxxxxxx
,然后覆盖到此地址的前5字节。挂钩完成,此时如果程序调用CreateProcessA
,会执行跳转指令,直接跳到钩子函数处。
来看一下钩子函数
因为下面还要调用CreateProcessA
,所以需要先解钩,不然又会jmp到钩子函数,死循环。钩子函数比较简单,我只是写了个弹窗验证一下钩子函数被调用了
然后如果点确定,则继续执行CreateProcessA
创建进程,创建完毕之后,再重新钩住。
回到DllMain继续看
如果挂钩成功,则出现一个弹窗
点确定,让delete.exe继续执行,当它调用CreateProcessA
时,实际已经跳转到了钩子函数MyCreateProcessA
点’是’让其继续,成功打开notepad++
关于为什么还能成功执行CreateProcessA
我不太清楚,有个猜想应该是钩子函数里做的事情都没有改变堆栈。本来它是将参数都压栈了准备call CreateProcessA
,然后就jmp到钩子函数了,但是jmp之后做的事情都没改变堆栈,然后钩子函数又调用了CreateProcessA
所以又jmp回到原位置,此时已经解钩了,且堆栈没问题,所以正常执行CreateProcessA
,执行完之后,又回到钩子函数继续执行。
先留坑吧,我对动态调试还不熟,以后有能力了可以验证一下。
之后就乏善可陈了,只有一点需要注意。
delete.exe结束,卸载DLL,此时要解钩
如果不解钩,下次此DLL没有被注入到进程中的话,调用CreateProcessA
就不知道jmp到哪里去了,可能会导致程序崩溃。