消息 HOOK
原理:
1. 用户输入消息, 消息被放到系统消息队列.
2. 程序发生了某些需要获取输入的事件, 就从系统消息队列拿出消息放到程序消息队列中.
3. 应用程序检测到有新的消息进入到程序消息队列中后, 调用相应的事件去处理该消息.
所以在系统消息队列与程序消息队列的中间安装 hook, 即可获取消息队列中的信息.
安装:
SetWindowsHookEx(键盘消息(WH_xxx),Hook 函数(处理键盘输入的函数), 句柄(hook 函数所在的 DLL 的句柄), 线程 ID(要 hook 的线程 ID,0 为所有线程))
API 在简单高效的同时也有一个弊端, 就是它只能监视较少的消息, 如: 击键消息, 鼠标移动消息, 窗口消息.
SEH(调试)HOOK
原理: 与调试器工作方式类似, 让进程发生异常, 然后自己捕获到异常, 对于除于被调试状态下的级进行操作.
1. 正常情况下, 进程未被其他进程调试时, 当进程发生异常事件, 系统将捕获该事件, 并进行事件处理.
2. 当进程被其他进程调试时, 处理该进程的异常事件的工作则交给了调试进程.(调试进程未处理或不关心的调试事件由系统处理)
3. 调试 HOOK 的核心思路就是将 API 的第一个字节修改为 0xCC(INT 3, 留给调试工具的中断, 调试工具运行完后, 会将下一条指令手动替换回原先的代码), 当 API 被调用时, 由于触发了异常, 控制权就被转交给调试器(调试进程).
利用调试技术来 HOOK API 函数的相关步骤如下
.1 对想要钩取的进程进行附加操作, 使之成为被调试者.
.2 将要钩取的 API 的起始地址的第一个字节修改为 0xcc(或者使用硬件断点).
.3 当调用目标 API 的时候, 控制权就转移到调试器进程.
.4 执行需要的操作.
.5 脱钩, 将 API 函数的第一个字节恢复.
.6 运行相应的 API.
注入 HOOK
原理: Hook 的核心思想就是修改 API 的代码, 使用 DLL 注入技术, 我们将 Hook 的代码写入一个 DLL(或直接一个 shellcode), 将此 DLL 注入到目标进程中, 此时因为 DLL 在目标进程的内存中, 所以就有权限直接修改目标进程内存中的代码了.
shellcode: 填充数据, 利用软件漏洞而执行的代码, 可在暂存器 eip 溢出后, 塞入一段可让 CPU 执行的 shellcode 机器码, 让电脑可以执行攻击者的任意指令.
API HOOK: 又分为 IAT HOOK 和 inline HOOK, 都是改变函数以达到跳转到自己的 HOOK API 的目的. 但又有差别.
一,
1.IAT HOOK: 通过修改 IAT(导入表)的函数地址, 实现对 API 进行 HOOK. 在把原函数替换成目标函数, 并在目标函数执行完后, 必须要调用回原函数(HOOK 前应保存原函数地址), 这样才能保证功能的完整性.
. 1. 计算出导入表的位置
. 2. 在导入表中找到原函数的位置(即将被执行的函数), 保存该函数的地址
. 3. 将原函数地址改为目标函数, 运行完目标函数后, 调用回原函数.
注: 该操作会将程序中所有调用被 hook 的函数变为调用 hook 后的函数, 可加判断, 如果为自己调用操作, 则进行 hook 函数的处理, 如果是系统调用, 直接将数据调用给原函数去处理
2.inline HOOK: 直接修改内存中任意函数的代码, 将其劫持至 Hook API. 同时, 它比 IAT Hook 的适用范围更广, 因为只要是内存中有的函数它都能 Hook, 而后者只能 Hook IAT 表里存在的函数(有些程序会动态加载函数).
inline HOOK 的目标是系统函数, 直接修改函数的前 5 字节, 改为 jmp 目标函数地址, 执行完后进行 unhook(脱钩)操作, 以便将原函数恢复. Hook 的目的是当调用某个函数时, 我们能劫持进程的执行流. 现在我们已经劫持了进程的执行流, 便可以恢复原函数代码, 以便我们的恶意代码可以正常调用.
. 1. 获取原 API 地址, 保存起来(方便后续还原)
. 2. 修改内存属性为 RWX(即 read 读(编号 4)write 写(2)execute 执行(1))
. 3. 备份原代码(同 1)
. 4. 实时计算 JMP 的相对偏移
. 5. 最后修改 API 前 5 字节的代码(跳转到目标函数地址)
. 6. 恢复内存属性.
注:
自编 inline hook: 从 hook 的位置跳到自己的函数中执行想要的动作, 但需要将原指令再执行一遍, 再跳转回到被 hook 的位置 + 指令长度的位置. 以达成栈平衡的目的.
如果 hook 的位置为跳转指令, 则需要将该指令所跳转的目标地址给计算并保存起来(普通跳转指令(非 FF15 FF25 push 时), 跳转的为偏移量, 一旦 hook 了偏移量就不再适用了), 并在自己的函数中跳转到目标地址.
mhook 库: 编写一个参数 返回类型与被 HOOK 函数一样的新函数, hook 后, 代替原函数被调用, 注意该 hook 只能 hook 一整个函数, 并且会将所以调用原函数的行为用于调用新函数, 因此应该加个判断条件, 判断是系统进行调用还是我们要 hook 的程序进行调用.
二,
HotFix HOOK
原理: Code Hook 存在一个效率的问题, 因为每次 Code Hook 都要进行 "挂钩 + 脱钩" 的操作(对 API 的前 5 字节修改两次), 当要进行全局 Hook 的时候, 系统运行效率会受影响. 而且, 当一个线程尝试运行某段代码时, 若另一个线程正在对该段代码进行 "写" 操作, 会程序冲突, 最终引发一些错误.
API 的起始代码上都有这样的特点, 5 个 NOP(空)指令, 1 个 "MOV EDI,EDI"(占 2 字节), 这 7 字节的指令实际没有任何意义, 因此可以通过修改这 7 字节来实现 HOOK 操作, 这种方法可以使得进程处于运行状态时临时更改进程内存中的库文件, 因此被称为打 "热补丁".
在上述 5 字节代码修改技术中, unhook(脱钩)是为了调用原函数, 但使用 HotFix HOOK API 时, 在 API 代码被修改的状态下仍然能够正常的调用原 API(从 [原 API 起始地址 + 2] 开始, 仍能正常调用原 API, 且执行动作一致).
. 1. 将内存属性修改为 RWX
. 2. 计算 HOOK 函数与被 HOOK 函数之间的地址偏移
. 3. 将 JMP [得到的结果]写入原函数 - 5 的位置(即 5 个 NOP)
. 4. 再将 JMP-7 写到原函数的位置(MOV EDI,EDI)
. 5. 恢复内存属性.
由于 HotFix Hook 需要修改 7 个字节的代码, 所以并不是所有 API 都适用这种方法, 若不适用, 请使用 5 字节代码修改技术.
SSDT HOOK
原理: SSDT Hook 属于内核层 Hook, 也是最底层的 Hook. 由于用户层的 API 最后实质也是调用内核 API(Kernel32->Ntdll->Ntoskrnl), 所以该 Hook 方法最为强大.
SSDT(System Service Descriptor Table): 系统服务描述符表
定义类型变量:
DD(Define Dword): 双字类型, 一个双字数据占 4 字节. DW: 字类型, 占 2 字节. DB: 字节类型, 占 1 字节
内核通过 SSDT 调用各种内核函数, SSDT 就是一个函数表, 只要得到一个索引值, 就能根据这个索引值在该表中得到想要的函数地址.
SSDT 所在地址后面的第一个 32 位数据即为 SSDT 的基地址, 跳到基地址后, 第一个位 32 位数据即为 SSDT 表中第一个函数的地址, 对该地址反汇编后, 就能得到该函数相关的信息(包括该函数的索引号).
例: 要找 AABBCC 函数的地址, 先对该函数进行反汇编 u nt!ZwAABBCC, 得到它的索引号为 0x12; 那么它的地址为: 基地址 + 0x12 对其反汇编后, 即可得到该函数的详细信息.
. 1. 修改内存属性为 RWX(即 read 读(编号 4)write 写(2)execute 执行(1))
. 2. 实时计算 JMP 的相对偏移
. 3. 备份原代码头 5 字节(同 1)
. 4. 将头 5 字节替换成 2. 的汇编码
. 5. 运行完后还原头 5 字节
. 6. 恢复内存属性.
例子及博主自身理解:
API HOOK--IAT HOOK 实例
技巧:
》通过模块句柄, 得到 PE 头:(PBYTE)PE 头 =(PBYTE)进程句柄
》通过 PE 头, 获得 dos 头: PIMAGE_DOS_HEADER dos 头 =(PIMAGE_DOS_HEADER)PE 头
》通过 PE 头和 dos 头, 获得 NT 头 PIMAGE_NT_HEADERS NT 头 =(PIMAGE_NT_HEADERS)(PE 头 + dos 头 ->e_lfanew)
》通过 NT 头的结构成员的数组成员, 获得导入表信息:
IMAGE_DATA_DIRECTORY 数据表 = NT 头 ->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
导入表的尺寸 = 数据表. size 起始地址(基址)= 数据表. VirtualAddress
》获取函数的地址(DWORD),GetProcAddress(GetModuleHandleA(DllName),ProcName)
》通过 PE 头 + 导入表基址定位到导入表: PIMAGE_IMPORT_DESCRIPTOR 导入表 = (PIMAGE_IMPORT_DESCRIPTOR)(PE 头 + 导入表基址);
》通过对比 DLL 名 循环查找每个导入表, 找到 DLL 所在的结构体
strcmp((char*)DLL 名,(char*)(PE 头 + 导入表 - name))找到目标 DLL 所在结构体; PIMAGE_THUNK_DATA 结构体 = (PIMAGE_THUNK_DATA)(PE 头 + 导入表 ->FirstThunk);
》通过对比函数地址, 循环查找每个结构体的 u1.Function 成员, 找到目标函数结构体 ->u1.Function == (DWORD)要被替换的函数地址
!! 切记, 应在 32 位下的 Release 版本生成 dll, 否则替换新的函数地址处会出错
》找到目标函数位置后, 替换成新的函数的地址, pthunk->u1.Function = NewFuncAddress;
然后将位置 (这位置保存的是地址) 保存起来, 方便事后还原.(保存至全局变量)
DWORD g_dwIatAddr = (DWORD)&pthunk->u1.Function;(<- 保存一个指针指向的变量所保存的地址)
事后还原: DWORD * pdwAddr = (DWORD*)g_dwIatAddr;// 把旧地址所在的位置传递给一个指针
*pdwAddr = oldFuncAddress; // 将旧地址放入该位置 达到还原
注意, 每次更改导入表的函数地址时, 都应该调用 VirtualProtect 来修改保护属性
VirtualProtect((LPVOID)&pthunk->u1.Function, 4, PAGE_EXECUTE_READWRITE, &oldprotect);
通过 REG 注入 DLL (即通过注册表 HOOK)
平时用程序来进行 DLL 注入时, 需要被注入程序在运行起来之后才能进行注入, 在某些初始化阶段就想通过 HOOK 来控制程序时, 达不到效果. 此时, 我们可以通过注册表表进行注入, 被注入的程序在运行前就已经被 HOOK. 注: 被注入的进程与 DLL 必须同为 32/64 位.
REG 注入原理: 利用在 Windows 系统中, 当 REG 以下键值中存在有 DLL 文件路径时, 会跟随 EXE 文件的启动加载这个 DLL 文件路径中的 DLL 文件. 当如果遇到有多个 DLL 文件时, 需要用逗号或者空格隔开多个 DLL 文件的路径.
1. 通过 WIN+R 运行 regedit 打开注册表
2. 分别打开: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows
HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\Windows
把路径下的 AppInit_DLLs 改为将要注入的 DLL 路径;
3. 并将 LoadAppInit_DLLs 注册表项的值修改为 1:(64 位改 2 个 32 位改 1 个)
注: 注入表注入的 DLL 由 mHOOK 生成, 因为注入表 HOOK 会给所有进程 HOOK
所以要加入限制条件: 获取当前进程名, 当进程名 = 目标进程名时, 往下执行 HOOK 的操作
自写 inline hook
1. 自定义一个全局变量 让 hook 只运行一次
2. 通过进程 IDGetProcessId(HANDLE(-1))得到当前进程 ID 和程序名(自定义), 获得目标进程的模块入口地址(即 HOOK 进程的基地址)(要 HOOK 的地方属于哪个模块, 就获取哪个模块的句柄)
2/1. 直接 GetModuleHandle("进程名"); 获得模块入口地址或者通过下面 4 个步骤
2-1. 根据进程 ID 获得进程快照, 该快照包含所有包含该进程 ID 的模块 CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid);
2-2. 通过该快照, 获得与该进程相关的第一个模块的信息 Module32First(2-1 返回的句柄, & 存放模块信息的结构)
2-3. 通过对比, 找到目标模块 srcmp(存放模块信息的结构. szModule, 进程名 (即 1 的程序名)) == 0) , 如果不匹配, Module32Nex(2-1 返回的句柄, & 存放模块信息的结构) 查看下一个.(进程名可通过任务管理器 - 详细信息查看)
2-4. 找到目标模块, 返回模块句柄存放模块信息的结构. hModule
3. 得到要替换的原指令的绝对偏移位置 RVA=VA-IB(要被替换的第一条指令 - 模块入口地址) (可通过 MDebug 查看, 如要 HOOK 的为自定义函数, 则该模块名称为进程名. exe, 如果要 HOOK 的为系统函数, 则模块名为该系统函数所在的 DLL)(找到合适的 HOOK 位置是逆向的关键)
4. 得到当前要 hook 的指令所在位置. 原指令的绝对偏移地址(3 得到的)+ 基地址(不定, 每次加载时都容易改变, 所以要通过 2 来求得)
5. 保存等下要返回的地址 返回地址 = 当前要 hook 的指令所在位置 + hook 指令的长度(5 字节或以上, 替换掉了多长就加多长)
6. 保存原始指令内容 UCHAR szOldInstruct[指令长度]={指令内容}, 指令内容为从左到右从上到下保存. 如不还原, 则可不保存
7. 计算跳转位置. DWORD 跳转位置 = (DWORD)自己编写函数(汇编编写)- 要 hook 的指令的起始位置 - 指令所占长度; 这样跳转后即能略过原本指令, 执行自定义指令(远跳转指令 e9 占 5 字节, 近跳转 EB 占 2 字节, 跳转的位置为偏移量)
偏移量 = 目标地址 - 指令地址 - 指令长度
8. 编写跳转指令 UCHAR 跳转指令[5] = { 0xe9, 0, 0, 0, 0 };(e9 为跳转指令 jmp), 如果指令长于 5 字节, 则在地址后填充 nop 空指令(0x90)
也可定死为 5 字节, 因为跳转指令由 e9+4 字节地址组成, 而返回地址自动跳过了该指令的位置, 所以只改前面 5 字节数据亦可
memcpy(跳转指令 + 1(e9 占 1 位), &(3 得到的), sizeof(3 得到的));
9. 修改要 hook 的指令所在位置的保护属性 VirtualProtect((LPVOID)(4 得到的),, 5, PAGE_EXECUTE_READWRITE, & 保存旧属性的变量);
10. 将要 hook 的指令替换成要跳转的指令 memcpy((LPVOID)(4 得到的),(8 得到的), sizeof((8 得到的)));
11. 还原指令所在位置的保护属性 此时 inline hook 完成
注: 跳转指令所指的函数必须为汇编指令编写
通过上面例子生成 DLL 后, 通过注入来 HOOK 实例
在生成完 DLL 后(具体参照前面例子), 如有. lib 文件 需拉到同一目录并加载
_T 在项目定义了 UNICODE 时, 等价于 L, 即 (_T"123") 同理(L"123"); 否则就是多字节.
在数组中 TCHAR 如果定义了宽字节 UNICODE TCHAR 就为 wchar_t 否则为 char
DLL 所在地址可用 UnICODE, 方便处理汉字这种双字节字符.
1. 搜索指定类名或窗口名(窗口标题)findwindow(类名, 窗口名);, 得到窗口句柄.
2.GetWindowThreadProcessId(窗口句柄,& 保存 ID 的变量); 得到该进程的 ID.
3.OpenProcess(PROCESS_ALL_ACCESS,FALSE, 进程 ID); 得到该进程的句柄
4.VirtualAllocEx(进程句柄, 内存地址, 内存大小, 分配方式(MEM_COMMIT), 权限(PAGE_READWRITE)); 在该进程中申请一个内存空间(用于打开 DLL)
5. 将 DLL 路径写入对方进程中
WriteProcessMemory (进程句柄(3), 内存位置(4),DLL 所在地址(绝对路径), 路径所占空间,& 写入字节数)
注意, 此时的路径含有汉字, 用的为宽字符声明: L"c\\xxx", 所以可用 wcslen( )求得长度, 长度 * 2+2(结束符) 即得到宽字符路径所占空间
6. 创建远程线程, 让目标进程调用 LoadLibrary 来打开注入的 DLL
CreateRemoteThread(进程句柄, 安全属性(NULL), 线程大小(0, 即默认), 目标线程调用的函数名(函数名即函数起始位置),
传递给函数的参数(此处为 4 申请的空间, 该空间存放着 DLL 的路径), 线程创建标志(NULL), 线程 ID 的指针(不需要保存则为 NULL) )
这里传递的函数名为 LoadLibrary 用来加载 DLL
7. 检测句柄的信号状态 WaitForSingleObject (线程句柄(6), 等待时间(-1 无限)); 当参 1 现场为有信号状态, 或者到了参 2 时间, 该函数返回. 否则挂起
8. 释放目标进程的空间 VirtualFreeEx(进程句柄(3), 空间首地址(4), 空间大小(与申请时一致), 释放类型(MEM_DECOMMIT));
注意: 可直接跳到步骤 3 开始执行 进程 ID 从任务管理器 - 详细信息 - 目标进程的 PID 中获得
小技巧: 如果想实现嵌套 hook 应该保存被 hook 的函数指令, 然后在 hook 的函数里面 用函数再次实现该指令, 如果为 jmp 指令, 则需获得跳转指令后接的 4 字节偏移地址 : 因为跳转指令后接的为偏移量, 而 hook 位置的地址为该指令所在的地址, 并非偏移量
具体操作如下: 1.memcpy(g_szOldInstruct, (char*)(HOOK 的位置), 5); // 将 hook 位置的指令保存下来(指令多长就复制多长 如果是 jmp 的话一般都是 5 字节 E9 或 2 字节 EB)
- lea eax, dword ptr[g_szOldInstruct]; // 取出这行指令的值(为 E9 XXXX XXXX)
- add eax, 1; mov eax, dword ptr[eax]; //+1 为跳过 e9, 然后取 4 字节则为取出跳转的偏移地址
得到了偏移地址 就可以求得被 hook 的指令所要实现的跳转位置 跳转的目标地址 = 偏移地址 + 跳转指令长度 + 跳转指令所在的位置(即被 hook 前 这行指令所在的位置)
调试 DLL 是否执行成功, 可在生成 DLL 的函数的代码段添加__asm int 3; 来进行调试
在下断点后, 如果运行起来一直会回到断点处, 说明系统在该程序运行时会一直调用断点处的函数, 此时应在 hook 函数前加判断条件, 如果是自己操作的 HOOK 下来, 如果是系统调用的, 就调用原函数
来源: https://www.cnblogs.com/aaaguai/p/12628329.html