96SEO 2026-02-20 00:05 16
我在之前研究文明6的联网机制并试图用Hook技术来拦截socket函数的时候熟悉了简单的Inline

Hook方法但是由于之前的方法存在缺陷所以进行了深入的研究总结出了一些有关Windows下x86和x64架构程序的Inline
本文使用的方法并非最优也没有保证安全但是用较少的代码实现了所需的功能非常适合用来学习Inline
由于本文是在Windows平台下的所以你需要对Windows系统的机制需要有一定的了解同时本文的代码基于C语言当然C编译器也可以编译所以你应该要有C语言的基础尤其是对指针的理解此外你还需要有一定的8086汇编如果x86和x64更好基础因为本文涉及到部分汇编指令。
本文假定你对以上这些内容有一定基础但并不非常熟悉如果你完全了解可以适当跳过部分内容。
如果你对更高级的内容有兴趣本文后面也会对这些东西做一个介绍有兴趣可以进一步了解。
在开始之前先说明一下本文所有提到的完整代码都可以在这个链接找到https://gitee.com/peazomboss/archive/tree/main/misc/230131-inlinehook。
本文的代码并不完善如果需要一个能用的同时又小巧的Hook库可以看看我最新的作品DrPeaboss/tinyhook:
(github.com)不仅支持x86和x64还支持ARM64架构。
当然如果需要功能完善的Hook库可以查看扩展内容部分推荐的两个。
Windows下的Hook机制最早是用来在提供类似于DOS下的中断机制当然还有更多其他功能。
Hook技术有许许多多的分类本文所用的就是其中一种Inline
Hook一般是修改一个函数头部的代码使其跳转到指定的地址。
这样当调用这个函数的时候实际上执行的是我们设定的代码。
正因为如此我们可以用Hook技术来拦截操作系统的API或者某个软件的关键函数然后拦截获取信息或者修改其内容从而达到我们的目的。
比如微信QQ的防撤回就是这样实现的游戏对战平台也一般是这样做的。
Hook的方法。
其中第一种比较简单但效果较差尤其是在x64和多线程的情况下而第二种效果好尤其是x64以及多线程的情况下但是操作较为复杂。
为了方便演示我选择了kernelbase.dll的函数WriteConsoleA因为这个函数可以直接在控制台输出一段指定字符串便于我们查看Hook的效果。
如果你通过windows.h头文件导入WriteConsoleA这个函数会发现它调用了kernel32.dll的WriteConsoleA而不是kernelbase.dll的这个你可以去反汇编看看但是在kernel32.dll内部你会发现函数头部就是一句jmp指令而真正执行的是kernelbase.dll里的函数所以一般选择要Hook的函数的时候如果这个函数头部是一句跳转指令则去修改跳转过去的地址。
这部分Hook方法是最简单的对于x86和x64仅有汇编指令的不同但根本逻辑是完全一样的。
这种方法之所以简单是因为不需要什么复杂的操作和概念只要简单修改函数的头部代码然后需要调用原来的代码的时候再给他改回去就行了。
但是因为要改来改去的所以在多线程的情况下会遇到问题这个在之后讨论。
addr_diff由于jmp指令有好多种用法我们这里用的是寻址范围±2G的指令所以编译成机器码有5个字节第一个字节是0xE9剩下4个字节是目标地址相对当前EIP的差值。
比如被Hook的函数地址是7FF01000我们就修改7FF01000处的代码使其跳转到我们00401000处代码代码如下
...注意这里的FBFF4F80实际上是用小端表示的0x804FFFFB记得刚刚说的吧是目标地址相对当前EIP的差值。
在执行7FF01000这一句的时候EIP已经不是7FF01000了而是7FF01005因为EIP始终指向当前执行指令的下一个指令。
我们可以计算得出0x7FF010050x804FFFFB0x100401000由于EIP是32位寄存器所以实际上执行这一句后EIP就会被设为00401000这样就使得代码执行到了我们的地方了。
所以我们可以得出这样一个计算公式假定被我们Hook的代码地址是addr_hook而我们替换的地址是addr_fake那么跳转语句jmp
addr_diff的addr_diffaddr_hook-addr_fake-5。
代入刚刚的数据0x804FFFFB0x00401000-0x7FF01000-0x5只取低32位可以发现这个等式成立。
那么方法就很简单了我们只要知道被Hook函数的地址用来替换的函数的地址即可计算出修改的指令当然修改之前要先保存一下原来的指令以便到时候改回去。
具体操作在后面的实例讲解会有说明。
对于x64来说除了头部修改的字节数和跳转的指令不同其余和x86的情况完全一致。
不过这个汇编指令就不能再像x86一样简单用jmp指令了因为似乎没有一个jmp指令可以跨大于±2G的内存地址空间。
作为jmp的替代我们可以用寄存器寻址或者压栈配合ret指令实现同样的效果
ret以上两段代码效果一样而且都占用12个字节但缺点一致——会改变寄存器的值。
由于改变寄存器的值可能会影响程序运行结果我们可以用如下代码避免这种情况
ret注意这里的address.low表示地址的低4字节address.high表示地址的高4字节。
这段代码的原理是在x64汇编中push指令只能处理4个字节的立即数但是由于栈是8字节对齐的所以执行第一句指令的时候栈里会压入8字节内容其中低4字节就是push的值而高4字节会补0此时我们可以通过rsp寄存器间接寻址再把那高4字节立即数放入栈里。
相对之前的两段代码这段代码的好处是不会修改寄存器不过缺点是指令长度要多2个字节。
不过为了确保不会出现问题我们就选择这个方法。
首先看一下微软文档关于WriteConsoleA这个函数的原型说明
nNumberOfCharsToWrite,_Out_opt_
lpNumberOfCharsWritten,_Reserved_
);注意这个函数原型就是一个宏在Unicode下实际调用的是WriteConsoleWANSI下则是WriteConsoleA。
推荐是直接调用WriteConsoleA以免遇到不必要的麻烦。
第一个参数是输出的控制台句柄这个句柄可以通过调用GetStdHandle(-11)来获取。
第二个参数是要写入到控制台的字符串缓冲区在WriteConsoleA中用char数组就行了。
第三个参数指示刚刚那个缓冲区里的字符数量不要超过缓冲区实际的长度。
第四个参数是一个DWORD类型的指针返回实际写入到控制台的字符数量可以为NULL。
现在编写一个替换原来函数的函数注意调用约定和参数列表要一模一样。
*)lpBuffer);buf[nNumberOfCharsToWrite
}这段代码首先调用了unhook()把被Hook函数的头几个字节改回原来的代码这样就可以重新调用原来的这个函数了。
之后一段代码很简单就是把原来想要输出的字符串后面的’\n’去掉并加上了\t[hook]\n然后再调用WriteConsoleA函数输出被替换的字符串。
最后再调用dohook()把头部的函数改成跳转代码这样又可以继续Hook这个函数了。
对于Hook的代码x86和x64基本一样除了硬编码部分存在差异所以我们可以用条件编译的方法来区分二者。
#endif其中__x86_64__和__amd64__是gcc定义的表明这是x64同理_M_X64和_M_AMD64则是由微软vc编译器定义的。
而__i386__是gcc定义的x86下的宏_M_IX86是微软定义的。
我们在此基础上重新定义了_CPU_X64和_CPU_X86这两个宏用来方便后续的使用。
接下来需要定义被Hook函数头部需要替换的字节数那么按照前面的方法我们如下定义
GetModuleHandleA(kernelbase.dll);
找到函数地址VirtualProtect(hook_func,
*)fk_WriteConsoleA;hook_jump[0]
}这里调用了VirtualProtect函数把目标函数的指定字节内存设为可读可写实际上不论设置与否读取的时候可以直接用指针或者memcpy函数但是如果不设置则无法写入而且写入的时候还必须要通过WriteProcessMemory函数。
{WriteProcessMemory(GetCurrentProcess(),
{WriteProcessMemory(GetCurrentProcess(),
}第一个参数从GetCurrentProcess()获得表示当前进程最后一个参数设为NULL就行了其余3个参数内容和memcpy是基本一样的。
i)ResumeThread(hthreads[i]);for
i)WaitForSingleObject(hthreads[i],
i)CloseHandle(hthreads[i]);WriteConsoleA(hstdout,
这个必须被Hookunhook();WriteConsoleA(hstdout,
}这一部分代码很好理解主函数进行基本的初始化然后启动5个线程每个线程都会打印自己的线程id和内容。
hook根据线程ID和Hook情况来看只有30664和29936这两个线程被成功Hook到了。
由于上述简单Hook存在较大的局限性所以这里介绍一种可以在多线程环境下使用的Hook方法。
其基本原理是提供一个跳板函数在需要调用原来函数的时候不是简单把函数头部字节改回去而是把头部字节的代码拷贝到一段内存执行再加入一段跳转代码。
这样只要通过这段内存就可以直接调用这个函数了。
对于x86假设我们的代码在00401000被Hook的函数在7FF0100A跳板代码地址在00600000。
...这里7FF01000处的代码已经被替换而00600000处的代码则是从7FF01000处拷贝而来。
这样当我们需要调用7FF01000这个函数的时候则不必再改写其头部内存而是直接调用00600000即可。
一个需要关注的细节是如果7FF01000处的前5字节不能构成完整汇编指令的时候就要多拷贝几个字节的指令使得一条指令是完整的但具体是几个字节需要提前反汇编得知。
如果前5个字节含有跳转类代码则容易造成错误不适合这个方法进行Hook。
在x64的情况下情况则有所不同了因为按照前面的方法至少需要修改头部的14个字节而14个字节出现跳转类代码的概率是很大的所以我们要避免修改这么多代码。
有一个很好的解决方法是修改头部5个字节的代码然后像x86一样改成jmp指令跳转到2G范围内的一处空白内存然后在这个空白的内存里再改成14字节的跳转代码跳转到我们真正要执行的代码。
这个方法经常出现在破解或修改他人程序的时候如果修改后的代码大小大于其原来的大小时就无法就地修改了这时就可以跳转到一处空白内存接着执行修改后的代码执行完了再跳回去就好了。
而跳板函数的原理和x86则是基本一样的唯一的区别是跳转回去的指令是14字节。
假设我们的代码在0000000100001000被Hook函数在00007FF000001000空白的内存在00007FF00003A000。
...上面这段代码清晰的展示了指令如何从被Hook的函数辗转到我们的函数地址。
那么怎么找到一片空白的内存呢这就涉及到了Windows下可执行文件包括动态链接库的文件结构——PE结构了。
所有的exe和dll文件头部都是一样的在他们被加载时都是按页4KB一页将文件的各部分加载到内存中而文件头的结构则是按照原始的格式完整地加载到了内存。
基于这个原理我们就可以找到一个exe或dll的代码段的内存地址。
又因为内存的一页是4KB那么意味着在内存中每个模块的代码段最后一部分必然存在冗余。
所以我们就可以根据模块的地址来读取其文件头部然后获取我们需要的信息。
幸运的是当我们调用LoadLibrary或者GetModuleHandle时若函数执行成功其返回值就是模块在内存中的地址而根据这个地址我们就可以解析文件头了。
有关Windows的可执行文件头的具体内容这里做一个简单的介绍。
第一部分是DOS头结构为IMAGE_DOS_HEADER具体可以在微软文档找到其字段e_lfanew指示了PE头的位置。
第三部分是PE文件头结构为IMAGE_FILE_HEADER。
第四部分是PE可选头结构为IMAGE_OPTIONAL_HEADER其大小由PE文件头SizeOfOptionalHeader字段指示。
第五部分是区段section结构为IMAGE_SECTION_HEADER其数量由PE文件头的NumberOfSections指示。
我们的目标就是获取区段然后找到其中的.text段这个就是代码段。
其区段名由Name字段指示其地址相对偏移由VirtualAddress字段指示其内存大小由VirtualSize字段指示。
所以根据模块地址和代码段偏移和大小即可找到代码段的空白内存了。
*FindModuleTextBlankAlign(HMODULE
sizeof(IMAGE_SECTION_HEADER);if
}参数是一个模块的地址返回值就是在这个模块找到的一片空白内存的地址。
GetModuleHandleA(kernelbase.dll);hook_func
允许func_ptr处最前面的5字节内存可读可写可执行VirtualProtect(hook_func,
使用VirtualAlloc申请内存使其可读可写可执行old_entry
FindModuleTextBlankAlign(hmodule);
写入真正的跳转代码到空白区域WriteProcessMemory(GetCurrentProcess(),
(WRITECONSOLEA)old_entry;WriteProcessMemory(GetCurrentProcess(),
{WriteProcessMemory(GetCurrentProcess(),
}这部分代码可能看着比较多比较乱但是如果对着前面原理说明来看应该不难理解。
重点是VirtualAlloc函数可以申请一段虚拟内存使其有可执行的属性而用malloc申请的内存一般是不可执行的。
*)lpBuffer);buf[nNumberOfCharsToWrite
i)ResumeThread(hthreads[i]);for
i)WaitForSingleObject(hthreads[i],
i)CloseHandle(hthreads[i]);WriteConsoleA(hstdout,
NULL);unhook();WriteConsoleA(hstdout,
}完整代码在本文开头链接文件是multithreadhook.cpp。
由于本文开头提到了本文的方法并不是最佳的因为这个代码并没有线程安全而且选择要保存的函数头部代码长度需要自己手动指定比较麻烦没有实现自动Hook。
关于线程安全比如你在替换被Hook函数头部的代码时某个线程刚好也执行到了这里那比如会造成线程执行出错最好的方式就是在Hook之前暂停进程所有正在执行的线程依次判断每个线程的指令位置如果刚好执行到了被Hook的函数头部那么就需要专门针对其进行处理。
关于要保存的头部代码长度则可以内置一个反汇编器自动判断指令的长度然后保存相应的代码到跳板函数。
如果要了解以上这些以及更多内容你可以了解一下微软的Detours开源库和TsudaKageyu的minhook开源库。
Detourshttps://github.com/microsoft/Detours
https://github.com/TsudaKageyu/minhook
当然这两个库我并不是很熟悉只是简单看过他们的代码不过基本原理应该都是差不多的。
本文内容较多篇幅有点长能看到这里相信你有足够的耐心了解这方面的知识。
但是这些内容确实有一定的门槛前置知识要求也相对比较高对于初学者来说可能比较困难。
如果你现在对本文的内容还是很困惑如果你依然想要了解那么建议你努力学习前置知识。
如果你有不懂的地方也可以提出。
这篇文章的原理我研究了很久又花了一整天时间才草草写成由于写得仓促一定存在不少纰漏如果你对这方面非常熟悉请指出错误以免误导他人。
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback