Hook原理
对于会Hook的人来说,Hook其实也就那么回事。对于没有Hook过的人来说,会感觉Hook很高大上(其实也没毛病)。
那么今天我们就来探讨一些Hook的原理是什么。
我认为任何Hook都可以分为以下三步(简称WFH):
-
需要Hook的是什么,在哪里(后面简称Where)
-
寻找到Hook的地方.(后面简称Find)
-
进行Hook.(后面简称Hook)
当然了同一个类型的Hook所在的地方一般是一样的。但寻找到Hook的地方,和进行Hook却会有许多不同的方法。我们要抓住的是不变的地方。
根据这3点我们举例来验证一下:
Hook API
第一个我尽量说得详细一些。
举例子:Hook API:OpenProcess 让win10 64位的任务管理器关闭不了任何程序。
1、Where
Hook API:OpenProcess. 在kernelbase.dll里面。
2、Find
方式1:
-
通过LoadLibrary加载kernelbase.dll模块基地址;
-
通过 GetProcAddress 获取 OpenProcess 的地址。
方式2:编程直接引用OpenProcess的地址,因为在Windows下3大模块user32.dll,kernelbase.dll,ntdll.dll 的加载基地址在每个应用程序中都是一样的.
方式3:通过寻找目标的IAT找到OpenProcess。
3、Hook
方式1:通过注入dll到目标进程进行,可以替换 kernelbase.dll 里面的OpenProcess 的前面5个字节为jmp跳转到我们自己的地址,也可以修改目标进程的IAT。
方式2:通过WriteProcessMemory写入代码,修改目标进程的 OpenProcess 跳转到我们的代码。
代码实例:F1+H1(Find的第二种方式,Hook的第一种方式,后面不再说明):
(1)新建一个dll文件:
(2)在dll文件里面写如下代码:
如果你的win10是64位的就编译64位的,32位就编译32位的
// dllmain.cpp : 定义 DLL 应用程序的入口点。
DWORD oldProtect;
BYTE JmpBtye[5];
BYTE OldByte[5];
void * OpenProcessaddr;
bool H1_OpenProcess();
void UnHook();
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
H1_OpenProcess();
break;
case DLL_PROCESS_DETACH:
UnHook();
break;
}
return TRUE;
}
HANDLE MyOpenProcess(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwProcessId)
{
dwDesiredAccess &= ~PROCESS_TERMINATE;//去掉关闭程序的权限
UnHook();//恢复Hook 任何调整到原来的地方执行.
HANDLE h = OpenProcess(dwDesiredAccess, bInheritHandle, dwProcessId);
H1_OpenProcess();
return h;
}
void * F1_OpenProcess()
{
//寻找到OpenProcess的地址
void * addr = 0;
//加载kernel32.dll
HMODULE hModule = LoadLibraryA("kernelbase.dll");
//获取OpenProcess的地址
addr=(void *)GetProcAddress(hModule, "OpenProcess");
return addr;
}
void * F2_OpenProcess()
{
return (void *)OpenProcess;
}
bool H1_OpenProcess()
{
//1.开始寻找地址
void * addr = F1_OpenProcess();
OpenProcessaddr = addr;
//判断是否寻找成功
if (addr == 0)
{
MessageBoxA(NULL,"寻找地址失败",NULL,0);
return false;
}
//2.进行Hook
/*
一般代码段是不可写的,我们需要把其改为可读可写.
*/
VirtualProtect((void *)addr, 5, PAGE_EXECUTE_READWRITE,&oldProtect);
//修改前面的5个字节为jmp 跳转到我们的代码.
//内联Hook 跳转偏移计算方式:跳转偏移=目标地址-指令地址-5
//jmp 的OpCode 为:0xE9
JmpBtye[0] = 0xE9;
*(DWORD *)&JmpBtye[1] = (DWORD)((long long)MyOpenProcess - (long long)addr - 5);
//保存原先字节
memcpy(OldByte, (void *)addr, 5);
//替换原先字节
memcpy((void *)addr, JmpBtye, 5);
}
void UnHook()
{
//恢复原先字节
memcpy((void *)OpenProcessaddr, OldByte, 5);
//恢复属性
DWORD p;
VirtualProtect((void *)OpenProcessaddr, 5, oldProtect, &p);
}
把dll注入任务管理器,因为注入不是我们主题,所以这里我只是简单的贴出代码,直接拿来用就可以。
```
#include <windows.h>
//获取进程句柄
HANDLE GetThePidOfTargetProcess(HWND hwnd)
{
DWORD pid;
GetWindowThreadProcessId(hwnd, &pid);
HANDLE hProcee = ::OpenProcess(PROCESS_ALL_ACCESS | PROCESS_CREATE_THREAD, 0, pid);
return hProcee;
}
//提升权限
void Up()
{
HANDLE hToken;
LUID luid;
TOKEN_PRIVILEGES tp;
OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);
LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid);
tp.PrivilegeCount = 1;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
tp.Privileges[0].Luid = luid;
AdjustTokenPrivileges(hToken, 0, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL);
}
//进程注入
BOOL DoInjection(char *DllPath, HANDLE hProcess)
{
DWORD BufSize = strlen(DllPath)+1;
LPVOID AllocAddr = VirtualAllocEx(hProcess, NULL, BufSize, MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(hProcess, AllocAddr, DllPath, BufSize, NULL);
PTHREAD_START_ROUTINE pfnStartAddr = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA");
HANDLE hRemoteThread;
hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, pfnStartAddr, AllocAddr, 0, NULL);
if (hRemoteThread)
{
MessageBox(NULL, TEXT("注入成功"), TEXT("提示"), MB_OK);
return true;
}
else
{
MessageBox(NULL, TEXT("注入失败"), TEXT("提示"), MB_OK);
return false;
}
}
int main()
{
//这里填写窗口标题
HWND hwnd=FindWindowExA(NULL, NULL, NULL, "任务管理器");
Up();
HANDLE hP = GetThePidOfTargetProcess(hwnd);
//开始注入
//这里填写Dll路径
DoInjection("E:\\studio\\VS2017\\F2H1.MessageBox\\x64\\Release\\F2H1.MessageBox.dll", hP);
}
```
注入之后看效果
其实还有很多方式,剩下的方式你就可以自己慢慢尝试了。
SSDT Hook
刚才说了用户层的Hook,接下来我们再说一下内核层的Hook,其实还是3歩曲。WFH
实现相似的功能,让所有程序关闭不了自己的程序。
1.Where
Windows 操作系统共有4个系统服务描述符。其中只用了两个,第一个是SSDT,第二个是ShadowSSDT。
系统描述符结构如下:
typedef struct _KSYSTEM_SERVICE_TABLE
{
ULONG *ServiceTableBase; // 服务表基址 第一个表示SSDT 紧接着下一个ShadowSSDT
ULONG *ServiceCounterTableBase; // 计数表基址
ULONG NumberOfServices; // 表中项的个数
UCHAR *ParamTableBase; // 参数表基址
}KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE;
SSDT Hook:NtOpenProcess,在ntkrnlpa.exe内核模块中的系统服务描述符表中的SSDT表中的第190号。
使用PCHunter32查看
2. Find
方式1:在Win7 32下,系统服务描述符表直接导出符号KeServiceDescriptorTable,可以直接获取其地址,然后通过其第一个ServiceTableBase 就是SSDT的地址,接着找到第190号函数。
方式2:可以通过 PsGetCurrentThread 获取ETHREAD结构,该结构的第一个字段KTHREAD有一个字段ServiceTable保存着系统描述符表的地址KeServiceDescriptorTable。通过其第一个 ServiceTableBase 就是SSDT的地址,接着找到第190号函数。
0: kd> u PsGetCurrentThread
nt!PsGetCurrentThread:
840473f1 64a124010000 mov eax,dword ptr fs:[00000124h] ;ETHREAD
840473f7 c3 ret
3.Hook
方式1:替换找到的地方,换成我们自己的函数
方式2:获取找到的地方的函数指针,改变其代码跳转到自己的代码(其实就是inline Hook)
例子:F2H1
新建一个驱动程序:
2.代码如下:
#include <ntifs.h>
#pragma pack(1)
typedef struct _KSYSTEM_SERVICE_TABLE
{
ULONG *ServiceTableBase; // 服务表基址 第一个表示SSDT 紧接着下一个是ShadowSSDT
ULONG *ServiceCounterTableBase; // 计数表基址
ULONG NumberOfServices; // 表中项的个数
UCHAR *ParamTableBase; // 参数表基址
}KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE;
#pragma pack()
void *OldNtProcess = 0;
// 导入系统描述符表
extern "C" NTSYSAPI KSYSTEM_SERVICE_TABLE KeServiceDescriptorTable;
typedef NTSTATUS(NTAPI *NTOPENPROCESS)(PHANDLE ProcessHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PCLIENT_ID ClientId);
NTOPENPROCESS g_NtOpenProcess = NULL;
NTSTATUS NTAPI MyOpenProcess(
PHANDLE ProcessHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PCLIENT_ID ClientId
)
{
if (ClientId->UniqueProcess == (HANDLE)916)//指定保护的进程ID
{
return STATUS_ABANDONED;
}
return g_NtOpenProcess(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId);
}
void OffProtect()
{
__asm { //关闭内存保护
push eax;
mov eax, cr0;
and eax, ~0x10000;//关闭CR0.WP位,关闭页保护
mov cr0, eax;
pop eax;
}
}
void OnProtect()
{
__asm { //恢复内存保护
push eax;
mov eax, cr0;
or eax, 0x10000;//开启CR0.WP位,开启页保护
mov cr0, eax;
pop eax;
}
}
void * F1_NtOpenProcess()
{
return (void *)&KeServiceDescriptorTable.ServiceTableBase[190];
}
void * F2_NtOpenProcess()
{
PETHREAD eThread = PsGetCurrentThread();
PKSYSTEM_SERVICE_TABLE kServiceTable = (PKSYSTEM_SERVICE_TABLE)(*(ULONG *)((ULONG)eThread + 0xbc));
return (void *)&kServiceTable->ServiceTableBase[190];
}
bool H1_NtOpenProcess()
{
OldNtProcess = F2_NtOpenProcess();//Find
g_NtOpenProcess = (NTOPENPROCESS)(*(ULONG *)OldNtProcess);//保存就地址
OffProtect();//由于SSDT表是只读的所以需要关闭页写入保护
(*(ULONG *)OldNtProcess) = (ULONG)MyOpenProcess;//写入自己的函数地址
OnProtect();//恢复
return true;
}
void UnHook()
{
OffProtect();//由于SSDT表是只读的所以需要关闭页写入保护
(*(ULONG *)OldNtProcess) = (ULONG)g_NtOpenProcess;//恢复函数
OnProtect();//恢复
}
void Unload(PDRIVER_OBJECT pDri)
{
UNREFERENCED_PARAMETER(pDri);
UnHook();
}
extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT pDri, PUNICODE_STRING pRegStr)
{
UNREFERENCED_PARAMETER(pRegStr);
pDri->DriverUnload = Unload;
H1_NtOpenProcess();
return STATUS_SUCCESS;
}
加载驱动程序(自己写的一个小工具,也可以网上下载)
4.查看效果
SYSENTRY Hook
这里我再说一些Hook,也是3歩曲WFH。但是我不再提供具体实现。
我们知道以前windows系统是通过int 2e中断进入系统内核的,但是现在是通过cpu提供的一个功能sysentry进入系统的(32位是sysentry,64位是syscall)。这是一个CPU指令,如果对该指令不知道的话,可以查看我另外一篇文章:
1.Where
SYSENTRY Hook:190号功能号,功能号保存在eax中.
SYSENTRY指令进入系统内核的地址保存在MSR寄存器里面的**IA32_SYSENTER_EIP** (0x176)号寄存器.
2.Find
通过指令rdmsr读取**IA32_SYSENTER_EIP** MSR寄存器。其中ecx保存的是读取msr的序号,也就是0x176号,返回的结果保存在edx:eax(64位,edx保存高32位,eax保存低32位)。因为是32位系统,所以只需要eax的值即可。
3.Hook
通过wrmsr写入我们自己的地址,地址放在edx:eax(64位,edx保存高32位,eax保存低32位),即可完成Hook。
Object Hook
每一个不同的内核对象,都对应着一个不同的类型索引:TypeIndex.通过该索引可以找到该内核对象的类型:OBJECT_TYPE
1.Where
在内核对象的TypeInfo中.
2.Find
通过 ObGetObjectType 内核函数获取内核对象类型(OBJECT_TYPE)的OBJECT_TYPE中有一个字段TypeInfo(类型_OBJECT_TYPE_INITIALIZER),其中保存着,在创建内核对象,销毁内核对象的一系列构造函数。
对应结构:
//OBJECT_TYPE-->TypeInfo:_OBJECT_TYPE_INITIALIZER
ntdll!_OBJECT_TYPE
+0x000 TypeList : _LIST_ENTRY
+0x010 Name : _UNICODE_STRING
+0x020 DefaultObject : Ptr64 Void
+0x028 Index : UChar
+0x02c TotalNumberOfObjects : Uint4B
+0x030 TotalNumberOfHandles : Uint4B
+0x034 HighWaterNumberOfObjects : Uint4B
+0x038 HighWaterNumberOfHandles : Uint4B
+0x040 TypeInfo : _OBJECT_TYPE_INITIALIZER //1.这个
+0x0b0 TypeLock : _EX_PUSH_LOCK
+0x0b8 Key : Uint4B
+0x0c0 CallbackList : _LIST_ENTRY
ntdll!_OBJECT_TYPE_INITIALIZER
+0x000 Length : Uint2B
+0x002 ObjectTypeFlags : UChar
+0x002 CaseInsensitive : Pos 0, 1 Bit
+0x002 UnnamedObjectsOnly : Pos 1, 1 Bit
+0x002 UseDefaultObject : Pos 2, 1 Bit
+0x002 SecurityRequired : Pos 3, 1 Bit
+0x002 MaintainHandleCount : Pos 4, 1 Bit
+0x002 MaintainTypeList : Pos 5, 1 Bit
+0x002 SupportsObjectCallbacks : Pos 6, 1 Bit
+0x004 ObjectTypeCode : Uint4B
+0x008 InvalidAttributes : Uint4B
+0x00c GenericMapping : _GENERIC_MAPPING
+0x01c ValidAccessMask : Uint4B
+0x020 RetainAccess : Uint4B
+0x024 PoolType : _POOL_TYPE
+0x028 DefaultPagedPoolCharge : Uint4B
+0x02c DefaultNonPagedPoolCharge : Uint4B
+0x030 DumpProcedure : Ptr64 void
+0x038 OpenProcedure : Ptr64 long//打开 回调函数
+0x040 CloseProcedure : Ptr64 void//关闭 回到函数
+0x048 DeleteProcedure : Ptr64 void
+0x050 ParseProcedure : Ptr64 long
+0x058 SecurityProcedure : Ptr64 long
+0x060 QueryNameProcedure : Ptr64 long //查询名称 回调函数
+0x068 OkayToCloseProcedure : Ptr64 unsigned char
3.Hook
根据找到的位置替换里面回调函数指针为我们自己写的函数即可,比如替换OpenProcedure。
IDT Hook
1.Where
在中断描述符表(IDT)中.
2.Find
idtr寄存器指向中断描述符表.通过idtr找到.
说明:idtr是一个48位寄存器,其中低16位保存中断描述符表长度,高32位是中断描述符表.的基地址。
3.Hook
通过构造一个中断门或者陷阱门,其中中断门或陷阱门的偏移地址写自己的地址。然后把中断门或者陷阱门写入都相应的IDT表项中。
总结:
从上面我们可以看到,其实Hook都是一样的,只是对应的地方不同,寻找的方法不同,替换(修改)的方法不同而已。
有的人可能就要反问了,SetWindowsHookEx,就不要知道Hook的地方在哪了,也不需要寻找。确实,这两歩不需要我们自己做,但并不代表不需要,这只是操作系统为我们做了而已,我们只需要提供一个回调函数即可。
所以下面我留下一个小测试:就是自己自己实现SetWindowsHookEx。
微信公众号: 架构师日常笔记 欢迎关注!