资源描述
1毕毕 业业 论论 文文论文题目论文题目: 基于基于 Windows 进程状态提取与恢复的研究进程状态提取与恢复的研究姓 名 学 号 学 院 计算机科学与技术学院 专 业 计算机科学与技术 年 级 2007 级 指导教师 2011 年 6 月 5 日 2目录目录摘 要.5ABSTRACT.6第 1 章 绪论.71.1 课题研究背景 .71.2 进程检查点系统的研究现状 .81.3 本文主要工作 .8第 2 章 WINDOWS 系统内存管理机制 .102.1 CPU 工作方式.102.2 进程地址空间 .102.2.1 Windows 地址空间基础 .102.2.2 用户地址空间分布 .112.2.3 系统地址空间分布 .132.3 内存映射 .132.3.1 内存映射基础 .132.3.2 虚拟内存的使用 .152.3.3 内存映射的可执行文件和 DLL 文件 .172.3.4 在 DLL 的多个实例之间共享静态数据 .18第 3 章 DLL 注入 .193.1 DLL 基础.193.2 DLL 的运行机制 .203.3 DLL 的创建.233.4 注入技术的分析和比较 .243.4.1. 利用注册表注入 .243.4.2. 建立系统范围的 Windows 钩子 .253.4.3 使用 CreateRemoteThread 函数.263.4.4 通过 BHO 来注入 DLL.283.5 编码实现.283.5.1 钩子的挂接 .283.5.2 挂起和恢复进程 .313.5.3 获取节数据 .313.5.4 保存和恢复进程状态 .343.6 PE 文件.36第 4 章 程序功能演示和结语.38 34.1 程序功能演示 .384.2 结语 .39致谢.41参考文献.42附录 1.英文原文 .43附录 2.中文翻译 .51 4基于基于 Windows 用户级进程状态提取与恢复用户级进程状态提取与恢复摘摘 要要 基于用户级 Windows 进程状态的提取与恢复是在进程正常运行的适当时刻注入 DLL 文件,在用户级实现进程的停止、启动和将进程状态保存到稳定存储器中。从磁盘文件中读出保存的进程状态,实现进程重启后的状态恢复。 Windows 内核分为 16 位的 Win95 和 98、32 位的 WinNT 以及用于嵌入式开发的 WinCE。本文针对 32 位的 WinNT 内核实现,支持 Windows 2003、XP 以上版本。本文重点讨论了基于用户级 Windows 进程状态的提取与恢复实现的基本原理和步骤,并对其进行了编码实现。文章首先对基于用户级 Windows 进程状态的提取与恢复实现的理论基础和实现机制进行了阐述,然后分析和编码实现了动链接库注入,最后对完成实例程序的功能进行了介绍。本文对不同的动态链接库注入技术进行了分析和比较,并采用钩子技术实现,给出了相应的重点代码,详细阐述了实现机制。关键字关键字:动态链接库、PE 文件结构、容错技术 5ABSTRACTThe extraction and recovery user-level process state in windows is to inject the DLL file at the appropriate time; while the application is running and you can stop or start it on the user-level .Process status can also be saved to stable storage. When restarted, the saved information can be read to restart the execution of the application. Windows kernel can be divided into 16-bit Win95 and Win98、32-bit WinNT and embedded used Wince. In the following, if not named in detail Windows refer to 32-bit WinNT. This article focuses on the basic principles and steps of extraction and recovery user-level process state in windows and implements its encoded. At first we describe the theoretical basis and implementation mechanisms of extraction and recovery user-level process state in Windows, then analyze and implement the injections of dynamic link library. Finally, we give a brief introduction to the functions of the application that we completed.In this paper, we take an analysis and comparison among different techniques of injection of the dynamic link library and implement it with hook technique. We present corresponding code implementation to elaborate the realization of mechanism described in detail.Keywords: dynamic link library, PE file structure, fault-tolerance 6第第 1 1 章章 绪论绪论1.11.1 课题研究背景课题研究背景随着 Windows 操作系统的不断普及,基于该操作系统下的应用也越来越广泛。Windows 软件的容错性越来越受到重视,由于 Windows 是不公开内核的操作系统,许多研究通过实现用户级进程检查点设置与回卷恢复1,在不修改系统内核的前提下提高 Windows 软件的容错性。本文研究基于 WinNT 内核下的用户级进程状态提取与恢复是基于后向恢复的异构分布式系统容错技术的研究与实现的子课题,基于检查点技术实现分布式计算系统故障检测,能够保存和恢复程序的运行状态,因此在许多相邻领域都有重要的应用2-4。1. 进程迁移 目前大多数操作系统还不能提供进程迁移功能,利用检查点可以保存进程在某台机器的运行状态,然后在其他机器上恢复进程的运行以实现进程迁移。进程迁移可以使集群系统负载平衡,从而提高计算速度和集群的利用率。2. 容错 分布式系统的故障率随系统机器数的增加而增加,长时间运行的作业若在每次出现机器故障都从头开始执行,该作业很难被执行完毕。因此利用检查点实现多机器系统容错称为人们日益关心的热点。3. 卷回调试 在程序调试过程中,利用检查点保存程序在某个时刻的运行状态。当错误发生时,把程序卷回到保存前的某一个时刻的状态重新向下运行,以再次产生相同的错误来查找错误发生条件的调试方法称为卷回调试。分布式程序包含较多的不确定成分,当进程发生运行错误重新运行程序查找错误原因时,同样的错误可能很难再次出现。采用卷回调试在很大程度上提高错误再次发生的概率。 71.21.2 进程检查点系统的研究现状进程检查点系统的研究现状 进程检查点可以分别在操作系统级、用户级或者应用程序级实现,但各自有其优缺点。操作系统级实现的进程检查点5对用户程序透明,容易得到进程的内核数据结构,但需要修改系统内核,因此对于基于封闭操作系统的进程检查点难以实现,并且可以配置性和移植性不高,检查点开销很大。 用户级的进程检查点6-11,将检查点功能编译为一个库连接到应用程序,可以实现对应用程序透明,易于配置且开销较小,但其实现机制与操作系统平台密切相关,无法实现不同系统平台的迁移。应用程序级的进程检查点12优点是能够实现平台无关,可在不同的操作系统间移植;缺点是只限于几种有限的编程语言,目前仅有基于 JAVA 虚拟机的应用程序级检查点。由于用户级的进程检查点具有对应用程序透明,易于配置、开销较小且实用等特点,因此大部分检查点系统都选择在用户级实现。而 Unix 和 WinNT 操作系统都是非常普及的操作系统,因此,大部分进程检查点系统都是基于 Unix 或者 WinNT 实现的。1.3 本文主要工作本文主要工作随着PC机使用范围的不断扩大,Windows系统已经成为一个日益流行的桌面平台,尤其在中低端市场,运行Unix系统的高级工作站逐渐被价格低廉、使用方便且符合行业开放性标准的WinNT的PC机所取代。与此同时,基于WinNT操作系统的集群系统也开始出现。典型的由美国Illinois大学研制的HPVM及由Rice大学开发的Brazosl20l。由于WinNT操作系统在集群系统中的运用,随故障负载和隶属关系的影响,具有变化特性。特别是随着系统规模的不断扩大,其在计算过程中发生故障的几率会以指数级增长,对于大规模科学工程计算任务来说,任务的计算时间比较长,一旦发生异常,可视为故障,会导致并行计算的彻底失败,此前的大量计算不再可用,尤其是与Unix系统相比较,WinNT系统是一个相对不太健壮的操作系统。因此研究WinNT系统的容错性越来越受到重视。 8为提高WinNT系统的鲁棒性,一般有两种方法提高系统可靠性。一是在硬件配置上或操作系统内部采取容错措施,如硬件冗余、文件备份等。对于大型系统而言这种方法是高效且实用的;但对于中低端市场而言,这种措施的代价过于昂贵。第二种方法是在应用层完全用软件实现。即在不修改操作系统的前提下,通过提供库文件或高可用性运行环境来实现,其中的重要措施就是进程检查点设置与回卷恢复(checkpoint and rollback recoveryCRR)。本文主要工作是基于Windows用户级进程状态提取与恢复的实现原理和编码。代理进程(Test_hook_api)将动态库(hooklib.dll)注入目标进程(例 XX.exe),使得动态库hooklib.dll与目标进程位于同一进程虚拟地址空间。Hook lib库中的服务线程定时(使用可等待定时器内核对象,避免占用CPU时间)停止进程,读取节数据,保存节数据至检查点文件。故障时,由故障检测模块通知代理进程,代理进程经共享内存通知动态库中的服务线程,服务线程停止进程,重启进程且处于挂起状态,把检查点文件中的节数据写入进程的各个节,然后启动进程。该系统由两个模块组成:(1)注入动态链接库模块Test_hook_api。使用该模块对应用程序进行预处理,在不需要修改应用程序源代码,不对应用程序进行重新编译或者链接的前提下。透明地将动态链接库hooklib.dll插入到可执行文件中。(2)动态链接库hooklib.dll。该库是系统的主要模块,实现API函数截获。进程状态的提取、保存和恢复。通过对WinNT进程机制的研究,本文提出了将进程状态的保存和恢复到用户地址空间的实现思想。本文分为六章,第一章介绍了进程检查点的现状、基于用户级进程提取与恢复相关的领域。第二章介绍了Windows虚拟内存分配机制,CPU的工作方式和Windows内存模型。接下来三章分别详细介绍了基于用户级的Windows进程状态提取与恢复实现的关键技术DLL的注入、钩子的实现、PE文件等技术。着重介绍了基于Windows用户级进程提取与恢复的原理和编码实现。最后是全文的总结、致谢、参考文献和附录。 9第第 2 2 章章 WindowsWindows 系统内存管理机制系统内存管理机制2.1 CPUCPU 工作方式工作方式首先,我们应该了解 Windows 系统内存管理机制,这是我们注入被拦截应用程序地址空间的基础。Windows 系统能够寻址空间随工作方式不同而不同,下面简要介绍其工作方式。CPU 有三种工作方式13:虚拟 8086 模式、实模式和保护模式。只有在刚刚启动的时候是实模式,等操作系统运行起来以后就运行在保护模式。 实模式也称为实地址模式,只能访问地址小于 1MB 内存称为常规内存,我们把地址在 1M 以上的内存称为扩展内存。由于 CPU 寄存器的地址仅有 16 位,这意味着应用程序可访问的连续线性地址空间仅有 64KB,通过 16 位段寄存器的帮助,这个 64KB 大小的内存窗口就可以在整个物理空间中上下移动,64KB逻辑空间中的线性地址作为偏移量和基地址(由 16 位段寄存器给出)相加,从而构成有效的 20 位地址。保护模式,而叫做受保护的虚拟地址模式是 80286 CPU 的工作方式。该模式全部 32 条地址线有效,可寻址 4G 物理地址空间,扩充的存储器分段管理机制和分页管理机制,不仅为存储器共享和保护提供了硬件支持,而且为实现虚拟存储器提供了硬件支持;支持多任务,快速任务切换和保护任务环境,4 个特权级和完善的特权检查机制。提供了选择器,该选择器由一个描述符表的索引构成。该描述符表的每一项都定义了一个 24 位的物理地址,允许访问 16MB RAM,但是线性地址空间仍然被限制在 64KB。虚拟 8086 模式时运行在保护模式中的实模式,为了在 32 位保护模式下执行纯 16 位程序。它不是一个真正的 CPU 模式,还属于保护模式。本文研究保护模式下的 Windows 进程信息提取与恢复。2.22.2 进程地址空间进程地址空间2.22.2 .1.1 WindowsWindows 地址空间基础地址空间基础CPU 寻址引入了 3 个术语,下面将进行简要介绍。 10逻辑地址:这是内存地址的精确描述,通常表示为 16 进制:xxxx: YYYY YYYY,这里 xxxx 为选择器,而 YYYYYYYY 是针对选择器所选段地址的线性偏移量。这里 xxxx 可以用段寄存器名字代替,如 CS(代码段),DS(数据段),ES(扩展段),FS(附加数据段#1),GS(附加数据段#2)和 SS(堆栈段)。线性地址:大多数应用程序和内核驱动程序都忽略虚拟地址。他们只对虚拟地址的偏移量部分感兴趣。而这一部分通常称为线性地址。此种类型的地址假定了一种默认的分段模型,这种模型由 CPU 的当前段寄存器确定。物理地址:仅当 CPU 工作于分页模式时,这种地址才会变得非常有趣。本质上,一个物理地址是 CPU 插脚上可测量的电压。操作系统通过设立页表将线性地址映射为物理地址。本文研究内容采用保护模式下的虚拟内存管理机制。由 32 位的 Intel CPU 提供的 4GB 虚拟内存空间被分割为相等的两部分。低于 0 x80000000 的内存地址由用户模式下的模块使用,这包括 Win32 子系统,剩余的 2GB 保留给了系统内核。表 2-1 显示了 32 位 Windows 系统中采用的虚拟地址空间的划分方法。表 2-2 2GB Windows 2000/XP 用户进程地址空间分布分区名称范围用途NULL 指针分区0 x0000 0000-0 x0000 FFFF保护内存非法访问独享用户区0 x0001 0000-0 x7FFE FFFF进程只能读取或访问这个范围的虚拟地址;超越这个范围的行为都会产生违规退出。共享内核区0 x8000 0000-0 xFFFF FFFF这个空间提供操作系统内核代码、设备驱动程序、设备I/O 高速缓存、页表等2.2.22.2.2 用户地址空间分布用户地址空间分布表 2-2 详细描述了 2GB Windows 2000/XP 用户进程地址空间的布局。表 2-3 显示的系统变量定义了用户地址空间的范围。表 2-4 列出了性能计数器提供的有关全部系统虚拟内存使用的信息。表 2-5 列出了进程性能计数器得到的单个进程地址空间的使用情况。表 2-2 2GB Windows 2000/XP 用户进程地址空间分布 11范围大小功能0 x0000 0000-0 x0000 FFFF64KB 拒绝访问区域,避免程序员不正确的指针引用 用于帮助查找错误0 x0001 0000-0 x7FFE FFFF2GB 减去至少 192KB进程私有地址空间0 x7FFD E000-0 x7FFD EFFF4KB第一个线程的线程环境块(TEB)0 x7FFD F000-0 x7FFD FFFF4KB进程环境块(PEB)0 x7FFE 0000-0 x7FFE 0FFF4KB共享的用户数据页面 该只读数据页面映射到系统空间中包括系统时间、时钟计数、版本号信息,该页面存在使这些信息可以直接从用户态读出0 x7FFE 1000-0 x7FFE FFFF60KB拒绝访问区域0 x7FFF 0000-0 x7FFF FFFF60KB拒绝访问区域 阻止线程跨过系统 用户边界传送那个缓冲区 MmUserProbeAddress中包含此区的起始地址表 2-3 windows 用户地址空间系统变量系统变量描述用户空间MmHighestUserAddress最高用户地址0 x7FFE FFFFMmUserProbeAddress最高用户地址+10 x7FFF 0000表 2-4 Windows 虚拟内存使用性能计数器性能计数器系统变量描述Memory:Committed BytesMmTotalCommittedPages提交的私有地址空间数量Memory:Commited LimitMmTotalCommitLimit在不增加页文件大小的情况下,可以提交的内存字节数Memory:%Committed Bytes in UseMmTotalCommittedPages/ MmTotalCommitLimit提交字节和提交限制之比表 2-4 Windows 虚拟内存使用性能计数器性能计数器描述Process:Virtual Bytes进程地址空间全部大小Process:Private Bytes私有地址空间占用地址大小Process:Page File Bytes私有地址空间占用地址大小Process:Peak Page File Bytes私有地址空间占用地址大小的峰值 122.2.32.2.3 系统地址空间分布系统地址空间分布本段简要描述了系统地址空间的布局和内容。表 2-6 显示了 x86 系统上的2GB 系统空间的全部结构。表 2-6 x86 系统地址空间布局系统代码和系统中一些初始的未分页缓冲池系统映射图或者会话空间附加系统 PTS 如高速缓存可以扩展到此进程的页表和页目录超空间和进城工作集表没有使用不可访问系统工作集列表系统高速缓存分页缓冲池系统 PTE未分页缓冲池扩充故障转储信息HAL 使用通过以上分析,可以知道,如果系统有 n 个进程,它所需的虚拟空间是:2G*n+2G (内核只需 2G 的共享空间)。 独立用户分区是进程的私有地址空间,进程不能够以任何方式读、写其他进程此部分空间中的数据。对每个进程来说,它自己的、未被共享的保存数据都被保存在这块空间里。共享内核分区放置操作系统的代码,包括内核代码、设备驱动代码、设备 I/0 缓冲区等。系统空间部分在所有的进程是共享的。2.32.3 内存映射内存映射2.3.12.3.1 内存映射基础内存映射基础内存块指的是地址空间中的一片连续地址,大小必须是 64k 的整数倍,大部分是 64k。内存块的状态有:空闲、私有、映射、映像。在创建进程并赋予进程地址空间时,这些可用地址空间的大部分是空闲的,即未分配。要使用这些地址空间的各个部分,必须调用 VirtualAlloc14函数分配。对一个地址空间进行分配的操作称为保留操作。删除空间的过程为释放,可以 VirtualFree 函数。在程序里预订了地址空间以后,仍然不可以存取数据,因为没有真实的 RAM 和它关联。这时候的区域状态是私有;默认情况下,区域 13状态是空闲;当 exe 或 DLL 文件被映射进了进程空间后,区域状态变成映像;当一般数据文件被映射进了进程空间后,区域状态变成映射。当系统访问一个字节的内存时,有两种情况。如图 2-1 所示。图 2-1 虚拟地址与物理存储器地址转换流程图在第一种情况下,线程试图访问的数据是在 RAM 中。在这种情况下, CPU将数据的虚拟内存地址映射到内存的物理地址中,然后执行需要的访问。在第二种情况中,线程试图访问的数据不在 RAM 中,而是存放在页文件中的某个地方。这时,试图访问就称为页面失效, CPU 将把试图进行的访问通知操作系统。这时操作系统就寻找 RAM 中的一个内存空页。如果找不到空页,系统必须释放一个空页。如果一个页面尚未被修改,系统就可以释放该页面。但是,如果系统需要释放一个已经修改的页面,那么它必须首先将该页面从 RAM 拷贝到页交换文件中,然后系统进入该页文件,找出需要访问的数据块,并将数据加载到空闲的内存页面。然后,操作系统更新它的用于指明数据的虚拟内存地址现在已经映射到 RAM 中的相应的物理存储器地址中的表。这时 CPU 重新运行生成初始页面失效的指令,但是这次 CPU 能够将虚拟内存地址映射到一个物理RAM 地址,并访问该数据块。已经分配的物理存储器各个页面可以被赋予不同保护属性。表 2-7 显示了这些属性。 14表 2-7 页面的保护属性保护属性描述PAGE_NOACCESS如果试图在该页面上读取、写入或执行代码,就会引发访问违规PAGE_READONLY如果试图在该页面上写入或执行代码,就会引发访问违规PAGE_READWRITE如果试图在该页面上执行代码,就会引发访问违规PAGE_EXECUTE如果试图在该页面上对内存进行读取或写入操作,就会引发访问违规PAGE_EXECUTE_READ如果试图在该页面上对内存进行写入操作,就会引发访问违规PAGE_EXECUTE_READWRITE对于该页面不管执行什么操作,都不会引发访问违规PAGE_WRITECOPY如果试图在该页面上写入内存,就会导致系统将它自己的私有页面(受页文件的支持)拷贝赋予该进程PAGE_EXECUTE_WRITECOPY该页面执行所有操作内存映射文件可以用于三个不同的目的: 1.系统使用内存映射文件,以便加载和执行.exe 和 DLL 文件。这可以大大节省页文件空间和应用程序启动运行所需的时间。 2. 可以使用内存映射文件来访问磁盘上的数据文件。这使你可以不必对文件执行 I/O 操作,并且可以不必对文件内容进行缓存。 3. 可以使用内存映射文件,使同一台计算机上运行的多个进程能够相互之间共享数据。Windows 确实提供了其他一些方法,以便在进程之间进行数据通信,但是这些方法都是使用内存映射文件来实现的,这使得内存映射文件成为单个计算机上的多个进程互相进行通信的最有效的方法。 2.3.22.3.2 虚拟内存的使用虚拟内存的使用使用管理虚拟内存的函数,可以直接保留一个地址空间区域,将物理存储器提交到该区域,并且可以设置自己的保护属性。1. 在地址空间保留一个区域通过使用 VirtualAlloc 函数,可以在进程的地址空间保留一个区域:PVOID VirtualAlloc(PVOID pvAddress,SIZE_T dwSize,DWORD dwSize,DWORD fdwAllocationType.DWORD fdwProtect) 第一个参数 pvAddress 包含一个内存地址,用于设定想让系统将地址空间保留在什么地方。NULL 表示地址由系统分配。如果VirtualAlloc 函数能够满足你的要求,那么它就返回一个指明保留区域的基地址值。 15 第二个参数 dwSize 用于设置想保留区域的大小,第三个参数表明分配的是保留区域还是物理存储器,最后一个参数设置文件属性(参考表2-7)。2. 提交物理存储器 若要提交物理存储器,必须再次调用 VirtualAlloc 函数。不过这次为 fdwAllocationType 参数传递的是 MEM_COMMIT 标志,而不MEM_RESERVE 标志。3. 同时进行保留区域和提交内存 只需要 fdwAllocationType 参数传递的是MEM_COMMIT|MEM_RESERVE 标志即可。可以采用以下四种方法确定何时提交物理存储器:总是提交、通过 VirtualQuery 函数、保留提交记录、使用结构化异常处理(SEH)。4. 物理存储器的回收和释放 若要回收物理存储器可以调用 VirtualFree 函数。参数和 Virtual Alloc 一样,由于系统知道释放的内存大小故应设为 0,第三个参数设为 MEM_REALSE 则释放内存,MEM_DECOMMIT 则回收内存。决定回收时间可以使用结构来记录、无用单元收集函数或者将每个结构设计为一个页面。当创建一个线程时,系统就会为线程的堆栈保留一个 1MB 堆栈空间区域,将一些物理存储器提交给这个已保留的区域。但是,当调用 CreateThread 或_begin ThreadEx 函数时,可以重载原先提交的内存数量。这两个函数都有一个参数,可以用来重载原先提交给堆栈的地址空间的内存数量。如果设定这个参数为 0,那么系统将使用/STACK 开关指明的已提交的堆栈大小值。表 2-2 显示了在页面大小为 4KB 的计算机上的一个堆栈区域。表 2-3 堆栈区域内存地址页面状态0 x080F F000堆栈顶部 已提交页面0 x080F DOOO带有保护属性标志的已提交页面0 x0800 2000 0 x0800 1000 0 x0800 3000保留页面0 x0800 0000堆栈底部:保留页面 162.3.32.3.3 内存映射的可执行文件和内存映射的可执行文件和 DLLDLL 文件文件当线程调用 CreateProcess 时,系统将执行下列操作步骤: 1) 系统找出在调用 CreateProcess 时设定的.exe 文件。如果找不到这个.exe 文件,进程将无法创建,CreateProcess 将返回 FALSE。 2) 系统创建一个新进程内核对象。 3) 系统为这个新进程创建一个私有地址空间。 4) 系统保留一个足够大的地址空间区域,用于存放该.exe 文件。该区域需要的位置在.exe 文件本身中设定。按照默认设置,.exe 文件的基地址是0 x0040 0000。 5) 系统注意到支持已保留区域的物理存储器是在磁盘上的.exe 文件中,而不是在系统的页文件中。 当.exe 文件被映射到进程的地址空间中之后,系统将访问.exe 文件的一个部分,该部分列出了包含.exe 文件中的代码要调用的函数的 DLL 文件。然后,系统为每个 DLL 文件调用 LoadLibrary 函数,如果任何一个 DLL 需要更多的DLL,那么系统将调用 LoadLibrary 函数,以便加载这些 DLL。每当调用LoadLibrary 来加载一个 DLL 时,系统将执行下列操作步骤,它们均类似上面的第 4 和第 5 个步骤: 1) 系统保留一个足够大的地址空间区域,用于存放该 DLL 文件。该区域需要的位置在 DLL 文件本身中设定。按照默认设置, Microsoft 的 Visual C+ 建立的 DLL 文件基地址是 0 x1000 0000 但是,你可以在创建 DLL 文件时重载这个地址,方法是使用链接程序的/BASE 选项。Windows 提供的所有标准系统 DLL都拥有不同的基地址,这样,如果加载到单个地址空间,它们就不会重叠。 2) 如果系统无法在该 DLL 的首选基地址上保留一个区域,其原因可能是该区域已经被另一个 DLL 或.exe 占用,也可能是因为该区域不够大,此时系统将设法寻找另一个地址空间的区域来保留该 DLL。如果一个 DLL 无法加载到它的首选基地址,这将是非常不利的,原因有二。首先,如果系统没有再定位信息,它就无法加载该 DLL(可以在 DLL 创建时,使用链接程序的/ F I X E D 开关,从 DLL 中删除再定位信息,这能够使 DLL 变得比较小,但是这也意味着该 DLL 17必须加载到它的首选地址中,否则它就根本无法加载)。第二,系统必须在DLL 中执行某些再定位操作。在 Windows 98 中,系统可以在页面被转入 RAM 时执行再定位操作。在 Windows 2000 中,这些再定位操作需要由系统的页文件提供更多的存储器,它们也增加了加载 DLL 所需要的时间量。 3) 系统会注意到支持已保留区域的物理存储器位于磁盘上的 DLL 文件中,而不是在系统的页文件中。如果由 DLL 无法加载到它的首选基地址,Windows必须执行再定位操作,那么系统也将注意到 DLL 的某些物理存储器已经被映射到页文件中。 如果由于某个原因系统无法映射.exe 和所有必要的 DLL 文件,那么系统就会向用户显示一个消息框,并且释放进程的地址空间和进程对象。CreateProcess 函数将向调用者返回 FALSE,调用者可以调用 GetLastError 函数,以便更好地了解为什么无法创建该进程。 当所有的.exe 和 DLL 文件都被映射到进程的地址空间之后,系统就可以开始执行.exe 文件的启动代码。当.exe 文件被映射后,系统将负责所有的分页、缓冲和高速缓存的处理。例如,如果.exe 文件中的代码使它跳到一个尚未加载到内存的指令地址,那么就会出现一个错误。系统能够发现这个错误,并且自动将这页代码从该文件的映像加载到一个 RAM 页面。然后,系统将这个 RAM 页面映射到进程的地址空间中的相应位置,并且让线程继续运行,就像这页代码已经加载了一样。当然,这一切是应用程序看不见的。当进程中的线程每次试图访问尚未加载到 RAM 的代码或数据时,该进程就会重复执行。2.3.42.3.4 在在 DLLDLL 的多个实例之间共享静态数据的多个实例之间共享静态数据全局数据和静态数据不能被同一个.exe 或 DLL 文件的多个映像共享,这是个安全的默认设置。但是,在某些情况下,让一个.exe 文件的多个映像共享一个变量的实例是非常有用和方便的。本节将介绍一种方法,它允许你共享.exe或 DLL 文件的所有实例的变量。不过在介绍这个方法之前,首先让我们介绍一些背景知识。 每个.exe 或 DLL 文件的映像都由许多节组成。按照规定,每个标准节的名字均以圆点开头。例如,当编译你的程序时,编译器会将所有代码放入一个名 18叫. text 的节中。该编译器还将所有未经初始化的数据放入一个. bss 节,而已经初始化的所有数据则放入.data 节中。 每一节都拥有与其相关的一组属性,这些属性如表 2 - 3 所示。表 2-9 DLL 文件各节的属性属性含义READ该节中的字节可以读取WRITE该节中的字节可以写入EXECUTE该节中的字节可以执行SHARE该节中的字节可以被多个实例共享(本属性能够有效地关闭 c o p y - o n - w r i t e 机制)除了编译器和链接程序创建的标准节外,也可以在使用下面的命令进行编译时创建自己的节:#pragma data_seg(“sectionname”) 定义全局变量#pragma data_seg() 常见的节名和作用如表 2-4 所示:表 2-10 常见的节名和作用节名作用.text.exe 和.dll 文件的代码.data已经初始化的数据.bss未初始化的数据.reloc重定位表(装载进程的进程地址空间).rdata运行期只读数据.CRTC 运行期只读数据.debug调试信息.xdata异常处理表.tls线程的本地化存储.idata输入文件名表.edata输出文件名表.rsrc资源表.didata延迟输入文件名表 19第第 3 3 章章 DLLDLL 注入注入3.13.1 DLLDLL 基础基础动态链接库(DLL)15是 Dynamic Link Library 的缩写形式,DLL 是一个包含可由多个程序同时使用的代码和数据的库。 (1)动态链接库是应用程序的一部分,它的任何操作都是代表应用程序进行的。所以动态链接库在本质上与可执行文件没有区别,都是作为模块被进程加载到自己的地址空间的。 (2)动态链接库在程序编译时并不会被插入到可执行文件中,在程序运行时整个库的代码才会调入内存,这就是所谓的“动态链接”。 (3)如果有多个程序用到同一个动态链接库,Windows 在物理内存中只保留一份库的代码,仅通过分页机制将这份代码映射到不同的进程中。这样,不管有多少程序同时使用一个库,库代码实际占用的物理内存永远只有一份。 由于动态链接库的“动态链接”特性使得我们可以将用于 Windows API 拦截的 DLL在被拦截程序运行过程中加载到它的进程地址空间中,也就是 DLL 注入。 另外,当多个应用程序是用同一个 DLL 时,该 DLL 只会被载入一次,所有应用程序都共享该 DLL 的页面,这样就可以在这些应用程序之间共享数据及其资源了。这是拦截进程如何得到获取的被拦截进程通信信息问题的基础,将在下文中进行详细的阐述。 Windows API 中的所有函数都包含在 DLL 中。3 个最重要的 DLL 是Kernel32. DLL,它包含用于管理内存、进程和线程的各个函数; User32. DLL,它包含用于执行用户界面任务(如窗口的创建和消息的传送)的各个函数; GDI32. DLL,它包含用于画图和显示文本的各个函数。3.23.2 DLLDLL 的运行机制的运行机制 为了全面理解 DLL 是如何运行的以及你和系统如何使用 DLL,让我们首先观察一下 DLL 的整个运行情况。图 3 - 1 综合说明了它的所有组件一道配合运 20行的情况。 现在要重点介绍可执行模块和 DLL 模块之间是如何隐含地互相链接的。 在图 3 - 1 中你可以看到,当一个模块(比如一个可执行文件)使用 DLL中的函数或变量时,将有若干个文件和组件参与发挥作用。图 3-1 应用程序如何创建和隐含链接 DLL 的示意图创建 DLL:1) 建立带有输出原型/结构/符号的头文件。2) 建立实现输出函数/变量的 C/C+源文件。3) 编译器为每个 C/C+源文件生成.obj 模块。4) 链接程序将生成 DLL 的.obj 模块链接起来。5) 如果至少输出一个函数/变量,那么链接程序也生成 lib 文件。创造 EXE:6) 建立带有输入原型/结构/符号的头文件。7) 建立引用输入函数/变量的 C/C+源文件。8) 编译器为每个 C/C+源文件生成.obj 源文件。9) 链接程序将各个.obj 模块链接起来,产生一个.exe 文件 21 (它包含了所需要 DLL 模块的名字和输入符号的列表)。运行应用程序:10) 加载程序为.exe 创建地址空间。11) 加载程序将需要的 DLL 加载到地址空间中进程的主线程开始执行; 应用程序启动运行。若要创建一个从 DLL 模块输入函数和变量的可执行模块,必须首先创建一个 DLL 模块。然后就可以创建可执行模块。 若要创建 DLL 模块,必须执行下列操作步骤: 1) 首先必须创建一个头文件,它包含你想要从 DLL 输出的函数原型、结构和符号。DLL 的 所有源代码模块均包含该头文件,以帮助创建 DLL。后面将会看到,当创建需要使用 DLL 中包 含的函数和变量的可执行模块(或多个模块)时,也需要这个头文件。 2) 要创建一个 C / C + +源代码模块(或多个模块),用于实现你想要在DLL 模块中实现的函 数和变量。由于这些源代码模块在创建可执行模块时是不必要的,因此创建 DLL 的公司能够保护公司的秘密。 3) 创建 DLL 模块,将使编译器对每个源代码模块进行处理,产生一个.obj模块(每个源代 码模块有一个.obj 模块)。 4) 当所有的.obj 模块创建完成后,链接程序将所有.obj 模块的内容组合在一起,产生一个 DLL 映象文件。该映像文件(即模块)包含了用于 DLL 的所有二进制代码和全局/静态数据变 量。为了执行这个可执行模块,该文件是必不可少的。 5) 如果链接程序发现 DLL 的源代码模块至少输出了一个函数或变量,那么链接程序也生成 一个.lib 文件。这个.lib 文件很小,因为它不包含任何函数或变量。它只是列出所有已输出函数 和变量的符号名。为了创建可执行模块,该文件是必不可少的。 一旦创建了 DLL 模块,就可以创建可执行模块。其创建步骤是: 6) 在引用函数、变量、数据、结构或符号的所有源代码模块中,必须包含DLL 开发人员创 建的头文件。 227) 要创建一个 C/C + +源代码模块(或多个模块),用于实现你想要在可执行模块中实现的 函数和变量。当然该代码可以引用 DLL 头文件中定义的函数和变量。 8) 创建可执行模块,将使编译器对每个源代码模块进行处理,生成一个.obj 模块(每个源 代码模块有一个.obj 模块)。 9) 当所有.obj 模块创建完成后,链接程序便将所有的.obj 模块的内容组合起来,生成一个可 执行的映像文件。该映像文件(或模块)包含了可执行文件的所有二进制代码和全局/静态变量。 该可执行模块还包含一个输入节,列出可执行文件需要的所有 DLL 模块名。此外,对于列出的每个 DLL 名字,该节指明了可执行模块的二进制代码 引用了哪些函数和变量符号。下面你会看到操作系统的加载程序将对该输入节进行分析。 一旦 DLL 和可执行模块创建完成,一个进程就可以执行。当试图运行可执行模块时,操作系统的加载程序将执行下面的操作步骤: 10) 加载程序为新进程创建一个虚拟地址空间。可执行模块被映射到新进程的地址空间。加载程序对可执行模块的输入节进行分析。对于该节中列出的每个 DLL 名字,加载程序要找出用户系统上的 DLL 模块,再将该 DLL 映射到进程的地址空间。注意,由于 DLL 模块可以从另一个 DLL 模块输入函数和变量,因此 DLL 模块可以拥有它自己的输入节。若要对进程进行全面的初始化,加载程序要分析每个模块的输入节,并将所有需要的 DLL 模块映射到进程的地址空间。如你所见,对进程进行初始化是很费时间的。 一旦可执行模块和所有 DLL 模块被映射到进程的地址空间中,进程的主线程就可以启动运行,同时应用程序也可以启动运行。下面各节将更加详细地介绍这个进程的运行情况。3.33.3 DLLDLL 的创建的创建下面我们进行第一步,也就是创建动态链接库16工程 hooklib.dll。 (1)新建工程 hooklib,工程类型选择 Win32 Dynamic-Link Library。 23(2) 单击 OK 按钮,VC+弹出要求选择动态链接库类型的对话框。这些选择会影响 VC+最终自动产生的框架代码,这里选中第一个选项,创建一个空的工程,避免 VC+产生不必要的代码。单击 Finish 按钮,完成工程创建。 要完成对被拦截程序的 DLL 注入,必须在程序运行过程中显式地去加载 DLL库,即运行期间动态链接。为了能够运行期间动态地导出函数 SetHook,一般需要在 hooklib 工程中建立一个 DEF 文件来指定要导出的函数 SetHook。可以这样向工程中添加 DEF 文件: 打开 hooklib 工程,选择菜单命令“File/ New.”,在弹出的 New 对话框中,选择 Text File 选项,输入文件名 hooklib.def 单击 OK 按钮即可。 在新添的 DEF 文件中写入如下的内容。 EXPORTSSetHookSuspendProcessSaveProcessState这两行说明此 DLL 库要向外导出 ExportFunc 函数。最后按 F7 键重新编译hooklib 工程,DLL 方的工程修改完毕。3.43.4 注入技术的分析和比较注入技术的分析和比较由于在 Win32 系统中各个进程的地址是互相独立的,因此我们无法在一个进程中对另一个进程的代码进行有效的修改,但如果你要完成 API 钩子的工作又必须如此。因此,我们必须采取某种独特的手段,使得 API 钩子(准确的说是钩子驱动器)能够成为目标进程中的一部分,才有较大的可能来对目标进程数据和代码进行有控制的修改。通常可采用的几种注入方式:3.4.1.3.4.1. 利用注册表注入利用注册表注入如果我们准备拦截的进程连接了 User32.dll,也就是使用了 User32.dll中的 API(一般图形界面的应用程序都是符合这个条件),那么就可以简单把你的钩子驱动器 DLL 的名字作为值添加在下面注册表的键下:HKEY_LOCAL_MACHINESoftwareMicrosoftWindowsNTCurrentVersionWindowsAppInit_DLLs 值的形式可以为单个 DLL 的文件名,或者是一组 DLL 的文件名, 24相邻的名称之间用逗号或空格间隔。所有由该值标识的 DLL 将在符合条件的应用程序启动的时候装载。当 User32 . dll 库被映射到进程中时,它将接收到一个 DLL _ PROCESS _ AT TACH 通知。当这个通知被处理时,User32 . dll 便检索保存的这个关键字中的值,并且为字符串中指定的每个 DLL 调用 LoadLibrary 函数。当每个库被加载时,便调用与该库相关的 DllMain 函数,其 fdwReason 的值是DLL_PROCESS_ATA CH,这样,每个库就能够对自己进行初始化。由于插入的DLL 在进程的寿命期中早早地就进行了加载,因此在调用函数时应该格外小心。调用 kernel32.dll 中的函数时应该不会出现什么问题,不过调用其他 DLL 中的函数时就可能产生一些问题。User32.dll 并不检查每个库是否已经加载成功,或者初始化是否取得成功。这是一个操作系统内建的机制,相对其他方式来说危险性较小,比较容易实现。但它也有一些比较明显的缺点:该方法仅适用于 NT/2K 操作系统,显然看看键的名称就可以明白;如果需要激活或停止钩子的注入,只有重新启动Windows,这个就似乎太不方便了;最后一点也很显然,不能用此方法向没有使用 User32.dll 的应用程序注入 DLL,例如控制台应用程序等。另外,不管是否为你所希望,钩子 DLL 将注入每一个 GUI 应用程序,这将导致整个系统性能的下降!只适用于 GUI 程序不适用于 CUI。3.4.2.3.4.2. 建立系统范围的建立系统范围的 WindowsWindows 钩子钩子这是本文采用的注入方法。要向某个进程注入 DLL,一个十分普遍也是比较简单的方法就是建立在标准的 Windows 钩子的基础上。Windows 钩子一般是在 DLL 中实现的,这是一个全局性的 Windows 钩子的基本要求,这也很符合我们的需要。当我们成功地调用 SetWindowsHookEx 函数之后,便在系统中安装了某种类型的消息钩子,这个钩子可以是针对某个进程,也可以是针对系统中的所有进程。一旦某个进程中产生了该类型的消息,操作系统会自动把该钩子所在的 DLL 映像到该进程的地址空间中,从而使得消息回调函数(在SetWindowsHookEx 的参数中指定)能够对此消息进行适当的处理,在这里,我们所感兴趣的当然不是对消息进行什么处理,因此在消息回调函数中只需把消 25息钩子向后传递就可以了,但是我们所需的 DLL 已经成功地注入了目标进程的地址空间,从而可以完成后续工作。我们知道,不同的进程之间是不能直接共享数据的,因为它们活动在不同的地址空间中。但在 Windows 钩子 DLL 中,有一些数据,例如 Windows 钩子句柄HHook,这是由 SetWindowsHookEx 函数17返回值得到的,并且作为参数将在CallNextHookEx 函数和 UnhookWindoesHookEx 函数中使用,显然使用 SetWind owsHookEx 函数的进程和使用 CallNextHookEx 函数的进程一般不会是同一个进程,因此我们必须能够使句柄在所有的地址空间中都是有效的有意义的,也就是说,它的值必须必须在这些钩子 DLL 所挂钩的进程之间是共享的。为了达到这个目的,我们就应该把它存储在一个共享的数据区域中。在 VC+中我们可以采用预编译指令#pragma data_seg 在 DLL 文件中创建一个新的段,并且在 DEF 文件中把该段的属性设置为“shared”,这样就建立了一个共享数据段。不过也可以利用内存映像技术来申请使用一块各进程可以共享的内存区域,主要是利用了 CreateFileMapping 和 MapViewOfFile 这两个函数,这倒是一个通用的方法,适合所有的开发语言,只要它能直接或间接的使用 Windows 的 API。在 Borland 的 BCB 中有一个指令#pragma codeseg 与 VC+中的#pragma data_seg 指令有点类似,应该也能起到一样的作用。一旦钩子 DLL 加载进入目标进程的地址空间后,在我们调用 UnHookWindowsHookEx 函数之前是无法使它停止工作的,除非目标进程关闭。这种 DLL 注入方式有两个优点: 这种机制在 Win 9x/Me 和 Win NT/2K 中都是得到支持的,预计在以后的版本中也将得到支持;钩子 DLL 可以在不需要的时候,可由我们主动的调用 UnHookWindowsHookEx 来卸载,比起使用注册表的机制来说方便了许多。尽管这是一种相当简洁明了的方法,但它也有一些显而易见的缺点:首先值得我们注意的是,Windows 钩子将会降低整个系统的性能,因为它额外增加了系统在消息处理方面的时间;其次,只有当目标进程准备接受某种消息时,钩子所在的 DLL 才会被系统映射到该进程的地址空间中,钩子才能真正开始发挥作用,因此如果我们要对某些进程的整个生命周期内的API 调用情况进行监控,用这种方法显然会遗漏某些 API 的调用 。 263.4.33.4.3 使用使用 CreateRemoteThreadCreateRemoteThread 函数函数注入 DLL 的第三种方法是使用远程线程。这种方法具有十分大的灵活性,然而不幸的是,CreateRemoteThread 这个函数只能在 Win NT/2K 系统中才得到支持,虽然在 Win 9x 中这个 API 也能被安全的调用而不出错,但它除了返回一个空值之外什么也不做。该注入过程也十分简单:我们知道,任何一个进程都可以使用 LoadLibrary 来动态地加载一个 DLL。由于除了自己进程中的线程外,我们无法方便地控制其他进程中的线程,因此这种解决方案要求我们在目标进程中创建一个新线程。由于是自己创建这个线程,因此我们能够控制它执行什么代码。幸好,Windows 提供了一个称为 CreateRemoteThread 的函数,使我们能够非常容易地在另一个进程中创建线程: HANDLE CreateRemoteThread( HANDLE hProcess, PSECURITY_ATTRIBUTES psa, DWORD dwStackSize, PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam, DWORD fdwCreate, PDWORD pdwThreadId);CreateRemoteThreadEx 与 CreateRemoteThread 很相似,差别在于它增加了一个参数 hProcess。该参数指明拥有新创建线程的进程。参数 pfnStartAddr指明线程函数的内存地址。当然,该内存地址与远程进程是相关的。线程函数的代码不能位于你自己进程的地址空间中。但问题是,我们如何让目标进程(可能正在运行中)在我们的控制下来加载我们的钩子 D
展开阅读全文