easyhook简要说明:

easyhook是一个开源的hook库(http://easyhook.github.io/),其支持托管代码(.NET)和非托管代码(C/C++)hook,这里只分析了其非托管下的hook代码,根据目前分析的情况来看,其有如下几个特点:

1. 同时支持X86和X64。

2. 支持针对不同的线程进行hook,例如可以设置当线程ID为0x1234的线程执行时执行hook功能函数,而线程ID为0x4321执行时不执行hook功能函数。

3. 支持在hook功能函数中执行被hook的函数,例如hook了MessageBoxA函数,那么在hook功能函数中,可以继续调用MessageBoxA函数,不会发生无限递归的情况。

4. 不支持任意点hook。

这里分析其代码的主要目的是了解其如何对X64代码进行hook,微软的detours hook库32位开源,但是64位则需要付费购买,通过分析easyhook库中64位hook的实现可以帮助我们理解64位hook的原理。

Hook相关函数:

在easyhook中hook一个函数的完整流程需要使用以下几个函数:

1. LhInstallHook,安装钩子函数。

2. LhSetInclusiveACL,设置包含ACL(access control list)。

3. LhSetExclusiveACL,设置排除ACL。

4. LhUninstallHook,卸载钩子(并不还原,只是禁用)。

5. LhWaitForPendingRemovals,删除钩子(还原)。

所使用到的关键数据结构为LOCAL_HOOK_INFO结构体:

LhInstallHook

该函数的原型为:

1. 调用LhAllocateHook函数,在该函数中主要做以下工作:

i.       调用LhAllocateMemory函数分配存放LOCAL_HOOK_INFO结构体和trampoline代码的内存,在32位下,该内存大小为4K。在64位下,其会获取当前系统下的内存页大小,之后以hook点为起始,尝试在其上下偏移0x7FFFFF00的范围内以页大小的间隔分配页大小的内存空间。 0x7FFFFF00乘以2即0xFFFFFE00,将近4GB的内存范围,刚好是jmp(E9)能够跳转的范围。

ii.      将分配的内存页的起始部分作为LOCAL_HOOK_INFO结构体的区域,并将部分值初始化。

iii.     获取trampoline汇编代码的地址,并拷贝到内存页中LOCAL_HOOK_INFO结构体之后。在X64中无法使用内联汇编,这里easyhook将32位下的Trampoline代码和64位的Trampoline代码放在了单独的.asm文件当中,使用C与汇编混编的方式,将汇编代码结合到程序中,这样就不受X64下不支持内联汇编的影响。

iv.     将hook点的头几个字节(保证能jmp的前提下完整指令的长度),拷贝到trampoline代码之后。如果头几个字节是跳转语句,这里对跳转语句进行了重定位。

v.      计算从内存页跳转回hook点之后代码的偏移,并拷贝。

vi.     如果是32位,那么还要替换trampoline汇编代码中的一些硬编码,在32位的trampoline汇编代码中需要使用一些变量,这些变量在编译时无法确定,使用类似0x12345678这种 硬编码进行标识,这里对其进行了替换。X64下无须替换。(原因见下方PS)

在LhAllocateMemory函数执行完毕之后,内存页的分布图是这样的:

2. 计算从hook点到trampoline代码的偏移。

3. 根据计算得出的偏移生成跳转代码并拷贝到hook点。

4. 将LOCAL_HOOK_INFO结构体添加到全局GlobalHookListHead链当中,同时将LOCAL_HOOK_INFO结构体的指针赋给最后一个参数(OutHandle)输出,设置线程ID以及删除钩子都需要该结构体。

PS

1. 在整个过程中,easyhook在安装钩子的时候,未挂起其他线程,同时在拷贝跳转代码到hook点时,使用的也只是  *((ULONGLONG*)Hook->TargetProc) = AtomicCache 这样的赋值语句,该语句在底层也并非是原子操作。

2. 上面提到,X64下并未对trampoline的代码进行替换。在X64的汇编代码开始处:

Intro:

    ;void* Entry; // fixed 0 (0)

    db 

    db 

    db 

    db 

    db 

    db 

    db 

    db 

OldProc:

    ;BYTE* OldProc; // fixed 4 (8)  

    db 

    db 

    db 

    db 

    db 

    db 

    db 

    db 

NewProc:

   db .....  ;由于占篇幅,这里省略定义。

Outro:

   db....

IsExecutePtr:

   db....

........      ;该处开始是实际的汇编代码。

  是类似这样的定义,在拷贝trampoline代码时,只拷贝了定义之下的代码语句,这些定义并未拷贝过去,由于trampoline汇编代码和LOCAL_HOOK_INFO结构体是挨着的,所以在汇编代码中,其也就将LOCAL_HOOK_INFO中的成员值当作了这些定义的变量,看下LOCAL_HOOK_INFO的最后的几个成员:

  发现它们和trampoline汇编代码中定义的变量的顺序是相同的。所以只要赋值了LOCAL_HOOK_INFO结构体,那么在汇编代码中就可以直接使用了。这点相当巧妙。

LhSetInclusiveACL与LhSetExclusiveACL

  这两个函数的函数原型为:

  这两个函数用来设置执行hook功能的线程ID。LhSetInclusiveACL函数执行成功后,其第一个参数中线程ID对应的线程执行到hook点时将会执行hook功能函数。LhSetExclusiveACL函数执行成功后,其第一个参数中线程ID对应的线程执行到hook点时将不会执行hook功能函数。

在LOCAL_HOOK_INFO结构体中的LocalACL成员结构体定义如下:

  在LhSetInclusiveACL函数中,将包含线程ID的数组拷贝到LOCAL_HOOK_INFO结构体中的LocalACL结构体的Entries数组中,同时将IsExclusive设置为假,更新Count的值。

  在LhSetExclusiveACL执行过程和LhSetInclusiveACL函数相同,唯一一点不同的是将IsExclusive设置为真。

LhUninstallHook

  该函数原型为:

  该函数将参数中指定的LOCAL_HOOK_INFO结构体指针从全局Hook链GlobalHookListHead中移除,并添加到全局移除Hook链GlobalRmovalListHead当中。

  除此之外,还将LOCAL_HOOK_INFO结构体中的HookProc值置为NULL。

LhWaitForPendingRemovals

  该函数原型为:

  该函数会遍历GlobalRemovalListHead链,根据LOCAL_HOOK_INFO结构体指针中的TargetBackup(64位为TargetBackup_x64)成员变量恢复hook点的原始指令,恢复之后释放结构体资源。因此,要删除hook点,必须先调用LhUninstallHook函数,再调用LhWaitForPendingRemovals函数。

32位HOOK执行流程

  安装完钩子后,函数执行的流程拓扑如下:

  关键点在于trampoline汇编代码,trampoline的流程图如下:  

  注意,在流程图中的 HookIntro与HookOutro函数是在执行硬编码替换时,将函数地址替换进去的。IsExecuted变量值是LOCAL_HOOK_INFO结构体中IsExecutedPtr指针指向的值,该值用来当调用LhWaitForPendingRemovals函数删除hook点时判断是否还有线程在trampoline中执行,如果有,则等待一段时间,再判断,直到没有线程执行trampoline时才删除hook点和trampoline内存页。

接下来以hook MessageBoxA函数为例说明上述流程如何执行。

 一、调用LhInstallHook函数安装钩子后,执行MessageBoxA函数

  1. 在MessageBoxA函数入口跳到trampoline代码中,判断HookProc函数处的值不为0,执行HookIntro函数。

  2. 在该函数中,判断当前线程信息是否在全局线程列表中(用来保存各个线程相关的hook信息,比如是否执行等),如果不存在就添加到全局线程列表中。之后判断当前线程ID是否被设置到ACL中,显然,由于这个时候未调用LhSetInclusiveACL或LhSetExclusiveACL函数,所以是未被设置到ACL中,那么函数返回假。

  3. 执行OldProc函数,并跳转回hook之后,正常执行MessageBoxA函数。

  流程图如下:

二、调用LhInstallHook,并调用LhSetInclusiveACL包含当前线程ID后,执行MessageBoxA函数

  1. 在MessageBoxA函数入口跳到trampoline代码中,判断HookProc函数处的值不为0,执行HookIntro函数。

  2. 在HookIntro函数中,判断当前线程信息是否在全局线程列表中(用来保存各个线程相关的hook信息,比如是否执行等),如果不存在就添加到全局线程列表中。之后判断线程ID是否被设置到ACL中,由于调用了LhSetInclusiveACL函数,所以被设置到ACL中,那么函数返回真。同时会更新该线程的hook信息,标识该线程已经是在trampoline中执行了的。

  3. 接下来执行HookProc函数。

  4. 在HookProc函数中又调用了MessageBoxA函数,因此便又会进入trampoline代码执行HookIntro函数,在该函数中获取该线程的hook信息,从而得知该线程已经是在trampoline中执行了的,所以返回假。

  5. 由于返回假,所以执行OldProc,再跳回正常MessageBoxA函数执行。执行完毕后返回HookProc函数。

  6. 执行HookOutro函数,在该函数中,重置线程的hook信息。同时更改返回地址为MessageBoxA函数的返回地址。

  7. HookOutro函数返回到trampoline中,执行一些扫尾工作后,使用ret指令返回。

  流程图如下:

三、调用LhUninstallHook后,执行MessageBoxA函数

  1. 调用LhUninstallHook函数后,会将LOCAL_HOOK_INFO结构体中的HookProc值置为0。

  2. 调用MessageBoxA函数进入trampoline,首先判断HookProc值是否为0,由于该值已经被LhUninstallHook函数置为0,所以跳到OldProc处继续执行。

  流程图如下:

四、调用LhWaitForPendingRemovals后,执行MessageBoxA函数

  1. 调用LhWaitForPendingRemovals后,hook点已经被恢复,MessageBoxA函数正常执行。

64位HOOK执行流程

  整体来看,64位的Hook执行流程和32位的相同,只是个别细节不同,以下是不同点:

  1. 在trampoline汇编代码中,引用变量的方式不同。32位是通过硬编码替换的,64位是通过和LOCAL_HOOK_INFO结构相近,引用LOCAL_HOOK_INFO结构体的变量。

  2.  64位函数的调用约定不同,因此在trampoline代码中,需要对64位函数的调用约定做一些额外的工作,64位的调用约定参考下方补充信息。

  3. 分配页内存的方式不同。

补充:

x64函数调用约定:

  1. 一个函数在调用时,前四个参数(整数型)是从左至右依次存放于RCX、RDX、R8、R9寄存器里面;

  2. 前四个浮点型和双精度浮点则从左至右依次存放于XMM0、XMM1、XMM2、XMM3寄存器里面;

  3. 剩下的参数从左至右顺序入栈;

  4. 调用者负责在栈上分配32字节的“shadow space”,用于存放那四个存放调用参数的寄存器的值(亦即前四个调用参数);

  5. 调用者负责维护堆栈平衡。

引用

easyhook库代码简要分析

easyhook源码分析一的更多相关文章

  1. easyhook源码分析三——申请钩子

    EasyHook 中申请钩子的原理介绍 函数原型 内部使用的函数,为给定的入口函数申请一个hook结构. 准备将目标函数的所有调用重定向到目标函数,但是尚未实施hook. EASYHOOK_NT_IN ...

  2. easyhook源码分析二——注入

    EasyHook 中的注入方法. 函数原型 // EasyHook 中的命名比较有意思,Rh 代表的就是Remote Hook,此函数就是远程钩子的一个子过程----注入,前面的宏代表它是导出函数. ...

  3. ABP源码分析一:整体项目结构及目录

    ABP是一套非常优秀的web应用程序架构,适合用来搭建集中式架构的web应用程序. 整个Abp的Infrastructure是以Abp这个package为核心模块(core)+15个模块(module ...

  4. HashMap与TreeMap源码分析

    1. 引言     在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Ja ...

  5. nginx源码分析之网络初始化

    nginx作为一个高性能的HTTP服务器,网络的处理是其核心,了解网络的初始化有助于加深对nginx网络处理的了解,本文主要通过nginx的源代码来分析其网络初始化. 从配置文件中读取初始化信息 与网 ...

  6. zookeeper源码分析之五服务端(集群leader)处理请求流程

    leader的实现类为LeaderZooKeeperServer,它间接继承自标准ZookeeperServer.它规定了请求到达leader时需要经历的路径: PrepRequestProcesso ...

  7. zookeeper源码分析之四服务端(单机)处理请求流程

    上文: zookeeper源码分析之一服务端启动过程 中,我们介绍了zookeeper服务器的启动过程,其中单机是ZookeeperServer启动,集群使用QuorumPeer启动,那么这次我们分析 ...

  8. zookeeper源码分析之三客户端发送请求流程

    znode 可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个功能是zookeeper对于应用最重要的特性,通过这个特性可以实现的功能包括配置的 ...

  9. java使用websocket,并且获取HttpSession,源码分析

    转载请在页首注明作者与出处 http://www.cnblogs.com/zhuxiaojie/p/6238826.html 一:本文使用范围 此文不仅仅局限于spring boot,普通的sprin ...

随机推荐

  1. .Net Core 认证系统源码解析

    不知不觉.Net Core已经推出到3.1了,大多数以.Net为技术栈的公司也开始逐步的切换到了Core,从业也快3年多了,一直坚持着.不管环境怎么变,坚持自己的当初的选择,坚持信仰 .Net Cor ...

  2. java 判断5张牌的组成

    题目: 一副牌中发五张扑克牌给你,判断是四条,三带二.三带一加一.两对.一对.顺子.还是什么都不是. 控制台输入: 1,1,1,1,2 示例输出: 四条 Java方法的代码: static Strin ...

  3. java 约瑟夫问题

    题目: 给定一个数组及数组的长度,另外给定一个数m,从数组的第一个元素出发,数到第m个元素出列(如果到最后则回到第一个元素).出列元素的值作为m的新值,从出列元素的下一元素继续开始数下去,直到所有元素 ...

  4. [转载]排序:长度为n的数组乱序存放着0至n-1. 现在只能进行0与其他数的swap

    长度为n的数组乱序存放着0至n-1. 现在只能进行0与其他数的swap 请设计并实现排序. google笔试小题.题目来源:http://wenku.baidu.com/view/5aa818dda5 ...

  5. Linux学习--第十二天--服务、ps、top、pstree、kill、&、jobs、fg、vmstat、dmesg、free、uptime、uname、crontab、ls

    服务分类 linux服务分为rpm包默认安装的服务和源码包安装的服务. rpm包默认安装的服务分为独立的服务和基于xinetd服务. 查询已安装的服务 rpm包安装的服务 chkconfig --li ...

  6. 使用django+rpc进行服务内部交互

    一.为什么使用rpc. 1)相比uwsgi,使用rpc的长连接可以不需要频繁创建连接,提高传输效率. 2)rpc支持同步和异步,对于不需要等待返回的消息可以不等待返回继续运行,减少客户端等待时间. 3 ...

  7. 火车采集用到的access查询命令小结

    #For zencart #图片网址路径替换 UPDATE Content SET v_products_image=replace(v_products_image, '<img src=&q ...

  8. discuz论坛后台部分设置更改之后,清除了缓存网站前台不更新不生效的解决办法

    discuz论坛后台部分设置更改之后,清除了缓存但网站前台不更新不生效的解决办法 在config/config_global.php  把  $_config['memory']['eaccelera ...

  9. P1903 奖学金题解

    众所周知,这是一道通过struct结构体进行排序的题目 思路:平常的输入.. 然后定义一个结构体grade,存放每个学生的学号.三科成绩.(也可以只存语文成绩和总分和学号) 自定义cmp函数,通过三层 ...

  10. P3332 [ZJOI2013]K大数查询 整体二分

    终于入门整体二分了,勉勉强强算是搞懂了一个题目吧. 整体二分很多时候可以比较好的离线处理区间\(K\)大值的相关问题.考虑算法流程: 操作队列\(arr\),其中有询问和修改两类操作. 每次在答案的可 ...