Detour开发包介绍(1):概述

Detours是微软研究院开发的工具包,可在二进制级别修改Windows程序中的函数。支持API截获、DLL注入等功能,适用于调试、性能测试等场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

微软研究院Detours开发包是一个用来在二进制级别上对程序中的函数(Function)或者过程(Procedure)进行修改的工具库。一般我们将这种技术称为Hook。在现实中,这种技术可以应用在很多场景下。比如截获某些Windows API,在实际调用到系统函数前进行一些过滤工作;软件中使用到了一些没有源代码的第三方库,但是又想增强其中某些函数的功能;修改函数返回值;为调试以及性能测试加入附加的代码;或者截获函数的输入输出作研究;破解使用。等等。Detours是在Windows的二进制PE文件基础上进行API截获。对于Linux平台,作这件事情将会非常的简单,由于最初的操作系统设计者引入了LD_PRELOAD。如果你设置LD_PRELOAD=mylib.so,那么应用程序在载入系统动态链接库时,会先查看mylib.so的符号表,在重定位(relocation)的时候会优先使用mylib.so里的symbol。假如你在mylib.so里有个printf() ,那么这个printf就会替代libc的printf。而在mylib.so里的这个printf可以直接访问libc.so里的printf函数指针来获得真正的printf的入口地址。这样,所有的API拦截在loader加载系统库时就已经完成,非常自然,和平台相关的部分全部交给loader去处理。

Detours的下载地址为http://research.microsoft.com/en-us/projects/detours/,最新为2.1版。Detours Express 2.1可免费使用(用于非商业目的),只支持32位的x86代码。Detours Professional 2.1需要购买版权,它提供的API接口其实与Detours Express是一样的,只不过它还可以支持x64和IA64处理器,以此为基础编写的代码拥有更强的可移植性。安装完Detours Express后,包括Detours库和使用示例的源代码,需要Visual C++环境来进行编译。打开Visual Studio的命令行提示符界面,切换到Detours Expressr的安装目录,用"nmake"命令即可完成构建。编译完后,我们要用到时的有四个文件,有四个文件,分别是detoured.dll、detoured.lib、detours.lib、detours.h。

1、截获二进制函数

众所周知,WINDOWS NT实现了虚拟存储器,每一WIN32进程拥有4GB的虚存空间, 关于WIN32进程的虚存结构及其操作的具体细节请参阅WIN32 API手册, 以下仅指出与Detours相关的几点:

(1) 进程要执行的指令也放在虚存空间中。

(2) 可以使用QueryProtectEx函数把存放指令的页面的权限更改为可读可写可执行,再改写其内容,从而修改正在运行的程序。

(3) 可以使用VirtualAllocEx从一个进程为另一正运行的进程分配虚存,再使用QueryProtectEx函数把页面的权限更改为可读可写可执行,并把要执行的指令以二进制机器码的形式写入,从而为一个正在运行的进程注入任意的代码。

Detours定义了三个概念:

(1) Target函数:要拦截的函数,通常为Windows的API。

(2) Trampoline函数:Target函数的部分复制品。因为Detours将会改写Target函数,所以先把Target函数的前5个字节复制保存好,一方面仍然保存Target函数的过程调用语义,另一方面便于以后的恢复。

(3) Detour函数:用来替代Target函数的截获函数。

Detours库使得截获函数调用更容易,截获代码是运行时动态加载的。Detours使用一个无条件转移指令来替换目标函数的最初几条指令,将控制流转移到一个用户提供的截获函数。而目标函数中的一些指令被保存在一个被称为“trampoline”的函数中,这些指令包括目标函数中被替换的代码以及一个转移到目标函数的无条件分支。

当程序执行到达目标函数的时候,会直接跳转到一个用户支持的截获函数。截获函数来执行适当的预处理。截获函数可以直接返回到原来的函数,或者它可以调用“trampoline”函数,后者可以按照截获以前的方式来调用目标函数。当目标函数执行完以后,它将控制返回到截获函数。而截获函数将执行恰当的收尾工作并将控制返回到源函数调用处。下图显示了被截获和未被截获的调用在逻辑上的控制流。

 

图1 有Detours和没有Detours的调用控制流对比

Detours库通过重写目标函数在进程中的二进制映像达到截获目标函数的目的。对每一个目标函数而言,Detours实际上重写了两个函数:目标函数和与之相匹配的trampoline函数。trampoline函数可以静态或者动态的创建。一个静态创建的trampoline函数总是不需要截获就可以调用目标函数。在之前的用于截获的插入中,静态trampoline函数保存了到目标函数的一个简单跳转。这个调整插入以后,trampoline函数保存了目标函数的初始化指令,以及到目标函数的跳转指令。

Detours在Target函数的开头加入JMP Address_of_ Detour_ Function指令(共5个字节)把对Target函数的调用引导到自己的Detour函数, 把Target函数的开头的5个字节加上JMP Address_of_ Target _ Function+5共10个字节作为Trampoline函数。如下图显示了截获过程的插入前后。要截获一个目标函数,Detours首先为动态trampoline函数分配内存(如果没有提供静态的trampoline函数),然后会让目标和trampoline函数可写。在开始了第一条指令之后,Detours会从目标函数拷贝至少五个字节的指令到trampoline 函数(五个字节足够放下一条无条件转移指令)。如果目标函数少于5个字节,Detours会终止执行并返回一个错误码。为了拷贝指令,Detours使用一个简单的表驱动的反汇编引擎。Detours会在trampoline函数的执行尾部添加一条跳转指令,这样执行完trampoline函数后,程序会跳转到目标函数没有拷贝的剩余部分继续执行。Detours会在截获函数中写入一条无条件跳转指令作为到目标函数的第一条指令。最后,Detours将保存目标函数和trampoline函数的原始的页面权限,并使用Flush­Instruction­Cache函数将CPU的指令缓冲区清空。


 

图2 目标函数和跳板函数代码(左边没有插入detour,右边插入了detour)

 

目标函数:函数体(二进制)至少有5个字节以上。按照微软的说明文档Trampoline函数的函数体是拷贝前5个字节加一个无条件跳转指令的话(如果没有特殊处理不可分割指令的话),那么前5个字节必须是完整指令,也就是不能第5个字节和第6个字节是一条不可分割的指令,否则会造成Trampoline 函数执行错误,一条完整的指令被硬性分割开来,造成程序崩溃。对于第5字节和第6个字节是不可分割指令需要调整拷贝到杂技函数(Trampoline)的字节个数,这个值可以查看目标函数的汇编代码得到。此函数是目标函数的修改版本,不能在Detour函数中直接调用,需要通过对Trampoline函数的调用来达到间接调用。

Trampoline函数:此函数默认分配了32个字节,函数的内容就是拷贝的目标函数的前5个字节,加上一个JMP Address_of_ Target _ Function+5指令,共10个字节。此函数仅供您的Detour函数调用,执行完前5个字节的指令后再绝对跳转到目标函数的第6个字节继续执行原功能函数。

Detour函数:此函数是用户需要的截获API的一个模拟版本,调用方式,参数个数必须和目标函数相一致。如目标函数是__stdcall,则Detour函数声明也必须是__stdcall,参数个数和类型也必须相同,否则会造成程序崩溃。此函数在程序调用目标函数的第一条指令的时候就会被调用(无条件跳转过来的),如果在此函数中想继续调用目标函数,必须调用Trampoline函数(Trampoline 函数在执行完目标函数的前5个字节的指令后会无条件跳转到目标函数的5个字节后继续执行),不能再直接调用目标函数,否则将进入无穷递归(目标函数跳转到 Detour函数,Detour函数又跳转到目标函数的递归,因为目标函数在内存中的前5个字节已经被修改成绝对跳转)。通过对Trampoline函数的调用后可以获取目标函数的执行结果,此特性对分析目标函数非常有用,而且可以将目标函数的输出结果进行修改后再传回给应用程序。

这是一种执行时注入的方式,Detours是执行时被插入的。即修改的是内存中的目标函数代码,而不是在硬盘上的DLL文件,因而可以在一个很好的粒度上使得截获二进制函数的执行变得更容易。例如,一个应用程序执行时加载的DLL中的函数过程可以被插入一段截获代码(detoured),与此同时,这个DLL还可以被其他应用程序按正常情况执行(也就是按照不被截获的方式执行,因为DLL二进制文件没有被修改,所以发生截获时不会影响其他进程空间加载这个DLL)。不同于DLL的重新链接或者静态重定向,Detours库中使用的这种中断技术确保不会影响到应用程序中的方法或者系统代码对目标函数的定位。

如果其他人为了调试或者在内部使用其他系统检测手段而试图修改二进制代码,Detours将是一个可以普遍使用的开发包。据我所知,Detours是第一个可以在任意平台上将未修改的目标代码作为一个可以通过“trampoline”调用的子程序来保留的开发包。而以前的系统在逻辑上预先将截获代码放到目标代码中,而不是将原始的目标代码作为一个普通的子程序来调用。独特的“trampoline”设计对于扩展现有的软件的二进制代码是至关重要的。

出于使用基本的函数截获功能的目的,Detours同样提供了DLL注入的方式,它可编辑任何DLL或EXE导入表的功能,达到向存在的二进制代码中添加任意数据节表的目的,向一个新进程或者一个已经运行着的进程中注入一个DLL。一旦向一个进程注入了DLL,这个动态库就可以截获任何Win32函数,不论它是在应用程序中或者在系统库中。

2、有效负荷和DLL导入表的编辑

Detours库提供了被称为有效负荷(payloads)的功能,它可以对Win32二进制文件附加任意数据节表的可逆支持(译注:可以添加,并卸栽)以及编辑DLL导入表。

下图显示了Win32的PE二进制文件的基本结构。PE格式的Win32二进制文件是COFF(普通对象文件格式)的一种扩展。一个Win32二进制文件包括一个对DOS兼容的文件头,一个PE头,一个包含了程序代码的text节表,一个数据节表保存了初始化数据,一个列出导入的DLL和函数的导入表,一个列出导出函数代码的导出表,以及调试符号。除了两个文件头以外,文件的每个节表都是可选的,二进制文件可以不包含它们。


 

图3 Win32 PE可执行文件的结构

为了修改一个Win32二进制文件,Detours在导出节表和调试符号之间生成了一个新的.detours节表。注意调试符号必须永远处于Win32二进制文件的最后面。这个新节表保存了一个截获文件头的记录和原始的PE头,如果修改了导入表,Detours会生成一个新的导入表,并将它附着到拷贝的PE头上,然后修改原始的PE头,让它的内部指向新的导入表。

最后,Detours会将一些其他信息写到.detours节表的最后并将调试信息附加到文件的最后面。Detours可以将二进制文件恢复到被它修改以前的状况,因为它可以恢复在.detours节表中保存的原始的PE文件头,并删除.detours节表。下图显示了一个被Detours修改过的Win32二进制文件的格式。

 

4 一个被Detours修改过的二进制文件的格式

生成一个新的导入表有两个目的。第一,它保留了原始的导入表,这样万一程序员想恢复到修改前的状况就不会出现问题。第二,新的导入表可以包含被更名的导入DLL和函数或者全新的DLL和函数。例如Detours包中的setdll.exe示例程序可以把一个用户Dll的初始化入口表插入到目标应用程序中,以作为目标应用程序导入表中的第一个入口,这样在应用程序地址空间中第一个运行的动态库总是这个用户Dll(这是指动态库加载时运行DllMain函数)。

Detours提供了API用来编辑导入表(DetourBinaryEditImprots),添加有效负荷(DetourBinarySetPayload),枚举有效负荷(DetourBinaryEnumeratePayloads),删除有效负荷(DetourBinaryPurgePayloads),再绑定动态库的函数。Detours同时还提供了API用来枚举映射到地址空间中的二进制文件(DetourEnumerateModules),以及定位这些被映射的二进制文件中的有效负荷(DetourFindPayload)。每一个有效负荷被用一个128位的全局唯一标识符(GUID)标识出来。有效负荷可以用来将每个应用程序的配置信息关联到应用程序的二进制代码中。有效负荷还能直接被拷贝到目标进程中(DetourCopyPayloadToProcess)。

    一旦有任何截获行为需要在不修改二进制文件的情况下被插入到应用程序中,Detours提供了函数来将DLL注入到一个新的或者是已经存在的进程。为了注入一个DLL,Detours使用AllocEx和WriteProcessMemory这些API在目标进程中写入一个LoadLibrary的调用代码,并使用CreateRemoteThread来进行这个调用(指使用一个新线程来调用写入的代码,包括LoadLibrary,在DLL的加载过程中,DllMain函数得以执行)。

Detours是微软开发的一个函数库, 用于修改运行中的程序在内存中的影像,从而即使没有源代码也能改变程序的行为。具体用途是: 拦截WIN32 API调用,将其引导到自己的子程序,从而实现WIN32 API的定制。 为一个已在运行的进程创建一新线程,装入自己的代码并运行。 ---- 本文将简介Detours的原理,Detours库函数的用法, 并利用Detours库函数在Windows NT上编写了一个程序,该程序能使有“调试程序”的用户权限的用户成为系统管理员,附录利用Detours库函数修改该程序使普通用户即可成为系统管理员 (在NT4 SP3上)。 一. Detours的原理 ---- 1. WIN32进程的内存管理 ---- 总所周知,WINDOWS NT实现了虚拟存储器,每一WIN32进程拥有4GB的虚存空间, 关于WIN32进程的虚存结构及其操作的具体细节请参阅WIN32 API手册, 以下仅指出与Detours相关的几点: ---- (1) 进程要执行的指令也放在虚存空间中 ---- (2) 可以使用QueryProtectEx函数把存放指令的页面的权限更改为可读可写可执行,再改写其内容,从而修改正在运行的程序 ---- (3) 可以使用VirtualAllocEx从一个进程为另一正运行的进程分配虚存,再使用 QueryProtectEx函数把页面的权限更改为可读可写可执行,并把要执行的指令以二进制机器码的形式写入,从而为一个正在运行的进程注入任意的代码 ---- 2. 拦截WIN32 API的原理 ---- Detours定义了三个概念: ---- (1) Target函数:要拦截的函数,通常为WindowsAPI。 ---- (2) Trampoline函数:Target函数的复制品。因为Detours将会改写Target函数,所以先把Target函数复制保存好,一方面仍然保存Target函数的过程调用语义,另一方面便于以后的恢复。 ---- (3) Detour 函数:用来替代Target函数的函数。 ---- Detours在Target函数的开头加入JMP Address_of_ Detour_ Function指令(共5个字节)把对Target函数的调用引导到自己的Detour函数, 把Target函数的开头的5个字节加上JMP Address_of_ Target _ Function+5作为Trampoline函数。例子如下: 拦截前:Target _ Function: ;Target函数入口,以下为假想的常见的子程序入口代码 push ebp mov ebp, esp push eax push ebx Trampoline: ;以下是Target函数的继续部分 …… 拦截后: Target _ Function: jmp Detour_Function Trampoline: ;以下是Target函数的继续部分 …… Trampoline_Function: ; Trampoline函数入口, 开头的5个字节与Target函数相同 push ebp mov ebp, esp push eax push ebx ;跳回去继续执行Target函数 jmp Target_Function+5 ---- 3. 为一个已在运行的进程装入一个DLL ---- 以下是其步骤: ---- (1) 创建一个ThreadFuction,内容仅是调用LoadLibrary。 ---- (2) 用VirtualAllocEx为一个已在运行的进程分配一片虚存,并把权限更改为可读可写可执行。 ---- (3) 把ThreadFuction的二进制机器码写入这片虚存。 ---- (4) 用CreateRemoteThread在该进程上创建一个线程,传入前面分配的虚存的起始地址作为线程函数的地址,即可为一个已在运行的进程装入一个DLL。通过DllMain 即可在一个已在运行的进程中运行自己的代码。 二. Detours库函数的用法 ---- 因为Detours软件包并没有附带帮助文件,以下接口仅从剖析源代码得出。 ---- 1. PBYTE WINAPI DetourFindFunction(PCHAR pszModule, PCHAR pszFunction) ---- 功能:从一DLL中找出一函数的入口地址 ---- 参数:pszModule是DLL名,pszFunction是函数名。 ---- 返回:名为pszModule的DLL的名为pszFunction的函数的入口地址 ---- 说明:DetourFindFunctio
<think>我们正在使用Detours库来Hook一个COM接口方法。IDXGIOutput1::DuplicateOutput是一个COM接口方法,因此我们需要理解COM方法的调用约定(通常是__stdcall)和虚函数表(vtable)的结构。步骤:1.获取目标函数的地址:由于DuplicateOutput是IDXGIOutput1接口的一个虚方法,我们需要通过对象的虚函数表(vtable)来获取它的地址。2.定义函数指针:指向原始DuplicateOutput函数的指针。3.编写钩子函数:该函数必须与原始函数具有相同的签名和调用约定。4.使用Detours进行钩子的安装和卸载。但是,Detours通常用于Hook普通的函数,而COM接口方法是通过虚函数表调用的。因此,我们不能直接使用DetourAttach来Hook虚函数表中的函数指针。我们需要修改虚函数表中的条目。然而,Detours提供了一个扩展库DetoursforCOM(DetoursCom),专门用于HookCOM接口方法。但请注意,标准的Detours库并不直接支持COM接口的Hook,我们需要手动操作虚函数表或者使用其他方法。另一种方法是使用“虚函数表Hook”(VTable Hook),这是HookCOM接口的常用技术。我们可以直接替换虚函数表中的函数指针。但是,Detours库本身并没有提供直接HookCOM接口的函数,因此我们需要结合Detours和手动修改虚函数表的方式。然而,在提供的引用中,并没有直接关于HookCOM方法的例子。因此,我们需要自己实现。思路:1.获取目标对象的虚函数表(vtable)。2.在虚函数表中找到DuplicateOutput方法的索引(通常通过查阅文档或头文件确定索引号)。3.保存原始的函数指针。4.将虚函数表中的该条目替换为我们的钩子函数。注意:DuplicateOutput在IDXGIOutput1接口中的索引。根据DXGI的头文件,IDXGIOutput1继承自IDXGIOutput,而DuplicateOutput是IDXGIOutput1的第一个方法(索引为0?不对,因为基类方法在前)。我们需要准确知道索引。实际上,在IDXGIOutput1中,DuplicateOutput是第10个方法(因为IDXGIOutput有8个方法,IDXGIOutput1从第9个开始,所以DuplicateOutput是第9个,索引为8?注意:C++中虚函数表索引从0开始)。我们可以通过查看头文件来确认。例如,在dxgi.h中,IDXGIOutput1的定义如下:```struct IDXGIOutput1: publicIDXGIOutput{//...其他方法virtualHRESULT STDMETHODCALLTYPE DuplicateOutput(/*[in] */IUnknown*pDevice,/*[annotation][out] */_COM_Outptr_IDXGIOutputDuplication **ppOutputDuplication) =0; };```由于IDXGIOutput1继承自IDXGIOutput,而IDXGIOutput有8个方法(包括IUnknown的3个方法和IDXGIObject的5个方法?实际上需要具体数)。通常,我们可以用以下方法确定索引: -IUnknown有3个方法(QueryInterface, AddRef,Release)- IDXGIObject(继承自IUnknown)有5个方法(SetPrivateData,SetPrivateDataInterface, GetPrivateData, GetParent,然后IDXGIOutput的方法)- IDXGIOutput有8个方法(包括继承的,所以前面3个是IUnknown,接下来5个是IDXGIObject,然后IDXGIOutput自己的方法?)实际上,在IDXGIOutput1中,DuplicateOutput的索引是8(从0开始,第9个)。我们可以通过计算:基类IDXGIOutput的方法数:根据文档,IDXGIOutput有8个方法(包括继承自IUnknown和IDXGIObject的)。因此,IDXGIOutput1的第一个新方法(DuplicateOutput)在索引8的位置。但是,为了确保正确,最好在运行时通过检查偏移量来确定,或者查看头文件。另一种方法是使用函数指针转换和偏移量计算,但这样容易出错。我们可以使用一个技巧:创建一个假的接口,然后通过这个假接口来获取DuplicateOutput的索引。然而,在实际操作中,我们通常直接使用已知的索引。根据微软文档和头文件,IDXGIOutput1的DuplicateOutput是第9个函数(索引8)。步骤:1.获取目标IDXGIOutput1对象的虚函数表指针(vtable)。2.计算DuplicateOutput在虚函数表中的位置:vtable[8](索引8)。3.保存原始的函数指针。4.将vtable[8]替换为我们的钩子函数的地址。但是,修改虚函数表需要小心,因为多个对象可能共享同一个虚函数表(通常每个COM类有一个虚函数表,而不是每个对象)。但是,在COM中,每个对象实例都有一个指向虚函数表的指针(即每个对象都有一个vptr,指向该类的vtable)。而同一个类的所有对象共享同一个虚函数表。因此,如果我们修改了一个对象的虚函数表,那么所有相同类的对象都会受到影响。然而,我们通常只想Hook特定的对象。因此,我们需要为这个对象单独创建一个新的虚函数表(复制原始虚函数表,然后修改其中的条目),然后修改这个对象的vptr指向我们新创建的虚函数表。具体步骤:a.为对象创建一个新的虚函数表(数组),大小与原始虚函数表相同(通常我们不知道大小,但可以假设一个足够大的值,或者通过遍历直到遇到nullptr?但这不是标准做法)。更安全的做法是复制原始虚函数表,然后修改我们关心的条目。 b.修改对象的vptr指向我们新创建的虚函数表。c.在钩子函数中,当不需要钩子时,可以调用原始函数(通过保存的原始函数指针)。但是,这种方法需要分配内存来存储新的虚函数表,并且要管理这个内存。而且,如果同一个类的多个对象需要不同的Hook,那么每个对象都需要一个单独的虚函数表副本。另一种方法是使用Detours的“trampoline”机制,但Detours主要针对非虚函数。对于虚函数,我们可以结合两种方法:先通过虚函数表Hook将调用重定向到我们的跳板函数,然后在跳板函数中使用Detours的trampoline来调用原始函数。然而,这里我们只讨论使用虚函数表Hook。由于操作虚函数表比较底层,而且需要处理内存分配和复制,我们写一个类来管理。下面是一个简化的步骤和代码框架:假设我们有一个IDXGIOutput1对象(pOutput),我们要Hook它的DuplicateOutput方法。步骤:1.定义原始函数指针类型和钩子函数类型。2.获取对象的虚函数表指针(vtable)。3.创建一个新的虚函数表(复制原始虚函数表)。4.保存原始函数指针(从原始虚函数表的索引8)。5.将新虚函数表的索引8替换为钩子函数地址。6.修改对象的虚函数表指针(vptr)指向新的虚函数表。注意:这个过程需要线程安全,并且要确保在Hook时没有其他线程正在使用该对象。代码框架: ```cpp#include <Windows.h> #include<dxgi.h> #include<vector> //定义函数指针类型typedefHRESULT (STDMETHODCALLTYPE*DuplicateOutput_t)(IDXGIOutput1* pThis,IUnknown*pDevice, IDXGIOutputDuplication **ppOutputDuplication); staticDuplicateOutput_tOriginalDuplicateOutput =nullptr;//钩子函数HRESULT STDMETHODCALLTYPE MyDuplicateOutput(IDXGIOutput1*pThis, IUnknown *pDevice, IDXGIOutputDuplication **ppOutputDuplication) {//在这里进行Hook处理//可以调用原始函数return OriginalDuplicateOutput(pThis, pDevice,ppOutputDuplication); }//假设我们有一个IDXGIOutput1对象,我们将其指针传递进来进行HookvoidHookDuplicateOutput(IDXGIOutput1* pOutput){//获取对象的虚函数表指针(vptr)void** vtable =*(void***)pOutput;//假设DuplicateOutput的索引为8constint index=8;//保存原始函数OriginalDuplicateOutput =(DuplicateOutput_t)vtable[index];//创建一个新的虚函数表,复制原始虚函数表//注意:我们不知道虚函数表的大小,所以需要假设一个足够大的值(例如20),或者通过其他方式确定。//这里为了简单,假设20个条目(对于IDXGIOutput1应该足够)std::vector<void*> newVtable(20);memcpy(&newVtable[0], vtable,20* sizeof(void*));//替换索引8为我们的钩子函数newVtable[index] =&MyDuplicateOutput;//将对象的vptr指向新的虚函数表*(void***)pOutput= &newVtable[0];//注意:这里有问题,因为newVtable是局部变量,不能这样用!//上面的代码有问题:newVtable是局部变量,当函数返回时,内存会被释放,导致虚函数表无效。//因此,我们需要动态分配新的虚函数表,并且需要管理其生命周期(直到卸载Hook)。}```为了解决局部变量的问题,我们需要在堆上分配新的虚函数表,并且要确保在对象销毁之前或者卸载Hook时,这个虚函数表不会被释放。改进:我们可以将新的虚函数表分配在堆上,并且记录每个对象的原始虚函数表和新的虚函数表。卸载Hook时,恢复原始虚函数表,并释放分配的内存。但是,这个例子比较复杂,而且需要管理内存。此外,对于同一个对象,我们可能多次Hook,所以需要设计好。由于这些复杂性,通常我们会使用一个已经封装好的Hook库来处理COM Hook,比如MicrosoftDetours的扩展(如果支持的话)或者其他的库。但是,根据引用[3]中的建议,Detours并不是解决所有问题的最佳方法。因此,我们需要权衡。另一种方法是使用“代理”对象:创建一个实现了IDXGIOutput1接口的代理类,将所有方法转发给原始对象,只重写DuplicateOutput方法。然后替换对象指向我们的代理对象。这需要实现整个IDXGIOutput1接口,比较繁琐,但是更安全。由于时间关系,这里我们只讨论虚函数表Hook的方法,并解决内存问题。我们修改代码: ```cpp#include <Windows.h>#include <dxgi.h>#include <vector>typedefHRESULT (STDMETHODCALLTYPE*DuplicateOutput_t)(IDXGIOutput1* pThis,IUnknown*pDevice,IDXGIOutputDuplication**ppOutputDuplication);static DuplicateOutput_t OriginalDuplicateOutput= nullptr; HRESULTSTDMETHODCALLTYPEMyDuplicateOutput(IDXGIOutput1* pThis,IUnknown*pDevice,IDXGIOutputDuplication**ppOutputDuplication){//示例:在调用原始函数之前,我们可以做一些事情//例如,修改参数或记录调用return OriginalDuplicateOutput(pThis, pDevice,ppOutputDuplication); }//全局变量,记录原始虚函数表和新虚函数表(实际应用中需要更复杂的管理,比如使用map来管理多个对象)staticvoid**g_originalVTable= nullptr;static void** g_newVTable =nullptr;voidHookDuplicateOutput(IDXGIOutput1* pOutput){//获取原始虚函数表void** vtable =*(void***)pOutput;g_originalVTable =vtable;//假设虚函数表大小为20(根据实际情况调整)const intvtableSize =20;g_newVTable= newvoid*[vtableSize];memcpy(g_newVTable, vtable,vtableSize *sizeof(void*));//索引8constint index=8;OriginalDuplicateOutput= (DuplicateOutput_t)vtable[index];g_newVTable[index] =MyDuplicateOutput;//修改对象的vptr*(void***)pOutput =g_newVTable; }void UnhookDuplicateOutput(IDXGIOutput1* pOutput){if (g_originalVTable){//恢复对象的vptr*(void***)pOutput =g_originalVTable;//释放新虚函数表delete[]g_newVTable;g_newVTable= nullptr;g_originalVTable =nullptr;}} ```注意:这个例子是全局的,只能Hook一个对象。如果同时Hook多个对象,需要为每个对象单独保存原始虚函数表和新虚函数表。另外,索引8是固定的,但可能因不同的SDK版本而不同,所以最好通过头文件确认。在WindowsSDK中,我们可以通过计算基类的方法数来确定索引。由于IDXGIOutput1继承自IDXGIOutput,而IDXGIOutput继承自IDXGIObject,IDXGIObject继承自IUnknown。我们可以通过以下方式计算:在IDXGIOutput1中,DuplicateOutput是第一个新增的方法,因此它的索引等于基类IDXGIOutput的方法数。我们可以通过以下代码在编译时获取索引:```cpp#include <dxgi.h>structIDXGIOutput1Dummy :publicIDXGIOutput1{//创建一个虚拟实现,只是为了获取索引virtual HRESULTSTDMETHODCALLTYPEDuplicateOutput(IUnknown*,IDXGIOutputDuplication**)override {return S_OK;} };constint index= (int)(offsetof(IDXGIOutput1Dummy, DuplicateOutput) /sizeof(void*));```但是,这种方法并不直接,因为虚函数表布局是编译器相关的。更可靠的方法是查看头文件并手动计数。根据DXGI.h(WindowsSDK的一部分),IDXGIOutput1的父接口IDXGIOutput有8个方法(包括继承自IUnknown和IDXGIObject的)。因此,DuplicateOutput的索引是8。总结:我们使用虚函数表Hook技术来Hook COM方法。但请注意,这种方法在多线程环境下需要谨慎,并且要确保在卸载Hook时对象仍然有效。由于问题复杂,这里只提供一个基本框架。实际应用中,还需要考虑错误处理、多对象Hook、线程安全等问题。相关问题:1.如何确定COM接口方法的虚函数表索引?2.除了虚函数表Hook,还有哪些方法可以Hook COM接口方法?3.在使用虚函数表Hook时,如何处理多个对象实例?4.如何安全地卸载Hook?注意:上述代码示例仅用于演示目的,实际使用中需要更健壮的实现。</think>### Hook IDXGIOutput1::DuplicateOutput 方法 在 Windows 平台上使用 C++ 和 Detour 库 Hook `IDXGIOutput1::DuplicateOutput` 方法需要特殊处理,因为这是一个 COM 接口方法。以下是完整实现步骤: #### 1. 定义函数指针和钩子函数 ```cpp #include <Windows.h> #include <dxgi.h> #include <detours/detours.h> // 原始函数指针类型 typedef HRESULT (STDMETHODCALLTYPE* OriginalDuplicateOutput_t)( IDXGIOutput1* pThis, IUnknown* pDevice, IDXGIOutputDuplication** ppOutputDuplication ); // 原始函数指针 static OriginalDuplicateOutput_t OriginalDuplicateOutput = nullptr; // 钩子函数 HRESULT STDMETHODCALLTYPE HookedDuplicateOutput( IDXGIOutput1* pThis, IUnknown* pDevice, IDXGIOutputDuplication** ppOutputDuplication ) { // 在此添加Hook处理逻辑 // 示例:修改设备指针或记录调用 OutputDebugString(L"[HOOK] DuplicateOutput called\n"); // 调用原始函数 return OriginalDuplicateOutput(pThis, pDevice, ppOutputDuplication); } ``` #### 2. 获取虚函数表并安装钩子 ```cpp void InstallHook(IDXGIOutput1* pOutput) { // 获取虚函数表指针 void** vtable = *(void***)pOutput; // 确定DuplicateOutput在虚函数表中的索引 // 根据DXGI.h,IDXGIOutput1中DuplicateOutput是第9个方法(索引8) const int duplicateOutputIndex = 8; // 保存原始函数地址 OriginalDuplicateOutput = (OriginalDuplicateOutput_t)vtable[duplicateOutputIndex]; // 开始Detour事务 DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); // 安装钩子(替换虚函数表条目) DetourAttach(&(PVOID&)vtable[duplicateOutputIndex], HookedDuplicateOutput); // 提交事务 if (DetourTransactionCommit() != NO_ERROR) { OutputDebugString(L"Failed to install hook\n"); } } void RemoveHook(IDXGIOutput1* pOutput) { void** vtable = *(void***)pOutput; const int duplicateOutputIndex = 8; DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourDetach(&(PVOID&)vtable[duplicateOutputIndex], HookedDuplicateOutput); DetourTransactionCommit(); } ``` #### 3. 使用示例 ```cpp int main() { // 创建DXGI工厂和输出对象(实际使用中需正确初始化) IDXGIFactory1* pFactory = nullptr; IDXGIOutput* pOutput = nullptr; IDXGIOutput1* pOutput1 = nullptr; CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void**)&pFactory); pFactory->EnumOutputs(0, &pOutput); pOutput->QueryInterface(__uuidof(IDXGIOutput1), (void**)&pOutput1); // 安装钩子 InstallHook(pOutput1); // 测试调用(实际使用中由应用程序触发) IDXGIOutputDuplication* pDuplication = nullptr; pOutput1->DuplicateOutput(nullptr, &pDuplication); // 移除钩子 RemoveHook(pOutput1); // 释放资源 pDuplication->Release(); pOutput1->Release(); pOutput->Release(); pFactory->Release(); return 0; } ``` #### 关键注意事项 1. **虚函数表索引**: - `DuplicateOutput` 在 `IDXGIOutput1` 虚函数表中的索引为 8(从 0 开始计数) - 可通过检查 `dxgi.h` 头文件确认: ```cpp struct IDXGIOutput1 : public IDXGIOutput { // ... 前8个方法来自基类 ... virtual HRESULT STDMETHODCALLTYPE DuplicateOutput( /* [in] */ IUnknown *pDevice, /* [annotation][out] */ _COM_Outptr_ IDXGIOutputDuplication **ppOutputDuplication) = 0; }; ``` 2. **COM 对象生命周期**: - 确保 Hook 时对象已完全初始化 - 在对象释放前卸载钩子 - 使用 `QueryInterface` 获取正确的接口指针 3. **线程安全**: - 在 Hook/Unhook 期间暂停相关线程 - 使用 `DetourUpdateThread` 更新所有线程上下文 4. **错误处理**: - 检查所有 COM 调用的返回值(`SUCCEEDED`/`FAILED`) - 验证 Detour 事务返回值 #### 替代方案 如果遇到稳定性问题,可考虑: ```cpp // 手动修改虚函数表(不使用Detour事务) void** vtable = *(void***)pOutput; DWORD oldProtect; VirtualProtect(&vtable[8], sizeof(void*), PAGE_READWRITE, &oldProtect); vtable[8] = &HookedDuplicateOutput; VirtualProtect(&vtable[8], sizeof(void*), oldProtect, &oldProtect); ``` #### 潜在问题解决 - **访问冲突**:确保使用正确的虚函数表索引 - **钩子未生效**:确认对象是否属于预期的 COM 类 - **死锁风险**:避免在钩子函数中调用可能被 Hook 的 DXGI 函数 [^1]: Detour 库通过修改函数指针实现 API Hook,适用于 COM 接口方法 [^2]: 虚函数表 Hook 是拦截 COM 方法的有效技术 [^3]: 操作虚函数表时需注意内存保护和线程同步 --- ### 相关问题 1. **如何确定 COM 接口方法的虚函数表索引?** 通过分析 SDK 头文件中的接口定义,计算基类方法数量来确定新方法的索引位置。 2. **Detour 库 Hook COM 方法时有哪些限制?** 需手动处理虚函数表修改,无法直接使用 `DetourAttach` 拦截接口方法,且需考虑 COM 对象的多实例问题。 3. **如何确保 COM Hook 的线程安全性?** 在修改虚函数表时挂起所有线程,或使用原子操作替换指针,同时配合内存保护属性修改。 4. **Hook 后如何访问原始 COM 方法?** 保存原始函数指针,在钩子函数中通过该指针调用原始实现,注意正确处理 `this` 指针。 5. **DXGI Hook 有哪些典型应用场景?** 屏幕捕获监控、游戏叠加渲染、性能分析、DRM 保护等需要拦截图形系统调用的场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值