EasyHook32位学习笔记(1)

32

1

简历怎么写?

深度学习EasyHook,对代码进行了深度的调试,优化扩展,做了一个 以这个引擎为基础的某某,对行为的预判,放在Remote的Dll里面,对qq注入dll,qq的什么操作,都能被我们给拦截住

学习的是如何勾取

EasyHook肯定是有一个Dll,注入到目标进程当中

EasyHook里面的这个安装钩子函数,RtlInstallHook 是导出函数

Windows DLL 的模块定义文件(.def 文件)

0

  1. LIBRARY这是.def 文件的必需关键字,用于声明这是一个 DLL 库。编译器会根据这个关键字识别该文件属于 DLL 项目。
  2. EXPORTS用于指定 DLL 需要导出的函数列表,这些函数可以被其他程序(如你的主程序)调用。
  3. RtlInstallHook @1
    • RtlInstallHook:是要导出的函数名称,必须与 DLL 中实际定义的函数名一致
    • @1:指定该函数的导出序号为 1,序号可以简化函数调用(通过序号而非函数名查找)

这个.def 文件的作用是告诉编译器,在生成EasyHookDll.dll时,需要将RtlInstallHook函数导出,使其能够被主程序通过GetProcAddress获取并调用

dll是不能够设置为启动项,必须靠exe去调试

0 0

我们自己写的测试程序,在自己写的exe当中去加载我们写的dl

函数指针定义

面试馆会问FF 15 指令

FF 15指令 call,但后面 跟着的是4个字节,是绝对地址(和64位不同,64后面跟着的是偏移)

(注意对比E8指令,后面跟着的是5个字节,是偏移,然后加5个字节到目的地址)

但是并没有跳转到7875B000h,而是跳转到了76C62A10

执行的是78B8B000里面的东西(相当于解一次星号)

面试官会提问Mov edi,edi

mov edi,edi(2个字节)热补丁指令

这条指令的添加主要是为了支持hotpatch,翻译过来好像是叫热修补,类似于Inline Hook

当调试到系统库函数的代码时,系统函数都是从一条热补丁指令开始,紧接着这条指令下面才是标准的建立函数局部栈的代码

主要作用是当修改一个函数行为时,可以把mov edi,edi修改为一条短跳转指令(2个字节)

,把热补丁上面的5个NOP修改为一条长跳转指令(一条长跳转指令恰好为五个字节)

短跳转指令jg(EB)跳到长跳转指令jmp(E9)上,长跳转指令跳到修改后的函数体上

不用其他指令的原因就是mov指令的效率更高

还能避免多线程出错,因为篡改指令不是原子操作,如果在修改的过程中有其他线程调用此函数,还没改完就调用,会发生报错

但是如果用热补丁指令(它并不影响原本函数的调用,就不会产生其他影响)

多任务下的数据结构与算法————————有讲多个Hook函数的实现

做内存管理,内存池,得会说,可以加到简历上

在EasyHook中

用结构体TraceInfo来维护Hook,是一个单向链表

里面一个指针,会生成一个节点,叫做LocalHookInfo

想到与TraceInfo里面会存放LocalHookInfo的信息,LocalHookInfo这个结构体里面又会存放一个勾取得函数地址

一个LocalHookInfo代表你勾取的一个源函数

写fake函数的时候,要保证和源函数保持一致,所谓一致,即保证返回值一致,调用约定,参数列表一致

改成的函数不会再蜂鸣,而是会弹出messsagebox

在EasyHookDLl中发现

我们勾的是KErnel32模块下的Beep函数我们已经找到他的地址(76C62A10)

FakeBeep是我们自己提供的地址

第一参数(原函数)地址

Fake地址

在函数里面需要对四参数进行判断(属于内存管理)

面试官可能会问RtlIsValidPointer这个函数(判断内存有效性)的判断方式:

可读的话返回0
这个函数返回1的话,代表地址无效

如何判断一块内存能写,能读,执行?

异常机制,在里面去写去读就行了

如何能判断执行?(不是进去执行),而是先获得页面属性(VirtualProtect),看看是不是执行属性

用FakeService把OriginalService换掉

CallBack没用

LocalHookInfo相当于是在记录当前信息(一维指针,动态申请内存),取个地址符号,相当于传二维指针

RtlALlocateHook 动态申请内存

第四个是二维指针,动态申请内存

RtlAllocateMemoryEx

在目标进程家申请内存

传一个源函数的入口,传一个大小

获得系统信息,并传递页面大小,0x1000—4096字节

64位在申请内存时候(0~~128TG),需要保证申请的内存距离原函数要够近,要在四个字节之内(上下页都可以)(0~0xffffffff 4G)

32位不用担心这个问题,随便申请,因为32位大小 一共就4G

面试官会问:你做Hook的时候32位和64位有什么区别?

维度

32 位 Hook(x86)

64 位 Hook(x86-64)

1. 基础数据模型

- 指针 / 地址:4 字节(DWORD),寻址上限 4GB

- 数据类型:ILP32 模型(int/long/ 指针均 4 字节)

- 内存页操作:VirtualProtect 参数为 DWORD

- 指针 / 地址:8 字节(ULONG64/UINT64),寻址上限远超 4GB(实际 48 位)

- 数据类型:LP64 模型(int4 字节,long / 指针 8 字节)

- 内存页操作:VirtualProtect 参数为 ULONG64

2. 函数调用约定

- 主流:__cdecl/__stdcall/__fastcall,以栈传递参数

- 栈平衡:__cdecl 调用者负责,__stdcall 被调用者负责

- 无影子空间要求

- 返回值存 EAX

- 唯一标准:__fastcall(x64 调用约定),寄存器传递为主

- 前 4 个参数存 RCX/RDX/R8/R9,多余参数压栈

- 必须分配 32 字节 “影子空间”(调用者责任)

- 栈平衡由调用者负责,返回值存 RAX

3. 寄存器 / 指令

- 通用寄存器:8 个(EAX/EBX 等),32 位宽度

- 跳转指令:默认 JMP rel32(4 字节相对跳转),范围 ±2GB(足够覆盖 4GB 寻址空间)

- 指令长度:短且固定,拆解 / 备份简单

- 通用寄存器:16 个(新增 R8-R15),64 位宽度

- 需保护非易失性寄存器(RBX/RBP/R12-R15 等)

- 跳转指令:

✅ 默认 JMP rel32(范围 ±2GB,Fake 函数需在源函数 4GB 内)

✅ 远跳转需用 MOV RAX+JMP RAX(14 字节绝对跳转)

- 指令长度:变长(1-15 字节),拆解 / 备份需保证指令完整性

4. Hook 实现细节

- 指令备份:仅需备份 4 字节原指令

- 上下文保护:只需维护 ESP 栈指针,逻辑简单

- 兼容性:无 WOW64 层,Hook 逻辑统一

- 内核限制:PatchGuard 保护宽松

- 指令备份:需备份至少 14 字节完整指令(避免指令断裂)

- 上下文保护:严格遵守影子空间、寄存器保护规则

- 兼容性:区分 64 位进程 / WOW64 下 32 位进程,跨位数 Hook 难度极大

- 内核限制:PatchGuard 严格限制驱动级 Hook(如 SSDT Hook)

fake函数内存申请位置问题,32位没有,64因为空间大,需要考虑在0~~4G 以内才可以

在 x64 Windows 下,当调用一个函数时,调用者必须在栈上分配 32 字节的空闲空间,再执行函数调用指令(CALL)。这个空间由调用者负责分配,被调用者可以自由使用(比如暂存寄存器参数、临时数据),且无需担心该空间被覆盖。

2;17

申请内存时又出现了默认堆申请和自定义堆申请

我们定义一个变量v1指向LocalHookInfo

用RtlProtectMemory修改这一页的页面属性

汇编引擎获得指令长度

添加汇编

汇编和机器指令转换

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

2

https://meeting.tencent.com/crm/NQ5RGnmX78

CR寄存器,CR0~3

生成依赖项 masm

0

trampoline弹簧

申请的一页内存当中,上半部分(绿色)会分成两块,一个是LocalHookInfo(结构体),一部分是我们的汇编指令

在结构体LocalHookInfo当中有一个指针 Flink,是用于和其他函数的LocalHookInfo作链接用的

0

修改页面属性就相当于修改的是LocalHookInfo+汇编指令 这一页,修改为能读能写能执行

因为红色的里面需要去跑shellCode

0

计算指令长度

0

讲解一下NativeSize(记录绿色部分的大小LocalHookinfo)

过滤函数

和源函数 调用约定,参数,保持一致

定义原函数 函数地址

原函数的指令被我们修改了(CodeSize5字节 对于32位)

0

在当时申请的那一页内存当中,基地址+2048的位置(一个空白的位置),申请一个int型指针 IsExecuted

在ShellCode没有工作之前是0 如果工作了 就是1

在汇编指令中 放了 三个函数

相当于写了三个接口,过滤数据

在LocalHookInfo当中有一个UCHAR* Trampoline,他会引导蓝色的那片内存(放的过滤函数)

v1指向的是这个结构体 v1+1相当于是越过这个结构体

v10 = v1+1

相当于Trampoline上搁的v10存的地址,就是结构体下面

下面(绿色部分)是要写汇编指令的

加入汇编的时候,记住要点击生成依赖项

把Masm勾选上

然后修改平台

汇编不和Cpp玩,所以得用C语言的环境编译

EXTERN_C

当出现E9指令,当前地址加上E9后面的四个字节才能得到

mov edi,edi所在的地址

为什么要知道汇编函数的长度?

因为需要在目标进程中申请内存,把汇编函数搬运到对方家中

v10+shellcodeSize

nativesize现在代表蓝色+红色大小(之前只是蓝色大小)

相当于把红色的区域留给汇编函数了,红色区域里面需要去放汇编指令

汇编指令第一条指令

mov eax,esp

这个函数得到汇编函数入口(RtlGetTrampolineCode)

OldProcedure定义在绿色部分,这一块是用来存放原函数的指令(5个字节),当把原函数的指令拷贝过来之后,再对原函数的指令进行修改

当原函数一旦被修改之后,就跳转到了Trampoline的位置(也就是存放汇编函数的位置)

当执行完过滤函数之后,然后先执行绿色部分刚刚拷贝过来的原函数的指令,然后又会有一个跳转指令Jmp offset

这时候又可以跳回原函数的5个字节之后的指令继续执行

然后理解一下这个拷贝函数RtlRelocateEntryPoint

(将原函数的前5字节指令拷贝到oldProcedure)

这里的拷贝牵到 Hook List

不是说原封不动的把原函数的前5字节给复制过来,而要防止原函数这个地方已经被其他Hook过,所以我们需要根据第一条指令进行计算跳转的地方,然后再加5字节,再进行复制操作

要根据不同的指令的不同,进行不同的判断

得到目的地之后,怎么修改OldProcedure地方的指令

有两种方案

第一种方案是直接把地址放到一个寄存器(eax)当中,然后在OldProcedure执行跳转 eax就行,这样的话不用计算偏移(但是这种方案会给 后面来的Hook提供方便,因为当他发现这个地方是个寄存器Eax之后,他会直接取eax的东西,就能更方便的去得到原函数指令)

第二种方案就是,得到原函数的目标指令之后,用

目的地的地址-OldProcedure地址,算出偏移,然后写一个jmp跳转+偏移offset(这种更好)

b1,b2获得原指令的前两个直接

如果第一个指令是0x67(是一个短跳转)

若第一个指令是E9(核心)(Hook list)jmp

E9 E8 都执行E8里面的内容

先获取目的地地址

然后就拷贝到咱们自己家中

先把目的地地址放到寄存器当中,然后呼叫寄存器

b8 就是 mov eax,offset

offset是算出来的偏移

对面是E8

家:就是Call eax

对面是E9(远跳)

家:jmp eax

对面是EB(近跳转)机器码:EB xx(xx 为 8 位有符号偏移量)偏移量 imm8是 ​​有符号补码​​

目标地址 = 当前 EIP + 指令长度(2) + imm8

(imm8就是8位---1字节)

然后开始进行指令的拷贝,直到有5字节的长度

而且前提是把对方的保护机制修改了,指令段是不可写的

拷贝完原函数的前5个字节之后,再将v10向下移动,这时候需要计算此时相对于原函数5字节之后的偏移量(方便待会执行完过滤函数之后跳转回去)

v1->originalService+v1->CodeSize即为原函数v1000的位置

然后计算出此时v10所在的位置jmp后面的offset

目的地-v10所在地址-5

我们还需要把原先的5字节指令保存到一个临时变量backup当中,用于钩子的恢复

修正拷贝过来的shellCode

因为在写汇编的时候,是在我们家写的,像一些地址我们并不知道,所以写一些特殊的数字先进行代替(预设值),当执行到这一句的时候,我们再进行修正

要有线程同步,不能说,我这个Cpu正在修改目标函数指令

另一个Cpu过来执行,会直接崩溃

所以要让多核CPU变成单核(就是在我修改的这个时间段,让其他CPU休息(遍历所有CPU,让CPU执行休眠函数))

覆盖原指令

HookTraceInfo相当于是管理员,存储每一次Hook的这些申请表,然后用flink进行连接

线程同步的实现用的是临界区

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

3

录制:EasyHook(3)

日期:2025-08-09 08:20:15

录制文件:https://meeting.tencent.com/crm/KDRO0Ye70e

0

Hook关键点

HookList

线程同步(担心我在修改的时候 有人过来执行,设置优先级页)

32和64最大区别,64是fastcall,32是想怎么样怎么样

另一个是调用函数:有几种调用方法

一种是E8 调用的是自己定义的函数

一种是FF 15 调用的是windows里面的库函数

调汇编的时候,我们在第二个Beep处下断点

0

出现这种情况说明执行的Dll和编译的Dll不匹配(重新编译)

先看没钩子之前

FF 15指令 后面是绝对地址 跳转

但是7A7A里面的东西才是我们真正要执行的东西

75dc2a10

来到mov edi,edi热补丁

才是我们真正要执行的指令

这时候我们来到挂钩之后的Beep函数

进去之后,发现里面的汇编指令就不对了

F11进去之后,就是汇编

我们把汇编弄到txt学习

粘贴汇编的时候到Ret指令

当Beep把参数压进去之后,直接跳转到汇编的位置了

第一句汇编mov eax,esp

相当于把Call指令的下一条指令,也就是7A778C3D压入了eax

我们看esp

第一行是call指令下一条

第二行是750

第三行是2000

然后下两句汇编语句

push ecx

push edx

压入两个寄存器 有两个原因

第一个是ecx和edx可能牵扯到呼叫函数,需要压入参数(当见到push指令之后,就考虑看看是不是要压入参数,如果没有,那就不是第一个原因)

第二原因就是有可能这两个寄存器都可能被用,但是他又担心以前的数据丢失,所以他就把原先的寄存器的值压入栈保存,腾出寄存器供使用

当看到push指令也能直接判断是32位,因为64位都是mov指令

查看esp

第一行edx 7a3f2a40(7开头的一般是指令段)

第二行ecx

第三行call指令下一条

如果在汇编当中出现这种绝对值,那么就是已修正的值

此时这个eax里面存储的就是之前申请的一页内存当中的IsExcuted (汇编指令是否被执行)

重新调试,汇编指令中的绝对地址会变化

eax存储 IsExcuted

再执行下一句汇编,就相当于把Isexcuted+1 = 1

eax = F4CAE8

我们在原汇编先看这个地方的预设值是什么

发现是FakeService

所以这个7a77141f才是这个函数的地址

进入之后 发现是跳转表。全是E9+4字节偏移

或者复制到 反汇编中去观察

E9 + 后面4个字节的偏移+5字节 = 7A779DF0

下一句汇编相当于 取出eax地址里面的值,然后在比较Fake函数到底存不存在

ht实际不存在 在ida里面就会只存在jne

作用就是当上面的cmp 发现FakeService不存在的话

就会跳转到 00F4CB20的位置

因为我们的FakeService存在,所以就不执行这中间的汇编指令

j开头的 就是跳转 按F11 跳转过去了

我们观察到这个前面三个push

然后来一个call

所以推测push的是call 的三个参数

然后顺便观察一下 call指令下一条是不是恢复堆栈

假如没有堆栈恢复。可推测是WINAPI(stdcall)调用约定

然后我们根据以上推测

可知 RtlBarrierIntro 应该就是三参数

最后再call这个函数

然后我们打开esp观察:

最上面的是三参数

edx

ecx

7a778c3d---call指令的下一条指令cmp

2ee---750

7d0---2000

esp所在的位置(0)就是第3行(bb fb 74)

这时候发现7a778c3d出现了两次

出现这种绝对值,我们就返回去看看汇编

发现是v1 是申请的4096的基地址(也是LocalhookInfo基地址)

包括 RtlBarrierIntro 也是修复进去的

将7A778c3D回到反汇编查看 实际上是call指令下一条

C2 0C stdcall调用约定

然后现在esp中就剩下

edx

ecx

call指令下一条

750

2000

然后下一条指令

test eax,eax逻辑与运算(判断eax里面的值 是真还是假)

test之后,我们跳到这个地方

往esp+8里面放入 0F4CB54

把绿色框修改了

因为是硬指令,我们去原汇编查看

Trampoline+92

变为F4 CB 54

这个地方是Eip,会产生EipHook

原先的指令,存放的是Call指令的下一条,是为了方便我们执行完call的函数回去,但是当我们把这个地方的指令修改之后,就把他回去的路给修改了

下一句

把FakeBeep放入eax

然后跳转

然后就是pop edx

pop ecx

esp下降2格, 就到eip位置了

(如果此时突然执行ret指令,那么就会执行 f4cb54

ret指令就是把esp存储地址里面的内容送给eip,然后当指令段执行)

但是这个地方没有执行

然后我们再看esp堆栈

只剩:

修改的

750

2000

此时是jmp进入Fakebeep函数

不仅现在截获了一开始没挂钩的Beep函数的两个参数750和2000,也写入了eip指令

就是当FakeBeep执行完之后,会再去执行00f4cb54

(此处汇编非常之精妙)

F10

然后就进入Fake了

fake执行完之后有ret

就会执行 00 f4 cb 54

然后清理两个参数

然后esp就落到了此时的位置

然后现在又开始push了

然后看esp里面东西

此时堆栈长这样

lea是去那个地方的地址然后压入堆栈

esp+8的位置

然后继续push

把v1的基地址压入进来了(我们申请的基地址)

然后把RtlBarrierOutro的地址压入到eax当中

并且调用

去 原始汇编查看

这个函数压入了两个参数eax和0F4CAA0

调用这个函数对下面也不会有什么影响,因为这个函数并没有调用test,cmp之类的

调用完函数之后,就恢复堆栈了

Hook结束

接着干inject

Hook的前提是要做注入的,因为我们整个思想,是先获得对方进程,然后把injectdll注入过去,控制对方加载HookDll

创建动态库

分析注入

这个函数RtlInjectLibrary是导出的,直接调用

注意这个地方的@4说明是32位(带一个参数的4字节),64位是不用写的

C语言函数,在汇编当中会去调用它

Dll下提供的函数都是导出

InjectData结构

在汇编函数里面

定义一个事件 EventHandle

当注入完成后,将事件授信,我们在原地等着

当整个注入完成后,我们需要将对方恢复原状(无痕注入)

我怎么能知道对方执行完没?

典型的内核对象共享,我们这个地方用的第三种方法 duplicate handle

如果你想要传入的数据长度大于了申请的长度,就return

判断内存有效性

RtlIsValidPointer

异常+读写操作

判断注入的进程

然后我们根据目标进程id打开目标进程

获得目标进程句柄

我们是32 对方也要是32 64-64

根据Is64BitTarget判断

是1 代表是64 是0代表是32

判断目标进程位数的函数RtlIsX64Process

在这个地方拉一下UAC级别

posted @ 2026-03-26 10:19  L1dk  阅读(12)  评论(0)    收藏  举报