API连接显示

& lt; !-下载链接->

& lt; !——文章图像——> 介绍 拦截Win32 API调用对大多数Windows开发人员来说一直是一个具有挑战性的课题,我不得不承认,这也是我最喜欢的课题之一。术语挂钩表示一种控制特定代码执行的基本技术。它提供了一种简单的机制,可以很容易地改变操作系统和第三方产品的行为,而不需要提供它们的源代码。 许多现代系统通过使用间谍技术来利用现有Windows应用程序的能力引起了人们的注意。挂钩的一个关键动机不仅是为了实现高级功能,而且是为了插入用户提供的代码以进行调试。 不像一些相对“老”的操作系统,如DOS和Windows 3。当前的Windows操作系统NT/2K和9x提供了复杂的机制来分离每个进程的地址空间。这种体系结构提供了真正的内存保护,因此任何应用程序都不可能破坏另一个进程的地址空间,或者在更糟糕的情况下甚至导致操作系统本身崩溃。这使得系统感知钩子的开发更加困难。 我写这篇文章的动机是需要一个非常简单的挂钩框架,它将提供一个易于使用的接口和捕获不同api的能力。它打算揭示一些技巧,可以帮助您编写自己的间谍系统。它提出了一种单一的解决方案,即如何构建一套用于在NT/2K和98/Me系列Windows上挂钩的Win32 API函数。为了简单起见,我决定不添加对UNICODE的支持。但是,只要对代码进行一些小的修改,就可以轻松地完成这项任务。 监视应用程序提供了许多优点: API函数的监测 控制API函数调用的能力非常有帮助,它使开发人员能够跟踪在API调用期间发生的特定“不可见”操作。它有助于对参数进行全面验证,并报告通常在幕后被忽视的问题。例如,有时候,监视与内存相关的API函数对于捕获资源泄漏可能非常有帮助。调试与逆向工程 除了用于调试的标准方法外,API挂钩还被认为是最流行的调试机制之一。许多开发人员使用API挂钩技术来识别不同的组件实现及其关系。API拦截是获取二进制可执行文件信息的非常强大的方法。窥视操作系统内部 开发人员通常渴望了解部门的操作系统,并受到“调试器”角色的启发。对于解码没有文档化或没有文档化的api,挂钩也是一种非常有用的技术。通过将定制模块嵌入到外部Windows应用程序中来扩展最初提供的功能,通过注入钩子重新路由正常的代码执行,可以提供一种更改和扩展现有模块功能的简单方法。例如,许多第三方产品有时不能满足特定的安全需求,必须根据您的特定需求进行调整。监视应用程序允许开发者在原始API功能的周围添加复杂的预处理和后处理。这种能力对于更改已编译代码的行为非常有用。 挂钩系统的功能需求 在您开始实现任何类型的API挂钩系统之前,有一些重要的决策是必须要做的。首先,您应该确定是挂接单个应用程序还是安装系统感知引擎。例如,如果您只想监视一个应用程序,您不需要安装一个系统范围的钩子,但是如果您的工作是跟踪所有对TerminateProcess()或WriteProcessMemory()的调用,那么唯一的方法就是拥有一个系统感知的钩子。选择何种方法取决于具体情况和具体问题。 一个API监视框架的一般设计 通常一个钩子系统至少由两部分组成——一个钩子服务器和一个驱动程序。钩子服务器负责在适当的时候将驱动程序注入目标进程中。它也管理驱动程序和可选的可以从驱动程序接收关于它的活动的信息,而驱动程序模块执行实际的拦截。 这种设计很粗糙,毫无疑问没有覆盖所有可能的实现。然而,它勾勒出了钩子框架的边界。 一旦你有了一个钩子框架的需求说明,有几个设计点你应该考虑: 你需要什么应用程序挂钩如何注入DLL到目标进程或什么植入技术来实现低哪个拦截机制使用 我希望接下来的几节将为这些问题提供答案。 注射技术 注册表 为了将DLL注入到与USER32链接的进程中。,您只需将DLL名称添加到以下注册表项的值: 微软HKEY_LOCAL_MACHINE \ Software \ \ Windows NT \ CurrentVersion \ Windows \ AppInit_DLLs 它的值包含一个DLL名称或一组用逗号或空格分隔的DLL。根据MSDN文档[7],由该键值指定的所有dll都由当前登录会话中运行的每个基于windows的应用程序加载。有趣的是,这些dll的实际加载是USER32初始化的一部分。USER32读取提到的注册表项的值,并在其DllMain代码中为这些dll调用LoadLibrary()。然而,这个技巧只适用于使用USER32.DLL的应用程序。另一个限制是这种内置机制只被NT和2K操作系统支持。虽然这是一个无害的方式注入一个DLL到一个Windows进程有一些缺点: 为了激活/停用注入过程,你必须重新启动Windows。您希望注入的DLL将只映射到使用USER32的这些进程中。DLL,因此你不能期望你的钩子注入到控制台应用程序,因为他们通常不导入函数从USER32.DLL。另一方面,您对注入过程没有任何控制。这意味着它被植入到每个单独的GUI应用程序中,而不管您是否需要它。这是一种冗余的开销,特别是当您打算只挂钩几个应用程序时。更多细节请参见[2]“使用注册表注入DLL” 整个系统的Windows钩子 当然,在目标进程中注入DLL的一种非常流行的技术依赖于Windows挂钩。正如MSDN中指出的,钩子是系统消息处理机制中的陷阱。应用程序可以安装自定义筛选器功能来监视系统中的消息流量,并在某些类型的消息到达目标窗口过程之前处理它们。 钩子通常在DLL中实现,以满足系统范围钩子的基本要求。这种钩子的基本概念是钩子回调过程在系统中每个被钩子连接的进程的地址空间中执行。要安装一个钩子,可以使用适当的参数调用SetWindowsHookEx()。一旦应用程序安装了一个系统范围的钩子,操作系统就会将DLL映射到它的每个客户端进程中的地址空间。因此,DLL中的全局变量将是“每进程”,不能在加载钩子DLL的进程之间共享。所有包含共享数据的变量必须放在共享数据部分中。下面的图表显示了一个由钩子服务器注册的钩子,并注入到名为“应用程序1”和“应用程序2”的地址空间。 图1 在执行SetWindowsHookEx()时,系统范围内的钩子只注册为1。如果没有发生错误,则返回该钩子的句柄。当需要调用CallNextHookEx()时,在自定义钩子函数的末尾需要returned 值。成功调用SetWindowsHookEx()之后,操作系统会自动(但不是立即)将DLL注入到满足这个特定钩子过滤器要求的所有进程中。让我们仔细看看下面的伪WH_GETMESSAGE过滤器函数: 隐藏,/ /——复制代码 / / GetMsgProc // 对于WH_GETMESSAGE的Filter函数—它只是一个哑函数 //--------------------------------------------------------------------------- LRESULT回调GetMsgProc ( int代码,//钩子代码 //删除选项 LPARAM //消息 ) { //我们必须将所有消息传递给CallNextHookEx。 CallNextHookEx(sg_hGetMsgHook, code, wParam, lParam); } 一个系统范围的钩子是由不共享相同地址空间的多个进程加载的。 例如,由SetWindowsHookEx()获得并在CallNextHookEx()中用作参数的钩子句柄sg_hGetMsgHook必须在所有地址空间中虚拟使用。这意味着它的值必须在钩子连接的进程和钩子服务器应用程序之间共享。为了使这个变量对所有进程“可见”,我们应该将它存储在共享数据部分。 下面是使用#pragma data_seg()的示例。这里我想指出,共享部分中的数据必须初始化,否则变量将被分配到默认数据段,而#pragma data_seg()将不起作用。 隐藏,/ /——复制代码 //由所有进程变量共享 //--------------------------------------------------------------------------- # pragma data_seg(“.HKT”) HHOOK sg_hGetMsgHook = NULL; BOOL sg_bhookinstall = FALSE; //我们从调用SetWindowsHookEx()的包装器的应用程序获得它 HWND sg_hwndServer = NULL # pragma data_seg () 您还应该在DLL的DEF文件中添加section语句 隐藏,复制CodeSECTIONS .HKT读写共享 或使用 隐藏,复制代码#pragma注释(链接器,"/section:。HKT,遥控武器站”) 一旦一个钩子DLL被加载到目标进程的地址空间中,就没有办法卸载它,除非钩子服务器调用UnhookWindowsHookEx()或者钩子应用程序关闭。当钩子服务器调用UnhookWindowsHookEx()时,操作系统循环通过一个内部列表,所有进程都被强制加载钩子DLL。操作系统减少DLL的锁计数,当它变为0时,DLL自动从进程的地址空间中取消映射。 以下是这种方法的一些优点: 该机制得到了NT/2K和9x Windows系列的支持,并有望在未来的Windows版本中得到维护。与注入DLL的注册表机制不同,该方法允许在钩子服务器决定不再需要DLL时卸载DLL,并调用UnhookWindowsHookEx() 虽然我认为Windows钩子是非常方便的注入技术,但它也有自己的缺点: Windows钩子会显著降低系统的整体性能,因为它们增加了系统必须为每条消息执行的处理量。调试系统范围的Windows挂钩需要付出很多努力。但是,如果您同时使用多个vc++实例运行,它将简化更复杂的场景的调试过程。最后但并非最不重要的是,这种钩子会影响整个系统的处理,在某些情况下(比如出现bug),您必须重新启动计算机才能恢复它。 使用CreateRemoteThread() API函数注入DLL 嗯,这是我最喜欢的一个。不幸的是,它只被NT和Windows 2K操作系统支持。奇怪的是,你也可以在win9x上调用(链接)这个API,但是它只会返回NULL而不做任何事情。 通过远程线程注入DLL是Jeffrey Ritcher的想法,在他的文章[9]“使用INJLIB将你的32位DLL加载到另一个进程的地址空间”中有详细的说明。 基本概念很简单,但很优雅。任何进程都可以使用LoadLibrary() API动态加载DLL。问题是,如果我们不能访问进程的线程,如何强制外部进程代表我们调用LoadLibrary() ?有一个名为CreateRemoteThread()的函数,用于创建远程线程。这里有一个技巧——看一下thread函数的签名,它的指针作为参数(即LPTHREAD_START_ROUTINE)传递给CreateRemoteThread(): 隐藏,复制代码字WINAPI ThreadProc(LPVOID lpParameter); 这是LoadLibrary API的原型 隐藏,复制CodeHMODULE WINAPI LoadLibrary(LPCTSTR lpFileName); 是的,它们确实有“相同”的图案。它们使用相同的调用约定WINAPI,都接受一个参数,并且返回值的大小相同。这个匹配给我们一个提示,我们可以使用LoadLibrary()作为线程函数,它将在创建远程线程之后执行。让我们看看下面的示例代码: 隐藏,复制CodehThread =::CreateRemoteThread( hProcessForHooking, 空, 0, pfnLoadLibrary, “C: \ \ HookTool.dll”, 0, 零); 通过使用GetProcAddress() API,我们可以获得LoadLibrary() API的地址。这里的问题是,Kernel32.DLL总是映射到每个进程的相同地址空间,因此LoadLibrary()函数的地址在任何运行进程的地址空间中都有相同的值。这确保了我们传递一个有效的指针(即pfnLoadLibrary)作为CreateRemoteThread()的参数。 作为线程函数的参数,我们使用DLL的完整路径名,将其转换为LPVOID。当远程线程被恢复时,它将DLL的名称传递给ThreadFunction(即LoadLibrary)。这就是使用远程线程进行注入的全部技巧。 如果通过CreateRemoteThread() API进行移植,我们应该考虑一件重要的事情。每次注入器应用程序操作目标进程的虚拟内存并调用CreateRemoteThread()之前,它都会首先使用OpenProcess() API打开进程,并传递PROCESS_ALL_ACCESS标志作为参数。当我们希望获得对该进程的最大访问权限时,将使用此标志。在这个场景中,OpenProcess()将为一些ID号较低的进程返回NULL。这个错误(尽管我们使用了一个有效的进程ID)是由于没有在具有足够权限的安全上下文中运行造成的ssions。如果你稍微思考一下,你就会意识到这很有道理。所有这些受限制的进程都是操作系统的一部分,不应该允许普通应用程序对它们进行操作。如果某个应用程序有错误并意外地试图终止操作系统进程,会发生什么?为了防止操作系统出现这种最终崩溃,一个给定的应用程序必须有足够的特权来执行可能改变操作系统行为的api。以访问系统资源(例如smss)。exe进程。exe、服务。通过调用OpenProcess(),您必须被授予调试特权。这个功能非常强大,它提供了一种访问系统资源的方法,这些资源通常受到限制。调整进程特权是一项琐碎的任务,可以通过以下逻辑操作来描述: 使用调整特权所需的权限打开进程令牌,给定一个特权的名称为“SeDebugPrivilege”,我们应该找到它的本地LUID映射。通过调用OpenProcessToken()进程令牌句柄获得的AdjustTokenPrivileges() api close来调整令牌以启用“SeDebugPrivilege”特权 有关更改特权的更多细节,请参阅[10]“使用特权”。通过BHO插件植入 有时您需要只在Internet Explorer中注入定制代码。幸运的是,微软为此提供了一种简单且有良好文档记录的方法——浏览器助手对象。一个BHO被实现为COM DLL,一旦它被正确注册,每次当IE被启动时,它加载所有实现了IObjectWithSite接口的COM组件。MS Office插件 类似地,对于bho,如果需要在MS Office应用程序中植入自己的代码,可以通过实现MS Office外接程序来利用所提供的标准机制。有许多可用的示例演示如何实现这类外接程序。 拦截机制 将DLL注入到外部进程的地址空间是监视系统的一个关键元素。它提供了一个很好的机会来控制进程的线程活动。但是,如果想在进程中拦截API函数调用,注入DLL是不够的。 本文的这一部分打算简要回顾几个现实世界中可用的挂钩方面。重点介绍了每一种方法的基本轮廓,揭示了它们的优缺点。 就应用钩子的级别而言,API监视有两种机制——内核级监视和用户级监视。为了更好地理解这两个级别,您必须了解Win32子系统API和本机API之间的关系。下图演示了不同钩子的设置,并说明了模块之间的关系以及它们在Windows 2K上的依赖关系: 图2 它们在实现上的主要区别是,用于内核级挂钩的拦截器引擎包装为内核模式驱动程序,而用户级挂钩通常使用用户模式DLL。 NT内核级挂钩 有几种方法可以在内核模式下实现NT系统服务的挂钩。最流行的拦截机制最初是由Mark Russinovich和Bryce Cogswell在他们的[3]文章“Windows NT系统-调用挂钩”中演示的。他们的基本思想是在用户模式下注入一种拦截机制来监视NT系统调用。这种技术非常强大,并且提供了一种非常灵活的方法来连接所有用户模式线程在被操作系统内核服务之前经过的点。 你也可以在“Undocumented windows2000 Secrets”中找到一个优秀的设计和实现。Sven Schreiber 在他的伟大著作中解释了如何从头开始构建内核级挂钩框架[5]。 Prasad Dabak在他的《无文档的Windows NT》[17]中提供了另一个全面的分析和出色的实现。 然而,所有这些挂钩策略都不在本文的讨论范围之内。 Win32用户级别挂钩 窗口子类化。 此方法适用于由于窗口过程的新实现而可能改变应用程序行为的情况。要完成此任务,只需使用GWLP_WNDPROC参数调用SetWindowLongPtr()并将指针传递给您自己的窗口过程。一旦您建立了新的子类过程,每当Windows向指定的窗口分派消息时,它就会查找与特定窗口关联的窗口过程的地址,并调用您的过程而不是原始的过程。 这种机制的缺点是,子类化只能在特定进程的边界内使用。换句话说,一个应用程序应该n不子类化由另一个进程创建的窗口类。 通常,这种方法适用于通过外接程序(即DLL / In-Proc COM组件)挂钩一个应用程序,并且可以获得你想要替换其过程的窗口的句柄。 例如,一段时间以前,我为IE写了一个简单的插件(浏览器助手对象),它用子类化代替了原来IE提供的弹出菜单。 代理DLL(特洛伊DLL) 黑客侵入API的一种简单方法是将DLL替换为具有相同名称的DLL,并导出原始DLL的所有符号。使用函数转发器可以毫不费力地实现此技术。函数转发器基本上是DLL的导出部分中的一个条目,它将一个函数调用委托给另一个DLL的函数。 您可以简单地使用#pragma comment来完成此任务: 隐藏,复制代码#pragma注释(链接器,"/export:DoSomething=DllImpl.ActuallyDoSomething") 但是,如果您决定使用这种方法,那么您应该负责提供与原始库的新版本的兼容性。更多细节见[13a]章节“导出转发”和[2]“功能转发器”。 代码覆盖 有几种方法是基于代码覆盖的。其中一个改变了调用指令所使用的函数的地址。这种方法难度大,容易出错。下面的基本思想是跟踪内存中的所有调用指令,并用用户提供的地址替换原始函数的地址。 另一种代码覆盖方法需要更复杂的实现。简单地说,这种方法的概念是定位原始API函数的地址,并用一条JMP指令更改这个函数的前几个字节,该指令将调用重定向到定制提供的API函数。这种方法非常复杂,涉及到针对每个单独调用的一系列恢复和挂钩操作。需要指出的重要一点是,如果函数处于unhook模式,并且在该阶段进行了另一个调用,那么系统将无法捕获第二个调用。 主要的问题是它与多线程环境的规则相矛盾。 然而,有一种聪明的解决方案可以解决一些问题,并提供了一种复杂的方法来实现API拦截器的大多数目标。如果您感兴趣,可以看看[12]Detours实现。 调试器监视 挂钩API函数的另一种方法是在目标函数中放置一个调试断点。然而,这种方法有几个缺点。这种方法的主要问题是调试异常会挂起所有应用程序线程。它还需要一个调试器进程来处理此异常。另一个问题是,当调试器终止时,Windows会自动关闭调试器。通过更改导入地址表进行监视 这种技术最初由Matt Pietrek发表,由Jeffrey Ritcher([2]“通过操作模块的导入部分实现API挂钩”)和John Robbins([4]“挂钩导入函数”)详细阐述。它非常健壮、简单并且很容易实现。它还满足以Windows NT/2K和9x操作系统为目标的挂钩框架的大多数要求。这种技术的概念依赖于可移植可执行(PE) Windows文件格式的优雅结构。要理解这种方法是如何工作的,您应该熟悉PE文件格式背后的一些基础知识,PE文件格式是Common Object file format (COFF)的扩展。Matt Pietrek在他的精彩文章[6]“窥视PE内部”和[13a/b]“深入研究Win32 PE文件格式”中详细介绍了PE格式。我将向您简要介绍PE规范,仅能让您了解通过操作导入地址表进行挂钩的概念。 通常,PE二进制文件是有组织的,因此它的所有代码和数据部分的布局符合可执行文件的虚拟内存表示。PE文件格式由几个逻辑部分组成。它们中的每一个都维护特定类型的数据,并满足操作系统加载器的特定需求。 我希望将您的注意力集中在.idata部分,其中包含关于导入地址表的信息。这部分的PE结构是特别非常重要的建立一个间谍程序基于改变IAT。 符合PE格式的每个可执行文件的布局大致如下图所示。 图3 程序加载器负责将应用程序及其所有链接的dll加载到内存中。由于无法预先知道每个DLL加载到的地址,加载器无法确定每个导入函数的实际地址。加载器必须执行一些额外的工作,以确保程序将成功调用每个导入的函数。但通过h内存中的每个可执行映像和一个接一个地修复所有导入函数的地址会占用不合理的处理时间,并导致巨大的性能下降。那么,加载器如何解决这个问题呢?关键的一点是,对导入函数的每次调用都必须分配到相同的地址,其中函数代码驻留在内存中。每个对导入函数的调用实际上都是一个间接调用,由一个间接的JMP指令通过IAT路由。这种设计的好处是加载器不必搜索文件的整个映像。解决方案看起来非常简单—它只修复IAT内所有导入的地址。下面是一个简单的Win32应用程序的快照PE文件结构示例,它是在[8]PEView实用程序的帮助下获得的。正如你所看到的,TestApp导入表包含了两个GDI32.DLL函数导入的TextOutA()和GetStockObject()。 图4 实际上,导入函数的挂钩过程并不像乍一看那么复杂。简而言之,一个使用IAT补丁的拦截系统必须发现保存导入函数地址的位置,并通过重写将其替换为用户提供的函数的地址。一个重要的要求是,新提供的函数必须具有与原始函数完全相同的签名。以下是替换循环的逻辑步骤: 从进程DLL模块加载的每个文件的IAT中找到导入部分,以及进程本身找到导出该函数的DLL的IMAGE_IMPORT_DESCRIPTOR块。实际上,我们通常根据dll的名称搜索这个条目,找到IMAGE_THUNK_DATA,其中包含导入函数的原始地址,用用户提供的地址替换函数地址 通过更改IAT中导入的函数的地址,我们可以确保所有对钩子函数的调用都将重新路由到函数拦截器。 替换IAT内部的指针是.idata部分不一定必须是可写的部分。这要求我们必须确保修改.idata节。这个任务可以通过使用VirtualProtect() API来完成。 另一个值得注意的问题与Windows 9x系统上的GetProcAddress() API行为有关。当应用程序在调试器外部调用此API时,它返回一个指向该函数的指针。但是,如果从调试器内部调用此函数,它实际上返回的地址与在调试器外部调用时返回的地址不同。这是因为在调试器内部,每次调用GetProcAddress()都会将一个包装器返回到真正的指针。由GetProcAddress()返回的值点推送指令,后面跟着实际地址。这意味着在Windows 9x上,当我们循环遍历thunks时,我们必须检查所检查函数的地址是否为推指令(x86平台上是0x68),并相应地获得address函数的正确值。 Windows 9x没有实现即写即拷,因此操作系统试图避免调试器进入超过2 gb边界的函数。这就是GetProcAddress()返回一个调试thunk而不是实际地址的原因。John Robbins在[4]的“挂钩导入函数”中讨论了这个问题。 找出何时注入钩子DLL 该部分揭示了当所选的注入机制不是操作系统功能的一部分时开发人员所面临的一些挑战。例如,当你使用内置的Windows钩子来植入一个DLL时,执行注入就不是你关心的问题了。操作系统负责强制满足这个特定钩子要求的每个正在运行的进程加载DLL[18]。实际上,Windows会跟踪所有新启动的进程,并强制它们加载钩子DLL。通过注册表管理注入非常类似于Windows挂钩。所有这些“内置”方法的最大优点是它们是作为操作系统的一部分出现的。 与上面讨论的注入技术不同,通过CreateRemoteThread()进行注入需要维护当前运行的所有进程。如果注入没有及时进行,这可能导致钩子系统错过它声称被拦截的一些调用。至关重要的是,钩子服务器应用程序要实现一种智能机制,以便在每次新进程启动或关闭时接收通知。在这种情况下,建议的方法之一是拦截CreateProcess() API家族函数并监视它们的所有调用。因此,当调用用户提供的函数时,它可以调用带有dwCreationFlags或带有CREATE_SUSPENDED标志的原始CreateProcess()。这意味着目标应用程序的主线程将处于挂起状态,而钩子服务器将处于h通过手工编码的机器指令注入DLL,并使用ResumeThread() API恢复应用程序。要了解更多细节,请参考[2]“使用CreateProcess()注入代码”。 第二种检测进程执行的方法是基于实现一个简单的设备驱动程序。它提供了最大的灵活性,值得更多的关注。Windows NT/2K提供了一个由NTOSKRNL导出的特殊函数PsSetCreateProcessNotifyRoutine()。此函数允许添加回调函数,该函数在创建或删除进程时调用。有关更多细节,请参见参考部分中的[11]和[15]。 枚举进程和模块 有时我们更喜欢使用CreateRemoteThread() API注入DLL,特别是当系统在NT/2K下运行时。在这种情况下,当钩子服务器启动时,它必须枚举所有活动进程并将DLL注入到它们的地址空间中。Windows 9x和Windows 2K提供了一个内置的工具帮助库实现(即由内核32.dll实现)。另一方面,Windows NT使用了用于相同目的的PSAPI库。我们需要一种方法来允许钩子服务器运行,然后动态地检测哪个进程“助手”是可用的。因此,系统可以确定支持哪个库,并相应地使用适当的api。 我将介绍一个面向对象的体系结构,它实现了一个简单的框架,用于检索NT/2K和9x[16]下的进程和模块。我的类的设计允许根据您的特定需要扩展框架。实现本身非常简单。 CTaskManager实现了系统的处理器。它负责创建一个特定库处理程序的实例(即CPsapiHandler或CToolhelpHandler),该处理程序能够使用正确的进程信息提供程序库(即PSAPI或ToolHelp32)。CTaskManager负责创建和存储一个容器对象,该对象保存一个包含所有当前活动进程的列表。在实例化CTaskManager对象之后,应用程序调用Populate()方法。它强制枚举所有进程和DLL库,并将它们存储到CTaskManager成员m_pProcesses保存的层次结构中。 下面的UML图显示了这个子系统的类关系: 图5 要强调的是,NT的内核32.dll没有实现任何ToolHelp32函数。因此,我们必须使用运行时动态链接显式地链接它们。如果我们使用静态链接,代码将无法在NT上加载,无论应用程序是否尝试执行这些函数。要了解更多细节,请参阅我的文章“枚举NT和Win9x/2K下的进程和模块的单一接口”。 钩具系统的要求 现在我已经简要介绍了挂钩过程的各种概念,是时候确定基本要求和探索设计一个特定的挂钩系统。这些是钩子工具系统解决的一些问题: 提供一个用户级挂钩系统,用于监视任何按名称导入的Win32 API函数,提供通过Windows挂钩以及CreateRemoteThread() API向所有正在运行的进程注入挂钩驱动程序的能力。框架应该提供的能力由INI文件设置使用一个拦截机制的基础上,改变导入地址表提供了一个面向对象的可重用和可扩展的分层架构提供了一个高效、可扩展的机制来连接API函数满足性能要求为数据传输提供可靠的通信机制提供司机和服务器之间实现自定义版本的TextOutA / W()和ExitProcess日志()API函数系统是为运行Windows 9x、Me、NT或Windows 2K操作系统的x86机器实现的 设计和实现 本文的这一部分讨论了框架的关键组件以及它们之间如何交互。该工具能够捕获通过名称函数导入的任何类型的WINAPI。 在我概述系统的设计之前,我想让您关注几种注射和挂钩的方法。 首先,有必要选择一种嵌入方法,以满足将DLL驱动程序注入到所有进程的要求。因此,我设计了一种带有两种注入技术的抽象方法,每一种都相应地应用于INI文件中的设置和操作系统的类型(即NT/2K或9x)。它们是全系统的Windows钩子和CreateRemoteThread()方法。示例框架提供了通过Windows钩子在NT/2K上注入DLL的能力,也提供了通过CreateRemoteThread()方法植入的能力。这可以由保存系统所有设置的INI文件中的一个选项来确定。 另一个关键时刻是挂钩机构的选择。毫不奇怪,我决定应用修改IAT作为监视Win32 API的一种非常健壮的方法。 为了达到预期的目标,我设计了一个简单的框架,由以下组件和文件组成: exe -一个简单的Win32测试应用程序,它使用TextOut() API输出文本。这个应用程序的目的是展示它是如何连接起来的。dll间谍库实现为Win32 dll hooktools .ini配置文件ntprocdrv。一个小的Windows NT/2K内核模式驱动程序,用于监控进程的创建和终止。这个组件是可选的,并且只针对基于NT的系统下的进程执行检测问题。 HookSrv是一个简单的控制程序。它的主要作用是加载hooktools . dll,然后激活间谍引擎。加载DLL后,钩子服务器调用InstallHook()函数,并将句柄传递给一个隐藏窗口,DLL应该将所有消息发布到该窗口。 dll是钩子驱动程序,是本文提出的监视系统的核心。它实现了实际的拦截器并提供了三个用户提供的函数TextOutA/W()和ExitProcess()函数。 尽管这篇文章强调的是Windows的内部原理,而且没有必要让它成为面向对象的,但我还是决定将相关的活动封装在可重用的c++类中。这种方法提供了更大的灵活性,并允许对系统进行扩展。它还使开发人员能够在项目之外使用单独的类。 下面的UML类图说明了HookTool中使用的一组类之间的关系。DLL的实现。 图6 在本文的这一节中,我希望您注意HookTool.DLL的类设计。为类分配职责是开发过程的一个重要部分。给出的每个类都封装了特定的功能,并表示特定的逻辑实体。 CModuleScope是该系统的主要门户。它使用“单例”模式实现,并以线程安全的方式工作。它的构造函数接受3个指针,指向在共享段中声明的数据,所有进程都将使用这个指针。这样,就可以很容易地在类中维护这些系统范围变量的值,从而保持规则的封装性。 当应用程序加载HookTool库时,DLL在收到DLL_PROCESS_ATTACH通知时创建CModuleScope的一个实例。这个步骤只初始化CModuleScope的唯一实例。CModuleScope对象构造的一个重要部分是创建一个合适的注入器对象。在解析HookTool.ini文件并确定[Scope]部分下的usewindowshake参数的值之后,将决定使用哪个注入器。如果系统在Windows 9x下运行,系统将不会检查该参数的值,因为Windows 9x不支持远程线程注入。 在主处理器对象实例化之后,将调用ManageModuleEnlistment()方法。以下是其实现的简化版本: 隐藏,复制Code

// Called on DLL_PROCESS_ATTACH DLL notification
BOOL CModuleScope::ManageModuleEnlistment()
{
	BOOL bResult = FALSE;
	// Check if it is the hook server 
	if (FALSE == *m_pbHookInstalled)
	{
		// Set the flag, thus we will know that the server has been installed
		*m_pbHookInstalled = TRUE;
		// and return success error code
		bResult = TRUE;
	}
	// and any other process should be examined whether it should be
	// hooked up by the DLL
	else
	{
		bResult = m_pInjector->IsProcessForHooking(m_szProcessName);
		if (bResult)
			InitializeHookManagement();
	}
	return bResult;
}

方法ManageModuleEnlistment()的实现很简单,它检查钩子服务器是否进行了调用,检查m_pbHookInstalled指向的值。如果钩子服务器启动了调用,它就直接将sg_bHookInstalled标志设置为TRUE。它告诉钩子服务器已经启动。 钩子服务器采取的下一个动作是通过调用InstallHook() DLL导出函数来激活引擎。实际上,它的调用被委托给CModuleScope的方法—InstallHookMethod()。这个函数的主要目的是强制钩子进程加载或卸载钩子工具. dll。 隐藏,复制Code

 // Activate/Deactivate hooking
engine BOOL	CModuleScope::InstallHookMethod(BOOL bActivate, HWND hWndServer)
{
	BOOL bResult;
	if (bActivate)
	{
		*m_phwndServer = hWndServer;
		bResult = m_pInjector->InjectModuleIntoAllProcesses();
	}
	else
	{
		m_pInjector->EjectModuleFromAllProcesses();
		*m_phwndServer = NULL;
		bResult = TRUE;
	}
	return bResult;
}

DLL提供了两种机制来将自身注入到外部进程的地址空间中——一种是使用Windows挂钩,另一种是通过CreateRemoteThread() API注入DLL。系统的体系结构定义了一个抽象类CInjector,它公开了用于注入和抛出DLL的纯虚函数。类CWinHookInjector和CRemThreadInjector继承了同一个基类CInjector。然而,它们提供了在CInjector接口中定义的纯虚拟方法InjectModuleIntoAllProcesses()和EjectModuleFromAllProcesses()的不同实现。 CWinHookInjector类实现Windows钩子注入机制。它通过以下调用安装筛选器函数 隐藏,复制Code

// Inject the DLL into all running processes
BOOL CWinHookInjector::InjectModuleIntoAllProcesses()
{
	*sm_pHook = ::SetWindowsHookEx(
		WH_GETMESSAGE,
		(HOOKPROC)(GetMsgProc),
		ModuleFromAddress(GetMsgProc), 
		0
		);
	return (NULL != *sm_pHook);
}

正如您所看到的,它向系统请求注册WH_GETMESSAGE钩子。服务器只执行一次此方法。SetWindowsHookEx()的最后一个参数是0,因为GetMsgProc()被设计为作为一个系统范围的钩子操作。该回调函数将被系统调用,每次当一个窗口即将处理特定的消息。有趣的是,我们必须提供GetMsgProc()回调的一个近乎虚构的实现,因为我们不打算监视消息处理。我们提供这个实现只是为了获得操作系统提供的免费注入机制。 在调用SetWindowsHookEx()之后,OS检查导出GetMsgProc()的DLL(即HookTool.DLL)是否已经映射到所有GUI进程中。如果DLL还没有加载,Windows会强制那些GUI进程映射它。一个有趣的事实是,系统范围的钩子DLL不应该在其DllMain()中返回FALSE。这是因为操作系统验证DllMain()的返回值,并一直尝试加载这个DLL,直到它的DllMain()最终返回TRUE。 CRemThreadInjector类演示了一种完全不同的方法。这里的实现基于使用远程线程注入DLL。CRemThreadInjector通过提供接收进程创建和终止通知的方法,扩展了Windows进程的维护。它持有一个CNtInjectorThread类的实例,该实例观察进程的执行。CNtInjectorThread对象负责从内核模式驱动程序获取通知。因此,每当创建一个进程时,都会发出对CNtInjectorThread::OnCreateProcess()的调用,相应地,当进程退出时,就会自动调用CNtInjectorThread::OnTerminateProcess()。与Windows挂钩不同,这种依赖于远程线程的方法在每次创建新进程时都需要手动注入。监视流程活动将为我们提供一种简单的技术,用于在新流程启动时发出警报。 CNtDriverController类为管理服务和驱动程序的API函数实现了一个包装器。它被设计用来处理内核模式驱动程序NTProcDrv.sys的加载和卸载。它的实现将在后面讨论。 成功地将HookTool.DLL注入到特定进程后,在DllMain()中发出对ManageModuleEnlistment()方法的调用。回想一下我前面描述的方法的实现。它通过CModuleScope的成员m_pbHookInstalled检查共享变量sg_bHookInstalled。由于服务器的初始化已经将sg_bHookInstalled的值设置为TRUE,系统将检查该应用程序是否必须连接,如果必须连接,它将实际激活该特定进程的间谍引擎。 打开黑客引擎,发生在CModuleScope::InitializeHookManagement()的实现中。该方法的思想是为一些重要函数安装钩子,如LoadLibrary() API家族和GetProcAddress()。通过这种方法,我们可以在初始化过程之后监视dll的加载。每次要映射一个新的DLL时,都需要对其导入表进行修复,这样我们就可以确保系统不会错过对捕获函数的任何调用。 在InitializeHookManagement()方法的末尾,我们为实际想要监视的函数提供了初始化。 由于示例代码演示了多个用户提供的函数的捕获,因此我们必须为每个钩接函数提供单个实现。这意味着,使用这种方法,您不能仅仅更改不同导入函数的IAT内部地址,以指向单个“通用”拦截函数。监视功能需要知道调用的是哪个功能。同样重要的是,拦截例程的签名必须与原始的WINAPI函数原型完全相同,否则将损坏堆栈。例如,CModuleScope实现了三个静态方法MyTextOutA()、MyTextOutW()和MyExitProcess()。一旦HookTool.DLL被加载到进程的地址空间并且间谍引擎被激活,每次当对原始TextOutA()的调用被发出时,CModuleScope:: MyTextOutA()被调用。 间谍引擎的设计本身是相当有效的,并提供了很大的灵活性。但是,它主要适用于预先知道拦截函数集且数量有限的情况。 如果您想向系统添加新的钩子,只需声明并实现拦截函数,就像我使用MyTextOutA/W()和MyExitProcess()所做的那样。然后必须按照InitializeHookManagement()实现所示的方式注册它。 拦截和跟踪进程执行是实现需要外部进程操作的系统的一种非常有用的机制。在新进程启动时通知相关方是开发进程监控系统和系统范围钩子的一个经典问题。Win32 API提供了一组很棒的库(PSAPI和工具帮助[16]),允许枚举系统中当前运行的进程。尽管这些api非常强大,但它们不允许你获得notificat一个新过程开始或结束时的离子。幸运的是,NT/2K提供了一组api,在Windows DDK文档中记录为由NTOSKRNL导出的“进程结构例程”。其中一个api PsSetCreateProcessNotifyRoutine()提供了注册系统范围内的回调函数的能力,该函数在每次新进程启动、退出或终止时被OS调用。通过实现一个NT内核模式驱动程序和一个用户模式的Win32控制应用程序,上述API可以被用作跟踪进程的简单方法。驱动程序的作用是检测进程的执行,并将这些事件通知控制程序。Windows进程的观察者NTProcDrv的实现提供了在基于NT的系统下进行进程监视所需的最小功能集。有关更多细节,请参阅文章[11]和[15]。驱动程序的代码可以在NTProcDrv.c文件中找到。由于用户模式实现动态地安装和卸载驱动程序,因此当前登录的用户必须具有管理员权限。否则你将无法安装驱动程序,它将干扰监控过程。一种方法是手动安装驱动作为一个管理员或运行HookSrv.exe使用提供的Windows 2K“作为不同的用户运行”选项。 最后但并非最不重要的是,提供的工具可以通过简单地更改INI文件(即hooktools . INI)的设置来管理。这个文件决定是否使用Windows挂钩(用于9x和NT/2K)或CreateRemoteThread()(仅用于NT/2K之下)来注入。它还提供了一种方法来指定必须连接的进程和不应该拦截的进程。如果您想要监视进程,在[Trace]部分下有一个选项(启用),它允许记录系统活动。此选项允许您使用CLogFile类公开的方法报告丰富的错误信息。实际上,ClogFile提供了线程安全的实现,您不必关心与访问共享系统资源(即日志文件)相关的同步问题。有关更多细节,请参阅CLogFile和HookTool.ini文件的内容。 示例代码 该项目使用VC6++ SP4编译,需要平台SDK。在生产Windows NT环境中,你需要提供PSAPI.DLL来使用提供的CTaskManager实现。 在运行示例代码之前,请确保HookTool.ini文件中的所有设置都已根据您的特定需要进行了设置。 对于那些喜欢底层内容并对进一步开发内核模式驱动程序NTProcDrv代码感兴趣的人来说,他们必须安装Windows DDK。 超出范围 为了简单起见,我有意将这些主题排除在本文的范围之外: 监视本机API调用一个驱动程序来监视Windows 9x系统上的进程执行。UNICODE支持,尽管您仍然可以连接UNICODE导入的api 结论 到目前为止,本文并没有为无限的API挂钩主题提供完整的指南,而且毫无疑问,它遗漏了一些细节。然而,我试图在这几页中放入足够重要的信息,可能对那些对用户模式Win32 API监视感兴趣的人有帮助。 参考文献 [1]“Windows 95系统编程秘密”,马特·派崔克 [2]“微软视窗程式设计应用”,Jeffrey Richter著 “视窗NT系统呼叫连结”,马克。罗斯诺维奇和布莱斯。科格韦尔,多布博士期刊1997年1月 [4]《调试应用程序》,John Robbins [5]“未登记的Windows 2000秘密”,Sven Schreiber [6]“窥视PE内部:Win32可执行文件格式之旅”,作者Matt Pietrek, 1994年3月 MSDN知识库Q197571 [8] PEview版本0.67,Wayne J. Radburn “使用INJLIB将你的32位DLL加载到另一个进程的地址空间 [10]“编程Windows安全”,Keith Brown [11],“检测Windows NT/2K进程执行”伊沃伊万诺夫,2002 [12]“弯路”盖伦亨特和道格布鲁巴赫 [13a]“深入研究Win32 PE文件格式”,第1部分,Matt Pietrek, MSJ 2002年2月 “深入研究Win32 PE文件格式”,第2部分,Matt Pietrek, MSJ 2002年3月 [14]“微软视窗2000第三版”,大卫·所罗门和马克·罗斯诺维奇 [15]“Nerditorium”,James Finnegan, MSJ 1999年1月 “枚举NT和Win9x/2K下的进程和模块的单一接口。”伊沃·伊万诺夫,2001年 [17]“无文档的Windows NT”,Prasad Dabak, Sandeep Phadke和Milind Borate [18]平台SDK: Windows用户界面,钩子 历史 2002年4月21日-更新的源代码 2002年5月12日-更新的源代码 2002年9月4日-更新的源代码 2002年12月3日-更新的源代码和演示 本文转载于:http://www.diyabc.com/frontweb/news339.html

原文地址:https://www.cnblogs.com/Dincat/p/13437784.html