MinHook 分析01 (x86的jmp+offset类型hook)
MinHook的原理就在于重写目标函数。在这次分析的X86模式中,32位相对JMP覆盖了整个地址空间。因为在相对地址计算中溢出的位被忽略,所以在X86模式中,函数的地址是容易掌控的。
直接来进入正题。
使用时需要通过#pragma来导出模块定义文件 (.def)中的DLL函数:
LIBRARY
EXPORTS
SeInitialize
SeUninitialize
SeCreateHook
SeEnableHook
SeDisableHook
#if defined _M_X64
#pragma comment(lib, "libMinHook.x64.lib")
#elif defined _M_IX86
#pragma comment(lib, "..\\Debug\\MiniHook.x86.lib")
#endif
Hook的第一步是用HeapCreate创建了一个自动增长的堆,首先是提交了PAGE_SIZE大小,详细内容的话可以参考MSDN:
__HeapHandle = HeapCreate(0,
0, //提交 PAGE_SIZE
0); //If dwMaximumSize is 0, the heap can grow in size.自动增长
随后开始创建Hook了。需要注意的是这里的创建Hook只是写好了进行Hook所需要的信息,原始指令备份,Trampline等,添加到HOOK_INFORMATION的HookEntry成员中。
填充的hook信息结构体:
typedef struct _HOOK_INFORMATION_
{
PHOOK_ENTRY HookEntry;
UINT MaximumLength;
UINT Length;
}HOOK_INFORMATION,*PHOOK_INFORMATION; typedef struct _HOOK_ENTRY
{
LPVOID TargetFunctionAddress;
LPVOID FakeFunctionAddress;
LPVOID MemorySlot;
UINT8 Backup[8]; //恢复Hook使用的存放原先数据 // UINT8 patchAbove : 1; // Uses the hot patch area. 位域:1位
UINT8 IsEnabled : 1; // Enabled.
// UINT8 queueEnable : 1; // Queued for enabling/disabling when != isEnabled. UINT Index : 4; // Count of the instruction boundaries.???
UINT8 OldIPs[8]; // Instruction boundaries of the target function.???
UINT8 NewIPs[8]; // Instruction boundaries of the trampoline function ???
} HOOK_ENTRY, *PHOOK_ENTRY; //44字节
首先第一步是检查函数是否已经被Hook过,如果已经被Hook过的话,就不必再一次填充HOOK_INFORMATION结构了。
UINT SeFindHookEntry(LPVOID FunctionAddress)
{
UINT i;
for (i = 0; i < __Hooks.Length; ++i)
{
if ((ULONG_PTR)FunctionAddress == (ULONG_PTR)__Hooks.HookEntry[i].TargetFunctionAddress)
return i;
}
return STATUS_NOT_FOUND;
}
如果没有被自己Hook过的话,就先准备一下TRAMPOLINE结构体:
#pragma pack(1)
typedef struct _TRAMPOLINE
{
LPVOID TargetFunctionAddress;
LPVOID FakeFunctionAddress;
LPVOID MemorySlot; // MemorySlot 32字节 #if defined(_M_X64) || defined(__x86_64__)
LPVOID pRelay; // [Out] Address of the relay function.
#endif
// BOOL patchAbove; // [Out] Should use the hot patch area? //Patch --->补丁 //0xA 0xB
UINT Index; // [Out] Number of the instruction boundaries.
UINT8 OldIPs[8]; // [Out] Instruction boundaries of the target function. //恢复
UINT8 NewIPs[8]; // [Out] Instruction boundaries of the trampoline function. //Hook
} TRAMPOLINE, *PTRAMPOLINE;
typedef struct _MEMORY_SLOT
{
union
{
struct _MEMORY_SLOT *Flink;
UINT8 BufferData[MEMORY_SLOT_SIZE];
};
} MEMORY_SLOT, *PMEMORY_SLOT; //32字节
对于TRAMPOLINE的构建是一块相对复杂的内容。可以看到结构体的前两个成员分别是原函数地址和我们想要到达的自定义函数地址,第三个成员MemorySlot,记录的是原函数地址开头的五个字节,以及跳转回原函数的jmp指令,OldPos和NewPos则分别保存了在构建MemorySlot的过程中,已经读取的目标函数偏移字节数,和已经写入的Detour函数字节数。
构建MemorySlot是通过一个do_While循环来实现的,首先它需要保存好被覆盖的那些字节数,所以在原函数已经读取的字节数少于jmp+四字节offset,一共五字节之前,MemorySlot要先备份下原来的指令,一旦原函数已经读取的字节数超过或者等于覆盖所需要的五字节时,也就不必再备份原来函数基地址处的指令了,因为仅仅只是覆盖五字节的内容:
do
{ HDE hde;
UINT CopyCodeLength;
LPVOID CopyCodeData;
//对于出现的相对偏移地址,在跳板中都要给出新的相对地址
/*
74CA8B80 8B FF mov edi,edi
74CA8B82 55 push ebp
74CA8B83 8B EC mov ebp,esp
74CA8B85 6A 00 push 0
74CA8B87 FF 75 14 push dword ptr [ebp+14h]
74CA8B8A FF 75 10 push dword ptr [ebp+10h]
74CA8B8D FF 75 0C push dword ptr [ebp+0Ch]
74CA8B90 FF 75 08 push dword ptr [ebp+8]
74CA8B93 E8 F8 FC FF FF call _MessageBoxExW@20 (74CA8890h)
*/
ULONG_PTR OldInstance = (ULONG_PTR)Trampoline->TargetFunctionAddress + OldPos;
ULONG_PTR NewInstance = (ULONG_PTR)Trampoline->MemorySlot + NewPos;
//指令长度 CopyCodeLength = HDE_DISASM((LPVOID)OldInstance, &hde);
if (hde.flags & F_ERROR)
return FALSE; CopyCodeData = (LPVOID)OldInstance; //第一次时,CopyCodeData是MessageBox入口地址
if (OldPos >= sizeof(JMP_REL))
{
// The trampoline function is long enough.
// Complete the function with the jump to the target function.
#if defined(_M_X64) || defined(__x86_64__)
jmp.address = pOldInst;
#else
//OldInstance = 74CA8B85 //目标 = 源 + Offset + 5
//Offset = 目标 - (源 + 5)
jmp.Operand = (UINT32)(OldInstance - (NewInstance + sizeof(jmp))); //计算跳转到目标的偏移 #endif
CopyCodeData = &jmp;
CopyCodeLength = sizeof(jmp); IsLoop = TRUE;
}
//......此处省略部分源码,突出重点 Trampoline->OldIPs[Trampoline->Index] = OldPos;
Trampoline->NewIPs[Trampoline->Index] = NewPos;
Trampoline->Index++; // Avoid using memcpy to reduce the footprint.
#ifndef _MSC_VER
memcpy((LPBYTE)ct->pTrampoline + newPos, pCopySrc, copySize);
#else
__movsb((LPBYTE)Trampoline->MemorySlot + NewPos, (const unsigned char*)CopyCodeData, CopyCodeLength);
#endif
NewPos += CopyCodeLength;
OldPos += hde.len;
} while (!IsLoop);
在这段do_while循环里,通过反汇编可以清晰地看到MemorySlot一步步的构建,在测试代码中我Hook的是MessageBox函数,所以在看MemorySlot的构造过程之前,先来看一看反汇编下原始的MessageBox的机器指令:
对应着MessageBox的机器指令,来看MemorySlot在do while循环中的构造过程:
局部变量窗口找到MemorySlot的地址:
先是申请了32字节的结构体大小:
通过反汇编引擎计算MessageBox函数基地址处开始,每几个字节是一条完整的机器指令,记录在Trampoline的OldIPs,NewIPs数组中:
//指令长度
CopyCodeLength = HDE_DISASM((LPVOID)OldInstance, &hde);
Trampoline->OldIPs[Trampoline->Index] = OldPos;
Trampoline->NewIPs[Trampoline->Index] = NewPos;
Trampoline->Index++;
每记录一次指令字节长度的同时,也将即将被覆盖的原指令备份到MemorySlot中:
现在备份的指令长度已经达到了5字节了,可以开始填充跳转回MessageBox基地址处+5字节的偏移地址跳转指令了:
if (OldPos >= sizeof(JMP_REL))
{
// The trampoline function is long enough.
// Complete the function with the jump to the target function.
#if defined(_M_X64) || defined(__x86_64__)
jmp.address = pOldInst;
#else
//目标 = 源 + Offset + 5
//Offset = 目标 - (源 + 5)
jmp.Operand = (UINT32)(OldInstance - (NewInstance + sizeof(jmp))); //计算跳转到目标的偏移 #endif
CopyCodeData = &jmp;
CopyCodeLength = sizeof(jmp); IsLoop = TRUE;
}
到此为止MemorySlot填充好了10字节的内容,构造完毕了,整个Trampoline结构也构造好了,现在Trampoline的内容应该是这样的:
创建好了Trampoline结构后,开始创建HookEntry结构体,对比两个结构体我们可以看到:HookEntry结构体只比Trampoline结构多了两个成员,所以其实HookEntry结构的构建只剩下两个成员了——一个Backup备份被覆盖的五字节内容,一个IsEnabled设置为FALSE,暂时不开始实际的Hook。
HookEntry->IsEnabled = FALSE; //存储源函数的数据内容 5字节的备份
memcpy(HookEntry->Backup, TargetFunctionAddress, sizeof(JMP_REL));
到此为止整个CreateHook的流程就结束了,可以进入下一话:EnableHook,使Hook生效——实际动手覆盖五字节了
//SHELLCODE
PJMP_REL jmp = (PJMP_REL)PatchData;
jmp->Opcode = 0xE9;
jmp->Operand = (UINT32)((LPBYTE)HookEntry->FakeFunctionAddress - (PatchData + sizeof(JMP_REL)));
而到了想要恢复的时候,将备份好的原五字节覆盖回去也就搞定了:
memcpy(PatchData, HookEntry->Backup, sizeof(JMP_REL));
其实还有不恢复原五字节指令而能够正确调用MessageBox的方法,回头一想,直接执行Trampoline结构中MemorySlot的指令,不久是正确执行了MessageBox的指令吗嘛!——想到这一点的时候,我才明白了构建了MemorySlot之后,还要另外用Backup成员保存原五字节的用意:一个用来直接调用正确的MessageBox函数,一个用来进行UnHook恢复。
谈到这里,顺便一提钩子链的问题,当一个函数被多次Hook的时候,就会出现Hook链了。Hook链中依次跳到各个模块对目标函数Hook的Detour函数中,执行自己的函数。根据不同的模块Hook的先后顺序,形成钩子链:
从图中可以看出,Module2是先挂的钩子,它将CreateProcessW的其实5字节的指令复制到了它的Trampoline函数中,并且添加了一个jmp到CreateProcessW+5Byte的地址,从而继续执行CreateProcessW之后的指令实现恢复。当Module1再次对CreateProcessW挂钩子时,它将Module2写的jmp到Module2地址的指令复制到了Module1 的Trampline 函数中,这样就形成了钩子链,即对同一个函数地址,多次Hook时就形成了钩子链。
从图中也可以看出,卸载钩子回复指令的方法有两种:
1. 按照挂钩的反顺序卸载钩子,也即先Module1卸载钩子,然后Module2卸载钩子,Module1会将JMP Hook2-Temp的指令恢复到了CreatProcessW挂钩指令上,那么这时CreateProcessW处就变成了单个挂钩的情况,无论此时调用CreateProcessW,还是继续Module2的UnHook都不会有问题。
2. 直接卸载第一个挂钩,也即先让Module2卸载,那么CreateProcessW被Hook的地方指令被恢复为了原始的情况(Kernel32.dll初始映射的情况)。这时调用CreateProcessW不会出现问题。也就是说虽然Module1没有UnHook,但是它的钩子被覆盖掉了,也不会调用到Module1的Detours函数了。
但是~如果此时,Module1 继续执行卸载,那么很明显 JMP Hook2-Temp 指令被覆盖会了CreateProcessW Hook的五个字节指令上了,而很明显它要跳转到Module2的自定义Detours函数。而Trampoline函数所占用的内存空间可能已经被释放掉了,一旦释放掉了,将跳转到无效的内存空间,就要GG崩溃了~
Hook MessageBox的测试结果:
MessageBoxW(NULL, L"HelloWorld", L"HelloWorld", 0); int WINAPI DetourMessageBox(
_In_opt_ HWND hWnd,
_In_opt_ WCHAR* lpText,
_In_opt_ WCHAR* lpCaption,
_In_ UINT uType)
{
__OriginalMessageBoxW(hWnd,L"FakeMessageBox",L"FakeMessageBox",uType);
return 0;
} if (CreateHook(&MessageBoxW, &DetourMessageBox,
reinterpret_cast<LPVOID*>(&__OriginalMessageBoxW)) != STATUS_SUCCESS)
{
return;
}
MinHook 分析01 (x86的jmp+offset类型hook)的更多相关文章
- windows7内核分析之x86&x64第二章系统调用
windows7内核分析之x86&x64第二章系统调用 2.1内核与系统调用 上节讲到进入内核五种方式 其中一种就是 系统调用 syscall/sysenter或者int 2e(在 64 位环 ...
- 一个由正则表达式引发的血案 vs2017使用rdlc实现批量打印 vs2017使用rdlc [asp.net core 源码分析] 01 - Session SignalR sql for xml path用法 MemCahe C# 操作Excel图形——绘制、读取、隐藏、删除图形 IOC,DIP,DI,IoC容器
1. 血案由来 近期我在为Lazada卖家中心做一个自助注册的项目,其中的shop name校验规则较为复杂,要求:1. 英文字母大小写2. 数字3. 越南文4. 一些特殊字符,如“&”,“- ...
- .NET的堆和栈01,基本概念、值类型内存分配
当我们对.NET Framework的一些基本面了解之后,实际上,还是很有必要了解一些更底层的知识.比如.NET Framework是如何进行内存管理的,是如何垃圾回收的......这样,我们才能写出 ...
- SparkRPC源码分析之RPC管道与消息类型
SparkRPC源码分析之RPC管道与消息类型我们前面看过了netty基础知识扫盲,那我们应该明白,ChannelHandler这个组件内为channel的各种事件提供了处理逻辑,也就是主要业务逻辑写 ...
- NIO 源码分析(01) NIO 最简用法
目录 一.服务端 二.客户端 NIO 源码分析(01) NIO 最简用法 Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html) J ...
- 恶意代码分析实战-x86反汇编速成班
x86反汇编速成 x86体系结构 3种硬件构成: 中央处理器:负责执行代码 内存(RAM):负责存储所有的数据和代码 输入/输出系统(I/O):为硬盘.键盘.显示器等设备提供接口 内存 一个程序的内存 ...
- JS-特效 ~ 01. 事件对象、offset偏移/检测、无缝滚动、自动循环轮播图
Math.round ( ) :正书四舍五入,负数五舍六入 用定时器,先清除定时器 事件对象 event event:事件被触动时,鼠标和键盘的状态,通过属性控制 Offset:偏移,检测 1. 获取 ...
- Spring IOC 源码简单分析 01 - BeanFactory
### 准备 ## 目标 了解 Spring IOC 的基础流程 ## 相关资源 Offical Doc:http://docs.spring.io/spring/docs/4.3.9.RELEASE ...
- 非常适合新手的jq/zepto源码分析01
(function(global, factory) { // 查看这里是不是定义成模块,如果定义模块就返回 一个模块 if (typeof define === 'function' &&a ...
随机推荐
- MySql中not in的优化
最近项目上用select查询时使用到了not in来排除用不到的主键id一开始使用的sql如下: select s.SORT_ID, s.SORT_NAME, s.SORT_STATUS, s.SOR ...
- 利用GSEA对基因表达数据做富集分析
image Gene Set Enrichment Analysis (GSEA) is a computational method that determines whether an a p ...
- IOS 模块并且发布到NPM
注释:导入出错 请使用这个 #import <React/RCTBridge.h> 参考文档:http://www.liuchungui.com/blog/2016/05/02/r ...
- git commit -m 提交的内容换行
网上说只需要通过单引号来换行,一直没理解,后面终于试出来了.总结一句话就是. . 先输入第一个引号,按Enter即可换行,完成后再补齐后面的引号 // 步骤一: 输入第一行 git commit -m ...
- vue 打开新窗口
const {href} = this.$router.resolve({ name: 'foo', query: { bar } }) window.open(href, '_blank')
- webpack-工程化工具
一.简介 1.webpack 是 facebook 公司发布的一款工程化工具,早期有 react 使用. 2.核心理念: 一切都是资源,是资源我们就能模块化打包加载. 3.webpack 默认支持 c ...
- leecode第二百三十一题(2的幂)
class Solution { public: bool isPowerOfTwo(int n) { bool is_flag=false; ) { ==)//如果为1,看是不是第一个1 { if( ...
- Windows Server 2012 R2 英文版安装中文语言包教程
Windows Server 是云操作系统的主要组成部分. 有了 Windows Server,再加上云操作系统内的开发者技术,您就可以构建现代业务应用程序. 现代业务应用程序通常涵盖内部部署资源和公 ...
- 自学PYTHON分享 --基础1
1.python2和python3的区别: 宏观上:python2 与 python3 区别: python2 源码不标准,混乱,重复代码太多, python3 统一 标准,去除重复代码. 2.pyt ...
- SQLserver提示事务日志已满无法重建索引,前台提示日志已满处理方案
1.数据库--属性--选项--恢复模式:简单. 2.数据库--任务--文件类型:日志 在释放未使用的空间潜重新组织页:1M 3.数据库--属性-- ...