图书在版编目（C I P）数据

C++黑客编程揭秘与防范 / 冀云编著．-- 北京：人民邮电出版社，2012.6

ISBN 978-7-115-28064-0

I．①C…II．①冀…III．①C语言—程序设计IV．①TP312

中国版本图书馆CIP数据核字（2012）第081089号

内容提要

本书旨在通过简单的语法知识及常用的系统函数编程，完成一些有特定功能的安全工具，让读者对系统知识等各方面有一个全面的了解，并且在笔者的带领下一步步完成书中的实例。本书主要内容为：

第1章了解黑客编程，主要讲解了VC （Visual C++的缩写）和Windwos下安全编程方面的基础知识。第2章从剖析简单的木马说起，讲解有关的网络编程和协议知识。第3章Windows应用编程基础，讲解API编程的技术。第4章加密与解密，讲解PE等加密有关的知识。第5章HOOK编程，讲解了与钩子有关的知识。第6章黑客编程剖析，剖析了病毒的原理和攻防技术，以及安全工具的开发。第7章最后的旅程——简单驱动开发及逆向。

本书适合网络安全人员、黑客爱好者，以及相关的程序员阅读。

C++黑客编程揭秘与防范

♦　编　　著　冀　云

责任编辑　张　涛

♦　人民邮电出版社出版发行　北京市东城区夕照寺街14号

邮编　100061　电子邮件　315@ptpress.com.cn

网址　http://www.ptpress.com.cn

北京鑫正大印刷有限公司印刷

♦　开本：787×1092　1/16

印张：17.25

字数：406千字　　　　 2012年6月第1版

印数：1 – 3 500册　　2012年6月北京第1次印刷

ISBN 978-7-115-28064-0

定价：39.00元

读者服务热线：(010) 67132692　印装质量热线：(010) 67129223

反盗版热线：(010) 67171154

目 录

前言

第1章　黑客编程入门

1.1　编程语言和开发环境的选择

1.1.1　何为SDK、API和MFC

1.1.2　VC6和SDK的配置

1.2　应用程序的调试

1.2.1　编写我们的第一个程序

1.2.2　用VC6调试第一个程序

1.2.3　专业的应用程序调试工具——OllyDbg

1.3　简单API的介绍

1.3.1　复制自身程序到Windows目录和系统目录下

1.3.2　获得系统的相关信息

1.3.3　Debug和Release的编译方式

1.3.4　查看函数定义

1.4　总结

第2章　木马开发剖析

2.1　网络通信基础

2.1.1　IP地址的作用与分类

2.1.2　端口的作用与分类

2.2　网络编程基础知识

2.2.1　通信模型

2.2.2　Winsock

2.2.3　Winsock的相关函数

2.2.4　字节顺序

2.3　简单的通信程序

2.3.1　基于TCP协议的“Hello World!”

2.3.2　基于UDP协议的“Hello World!”

2.4　实现一个C/S模式的简单木马

2.4.1　木马服务器端的实现

2.4.2　木马客户端的实现

2.5　总结

第3章　Windows应用编程基础

3.1　文件

3.1.1　打开文件

3.1.2　文件操作

3.2　AutoRun免疫程序的编写

3.2.1　AutoRun免疫原理

3.2.2　AutoRun免疫程序的代码实现

3.2.3　界面设置

3.2.4　代码相关部分

3.3　注册表操作

3.3.1　注册表

3.3.2　与注册表操作相关的常用API函数

3.3.3　注册表启动项的管理

3.3.4　程序的界面设置及相关代码

3.3.5　启动项的枚举

3.3.6　添加启动项的代码

3.3.7　删除启动项的代码

3.4　服务相关的编程

3.4.1　如何查看系统服务

3.4.2　服务控制管理器的开发

3.4.3　枚举服务的相关API函数

3.4.4　服务的停止

3.4.5　停止服务的相关API函数

3.4.6　服务的启动

3.5　进程与线程

3.5.1　进程

3.5.2　进程的创建

3.5.3　“下载者”的简单演示

3.5.4　CreateProcess()函数介绍与程序创建

3.5.5　进程的结束

3.5.6　进程的枚举

3.5.7　调整当前进程的权限

3.5.8　进程的暂停与恢复

3.5.9　多线程

3.6　DLL编程

3.6.1　什么是DLL

3.6.2　编写一个简单的DLL程序

3.6.3　对DLL程序的调用方法一

3.6.4　对DLL程序的调用方法二

3.7　远程线程

3.7.1　DLL注入

3.7.2　DLL卸载

3.7.3　无DLL的代码注入

3.8　总结

第4章　加密与解密

4.1　PE文件结构

4.1.1　PE文件结构全貌

4.1.2　MZ头部

4.1.3　PE头部

4.1.4　节表

4.1.5　节表数据

4.2　详解PE文件结构

4.2.1　DOS头部详解 IMAGE_D〇S_HEADER

4.2.2　PE头部详解IMAGE_NT_HEADERS

4.2.3　IAMGE_FILE_HEADER

4.2.4　IMAGE_OPTIONAL_HEADER

4.2.5　节区详解IMAGE_SECTION_HEADER

4.2.6　与PE结构相关的3种地址

4.2.7　3种地址的转换

4.3　PE查看器

4.4　简单的查壳工具

4.5　地址转换器

4.6　添加节区

4.6.1　手动添加一个节区

4.6.2　通过编程添加节区

4.7　破解基础知识及调试API函数的应用

4.7.1　CrackMe程序

4.7.2　用OD破解CrackMe

1.破解方法一

2.破解方法二

4.8　文件补丁及内存补丁

4.8.1　文件补丁

4.8.2　内存补丁

4.9　调试API函数的使用

4.9.1　常见的3种断点方法

4.9.2　调试API函数及相关结构体介绍

4.9.3　判断是否处于被调试状态

4.9.4　断点异常函数

4.9.5　调试事件

4.9.6　调试循环

4.9.7　内存的操作

4.9.8　线程环境相关API及结构体

4.10　打造一个密码显示器

4.11　总结

第5章　HOOK编程

5.1　HOOK知识前奏

5.2　内联钩子——Inline Hook

5.2.1　Inline Hook的原理

5.2.2　Inline Hook的实现

5.2.3　HOOK MessageBoxA

5.2.4　HOOK CreateProcessW

5.2.5　7字节Inline Hook

5.2.6　Inline Hook的注意事项

5.3　导入地址表钩子——IATHOOK

5.3.1　导入表简介

5.3.2　导入表的数据结构定义

5.3.3　手动分析导入表

5.3.4　枚举导入地址表

5.3.5　IAT HOOK介绍

5.3.6　IAT HOOK之 CreateFileW ()

5.4　Windows钩子函数

5.4.1　钩子原理

5.4.2　钩子函数

5.4.3　键盘钩子实例

5.4.4　使用钩子进行DLL注入

5.5　总结

第6章　黑客编程剖析

6.1　恶意程序剖析

6.1.1　恶意程序的自启动

6.1.2　木马的配置生成与反弹端口

6.1.3　代码实现剖析

6.2　简单病毒剖析

6.2.1　病毒的感染剖析

6.2.2　缝隙搜索的实现

6.2.3　感染目标程序文件剖析

6.2.4　添加感染标志

6.2.5　自删除功能的实现

6.3　隐藏DLL文件

6.3.1　启动WinDBG

6.3.2　调试步骤

6.3.3　编写枚举进程中模块的函数

6.3.4　指定模块的隐藏

6.4　安全工具开发基础

6.4.1　行为监控工具开发基础

6.4.2　专杀工具

6.4.3　U盘防御软件

6.4.4　目录监控工具

6.5　引导区解析

6.5.1　通过WinHex来手动解析引导区

6.5.2　通过程序解析MBR

6.5.3　自定义MBR的各种结构体

6.5.4　解析MBR的程序实现

6.6　加壳与脱壳

6.6.1　手动加壳

6.6.2　编写简单的加壳工具

第7章　最后的旅程——简单

驱动开发及逆向

7.1　驱动版的 “Hello World”

7.2　驱动下的进程遍历

7.2.1　配置VMware和WinDbg进行驱动调试

7.2.2　EPROCESS和手动遍历进程

7.2.3　编程实现进程遍历

7.3　HOOK SSDT（系统服务描述表）

7.3.1　SSDT简介

7.3.2　HOOK SSDT

7.3.3　Inline HOOK SSDT

7.4　应用程序与驱动程序的通信

7.4.1　创建设备

7.4.2　应用程序与驱动程序的通信方式

7.4.3　应用程序与驱动程序的通信实例

7.5　C语言代码逆向基础

7.5.1　函数的识别

7.5.2　if……else……分支结构

7.5.3　switch分支结构

7.5.4　for循环结构

7.5.5　do while与 while循环结构

参考文献

后封皮

前言

什么是黑客？摘自百度百科中的一句话，“黑客一词，原指热心于计算机技术，水平高超的电脑专家，尤其是程序设计人员”。其实，黑客并不利用自己已有的技术去对他人的系统进行渗透并破坏。黑客的为人处世也非常低调，不会整天拿着别人写好的工具去入侵网站或“抓肉鸡”，做这么没意义的事。如果是黑客天天做这些事，怎么可能有多余的时间真正地研究技术？

编程、破解、入侵

编程、破解、入侵是黑客所掌握的技能，但是后两者都是以前者的编程为基础的。破解别人的程序是站在写程序的角度去考虑的，而入侵依靠的是系统的漏洞，发掘漏洞同样是需要编程知识、系统底层知识和调试技术。也就是说，想做一名黑客，在自身的知识体系中编程知识是占据很大份额的。也就应了网上的一句话——“不会编程的黑客就不是黑客”.

黑客编程与普通编程的区别

黑客编程，其实也就是利用普通的编程技术编写一些黑客工具，或者是网络安全工具。该方面的知识是一把双刃剑，无论是编写黑客工具，还是编写安全工具，都离不开这些知识。本书的重点是通过简单的编程知识配合良性的实例让大家了解黑客编程，并对漏洞进行防范，希望大家正确对待技术的合理应用。

本书的前置知识

阅读需要有C、C++语言的基础知识，本书并不是一门编程语言关于语法知识的教科书。如果读者希望能够顺利阅读此书，至少要有阅读C、C++语言编程的能力。如果没有C、 C++语言的基础，而有其他语言的基础，那么也是没有问题的。在掌握了编程思想，或者会使用API函数后，用自己熟悉的语言进行相应的开发也是可以的。但是，为了将来能更好、更深入地学习系统的底层知识，建议学习C、C++和汇编语言。

本书适合的读者

长期研究黑客工具的程序员，且有打算自己开发网络安全工具的人员。

掌握编程基本语法想要进行相关编程实践的读者。

本书的定位

本书并非高深的技术性书籍，市场上较深入的研究系统底层、加解密相关的、网络安全编程相关的书籍非常多。但是，很多并非入门类的书籍。本书旨在，通过简单的语法知识及常用的系统函数完成一些有特定功能的安全工具。在读者有基础的前提下，让读者对系统知识等各方面有一个全面的了解，并且在笔者的带领下一步步地完成书中的实例，也可以增强读者的动手能力，源程序下载地址www. ptpress.com.cn。

需要声明的是：

本书的内容是帮助读者提升防范黑客攻击的能力和技术，普及网络安全知识，绝不是为那些怀有不良动机的人提供支持，也不承担因为技术被滥用所产生的连带责任，请读者自觉遵守国家相关法律。

第1章　黑客编程入门

你是否曾经在用别人开发的工具尝试“入侵”，你是否希望开发出自己的黑器……相信很多人有着这种近似相同的经历。本章将简单介绍黑客编程及工具开发。如果你是初学编程，如果你从来没有接触过黑客软件的开发，如果你急于想了解黑客编程方面的知识……那么就请继续往下阅读。

1.1　编程语言和开发环境的选择

初学者刚开始学习编程语言最头疼的问题就是如何选择编程语言及合适的开发环境，下面就来具体介绍一下。

有人认为学编程就是学编程语言，而VC、VB这样的开发环境只是工具，不需要学。这个想法是错误的，因为开发环境提供了很多开发工具，如VC这个集成开发环境就提供了与之对应的PSDK、MFC等。除了语言以外，要开发特定的软件是需要开发包和开发工具支持的。况且，编程语言也是一种工具，用于和计算机进行交流的工具。所以我们既要学习编程语言，也要学习开发工具。

对于选择哪种编程语言或者开发环境其实也没有特定的标准。有这样一句话，“真正的程序员用VC，聪明的程序员用Delphi，用VB的不是程序员”。笔者却并不这么认为，因为在很多编程的书籍上常常这样提醒并告诫学习者，编程的精髓是“算法”，而语言是用来描述算法的。因此，大家也不必因为无法选择而无从下手。

黑客一般都掌握多种编程语言，他们不但掌握着与底层相关的如汇编、C之类的编程语言，而且还掌握很多脚本语言，如Python、Perl、Ruby……很多黑客在发现0Day以后用Perl或者Python来写POC; MSF使用的是Ruby来进行开发Exploit;有的黑客在反病毒时竟然写个批处理就搞定了……对于黑客来说，一切语言都是服务于自己的思想的，只要能快速实现自己的想法，能完成自己所要完成的功能就行，从不拘泥于任何语言和工具。在网上有很多学习不同编程语言的人们之间经常互相攻击，这其实是一种极端的行为，大家还是理性地对待这些问题比较好。

前面说了这么多，仿佛是在绕圈子，一直没有介绍到底应该选择什么编程语言和开发环境。我们这里选择使用C语言作为黑客编程的学习语言，选择VC6（Visual C++6.0的缩写）来作为我们的开发环境。VS 6相对于Visual Studio 2005、Visual Studio 2008和Visual Studio2010之类的开发环境来说要小巧很多，当前是可以满足我们的开发需求的。选择C语言的原因是由于Windows的API都是用C语言定义的，相对于使用其他编程语言会方便很多。笔者认为在VB下使用API就非常不方便，尤其是涉及指针这个概念的时候。除了VC6以外，还需要下载新版的PSDK，因为VC6中包含的PSDK过于旧，有些新的API我们无法通过包含头文件而直接使用，因此这个也是必须的。

1.1.1　何为SDK、API和MFC

既然选择VC作为开发环境，那么先来了解一下VC开发环境中今后会用到的一些工具的概念，这些概念都相对比较简单，常见的概念有SDK、API和MFC。

SDK是Software Develop Kits的缩写，即软件开发工具包。SDK是一个泛指，比如对视频采集卡进行二次开发，那么视频采集卡会提供SDK;再比如对动态令牌进行二次开发，那么动态令牌也会提供SDK。操作系统为了程序员在其平台下开发应用程序也会提供SDK，我们对系统提供的开发包称之为PSDK。PSDK是Platform SDK的意思，也就是平台SDK。对于我们来说，平台就是Windows操作系统。Windows下的PSDK包含了进行Windows软件开发的文档和API函数的输入库、头文件等一些与Windows开发相关的工具。PSDK是一个单独的开发包，不过每个不同版本的VC和其他一些开发环境中也已经包含了它。

API是Application Programming Interface的缩写，即应用程序接口。不同的SDK提供不同的API。PSDK提供的API就是操作系统提供的开发应用程序的接口，比如Windows系统下删除文件的API函数是DeleteFile();再比如Windows系统下移动文件的API函数是MoveFile()，而其他一些供二次开发的SDK中附带的API，也是为了进行开发应用程序而提供的接口。

MFC是Microsoft Foundation Class的缩写，即微软基础类库。它是微软为了简化程序员的开发工作量所提供的基于C++类的一套面向对象的库，它封装了常见的API函数，使得开发较为方便。

我们在书中会用到API进行开发，也会使用MFC进行开发。不过对于MFC的使用，基本上用在与界面相关的部分，一般是简单地带过，不会进行过多的讨论。我们的重点是放在API函数的使用上。关于MFC的相关内容，还请大家自行参考学习。

1.1.2　VC6和SDK的配置

新版的PSDK （Windows Server 2003 SP1 Platform SDK）的下载地址为 http://www.microsoft.com/downloads/en/details.aspx?FamilyID=eba0128f-a770-45fl-86f3-7ab010b398a3。如果此地址过期的话，请大家在网上自行搜索并下载。

SDK和VC6互相是独立的，不需要安装在同一个目录下，根据自己的实际情况安装就可以了。在安装好VC6和新版的SDK后，需要在VC6中进行相应的设置才能使用新版的SDK，否则VC6仍然使用其自带的旧的SDK。SDK和VC6的安装步骤这里就不介绍了（提示：请把VC6安装完整，VC6会提供一些代码，对我们的学习是非常有帮助的），下面介绍新版的SDK如何配置才能在VC6中使用。

启动VC6，单击菜单“Tools”-> “Options”命令，打开“Options”对话框，如图1-1所示。

图1-1　“Options”对话框

选择“Directories”选项卡，在“Show directories for”下拉列表中选择“Include files”，选项并在“Directories”列表框中添加新的PSDK头文件的目录，放在列表的最上面，如图1-2所示。

图1-2　头文件的路径

在“Show directories for”下拉列表中选择“Library files”选项’并在“Directories”列表框中添加新的PSDK库文件的目录，放在列表的最上面，如图1-3所示。

图1-3　库文件的路径

切记要把所添加的目录放到列表的最上边，因为在VC编译代码的时候会搜索这些目录里的文件，如果随便放，编译器会因找不到相关API函数定义而报函数未定义的错误。

另外，还必须下载一个MSDN。MSDN即Microsoft Developer Network，它是微软开发的联机帮助文档，可以帮助我们在使用API的时候进行快速的查阅，以方便我们对API的使用和理解。但是MSDN里的内容全部都是英文的，如果你英文不太好可以借助搜索引擎来学习API的使用。本书只对所提到的API函数常用的参数进行介绍，其他参数需要大家自行进行学习。

1.2　应用程序的调试

在开发程序的过程中，除了编码以外还需要对程序进行调试，当编写的程序出现问题后，就要对程序进行调试。调试不是仅使用一个printf()或MessageBox()进行简单的输出来观察某个函数的返回值（虽然在调试的时候的确是对返回值观察较多），也不是对某个变量、某一时间的具体值的输出。调试是有专业的调试分析工具的，VC6不但提供代码编辑、代码编译、编译连接等功能，还提供了一个非常好用的调试工具。在编写完代码后，如果程序输出的结果是未知的，或者是没有预测到的，都可以通过调试来对代码的逻辑进行分析，以找到问题的所在。掌握调试的技能，对软件的开发有非常大的帮助。掌握好的调试工具，对于调试者来说，也同样会起到事半功倍的作用。下面通过一个简单的例子了解一下VC6提供的调试功能吧。

1.2.1　编写我们的第一个程序

下面介绍用VC6写一个控制台版的HelloWorld来学习VC6的开发。也许大家认为这个程序很简单，但是请记住，我们的重点是要介绍VC6这个集成开发环境中提供的调试功能。

启动VC6，单击菜单“File” -> “New”命令，在弹出的对话框中选择“Projects”选项卡，然后在左边的列表框中选择“Win32 Console Application”选项,在“Project Name:”文本框中填写“HelloWorld”，如图1-4所示。

图1-4　“Projects”选项卡

单击“OK”按钮，出现如图1-5所示窗口。

图1-5　“Win32 Console Application”项目向导

选择“An empty project”单选项，单击“Finish”按钮，然后在弹出的对话框中单击“OK”按钮。

单击菜单“File” -> “New”命令，选择“Files”选项卡，在左边的列表中选择“C++ Source File”选项’在右边的“File”文本框中填写“HelloWorld”，如图1-6所示。

图1-6　“Files”选项卡

单击“OK”按钮就可以进行代码编辑了。

在代码编辑处录入如下代码：

按F7键进行编译连接（按Ctrl+ F7组合键是只编译不进行连接），按Ctrl+F5组合键进行运行，如图1-7所示。

图1-7　“Hello World”运行界面

这就是我们值得纪念的第一个程序。这个程序很简单，有C语言基础的读者应该都能看懂，这里就不进行介绍了。如果看不懂，请先找本关于C语言入门的书学习一下。

1.2.2　用VC6调试第一个程序

现在来学习如何使用VC6对第一个程序进行调试。在代码编辑状态下，按下键盘上的F10键，进入调试状态，如图1-8所示。

图1-8　VC6处于调试状态

常用的调试窗口有两个，一个是“Watch”窗口（标注“1”的那个窗口），一个是“Memory”窗口（标注“2”的那个窗口）。打开“Watch”窗口的方法是单击“View” -> “Debug Windows” -> “Watch”命令（或按Alt+3组合键）打开。打开“Memory”窗口的方法是单击“View” -> “Debug Windows” -> “Memory”命令（或按Alt+6组合键）打开。“Watch”窗口用来监视我们感兴趣的变量，而当我们有时无法通过变量的值进行判断时，就需要借助“Memory”窗口中的值，比如，指针的值来进行判断。

除了这两个窗口以外，还有“Call Stack”、“Register”和“Disassembly”这3个窗口，分别如图1-9、图1-10和图1-11所示。

图1-9　“Call Stack”窗口

图1-10　“Register”窗口

图1-11　“Disassembly”窗口

“Call Stack”窗口是调用栈窗口，该窗口可以很方便地查看调用关系，很容易通过调用栈来找到上层、上上层的调用者。另外，也可以通过调用栈来定位错误。比如，有时程序会崩溃，但是发生崩溃的地方却在系统提供的代码中，而不在我们编写的代码中，这种错误在通常情况下是我们的程序对于参数的输入有误造成的，我们可以通过调用栈查看是谁调用了该函数，以便进行进一步分析。

“Register”窗口是用来观察寄存器的。有时需要观察返回值或者参数。

“Disassembly”窗口是用来观察C代码对应的反汇编代码的。有时在看C的代码无法解决的问题时，需要查看在底层实现时分析程序的问题。

以上就是VC6下常用的调试窗口，可根据实际情况使用，并不是每次调试都会用到这些窗口。下面再简单介绍一下常用的调试快捷键，以方便今后进行调试时使用。

VC6调试时的常用快捷键如下。

F5键：运行程序。

F9键：设定断点/取消断点。

F10键：单步步过，依次执行每一条代码。

F11键：单步步入，依次执行每一条代码，遇到函数调用时则进入到被调用的函数中。

F7键：停止调试。

在后面的章节中我们会用到这些快捷键来调试程序，让大家在学习的过程中真正地应用起这些调试功能。

1.2.3　专业的应用程序调试工具——OllyDbg

OllyDbg，简称OD，是专业的应用程序调试工具。接触过破解，或者做过外挂开发的读者一定对这款工具不陌生。在这里，简单介绍一下这款工具。

让我们先来看看它的界面吧，如图1-12所示。

图1-12　OllyDbg的窗口

OD的大多数情况是在没有源代码的情况下对软件进行调试的。也许没有源代码也就不叫调试了，而叫做动态分析。OD的主界面中有6个主要的窗口，分别是反汇编窗口、寄存器窗口、提示信息窗口、数据窗口（也叫转存窗口）、栈窗口和命令提示窗口。

下面逐个介绍一下各个窗口的作用。

（1） 反汇编窗口：这是调试或动态分析时的主要窗口，我们主要是针对软件的功能实现进行分析，因此主要需查看的就是反汇编窗口的内容。

（2） 寄存器窗口：该窗口的作用是实时地显示寄存器的变化情况。寄存器也可以反映代码的执行情况。例如，我们常常查看返回值的eax的值。

（3） 提示信息窗口：这里往往会显示一些内存地址的值、寄存器的值、调用方的地址等信息。

（4） 数据窗口：该窗口主要是用来显示数据的，单击右键可以把数据按照不同的方式进行解析，对于我们分析程序的过程是非常有用的。

（5） 栈窗口：该窗口可以用来查看函数调用时参数的值。

（6） 命令提示窗口：该窗口是用来输入调试命令的。

OD调试时的常用快捷键如下。

F8键：单步步过，依次执行每一条代码。

F7键：单步步入，依次执行每一条代码，遇到函数调用时则进入到被调用的函数中。

F4键：执行到选中的代码处（前提条件是该条代码在程序的流程中一定会被执行到）。

F2键：断点中断。

F9键：运行程序。

OD的介绍到此为止，在后面的内容中我们会再次提到OD，到那时会有一定的机会练习使用OD。如果有对OD感兴趣的读者，请另行阅读其他书籍。

1.3　简单API的介绍

下面介绍一些在黑客编程中会用到的API函数，尽量排一点简单易用的函数，用简单的几行代码来完成一定的功能，希望大家能在这里体会到编程乐趣，不至于被大段的代码影响了自己前进的心情。

1.3.1　复制自身程序到Windows目录和系统目录下

一般的病毒木马都有这种类似的功能，完成这个功能其实并不复杂，我们来拆解思考一下实现这段代码的步骤。

复制是一个拷贝的过程。既然是拷贝，就要知道拷贝的原位置和目的位置。也就是整个过程其实分3步，首先要得到自身程序所在的路径，然后获得Windows目录和系统目录，最后分别拷贝自身程序到这两个目录中。这3个步骤要如何完成，下面我们来看看完成这些功能的API函数。

获得自身程序所在路径的API函数的定义：

该函数有3个参数，分别如下。

（1） hModule：该参数在获得自身程序时使用为NULL。

（2） lpFilename：该参数指定一个字符型的缓冲区，用于保存程序自身所在的路径。

（3） nSize：该参数指定缓冲区的大小。

获得Windows目录的API函数的定义：

该函数有两个参数，分别如下。

（1） lpBuffer：该参数指定一个字符型的缓冲区，用于保存Windows目录的路径。

（2） uSize：该参数指定缓冲区的大小。

获得系统目录的API函数的定义：

该函数有两个参数，分别如下。

（1） lpBuffer：该参数指定一个字符型的缓冲区，用于保存系统目录的路径。

（2） uSize：该参数指定缓冲区的大小。

拷贝文件的API函数的定义：

该函数有3个参数，分别如下。

（1） lpExistingFileName:该参数指向一个已存在文件的路径，即原文件路径。

（2） lpNewFileName：该参数指向一个新的文件的位置，即欲拷贝到的文件的目的路径。

（3） bFailIfExists：该参数是一个布尔型参数，如果参数为TRUE，若目的文件已存在则返回，复制失败;如果参数为FALSE，若目的文件已存在则强行覆盖原有的文件。

需要使用的API函数已经介绍完了，下面就来真正完成这个复制自身程序到Windows目录和系统目录下的程序，代码如下：

该函数需要包含Windows.h这个头文件，也就是在该段程序的最开始处加一句：

1.3.2　获得系统的相关信息

了解一个系统相关信息也是一项比较重要的内容，强大的扫描软件Nmap在对目标主机进行扫描时，也能对目标主机的系统等信息进行识别，真的是很强大。这里简单地获取一些与系统相关的信息，主要获取的内容有操作系统的版本、操作系统的名字及当前登录的用户名称。接下来逐个介绍这些API函数。

（1）获取操作系统版本

代码如下：

该函数就一个参数，这个参数是指向一个OSVERSIONINFO结构的指针。看一下OSVERSIONINFO这个结构体。

dwPlatformId的取值有3个，而现在主要使用一个，即VER_PLATFORM_WIN32_NT。

（2）获取计算机名称

代码如下：

该函数有两个参数，介绍如下。

①lpBuffer：保存计算机名称缓冲区。

②lpnSize:保存缓冲区的长度，该参数是一个输入/输出参数。

（3）获取当前用户名称

代码如下：

该函数有两个参数，介绍如下。

①lpBuffer：保存当前用户名称的缓冲区。

②nSize：保存缓冲区的长度，该参数是一个输入/输出参数。

我们封装一个简单的函数来获取系统的这3个信息，代码如下：

将代码进行编译连接并运行，其执行结果如图1-13所示。

图1-13　当前操作系统版本、计算机名及当前用户名

这个程序完成了我们想要的功能，对于编程的部分就介绍到这里。下面介绍Debug和Release方面的内容。

1.3.3　Debug和Release的编译方式

关于获取系统信息的程序，我们编写完成了，也编译连接并运行过了。找到刚才编译的程序，查看一下它的文件大小，如图1-14所示。

图1-14　GetSysInfo的程序大小

从图1-14中可以看出，该程序竟然有153KB大小。是不是很惊人？我们一共写了不过十几行代码，但是却生成了如此大体积的程序，这是为什么呢？因为代码默认编译连接是Debug版本的，如图1-15所示。

图1-15　Win32 Debug方式

从图1-15中可以看出，我们的代码是由Debug方式编译的。Debug被称为调试版本，在这种方式的编译下，可执行程序中会附带很多和调试相关的数据或代码，而且不做任何的优化，以此为开发人员提供大量的调试信息，从而方便了程序的调试工作。除了Debug方式编译以外，还有一种方式是Release方式编译，单击“Win32 Debug”右边的下拉箭头可以选择“Win32 Release”，如图1-16所示。

图1-16　Win32 Release方式

Release方式被称作发布版本，是为最终用户使用的，这种方式对代码做了大量的优化工作，不再包含与调试相关的信息，从而使程序的运行效率更高，体积更小，如图1-17所示。

图1-17　Release版的GetSysInfo的文件大小

从图1-17可以看出，两个程序的文件大小发生了截然不同的变化。因此，当我们自己写程序调试时，应该使用调试版，以方便我们对程序进行调试。当我们的程序已经调试完毕，那么可以使用发布版来与大家进行交流。

1.3.4　查看函数定义

很多时候，我们都需要查看函数的定义，而函数的定义都在SDK的头文件中。虽然从MSDN中也能找到函数的定义，但是还是有略微的不同，而且对于查找自定义函数的函数定义也是很方便的。

回到我们的代码当中，随便选中一个API函数，比如GetComputerName()这个函数。加入要查看该函数的定义应该如何查看呢？我们在GetComputerName()这个函数上单击鼠标右键，在弹出的快捷菜单上选择“ Go To Definition Of GetComputerName ”（到GetComputerName函数的定义处）命令，如图1-18所示。

图1-18　“ Go To Definition Of GetComputerName ”命令

当选择“Go To Definition Of GetComputerName”命令以后，会来到“Winbase.h”头文件中的GetComputerName()函数的定义处，如图1-19和图1-20所示。

图1-19　“Winbase.h”头文件

图1-20　GetComputerName()的定义

从图1-20中可以看出，GetComputerName是一个宏，其对应的函数为GetComputerNameA()。关于GetComputerName()和GetComputerNameA()，包括可以看到的GetComputerNameW()，我们都不进行介绍。通过图1-20的函数定义和前面介绍这个函数的定义来比较一下，可以看到，头文件中的定义比MSDN中的定义对于函数的描述更加详细，比如WINAPI表示函数的调用方式。

除了“Go To Definition Of GetComputerName”以外，还有一个“Go To Reference To GetComputerName”，这个是查看何处引用了函数。大家可以自行进行练习。

1.4　总结

本书的编程内容主要以C语言为编程语言（本书部分内容会涉及其他语言，但C语言是主要的），以VC6为开发环境，着重介绍了VC6的基本概念及简单的使用，在此基础上带领大家认识了专业的应用程序的调试工具——OD。在最后的内容中介绍了一些简单的API函数的使用。万丈高楼平地起，希望每一位初学者不要过于着急，在后面的章节中我们会慢慢地深入学习黑客编程的内容。希望在每学完一个知识后，大家多思考多动手，这样才能真正地起到学习的效果。

第2章　木马开发剖析

黑客们对系统成功入侵后，为了方便下次登录被入侵的计算机，通常会留下一个后门，或者是木马。国产著名的木马有冰河、广外女生、灰鸽子、上兴、PCShare……这些木马对每个黑客来说都已经很熟悉了，那么接下来就介绍一下它们的共性。不管是哪种木马，它们都要依靠网络来进行客户端与服务器端的通信，也就是说，不管木马有多么复杂的功能，其基础都是建立在网络的通信上。

2.1　网络通信基础

一台计算机和另一台计算机通信，只需要知道两个内容就可以了，即IP地址和端口号。这两个名词想必大家不但听过，而且已经有感性上的认识了。我们在扫描网络主机的时候，会在扫描器中填写扫描主机的IP地址，然后扫描器会逐个扫描IP地址，并且把指定IP的所有开放端口全部列出来。那么IP地址是什么？端口号是什么？下面简单介绍一下两者的作用。

2.1.1　IP地址的作用与分类

IP地址的全称是网际协议地址，互联网上的每个主机和路由器都有IP地址，没有两个机器有相同的IP地址，因此它是用来标识一台网络主机的。所有的IP地址都是32位长，其用点分十进制法来表示，比如“10.10.30.12”。IP地址指定的不是主机，而是网络接口设备。因此，一个主机有两个网络接口，那么就会有两个IP地址。通常情况下，一台主机只有一个IP地址。

IP地址被分为5类，分别是A类、B类……E类，每一类IP地址的范围如表2-1所示。

上面的IP地址划分是照搬了其他参考书里面给出的IP地址的划分方式，笔者把这些IP地址重新进行了归类。共分为4类，一类是环回IP地址（表示本机IP地址），一类是内网IP地址，一类是公网IP地址，一类是其他。下面分别进行简短的介绍。

1. 环回IP地址。

A类网络号为127就是为环回地址预留的。通常情况下是127.0.0.1这个地址，或者是localhost，该IP地址不管机主是否联网，对这个IP地址的通信都是可以连通的，因为它就表示本地主机。ping一下环回地址试试，在命令行下输入：ping 127.1，不论是否联网这个地址都是通的。

2. 内网IP地址。

用于局域网内，比如网吧、企业、政府、学校的内部。它主要负责局域网的通信。内网地址开头都是10、192……一些地址。

3. 公网IP地址。

是ISP（网络服务供应商），比如网通、电信、铁通……提供的这些地址都是由动态ISP分配的，这个地址是真正在互联网上进行通信的IP地址。

4. 其他IP地址。

是TCP/IP中保留的用来做实验的一些IP地址，就比如D类和E类这些基本我们普通用户用不到的IP地址。

2.1.2　端口的作用与分类

IP地址可以在互联网上唯一地标识一台主机，那么端口号有什么用呢？两台计算机通信，其实是两台计算机上的不同进程间的通信。也就是说，木马的客户端和木马的服务端都是操作系统下的一个进程。Windows操作系统下是多进程的，即使IP地址可以标识一台主机，却无法得知是和哪个进程在通信，因为打开的网络软件实在太多了，比如QQ、IE……因此，端口号就是用来标识进程的。

端口号是16位的，范围在0〜65535之间。端口号根据传输层的传输协议的不同分为两种，一种是TCP端口，另一种是UDP端口。用扫描端口的软件可以区分出TCP和UDP端口。TCP和UDP是两种不同的协议，TCP协议用于可靠传输中，而UDP协议用于不可靠传输中。TCP的端口在通信过程中始终保持连接，直到通信结束，而UDP的通信是不需要连接的。两种协议各有优缺点，视具体情况而定。

2.2　网络编程基础知识

上面对两台计算机之间如何进行通信有了一个大概的了解，现在开始真正学习Windows下网络编程的相关知识。

2.2.1　通信模型

既然是通信，必然要遵循一个通信模型，常见的通信模型有C/S模型（即客户端/服务端模型）、B/S模型（即浏览器/服务端模型）。常见的木马是C/S模式的，不过也有木马是B/S模式的，只不过这种模式相对少见。接下来主要介绍C/S模型的开发。

C/S模型的开发，就是在服务器端上绑定一个IP地址和一个端口号，然后进行监听，等待客户端的连接，而客户端则是向服务器端指定的IP地址和端口号发起连接，服务器接受客户端的连接后，双方可以进行通信，这是基于TCP协议的通信。而基于UDP协议就简单多了，服务器只要绑定IP地址和端口号就可以了，客户端不需要进行连接直接就可以和服务器进行通信。从基于TCP和基于UDP的服务器可以看出，TCP比UDP要可靠，而UDP要比TCP效率高。

2.2.2　Winsock

Windows下的网络应用开发大部分是通过Winsock完成的（除了Winsock以外还有其他的），Winsock有两种开发模式，一种是阻塞模式，另一种是非阻塞模式。阻塞模式是基于同步的开发方式，非阻塞模式是基于异步的开发方式。非阻塞模式结合了Windows的消息机制，更符合Windows下的开发。在我们的学习过程中主要讨论非阻塞模式的开发。

下面开始讲解Windows下网络开发所需要了解的API函数。在介绍这些函数的时候，笔者只重点介绍部分参数，详细的参数介绍不是本书的重点，但是很多函数都有其固定的参数，大家会用即可。

2.2.3　Winsock的相关函数

每个需要使用Winsock进行网络开发的Windows应用程序都必须包含Winsock2.h（这是第二个版本的Winsock库），除了这个头文件以外，还有一个静态库ws2_32.1ib。在使用它们的时候需要对这个库进行一次初始化，使用完毕后要对该库进行释放。下面分别介绍这两个函数。

首先来看初始化ws2_32.dll动态链接库的函数：

这个函数是用来初始化ws2_32. dll动态链接库的，这个动态链接库是所有网络应用程序会加载的动态链接库，在使用这个动态链接库时就需要用WSAStartup()函数进行初始化。如果不初始化这个动态链接库，其余相关的基于这个动态链接库的网络函数的调用都会失败。

参数说明如下。

（1） wVersionRequested: Windows Sockets API提供的调用方可使用的最高版本号。高位字节指出副版本（修正）号，低位字节指出主版本号。

（2） lpWSAData:指向WSADATA数据结构的指针，用来接收Windows Sockets实现的细节。

释放ws2_32.dll动态链接库：

这个函数是结束这个动态链接库的，一般在程序退出时使用。

创建套接字：

参数说明如下。

（1） af：指定应用程序使用的通信协议族，对于TCP/IP协议族，该参数始终为PF_INET。有的教材里这个参数使用的是AF_INET.AF_INET是地址族，虽然使用这个没错，但还是建议使用PF_INET。

（2） type：指定要创建的套接字的类型，流套接字类型为SOCK_STREAM，数据包套接字类型为SOCK_DGRAM。前者通常是TCP协议使用，后者通常是UDP协议使用;

（3） protocal:指定应用程序所使用的通信协议。该参数根据第二个参数的不同而不同，第二个参数为SOCK_STREAM，该参数为IPPROTO_TCP;如果第二个参数为SOCK_DGRAM，那么该参数为IPPROTO_UDP。

该函数的返回值是一个新创建的类型为SOCKET的套接字的描述符。

关闭套接字：

程序结束时要对Socket创建的套接字进行关闭,完成资源的释放。

参数说明如下。

s: socket()函数创建的套接字描述符。

当创建了一个Socket后，服务器程序必须要绑定一个IP地址和特定的端口号。客户端程序不需要绑定端口号和IP地址，因为Socket会选择合适IP地址及端口来使用。

绑定IP地址和端口号：

参数说明如下。

（1） s：指定待绑定的Socket描述符。

（2） name：指定一个sockaddr结构，该结构的定义如下：

函数中提供的参数类型是sockaddr，在实际使用的过程中，结构体是sockaddr_in，该结构的定义如下：

成员变量sin_family设置为PF_INET; sin_port设置为端口号;sin_addr结构体中只包含一个公用体，in_addr的定义如下：

该成员变量是一个整数，一般用函数inet_addr()把字符串形式的IP地址转换成unsigned long整型的整数值。

namelen：指定name缓冲区的长度。

inet_addr函数的原型如下：

参数cp为一个点分多进制的IP地址。

inet_addr函数的逆函数如下：

参数为一个addr_in类型的变量。

监听端口：

参数说明如下。

（1） s：使流套接字s处于监听状态。

（2） backlog：为处于监听状态的流套接字s维护一个客户连接请求队列。

接受请求：

服务端程序调用该函数从处于监听状态的流套接字的客户端请求队列中取出第一个请求，并创建一个新的套接字与客户端进行连接通信。

参数说明如下。

（1） s：指定处于监听状态的套接字。

（2） addr:用来返回新创建的套接字的地址。

（3） addrlen:用来返回新创建套接字的地址结构的长度。

连接函数如下:

客户端程序调用该函数来完成与远程服务器端的连接。

参数说明如下。

（1） s:客户端创建的套接字。

（2） name:该结构中包含了要服务器端中的IP地址和端口号。

﻿

（3） namelen：指定name缓冲区的长度。

具体进行通信的函数分为两类，一类是基于TCP协议的，另一类是基于UDP协议的。数据的通信主要体现在数据的收发上，分别看一下这两种协议的收发数据的函数定义。

基于TCP的发送函数：

参数说明如下。

（1） s：指定发送端套接字描述符。

（2） buf：指明一个存放应用程序要发送数据的缓冲区。

（3） len：指明实际要发送到数据的字节数。

（4） flags：一般设置为0。

基于TCP的接收函数：

参数说明如下。

（1） s：指定接收端套接字描述符。

（2） buf：指定一个缓冲区，用来存放接收到的数据。

（3） len：指定缓冲区的长度。

（4） flags：一般设置为0。

基于UDP的发送函数：

基于UDP的接收函数：

2.2.4　字节顺序

数据在存储器中是按一定方式的，根据不同的CPU架构，其存储方式也不相同。比如Intel ×86CPU架构使用小尾顺序，即高位存放高字节，低位存放低字节。例如0×12345678，在内存里的表示方式为78 56 34 12。

还有一种是大尾方式，即按网络字节顺序，也就是说所有的网络中传输的数据使用的均是大尾方式。大尾方式是高位存放低字节，低位存放高字节。例如0×12345678，在内存里的表示方式为12 34 56 78，如图2-1所示。

图2-1　字节顺序

本地字节顺序根据CPU架构的不同可能是小尾方式，也可能是大尾方式，但是对于网络字节顺序来说，一定是大尾方式。Winsock提供了一些用来处理本地的字节顺序和网络的字节顺序的转换函数。这些函数如下：

本地字节顺序转换为网络字节顺序：

网络字节顺序转换为本地字节顺序：

当我们在调试软件的时候，经常会在内存中查找某一个数据，但是怎么也找不到，在这个时候就应该想到字节顺序的问题。在做破解和外挂的时候，不了解这些基本概念的话经常会感到困惑，请大家一定要注意这个细小的问题。

2.3　简单的通信程序

网络通信方面的基础函数我们已经了解了一些，把这些知识都连起来，就可以编写一个网络程序了。我们分别来写一个基于TCP协议和基于UDP协议的小程序。这是一个非常经典的程序，就是在编程界举世闻名的输出“Hello World!”！的程序，只不过是Winsock版本。

2.3.1　基于TCP协议的“Hello World!”

服务器端的代码编写流程如下：

只要把这些函数依次写完，服务器端的代码就写完了。

服务器端代码如下：

客户端的代码编写流程如下：

客户端的流程比服务器端的流程要更少一些，主要是省去了绑定IP和端口、监听等一些步骤。

客户端代码如下：

在上面的代码中，我们只是简单地把所有的API函数都串起来写了一遍，就能实现想要的功能。但是请注意，大家在写的时候一定要把所有函数的返回值加以判断，形成一个好的习惯。

2.3.2　基于UDP协议的“Hello World!”

UDP客户端与服务器端的编写方法与TCP的相似，只要注意其中的差别就可以了。

服务端代码如下：

客户端代码如下：

2.4　实现一个C/S模式的简单木马

C/S模式的木马的工作方式和网上聊天的工作方式没什么差别，都是基于TCP/IP协议的通信，都是在传递信息。不同的是，木马的客户端向服务器端发送的内容是控制命令，服务器端收到控制命令执行相应的功能，并将执行结果反馈给客户端，这就是远程控制。如果客户端加一些隐藏进程，复制自身到系统目录，然后自动启动……那么服务端就是一个木马了。

这个木马主要实现以下几个简单的功能：

（1）完成帮助命令的发送；

（2）可以查看远程主机的相关信息，以及把自身复制到可执行目录中；

（3）打开光驱和关闭光驱；

（4）交换鼠标左右键和恢复鼠标左右键功能。

2.4.1　木马服务器端的实现

这里，由于有一些内容还没有学到，所以暂时不去实现，在后面的章节里，再把木马的内容充实起来。不过这仅仅是学习，要开发一个专业的木马并不是一件容易的事情。最关键的是，我们做的一切都是为了学习编程，为了学习计算机安全，而不是为了搞破坏。希望大家谨记于心。

我们的木马是命令行下的木马，所以当实现功能多了以后很多命令就会忘记。为了方便使用，在木马中加入帮助系统，也就是在输入指定命令后，客户端会把命令发给我们。把相关的命令定义成宏，方便我们修改，定义如下：

这里有个问题，这个命令是定义在客户端好，还是定义在服务器端好？定义在客户端当我们查看命令字的时候，省去了一次网络的通信传输，速度相对较快。但是，我们的木马客户端只是简单地发送命令，服务器端根据不同的命令来完成相应的功能，从这样的角度来看，可能客户端基本上定下来以后就不再变了，而有变化的就是服务器端了。当连接了木马的服务器端以后，要确定当前连接的木马提供多少种功能，为了确定木马能执行哪些功能，因此笔者认为帮助定义在服务器端较好。

我们木马的主体形式是接收命令，其实就是让recv()函数接收字符串，根据接收的字符串进行匹配从而完成相应的动作。代码的大体形式如下：

以上代码是用来进行循环接收命令的，分发命令的部分就是一些判断和比较了，如下：

根据前面列举的那些功能我们逐个实现。

首先是完成帮助命令的发送，这个非常简单，只要调用send()就可以完成了，代码如下：

对于复制到系统目录这个功能大家请参考第1章，这里就不重复了，但是对于获得系统信息，就需要修改一下了。不能直接输出，而是需要定义一个结构体保存获取到的信息，以便发送给客户端，结构体如下：

把获取到的信息保存在该结构体中，发送给客户端就可以了。客户端的显示与第1章介绍的类似。发送代码如下：

对于光驱的打开与关闭、交换与恢复鼠标左右键功能的实现也非常简单，函数原型如下：

不对这两个函数进行介绍了，直接看代码吧。

以上就是木马服务器端的全部代码了。我们只是把一些简单的知识连贯起来使用，就是一个木马了。

2.4.2　木马客户端的实现

木马客户端的代码就是完成字符串的发送，代码非常简单，直接看完整的源代码。

上面是一个最简单的C/S模式的木马。是不是很简单啊？感觉我们的小马很“瘦”是吧？不足以日行千里是吧？有奢求，有欲望是好的。在后面的章节当中大家可以慢慢地补足这匹马儿的功能。再次提醒，本书的目的是和大家学习黑客编程，旨在通过黑客编程学习系统知识和安全知识，并非拿来做坏事的。

2.5　总结

本章内容简单易学，不会因为过多复杂难懂的代码而让大家失去了学习的热情。也许你觉得意犹未尽，也许觉得不够深入，我们的学习目标就是通过简单的知识，完成实现特定功能的程序。在后面学习的过程中，我们仍然以这种方式进行学习，也会随着这种学习方式逐步地深入了解编程知识。

第3章　Windows应用编程基础

刚开始接触编程的“玩黑”的读者可能都会觉得API是一个很神奇的且很万能的东西。其实，API只是Windows下进行开发的基础知识，当然基础并不代表简单，能真正用好API来开发程序也是非常不容易的，本章将重点介绍很多今后在编程中常会用到的API函数，包括进程、线程、文件、注册表、注入等相关的API函数。

3.1　文件

在Windows下文件有很多种，比如图片文件、视频文件、音频文件……除了常见的文件格式外，其实对于管道、邮槽，甚至是设备对象，在Windows下也被当做文件来对待，这样对于我们操作管道、邮槽、设备对象就像操作文件一样简单了。

对于操作任何文件，我们最先的操作是对文件的打开，操作结束时为了释放资源要进行关闭。我们依次学习如何使用这些API函数，然后完成一个简单的小例子。从“文件操作”开始，我们要接触MFC方面的编程，但不会很难，都是一些对界面和控件之类的使用。

3.1.1　打开文件

要对文件进行操作，就要首先对文件进行打开操作，文件打开成功会返回一个句柄，然后通过这个句柄对文件进行读写操作。

打开文件的API函数如下定义：

参数说明如下。

（1） lpFileName：要打开或创建的文件名。这里也可以不是文件名，可以是管道名、设备对象名。

（2） dwDesiredAccess：对文件的访问模式，通常是GENERIC_READ和GENERIC_WRITE。

（3） dwShareMode：对文件的共享模式。Windows是多进程的操作系统，在一个文件被访问时，可能会有另一个进程也访问该文件。这时第一个打开该文件的进程就要对其设置一个访问模式，即打开此文件后，其他进程对该文件可以有哪些操作。

（4） lpSecurityAttributes：安全属性，一般为NULL。

（5） dwCreationDisposition：在创建或打开的文件存在或不存在时该函数的处理方式。

（6） dwFlagsAndAttributes：打开或创建文件时的标志和属性。一般是FILE_ATTRIBUTE_NORMAL。

（7） hTemplateFile：文件模板，一般为NULL。

该函数若执行成功，则返回一个文件句柄；若执行失败，则返回INVALID_HANDLE_VALUE。

在对文件使用完毕后，应该对打开的句柄进行关闭以释放资源，关闭句柄的函数如下：

该函数就一个参数，这个参数就是调用CreateFile()函数时的返回值，也就是文件句柄。

3.1.2　文件操作

文件操作的种类有4种，分别是“增、删、改、查”。熟悉数据库操作的读者一定感觉这4种操作是针对数据库的，怎么对于文件的操作也是这4种呢？其实，不单单是对文件的操作，对注册表的操作，对服务的操作，对进程的操作……也都存在着“增、删、改、查”这些操作。还是讨论文件的这4种操作，文件的“增”其实就是“创建”文件，文件的“删”就是“删除”文件，文件的“改”就是对文件的“写”操作，文件的“查”就是对文件的“读”操作。当然了，对于文件的“读操作”分为狭义的和广义的两种。狭义的“读文件”就是读取文件的内容，对于广义的“读文件”来说，可以是读取文件的大小、读取文件的创建时间和修改时间……文件的创建是通过CreateFile()函数来完成的，文件的打开也是通过CreateFile()函数来完成。对于删除文件、写文件、读文件……这些API函数，下面将逐一进行介绍。

删除文件的API函数如下：

该函数的参数就只有一个，而且非常简单，就是要删除的文件的文件名。

文件的读函数如下：

参数说明如下。

(1) hFile：该参数是CreateFile()函数返回的句柄。

（2） lpBuffer：指向一个缓冲区，函数会将从文件中读出的数据保存在该缓冲区中。

（3） nNumberOfBytesToRead：要求读入的字节数，通常情况下是缓冲区的大小。

（4） lpNumberOfBytesRead：该参数是一个指向DWORD类型的变量，用于返回实际读入的字节数。

（5） lpOverlapped：一般设为NULL。

文件的写函数如下：

WriteFile()函数与ReadFile()函数的参数意义基本相同，所不同的是第二个参数，第二个参数仍然指向一个缓冲区，函数会将该缓冲区的内容写入到文件中。

设置文件指针的函数如下：

参数说明如下。

(1) hFile：CreateFile()函数打开文件的句柄。

（2） lDistanceToMove：指定要移动的距离。

（3） lpDistanceToMoveHigh：一个指向LONG型的指针，其保存一个要移动的距离的高32位。

（4） dwMoveMethod：指定移动的起始位置。

创建目录的函数如下：

参数说明如下。

（1） lpPathName：创建目录的目录名称；

（2） lpSecurityAttributes：一般为NULL。

移除目录的函数如下：

该函数的参数指定要移除的目录的目录名。

关于文件操作的函数就简单介绍这么几个，在以后的内容中将继续介绍。文件操作是Windows下编程的重要基础，应用非常广泛，希望读者能掌握以上介绍的函数。

3.2　AutoRun免疫程序的编写

我们通过上面学的几个函数来完成一个简单的AutoRun免疫程序。

3.2.1　AutoRun免疫原理

每当我们打开U盘时，看到有一个AutoRun.inf文件时都会不由地倒吸口凉气，“该死！又中病毒了！”。是的，有一种病毒程序通过AutoRun.inf文件使其自动运行起来，想必这不用做过多的介绍每一位都非常清楚。网上有很多免疫工具，现在一些修改版的操作系统里面也会提供这样一个免疫的小工具。它免疫的原理是建立一个不被删除的文件AutoRun.inf文件夹，以防止病毒生成一个运行病毒的AutoRun.inf。

3.2.2　AutoRun免疫程序的代码实现

我们的程序使用MFC编写，上面介绍的API函数会使用到两个，并且还会加入新的API函数。好了，开始我们的代码吧。

启动VC6，选择菜单“File”->“New”命令，在弹出的“New”对话框中选择“Projects”选项卡，在左侧的列表中选择“MFC AppWizard（exe）”，在右侧填写项目的名称“ImmunityU”，如图3-1所示。

图3-1　新建MFC项目工程

单击“OK”按钮后，出现“MFC AppWizard-Step 1”的界面，选择“Dialog based”单选钮，如图3-2所示。

图3-2　“MFC AppWizard-Step 1”界面

这里，我们选择以对话框为基础进行开发。单击“Finish”按钮会出现最后一个对话框，直接单击“OK”按钮关闭对话框。

3.2.3　界面设置

我们的程序是有界面的，则首先要对界面进行一些设置。先把界面上所有的控件都删除掉，然后添加一个ComBoBox控件和两个按钮控件，如图3-3所示。

给添加的ComBoBox控件重新命名一个ID为：IDC_COMBO_DRIVER，并为其添加一个控件变量。添加的方法如下：

在IDC_COMBO_DRIVER上单击右键，在弹出的菜单上选择“ClassWizard”命令，弹出“MFC ClassWizard”对话框，选择“Member Variables”选项卡，双击IDC_COMBO_DRIVER，﻿弹出“Add Member Variable”对话框，添加变量名为m_ComboDriver，选择类型为“Control”，然后单击“OK”按钮。添加情况如图3-4所示。

图3-3　程序界面

图3-4　给IDC_COMBO_DRIVER添加控件变量m_ComboDriver

3.2.4　代码相关部分

我们进行免疫时是对某个分区进行免疫的，因此要在ComboBox控件中显示出所有的磁盘分区供用户选择。在ComboBox中显示所有的磁盘分区，我们定义成一个函数来完成该功能。

把这个函数添加到OnInitDialog()函数的最后面，ComboBox中就会有所有的磁盘分区的盘符了。这个自定义函数中出现了两个我们尚不熟悉的API函数，一个是SetDlgItemText()，另一个是GetLogicalDriveStrings()。下面分别介绍这两个函数。

设置编辑框显示的内容：

该函数的第一个参数用来指定控件变量的ID，第二个参数指定一个字符串。

获取字符串类型的驱动器列表：

该函数的第一个参数指定缓冲区的长度，第二个参数指定一个缓冲区。

了解了这两个函数后，再来看代码就很容易理解了。在while循环中的AddString()是CComboBox类的一个成员函数，该函数是用来向CComboBox添加一个字符串的。如果你不理解那个while循环的话，建议在VC6下调试一下看看szDriverString里保存的内容，就会明白while循环的作用了。这个调试建议大家亲自动手练习一次，这是第一次要求大家动手调试。

接下来添加“免疫”按钮的代码，代码很简单，如下：

这段代码中加了详细的注释，大家应该可以看懂，就不做过多介绍了，如果哪里有疑惑的话，请大家自己动手调试一下。最后看一下“取消”按钮的代码：

整个对Autorun进行免疫的代码就完成了，大家可以自己进行一下试验。启动我们的程序，选择需要进行免疫的盘符，单击“免疫”，打开被免疫的那个磁盘，看看是不是真的免疫了，再试着用右键删除看看效果，可以看到我们的实现是成功的。

3.3　注册表操作

3.3.1　注册表

免疫Autorun的程序虽然完成了，但是并不是很完整，我们可以在注册表中把“自动播﻿放”禁止掉，这样即使有Autorun.inf文件，系统也不会运行它，系统的安全系数就会更高了。

注册表是Windows操作系统的一个重要的数据库，里面记录了系统几乎所有的信息。当然，由于注册表的功能非常强大，因此，注册表对于病毒、木马来说是非常有利用价值的地方。而对于反病毒软件来说，注册表是其需要加强守卫的地方。注册表，是一个正义与邪恶的必争之处。

注册表对于病毒来说可以利用的地方非常多，比如修改文件关联，增加系统的启动项，映像劫持，篡改浏览器主页……病毒的这些操作让我们不得不了解注册表相关的编程。

3.3.2　与注册表操作相关的常用API函数

对于学习黑客编程来说，注册表的操作当然不能少，现在就来学习关于注册表的操作。注册表的操作和文件的操作类似，也有打开、关闭、写入、查询等操作，也就是我们所说的“增、删、改、查”，只是使用的API函数不同而已。下面分别介绍每个API函数的使用方法。

打开注册表：

参数说明如下。

（1） hKey：指定一个父键句柄。

（2） lpSubKey：指向一个字符串，用来表示要打开的子键名称。

（3） phkResult：返回打开的子键句柄。

关闭注册表：

该函数只有一个参数，参数是打开子键的句柄。

注册表键值的查询：

参数说明如下。

（1） hKey：指向一个已被打开或创建的子键句柄。

（2） lpValueName：指定要被查询的键值的名称。

（3） lpReserved：保留，始终为NULL。

（4） lpType：返回被查询的值的类型。

（5） lpData：指向要查询数据的缓冲区。

（6） lpcbData：缓冲区的长度。

注册表键值的写入：

参数说明如下。

（1） hKey：指向一个已被打开或创建的子键句柄。

（2） lpValueName：指定要被查询或写入的键值的名称。

（3） Reserved：保留，始终为0。

（4） dwType：写入键值的类型。

（5） lpData：写入键值的缓冲区。

（6） cbData：写入键值缓冲区的长度。

注册表键值的枚举：

参数说明如下。

（1） hKey：指向一个已被打开或创建的子键句柄。

（2） dwIndex：查询的索引值。

（3） lpValueName：键名的缓冲区。

（4） lpcValueName：键名缓冲区的长度。

（5） lpReserved：保留，始终为NULL。

（6） lpType：返回被查询的值的类型。

（7） lpData：被查询的键值的缓冲区。

（8） lpcbData：被查询的键值的缓冲区的大小。

注册表键值项的删除：

参数说明如下。

（1） hKey：指向一个已被打开或创建的子键的句柄。

（2） lpValueName：指向欲删除的键值项的名称。

3.3.3　注册表启动项的管理

对于Windows操作系统来说，注册表中保存了非常多的系统配置，例如常见的IE主页﻿保存在HKEY_LOCAL_MACHINE\Software\Microsoft\Internet Explorer\Main下的Start Page中；再比如禁止磁盘驱动器的AutoRun功能在注册表的HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer下的NoDriveTypeAutoRun中进行设置；还有映像劫持、文件关联……很多配置都可以在注册表中完成。

对于安全工具来说，通过注册表发现所有的Windows启动项是十分必要的。在注册表的启动项中，除了正常的系统工具、软件工具外，病毒和木马也会利用注册表的启动项悄然地让自身进行启动。我们通过编写一个枚举注册表启动项的工具，来学习注册表相关的操作。我们的程序依然会使用到MFC中的控件，下面随笔者一步一步地学习吧。

3.3.4　程序的界面设置及相关代码

注册表中可以用来完成开机启动的地方非常多，我们不一一介绍了，这里只对注册表中某个可以完成开机启动的地方进行介绍，至于其他的地方大家可以自行搜集并完成。我们的程序依然是使用对话框的形式，其界面如图3-5所示。

图3-5　注册表启动管理器

这个界面就是我们已经编写好的软件的界面。在这个界面中，用到了CListCtrl控件，大家对其进行添加并进行相应的设置即可。在实例的介绍中，尽可能地少提及控件，因为这实非本书应该提及的内容，大家可以自行参考MFC开发相关的书籍。这里，给出一个关于CListCtrl初始化的代码：

3.3.5　启动项的枚举

在这个实例中，主要是枚举注册表中“HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run\”这个子键下的键值项。对于我们软件启动后，应该显示出在这个注册表子键位置下的所有启动项的内容，枚举的代码如下：

对于注册表启动项的管理来说，应该具备几个功能，首先是屏蔽启动项，然后是删除启动项，最后是添加启动项。在我们的程序中主要完成后两个功能，一个是删除启动项，另一个是添加启动项，至于屏蔽启动项这个功能就留给大家自己实现。在很多安全软件中，如Windows优化大师、360安全卫士中都有屏蔽启动项的功能，大家自己思考一下屏蔽启动项与删除启动项的区别在哪里，并如何实现。

下面分别来看一下添加启动项和删除启动项的代码。

3.3.6　添加启动项的代码

添加启动项，需要添加一个窗口，至于窗口的添加方法大家自行参考MFC开发相关的介绍，这里就不做过多的介绍了。完整的代码会提供给大家，方便大家进行参考，这里主要﻿是介绍与注册表操作相关的内容。添加启动项的代码如下：

3.3.7　删除启动项的代码

删除启动项的代码比添加启动项的代码要简单，但是在删除的时候要涉及到一个关于CListCtrl控件的编程，也就是选中哪个启动项进行删除，这是一个主要的问题，获取选中的启动项后的删除就非常简单了。代码如下：

对于启动项的管理软件的编写就到这里了，大家也可以在注册表中其他的可以使软件开机启动的位置添加进去，这样我们的软件就更加强大了。在我们不断地在注册表中发现有新的位置可以启动软件时，每次都需要进行代码添加，这样的工作太繁琐了。有没有什么好的方法当每次发现新的位置后可以不改变代码呢？请大家把前面学到的文件操作的知识联系起来进行考虑一下！我们可以把要枚举的注册表的子键保存到一个文件中，然后让程序去读取这些子键，在注册表中分别进行枚举。这样，以后每当在注册表中有新的需要枚举的内容，那么只要对这个文件进行修改就可以了，就不需要对程序本身进行修改了。大家可以试着写一个这样的程序。

3.4　服务相关的编程

服务是一种在操作系统启动时就启动的进程。在操作系统启动时有两种程序会一起随着系统启动，一种是普通的Win32程序，另一种是驱动程序。这里讨论的并不是要如何编写一个系统的服务，而是编写一个如何可以显示出这些随系统启动而启动的服务项。

3.4.1　如何查看系统服务

在Windows下，有很多服务是跟随操作系统一起进行启动的，具体有哪些服务是跟随操作系统一起启动的呢？如何查看呢？其实非常得简单。在“我的电脑”上单击鼠标右键，然后在弹出的菜单上选择“管理”命令会打开“计算机管理”工具，单击左侧树形列表中的“服务和应用程序”会出现一个列表，选择“服务”则在右侧出现了服务项列表，如图3-6所示。

图3-6　Windows下的服务管理程序

在这个列表中，只能查看到Win32应用程序的服务，无法查看关于驱动程序的服务。我们可以借助其他一些小工具来查看驱动服务的程序，如图3-7所示。

图3-7　使用SREng查看驱动服务程序列表

接下来编写一个程序，既可以查看应用程序服务列表，也可以查看驱动程序服务列表。编写的程序如图3-8所示。

图3-8　服务管理程序的界面

这个服务控制管理器依然是使用MFC的对话框进行开发的，其中还是用到了CListCtrl这个控件。让我们现在就动手开始打造一个自己的服务控制管理器吧。

3.4.2　服务控制管理器的开发

服务控制管理器的开发和注册表启动管理器的开发是差不多的，主要也是枚举服务并将其显示到列表控件中。至于对服务状态的控制，是通过服务相关的API函数来完成的。这次，我们先来看代码，希望通过阅读代码大家能自己掌握与服务相关的API函数。在代码的后面，﻿会对开发服务控制管理器涉及到的API进行相应的解释。

这里笔者强调一下，学习API函数的使用，最好的老师是MSDN。在大家学习编程的道路上，要通过阅读大量的代码来提高自己的编程能力，那时大家就要自行学习API函数的使用，因此希望大家能养成好的学习习惯，也就是自己能够独立查阅MSDN来进行API函数的使用。

服务控制管理器的界面在前面大家已经都见过了，界面的布局方面大家可以按照书中安排的那样来，也可以根据自己的喜好来。我们要枚举的服务分为“Win32服务应用程序”和“驱动程序”两类。对于枚举这两类服务，只是枚举函数的参数有所不同，这个函数参数是系统定义的两个常量，定义分别如下：

看一下服务枚举时的代码：

该成员函数有一个参数，这个参数就是用来指明是枚举“Win32应用程序服务”，还是用来枚举“驱动程序服务”的。也就是我们前面提到的两个系统定义的常量了，参数根据两个常量的不同，枚举到的服务也不同。

3.4.3　枚举服务的相关API函数

打开服务管理器：

参数说明如下。

（1） lpMachineName：指向欲打开服务控制管理器数据库的目标主机名，本机则设置为NULL。

（2） lpDatabaseName：指向了目标主机SCM数据库名字的字符串。

（3） dwDesiredAccess：指定对SCM数据库的访问权限。

这里介绍一下，SCM是服务控制管理器的意思，它是系统服务的一个组成部分，跟我们开发的软件不是一个概念。由于我们不是编写一个具体的服务，而只是对系统现有的服务进行一个枚举，因此，一些概念性的知识希望大家可以自行查阅相关的资料。

该函数调用成功，返回一个SCM句柄，否则返回NULL。

关闭服务句柄：

该函数用来关闭OpenSCManager()和OpenService()打开的句柄。

枚举服务：

参数说明如下。

(1) hSCManager：OpenSCManager()函数返回的句柄。

（2） dwServiceType：指定枚举的服务类型，也就是我们自定义函数的那个参数。

（3） dwServiceState：枚举指定状态的服务。

（4） lpService：指向ENUM_SERVICE_STATUS类型的指针。

（5） cbBufSize：指定缓冲区的大小。

（6） pcbBytesNeeded：返回实际使用的内存空间大小。

（7） lpServicesReturn：返回枚举服务的个数。

（8） lpResumeHandle：返回枚举是否成功。

ENUM_SERVICE_STATUS结构的定义：

SERVICE_STATUS结构的定义：

3.4.4　服务的停止

对于枚举到的服务，对我们来说只有两种操作，一种是启动服务，另一种是停止服务。系统中有很多我们不用的服务，它会随着电脑的启动而启动，这样既会影响系统的启动速度，也会占用宝贵的系统资源，因此，没用的系统服务要停掉。先来看看停止系统服务的代码。

3.4.5　停止服务的相关API函数

打开服务函数：

参数说明如下。

(1) hSCManager：指定由OpenSCManager()函数打开的服务句柄。

（2） lpServiceName：指向要打开的服务的名称。

（3） dwDesiredAccess：对要打开服务的访问权限。

控制服务函数：

参数说明如下。

(1) hService：指定一个由OpenService()打开的服务句柄。

(2) dwControl：指定了要发送的控制码，ControlService()可以对服务进行多种控制，每种控制对应一种控制码。

（3） lpServiceStatus：用于返回服务的状态。

3.4.6　服务的启动

启动服务的代码与停止服务的代码类似。这里简单地给出启动服务的部分代码，具体的代码自己动手写写。启动服务的部分代码如下：

启动服务相关API函数解释：

参数说明如下。

（1） hService：指定了要启动服务的句柄。

（2） dwNumServiceArgs：指向了启动服务所需的参数个数。

（3） lpServiceArgVectors：指向了启动服务的参数。

该函数最后两个参数类似main（int argc, char *argv[]）中的两个参数的作用。

3.5　进程与线程

3.5.1　进程

当按下Ctrl+Shift+Esc组合键时，就打开了Windows任务管理器对话框，里面有很多进程列表，如图3-9所示。

图3-9　任务管理器

进程是运行当中的程序，是向操作系统申请资源的基本单位。我们运行一个记事本程序，那么就相应会创建一个记事本的进程。当关闭记事本时，进程也随即结束了。对于这个任务管理器中，我们对进程比较关心的是“映像名称”、“PID”两项。对于进程相关的，我们主要学习进程的启动、结束、枚举等编程。

3.5.2　进程的创建

任何一个计算机文件都是一个二进制文件，对于可执行程序来说，它的二进制数据是可以被CPU执行的。程序的概念是一个静态的概念，程序本身只是存在于硬盘上的一个二进制文件。当用鼠标双击了某个可执行程序以后，这个程序就被加载，如内存，这时就产生了一个进程。进程会向系统申请各种所需的资源，并且会生成一个主线程，线程会拥有CPU执行时间，占用进程申请的内存……那么在编程的时候用API函数启动一个进程也是我们要学习的内容。可以创建进程的API函数有WinExec()、ShellExecute()和CreateProcess()等。这里主要介绍WinExec()和CreateProcess()两个函数。

3.5.3　“下载者”的简单演示

WinExec()函数介绍：

参数说明如下。

（1） lpCmdLine：指向一个欲执行的可执行文件。

（2） uCmdShow：程序运行后的窗口状态。

对于第一个参数比较好理解，比如要执行“记事本”程序，那么这个参数就可以是“c:\windows\system32\notepad.exe”。对于第二个参数是指程序运行窗口的状态，常用的有两个，一个是SW_SHOW，另一个是SW_HIDE。第一个参数是程序运行后窗口显示，另一个是程序运行后窗口不显示。大家可以试着创建一个不显示窗口的“记事本”程序。代码很简单，如下：

这样创建的“记事本”进程，在任务管理器中可以看到“notepad.exe”这个进程，但是无法看到其窗口界面。

WinExec()这个函数多用在“下载者”中，“下载者”的英文名字叫“Downloader”，也就是下载器的意思，它是一种恶意程序，该恶意程序的功能较为单一（相对木马来说功能单一）。该恶意程序的功能是让受害计算机到黑客指定的URL地址去下载更多的病毒文件或木马文件并运行。下载者的体积较小，容易传播。当“下载者”下载到病毒或木马后，通常都会使用WinExec()来运行病毒。

我们简单地来做一个“下载者”的演示，记住，这只是一个演示。不要企图拿来做坏事，因为我们的演示代码很轻易地会被杀毒软件杀掉，我们的目的是学习编程知识。

我们要完成一个模拟的“下载者”，既然是“下载者”，重要的当然是文件的下载了。文件的下载方式比较多，但是相对简单而又比较常用的函数是URLDownloadToFile()，这个函数也是经常出现在“下载者”中的函数，该函数的定义如下：

参数说明如下。

（1） szURL：指向URL地址的字符串。

（2） szFileName：指向要保存地址的字符串。

其余参数我们不做介绍，如果需要了解大家可以自行查阅MSDN。

对于使用URLDownloadToFile()函数，需要包含Urlmon.h头文件及Urlmon.lib库文件，否则当编译和链接时会无法通过。

既然了解了该函数的使用，那么我们就来完成一个模拟的“下载者”吧。代码如下：

我们的模拟是把C盘系统目录下的记事本程序下载到D盘下并保存为Virus.exe，然后运行它。这里只是一个简单模拟，如果要真正完成一个“下载者”的话，其代码要复杂很多。如果要在源代码上对其进行“免杀”，那么要考虑的问题也会很多。

3.5.4　CreateProcess()函数介绍与程序创建

通常情况下，我们创建一个进程都会选择使用CreateProcess()，该函数的参数非常多，功能更强大，使用也更为灵活。WinExec()函数的使用相对简单，只能完成简单的进程创建工作。如果要对创建的进程有控制的能力，那么必须使用CreateProcess()函数。

在介绍CreateProcess()函数之前，先来考虑一个问题。在我们编写C程序时，如果是控制台下的程序，那么编写程序的入口函数是main()函数，也就是我们通常所说的主函数。如果是一个Windows程序，那么入口函数是WinMain()函数，即使使用MFC进行开发，也是有WinMain()函数的，只不过它被庞大的MFC框架给隐藏了。那么我们的函数真的是从main()函数或者是WinMain()开始执行的吗？我们在写控制台程序时，如果需要给程序提供参数，那么这个参数是从哪里来的呢？

我们使用VC6来写一个简单的程序，并调试一下，看程序是否真的是由main()函数开始的。我们选择第1章介绍的输出“Hello World”的程序来演示。

是否还记得第1章写的在控制台下打印“Hello World”字样的程序呢？好，让我们回忆一下当时写的程序吧，如图3-10所示。

图3-10输出“Hello World”的程序

接下来要怎么做呢？按下F10键，这时，我们的程序在VC6下处于调试状态，打开“CallStack”窗口，如图3-11所示。

图3-11　CallStack窗口内容

双击“mainCRTStartup() line 206 + 25 bytes”这一行，查看代码编辑窗口的内容，如图3-12所示。

图3-12　当前代码编辑窗口内容

可以看到，绿色三角指向那行代码是对main()函数的调用，并且main()函数还有返回值。滚动代码，查看一下mainret变量的类型。该变量的定义如下：

int mainret;

该变量的类型为int型。那么，当定义main()函数时，main()函数的返回值是什么呢？通常情况下，定义main()函数的返回值是int型。但是，也有一部分人喜欢把main()函数的返回值定义为void型，也就是空型。那么，如果定义为void型，mainret接收的返回值是什么呢？在Windows下约定，函数的返回值保存在eax寄存器中。如果对main()函数的返回值定义为void型，那么当main()函数返回后，mainret中保存的是退出main()函数后eax寄存器中的值。

除了这个以外，大家看一下VC6的标题栏，标题栏上给出的这个“.C”文件，我们叫做启动代码，用于在启动一个进程后对全局变量等内容的初始化。再看一下图3-11，CallStack还有一行，双击它，观察代码编辑器中的内容。由于这部分不是我们讨论的重点，因此就不进行介绍了。如果有兴趣的话，大家可以自行学习研究。

下面介绍CreateProcess()函数的使用。

CreateProcess()函数原型：

参数说明如下。

（1） lpApplicationName：指定可执行文件的文件名。

（2） lpCommandLine：指定可执行文件的运行参数。

（3） lpProcessAttributes：进程安全属性，该值通常为NULL，表示为默认安全属性。

（4） lpThreadAttributes：线程安全属性，该值通常为NULL，表示为默认安全属性。

（5） bInheritHandles：指定当前进程中的可继承句柄是否可被新进程继承。

（6） dwCreationFlags：指定新进程的优先级以及其他创建标志。

该参数一般情况下可以为0。

如果要创建一个被调试进程的话，需要把该参数设置为DEBUG_PROCESS。创建进程的进程为父进程，被创建的进程为子进程。也就是说，父进程要对子进程进行调试的话，需要指定DEBUG_PROCESS。在指定了DEBUG_PROCESS后，子进程创建的子进程，同样也处在被调试状态中。

如果不希望子进程创建的子进程处在调试状态，那么需要同时指定DEBUG_ONLY_THIS_PROCESS。

在有些情况下，如果希望被创建的子进程暂时不要运行，那么可以指定CREATE_SUSPENDED参数。事后希望该子进程运行的话，那么可以使用ResumeThread()函数使子进程恢复运行。

（7） lpEnvironment：指定新进程的环境变量。

（8） lpCurrentDirectory：指定新进程使用的当前目录。

（9） lpStartupInfo：指定新进程的启动信息。

该参数是一个结构体，该结构体决定进程启动的状态，该结构体的定义如下：

该结构体在使用前，需要对cb这个成员变量赋值，该成员变量用于保存结构体的大小。该结构体的使用不做过多的介绍了，如果要对新进程的输入输出重定向的话，会用到该结构体。

（10） lpProcessInformation：返回新进程、进程线程等相关信息。

该参数同样是一个结构体，该结构体的定义如下：

该结构体中保存着新创建进程的句柄、进程主线程的句柄，还有进程ID和主线程的ID。

下面进行一个简单的演示，用CreateProcess()创建一个记事本进程代码如下：

对于进程创建后PROCESS_INFORMATION接收的两个句柄，需要进行关闭。

3.5.5　进程的结束

介绍完进程的创建，对于进程的结束，通常情况下希望程序可以自己进行结束，也就是进程正常退出运行状态。在进程正常进行退出时，会调用ExitProcess()函数，该函数使用简单，这里就不进行介绍了。我们主要介绍一下，如何实现类似任务管理器那样结束其他的进程。

结束其他进程需要使用到TerminateProcess()这个函数，该函数的定义如下：

该函数的参数比较少，只有两个，第一个参数是要结束进程的进程句柄，第二个参数通常给0。那么这里遇到一个问题，我们如何获得要结束进程的进程句柄呢？方法有两种，第一种方法是枚举进程列表，找到要结束的进程，枚举进程的内容稍后会介绍。第二种方法是根据要结束进程的窗口标题获得该窗口的句柄，然后通过窗口句柄得到进程的PID，有了进程的PID后，我们可以打开该进程得到进程的句柄，然后将其结束。我们这节主要介绍第二种方法。

下面通过介绍如何结束“记事本”进程来学习笔者所说的第二种方法。先来看一下代码。

我们打开一个“记事本”，然后编译运行这段代码，会发现“记事本”被关闭了。我们逐个介绍我们使用到的这些API函数。

首先来介绍FindWindow()这个函数，FindWindow()函数的原型如下：

该函数有两个参数，第一个参数是类名，第二个参数是窗口名。该函数的返回值就是窗口的句柄。这两个参数只要指定一个就可以了。对于我们来说，希望通过进程的窗口名来获得窗口的句柄，因此这里只要给出窗口名就可以了。在我们的代码中，“记事本”的窗口名为“无标题-记事本”，如果窗口名取得不准确的话，那么将无法获得该窗口的窗口句柄。怎样才能准确无误地获取窗口的名称呢？这里推荐大家使用一款VC6中自带的工具——SPY++。只要安装了VC6后，就会安装该工具，大家可以在开始菜单中找到，其具体位置为：“开始” -> “程序” -> “Microsoft Visual Studio 6.0” -> “Microsoft Visual Studio 6.0 Tools” -> “Spy++”。打开“Spy++”这个工具，如图3-13所示。

单击工具栏中的按钮，该按钮为“Find”按钮，出现如图3-14所示的“Windows Search”界面。

用鼠标拖动“Finder Tool”后面的那个图标到“记事本”进程的标题栏上，该窗口会显示出“记事本”的窗口名，如图3-15所示。

图3-13　Spy++工具界面

图3-14　“Window Search”界面

图3-15　“Window Search”显示“记事本”的窗口名称

在“Caption”中的内容就是“记事本”程序的窗口名称，我们把它作为FindWindow()函数的第二参数，这样，可以获取该窗口的窗口句柄，再通过窗口句柄获得该窗口所属进程的ID。代码如下：

该函数第一个参数是获取到的窗口句柄，第二个参数是一个输出参数，会返回该进程的ID。

在获得了进程ID后，通常通过使用OpenProcess()来获得进程的句柄。该函数的原型如下：

参数说明如下。

（1） dwDesiredAccess：进程欲获得的访问权限，该参数为了方便可以始终为PROCESS_ ALL_ACCESS。

（2） bInheritHandle：指定获取的句柄是否可以继承，一般情况下为FALSE。

（3） dwProcessId：指定欲打开的进程ID号。

该函数的返回值为进程的句柄，通过这个句柄可以结束进程。

既然已经获得了进程的句柄，那么就可以结束指定的进程了。在结束完进程后，也要记得用CloseHandle()关闭用OpenProcess()打开的进程句柄。

3.5.6　进程的枚举

进程的枚举就是把所有的进程都显示出来。当然了，有一些特意隐藏的进程是无法通过常规的枚举方式枚举到的。这里只介绍应用层的进程枚举。在应用层枚举进程有多种方法，这里只介绍相对常见的枚举进程的方法。在学习进程的枚举的过程中，我们会完成一个自己的进程管理器，如图3-16所示。

图3-16　进程管理器

在进程管理器中，除了对进程的枚举，还会对线程进行枚举，还有对进程中加载的DLL进行枚举。要枚举的内容非常多，但枚举的API函数都类似。下面介绍以下几个API函数。

枚举进程需要的API函数分别有CreateToolhelp32Snapshot()，该函数的作用是对当前系统中的进程进行一个快照，在创建快照以后进行逐个进程的枚举。枚举进程的函数是Process32First()/Process32Next()。如果是枚举线程的话，那么枚举函数是Thread32First()/Thread32Next()。如果是枚举进程中的DLL的话，那么枚举函数是Module32First()/Module32Next()。在使用以上这些函数的时候，需要包含Tlhelp32.h头文件，否则在编译的时候会提示使用了未定义的函数。针对以上函数，分别进行一个简单的介绍。

CreateToolhelp32Snapshot()函数的原型：

参数说明如下。

（1） dwFlags：该参数指明要建立系统快照的类型，对于要枚举的内容，该参数可以指定如下值。

①TH32CS_SNAPMODULE：在枚举进程中的DLL时指定。

②TH32CS_SNAPPROCESS：在枚举系统中的进程时指定。

③TH32CS_SNAPTHREAD：在枚举系统中的线程时指定。

（2） Th32ProcessID：该参数根据dwFlags的不同而不同。如果枚举的是系统中的进程，或系统中的线程时，该参数为NULL;如果枚举的是进程中加载的DLL的话，那么该参数为进程ID号。

该函数返回一个快照的句柄，在进行枚举时都会用到该句柄。

Process32First()函数的原型：

参数说明如下。

(1) hSnapshot：该参数为CreateToolhelp32Snapshot()返回的句柄；

（2） lppe：为指向一个PROCESSENTRY32结构体的指针，该结构的定义如下。

在使用该结构体时，需要对该结构体中的成员变量dwSize进行赋值，该变量保存PROCESSENTRY32结构体的大小。

Process32Next()函数的原型：

该函数的使用与Process32Next()类似。对于枚举进程中加载DLL的枚举，对于系统中线程的枚举，都与此函数类似，只是枚举DLL与线程时，XXX32First()与XXX32Next()的第二个参数指向的结构体不同。这4个函数的使用请大家参考MSDN自行学习。

对于枚举进程的API函数已经学习了，下面看一下枚举进程、枚举进程中加载DLL的代码吧。

枚举系统进程的代码：

枚举指定进程中加载DLL的代码：

3.5.7　调整当前进程的权限

我们枚举的代码基本上是完成了，到VC6下直接按Ctrl+F5组合键运行程序，可以看到我们枚举的进程都出来了。然后选中“svchost.exe”进程，单击“查看DLL”按钮，“svchost.exe”进程中加载的DLL也都枚举出来了，这样运行是没有问题的。接下来找到编译好的任务管理器运行（不要直接在VC6下运行），可以看到，我们枚举的进程也都显示出来了。仍然选中“svchost.exe”，然后单击“查看DLL”，是不是没有查看到“svchost.exe”进程加载的DLL文件，这是什么原因？换一个其他的进程试试，比如选择自己编写的任务管理器试试，可以查看其DLL文件。通过试验发现，系统文件的DLL我们都无法枚举到，可是在VC6下直接运行是可以枚举到的。不单单是这方面的问题，而且在使用OpenProcess()函数打开如smss.exe、winlogon.exe等系统进程的时候，也同样会导致函数的调用失败，其实这个问题是当前进程权限级别不够所导致的。解决这个问题很容易，只要当前进程具有“SeDebugPrivilege”权限就可以了，接下来就来说明这个调整当前进程的权限。

调整权限其实并不复杂，主要有3个步骤。

(1)使用OpenProcessToken()函数打开当前进程的访问令牌。

(2)使用LookupPrivilegeValue()函数取得描述权限的LUID。

(3)使用AdjustTokenPrivileges()函数调整访问令牌的权限。

调整权限使当前进程拥有“SeDebugPrivilege”权限，拥有这个权限后，当前进程可以访问一些受限的系统资源。在后面讲到远程线程注入的时候，同样需要调整当前进程的访问令牌权限，否则是无法对系统进程进行注入的。因为在进行注入的时候，同样要用到OpenProcess()这个函数。下面给出调整权限的代码，不过在这个代码里面没有做返回值的判断，通常情况下是没有问题的。代码如下：

3.5.8　进程的暂停与恢复

在有些时候，我们不得不让进程暂停运行。比如，病毒有两个运行的进程，它们在不断的“互相帮助”，当一个病毒进程发现另一个病毒进程被结束了，那么它会再次把那个病毒运行起来。由于两个进程一直在做这样的事情，而且频度很高，因此很难把两个进程都结束掉。这样，就不得不让病毒的进程暂停了，当两个进程都暂停掉以后，就可以把病毒结束掉了。

让进程暂停，通常使用的是SuspendThread()函数。该函数的定义如下：

该函数就一个参数，是一个线程的句柄。获得线程的句柄需要使用OpenThread()来打开一个指定的线程。对于得到线程ID，我们可以对线程进行枚举，然后就可以打开线程并将其暂停了。

注：OpenThread()函数在VC6提供的PSDK中是不存在的，必须更新PSDK才可以使用。如果没有更新PSDK的话，需要使用LoadLibrary()和GetProcAddress()来使用该函数。对于LoadLibrary()和GetProcAddress()函数的使用在DLL编程中将会进行介绍。

枚举线程的函数是Thread32First()和Thread32Next()这两个，对于枚举线程前，我们用CreateToolhelp32Snapshot()只能创建系统的线程快照，不能创建指定进程中的线程的快照。这样在暂停线程时，必须对枚举到的线程进行判断，看其是否为指定进程中的线程。如何判断一个线程是属于哪个进程的呢？我们看一下THREADENTRY32这个结构体。

该结构体中，th32ThreadID标识了当前枚举到的线程的线程ID，th320wnerProcessID标识了该线程归属的进程ID。因此，只要进行一次简单的判断就可以了。看一下暂停的代码：

与进程暂停相对应的是恢复暂停的进程。恢复暂停的进程的函数是使用ResumeThread()函数，该函数原型如下：

该函数的使用方法与SuspendThread()一样。恢复暂停的进程的代码大家可根据暂停进程的代码自行修改，这里就不给出完整的代码了。

在进程相关的最后部分给大家介绍一个不错的工具，该工具的界面如图3-17所示。

图3-17　Process Explorer的界面

该软件的功能非常强大，当启动一个进程或者结束一个进程的时候，该软件会高亮显示被启动或结束的进程。当然了，它的功能非常多，还是大家自己研究挖掘一下。在这里重点介绍该工具中的一个小功能，单击菜单“Options” -> “Replace Task Manager”命令，该功能是用来替换系统的任务管理的，也就是将Process Explorer设为默认的任务管理器。大家替换一下任务管理器，然后按下Ctrl+Shift+Esc组合键试试看，是不是Process Explorer被打开了，原来的任务管理器不见了。如果想要还原到原来的任务管理器，只要再次单击“Replace Task Manager”菜单项就可以了，我们单击一下该菜单还原到原来的任务管理器。

该功能是如何实现的呢？原理其实是对注册表做了手脚，对注册表的哪些地方做了手脚呢？我们介绍另外一个值得推荐的工具，叫做Regmon，它是用来监控注册表的。该软件如图3-18所示。

图3-18　RegMon界面

按Ctrl+L组合键，弹出“Regmon Filter”界面，我们在“Include”文本框中输入“procexp.exe”，如图3-19所示。

图3-19　RegMon Filter界面

输入完后单击“OK”按钮，再单击Process Explorer的“Replace Task Manager”菜单项，看RegMon捕获到的注册表的信息，如图3-20和图3-21所示。

图3-20　修改的注册表项

图3-21　修改的注册表键的值

打开注册表编辑器看一下被修改的内容，如图3-22所示。

图3-22　注册表中被修改的值

将该值删掉，再按下Ctrl + Shift + Esc组合键看一下，默认的任务管理器出现了，这就是注册表中有名的映像劫持。大家可以自己在我们编写的任务管理器中添加这样一个替换系统任务管理器的功能以做练习。关于注册表的操作，大家可以参考前面学习的内容。

3.5.9　多线程

线程是进程中的一个执行单位（每个进程都必须有一个主线程），一个进程中可以有多个线程，而一个线程只存在于一个进程中。在数据关系上，这是一对多的关系。线程不拥有系统资源，线程所使用的资源全部由进程向系统申请。

在多处理器中，不同的线程可以同时运行在不同的CPU上，这样可以提高程序运行的效率。除此而外，在有些方面必须要使用多线程。比如，如果扫描磁盘并同时在程序界面上显示当前扫描的位置，这样就必须使用多线程。因为在程序界面上显示和磁盘的扫描工作在同一个线程中，而且界面也不停地进行重新显示，这样就会导致软件看起来像是卡死一样。如果分为两个线程就可以解决该问题。界面的显示由主线程完成，而扫描磁盘的工作由另外一个线程工作，两个线程协同工作，这样就可以达到我们想要的效果了。

首先了解一下线程的创建，线程的创建使用CreateThread()函数，该函数的原型如下：

参数说明如下。

（1） lpThreadAttributes：该函数指向一个安全属性，该参数一般设置为NULL。

（2） dwStackSize：该参数指定线程的栈大小，该参数一般设置为0，表示默认栈大小。

（3） lpStartAddress：该参数指向一个线程函数地址，该函数属于一个回调函数。所谓回调函数，就是由系统去调用该函数，并不是由我们直接调用该函数。

线程函数的定义如下：

线程函数的返回值为DWORD类型，该函数只有一个参数，该参数由CreateThread()函数给定。该函数的函数名称可以任意给定。这里介绍一下WINAPI, WINAPI是一个宏，该宏的定义如下：

这是一种函数的调用约定，该调用约定会在后面的章节中介绍，在这里只需要了解就可以了。

（1） lpParameter：该参数是传递给线程函数的一个参数。

(2) dwCreationFlags：该参数指明创建线程后的线程状态，在创建线程后可以让线程立刻执行，也可以让线程处于暂停状态。如果需要立刻执行，将该参数设置为0；如果要让线程处于暂停状态，那么该参数值设为CREATE_SUSPENDED;需要线程执行时调用我们前面介绍过的ResumeThread()函数。

（3） lpThreadId：该参数用于返回新创建线程的线程ID。

该函数返回新创建线程的句柄，在线程结束后需要使用CloseHandle()函数关闭该句柄以便释放资源。我们来写一个简单的CreateTheadTest例子，代码如下：

从该代码的意图来看，主线程会打印一行“main”，而创建的线程会打印一行“ThreadProc”。编译运行，查看一下运行结果，如图3-23所示。

图3-23　CreateTheadTest运行结果

跟我们想要的结果不一样，这是怎么回事呢？每个线程都有自己的CPU时间片，当主线程创建了新线程后，它的CPU时间片并没有完，它还可以继续执行。由于主线程的代码非常少，在CPU指定的CPU时间片中主线程执行完后就退出了。主线程结束，那么意味着程序也就结束了，所以我们自己创建的线程根本就没有被执行到。如果要等我们自己创建的线程结束后才可以输出“main”的话，主线程要如何等待我们创建的线程呢？答案是使用WaitForSingleObject()函数，该函数的原型如下：

参数说明如下。

（1） hHandle：该参数指向要等待的对象句柄。

（2） dwMilliseconds：该参数指定等待超时的毫秒数，设置为INFINITE则表示一直等待到线程函数返回。INFINITE是系统给出的一个宏，该定义如下。

该函数如果返回为WAIT_OBJECT_0，表明制定的对象已经是处于完成状态；如果返回值为WAIT_TIMEOUT，则表明在指定的时间内没有完成，处于潮湿状态。若该函数调用失败，则返回WAIT_FAILED。

修改上面的代码，在CreateThread()函数的后面加入以下代码：

添加WaitForSingleObject()以后，主线程会等待我们的创建的线程结束后再执行主线程后续的代码。这样，在控制台上就会分别打印“ThreadProc”和“main”了。

WaitForSingleObject()只能等待一个线程，可是在程序中往往要创建很多个线程来执行，那么如果需要等待若干个线程的完成状态的话，WaitForSingleObject()就无能为力了。不过，系统提供了其他的函数来让我们等待多个线程的完成状态，该函数的定义如下：

在使用多线程时常常需要注意很多问题。比如，多个线程同时对某一个共享资源进行操作，那么可能就会出现问题。来看一个简单的例子：

每个线程都有一个CPU时间片，当自己的时间片运行完成后，CPU会停止该线程的运行，并切换到其他线程去运行。当多线程同时操作一个共享资源时，这样的切换会带来隐形的问题。我们的代码比较短，在一个CPU时间片内会完成，因此可能看不出错误，为了达到出错的效果，在代码中加入Sleep（1），主动让出CPU，让CPU进行线程的切换。这里的共享资源就是那个全局变量g_Num_One。调用该线程函数的代码如下：

我们创建10个线程，每个线程都让g_Num_One进行10次自增1的操作，那么g_Num_One的结果应该是100。我们实际运行一下看看结果是多少，如图3-24所示。

这个输出结果并不是我们想要的输出结果。在多线程环境中，对共享资源的操作要进行保护，在这里，我们可以使用临界区对象对该全局变量进行保护。

图3-24　运行结果

临界区对象是一个CRITICAL_SECTION的数据结构，Windows操作系统使用该数据结构来进行对关键代码的保护，以确保多线程下的共享资源。

对于临界区的函数有4个，分别是初始化临界区对象（InitializeCriticalSection()）、进入临界区（EnterCriticalSection()）、离开临界区（LeaveCriticalSection()）和删除临界区对象（DeleteCriticalSection()）。这4个函数的定义分别如下：

这4个函数的参数都是指向CRITICAL_SECTION结构体的指针。我们修改的代码如下：

再次编译运行该代码，输出的结果为正确结果，即g_Num_One的值为100。除了临界区对象以外，对于线程的同步还有其他的方法，这里就不进行一一的介绍了。希望大家在今后开发多线程编程时，切记要注意多线程的同步问题。

3.6　DLL编程

DLL （Dynamic Link Library，动态链接库）是一个可以被其他应用程序调用的程序模块，其中封装了可以被调用的资源和函数。动态链接库的扩展名一般是DLL，不过有时也可能是其他的。DLL文件也属于可执行文件，只不过它是依附于EXE文件来被执行的。一个DLL文件可以被多个EXE文件加载。

3.6.1　什么是DLL

Windows操作系统下有非常多的DLL文件，有的是操作系统的DLL文件，有的是应用程序的DLL文件。DLL文件有什么好处呢？ DLL是动态链接库，相对应的有静态链接库。动态链接库是在EXE文件运行时被加载执行的，而静态链接库是在OBJ文件进行连接时同时被保存到程序中。动态链接库可以减少可执行文件的体积，在需要的时候进入内存等很多好处。

3.6.2　编写一个简单的DLL程序

我们编写一个简单的DLL程序，并在DLL程序中添加一个导出函数。所谓导出函数，就是DLL提供给外部EXE或其他类型的可执行文件调用的函数，当然DLL本身也可以自己进行调用。我们启动VC6来编写一个DLL程序。

启动VC6程序，单击菜单“文件” -> “新建”命令，在“Projects”选项卡中的左边选择“Win32 Dynamic-Link Library”，在“Project name:”文本框中填写“FirstDll”，如图3-25所示。

单击“OK”按钮，出现“Win32 Dynamic-Link Library-Step 1 of 1”界面，选择“A simple DLL Project”项，单击“Finish”按钮。在然后出现的对话框中直接单击“OK”按钮即可。在创建好该工程后，VC6自动生成如下代码：

图3-25　新建DLL工程

DLL程序的入口函数是DllMain()，该函数有以下3个参数。

（1） hModule: DLL模块的句柄。

（2） ul_reason_for_call: DllMain函数被调用的原因。

该参数的取值有4种，分别是DLL_PROCESS_ATTACH（当DLL被某进程加载时DllMain被调用）、DLL_PROCESS_DETACH（当DLL被某进程卸载时DllMain被调用）、 DLL_THREAD_ATTACH （进程中有线程被创建时DllMain被调用）和DLL_THREAD_DETACH（进程中有线程结束时DllMain被调用）。

（3） lpReserved：保留项，也就是Windows的保留参数。所谓保留参数不是不使用的参数，是Windows不想让我们知道作用的参数。

DllMain()前面有一个APIENTRY，这是一个宏，该宏定义如下：

我们在前面介绍过，WINAPI也是一个宏，该宏表示一种函数调用约定。

DllMain()函数不只一次地被调用，每次的调用可能执行不同的代码，比如当进程加载该DLL时，可能在DLL中要申请一些资源，在进程卸载该DLL时，DLL则要释放自己所申请的资源。处于这些原因，在写DLL程序时，需要这样把DllMain()函数写成如下形式：

这样写就可以达到根据不同的调用原因执行不同的代码。我们添加一个简单的导出函数。

该函数的定义如下：

extern “C”表示该函数以C方式导出，由于源代码是。CPP文件，因此，如果按C方式导出的话，那么在编译后函数名会被名字粉碎，因此在动态调用该函数时就会极为不方便。该函数的实现如下：

运行函数后弹出一个对话框，显示一个字符串，并显示其所在的进程的进程名。我们分别在DLL_PROCESS_ATTACH和DLL_PROCESS_DETACH下加一个对该函数的调用，代码如下：

编译该代码，会生成两个对我们有用的文件，一个是“FirstDll.dll”，另外一个是“FirstDll.lib”，前面的是DLL文件，后面的是库文件，该库文件中包含着导出函数的相关信息。

3.6.3　对DLL程序的调用方法一

DLL程序就写到这里，接下来写个调用该DLL的程序测试一下吧。我们需要对该DLL进行两方面的测试，一个是看当加载和卸载该DLL时，是否会弹出对话框；另外一个是调用这个DLL的导出函数看是否能成功。

在工作区的“Workspace ‘FirstDll’:1 project”上单击右键，在弹出的菜单中选择“Add New Project to Workspace …”，如图3-26所示。

弹出“New”对话框，在“Projects”选项卡的左面选择“Win32 Console Application”，在“Project name:”文本框中填写：“DllTestl”。单击“OK”按钮，在出现的对话框中选择“A Simple Application”，该处和前面内容类似。在左面的工作区打开我们新建的工程，在DllTest1.cpp中添加代码：

图3-26　添加测试工程

对该代码进行编译连接，这时并没有生成我们想要的可执行文件，在编译连接过程中出错了，出错信息如图3-27所示。

图3-27　出错信息

这个错误确切地说是连接错误，原因是找不到DLL的Lib文件，我们把“FristDll.lib”文件复制到DllTest1这个工程的目录下，再次编译连接，这次通过了。那么我们就来运行这个DllTest1的程序。不过很可惜，运行出错，错误提示如图3-28所示。

图3-28　运行DllTest1时的错误信息

这个错误的原因是DllTest1找不到FirstDll.dll这个文件，把开始编译好的这个文件也复制到DllTest1的工程目录下，再次运行，这次一切正常，并且看到弹出3次对话框，说明DLL程序已经能够正常运行了。

3.6.4　对DLL程序的调用方法二

第一种方法是属于静态调用，现在的第二种方法属于动态调用。静态调用就是在编译测试程序DllTest1时，FirstDll.dll的信息就已经写入了DllTest1的程序中了。对于动态加载的话，就不是编译时完成了，而是在运行时完成，那么FirstDll.dll的信息也不会写入测试程序中了。现在来写一个DllTest2程序，该程序的建立方法与DllTest1的建立方法相同。DllTest2的代码如下：

我们对代码进行编译连接，正常编译通过。那么就运行该程序，提示“FirstDll.dll文件不存在”，这说明DllTest2程序没有找到FirstDll.dll文件。把FirstDll.dll文件拷贝到DllTest2的工程目录下，再次运行该程序，这次运行成功了，并且应该弹出的3个对话框也都正常弹出了。我们的测试也是成功的。

DLL的动态加载是非常有用的，在DllTest1中，如果无法找到DLL文件，系统会直接报错而退出，而在DllTest2中，如果无法找到DLL文件，程序会给出一个错误提示，并且可以继续运行，而不影响其他代码的运行。除此而外，如果知道一些API函数，而这些API函数是未文档化的函数，或者是没有提供头文件的API函数，要怎么办呢？比如在前面的内容中用到的函数OpenThread()，该函数在VC6默认的PSDK中是没有提供定义的，在新的PSDK中才有，那如何使用呢？那就需要用到LoadLibrary()和GetProcAddress()这两个API函数了。看一下LoadLibrary()和GetProcAddress()这两个函数的定义：

该函数只有一个参数，就是要加载的DLL文件的文件名。

该函数有两个参数，hModule是模块的句柄，lpProcName指定要获取函数地址的函数名称。

在关于DLL话题的最后，介绍一下如何查看DLL程序的导出函数。在这里介绍两个工具，一个工具是VC6自带的工具“Depends”，另一个工具是用来查看PE结构（关于PE结构的内容在后面的章节会讲到）的工具“PEID”。

首先用“Depends”来查看DLL的导出函数，如何找到这个工具呢？在VC6的安装菜单下就可以找到该工具。方法如下：单击菜单“开始”-> “程序” -> “Microsoft Visual Studio 6.0” -> “Microsoft Visual Studio 6.0 Tools” -> “Depends”命令，打开该程序，再单击菜单“File”-> “Open…”命令，在“打开”对话框中找到我们写的FirstDll.dll文件并打开，如图3-29所示。

图3-29　Depends界面

右下角这个区域范围就是我们的导出函数部分，这里可以看到导出函数“MsgBox”。

除了这个工具以外，再介绍一个工具——PEID。该工具是用来进行查壳的工具，我们把FirstDll.dll文件拖曳到PEID界面上，PEID会自动解析出该DLL文件的PE结构，该界面如图3-30所示。

图3-30　PEID界面

可以看到，PEID最下方的编辑框处显示出DLL是由VC6开发的，而且版本是Debug版本。单击“子系统”右边的“ ”按钮，会显示PE结构的详细信息，在详细信息的下半部分有一个“目录信息”，在“目录信息”中第一个就是我们想要查看的导出函数的内容，单击“导出表”右面的“大于号按钮 ”按钮，出现如图3-31所示的界面。

图3-31　导出表信息

因为DLL中只有一个导出函数MsgBox()，那么该导出表中就只有一个导出项。具体关于PE结构的内容在下一章节中进行讲解。

对于DLL的编程就介绍到这里了，后面的内容同样会涉及跟DLL有关的内容，希望大家可以自己动手完成这个简单的DLL文件。

3.7　远程线程

在Windows操作系统下，为了避免每个进程互相影响，每个进程的内存空间都是被隔离的。但是在某种情况下需要两个进程协调工作或者是跨进程的操作。所谓的“远程线程”并不是跨计算机的，而是跨进程的，简单说就是进程A在进程B中创建一个线程，这样就叫做远程线程。关于远程线程的知识，我们从3个例子来学习，一个是DLL的注入，一个是DLL的卸载，一个是不依赖DLL的注入代码，3个例子的原理是一样的。只要掌握其中一个例子就能了解其他的两个例子了，之所以讲述3个例子，是为了起到举一反三的作用。

远程线程不但在木马、外挂方面应用广泛，而且在反病毒软件方面也应用广泛。

3.7.1　DLL注入

木马或者病毒编写质量的好坏取决于其隐藏程度，而不在于其功能多少。无论是病毒或是木马，它们都是可执行程序，如果它们是EXE文件的话，那么就必将会产生一个进程，产生进程就很容易被发现。为了不被发现，可以选择为DLL文件，DLL文件加载到进程的地址空间中，不会有进程名，因此其隐蔽性相对较好。DLL文件如果不被进程加载又如何在进程中呢？方法是强制让某进程加载DLL文件，这个强制的手段就是下面要介绍的通过创建远程线程将DLL注入到某个指定的进程中。

创建远程线程的函数如下：

该函数的功能是创建一个远程的线程。还记得前面介绍过的CreateThread()函数吗？如果还记得CreateThread()的定义的话，那么就来和CreateRemoteThread()进行一个比较吧。其实对于CreateThread()来说，CreateRemoteThread()就比其多了一个hProcess的参数，该参数是指定要创建线程的进程句柄。其实CreateThread()函数的实现就是依赖CreateRemoteThread()函数来完成的，CreateThread()函数的代码实现如下：

在上面代码中GetCurrentProcess()函数的功能是获取当前进程的句柄。Windows并没有提供源代码，而CreateThread()函数的代码为什么是这样的呢？这里提供的代码是有据可依的，这个将在后面的内容中进行介绍。

回到我们的主题，CreateRemoteProcess()函数是给其他进程进行注入的，那其第一个参数是指定某进程的句柄的，那么某进程的句柄是如何取到的呢？只要有该进程的PID（进程ID），就可以通过OpenProcess()获取该进程的句柄。

除了hProcess参数以外，关键的参数就只有lpStartAddress和lpParameter两个了。 lpStartAddress指定线程函数的地址，lpParameter指定传递给线程函数的参数。前面提到，每个进程的地址空间是隔离的，那么新创建的线程函数的地址也应该在目的进程中，而不应该在本进程中，同样的传递给线程函数的参数也应该在目的进程中。

如何让线程函数的地址在目的进程中呢？如何让线程函数的参数也在目的进程中呢？在讨论这个问题以前，先来考虑一下线程函数需要完成的功能。我们主要介绍的是注入DLL文件，那么线程函数的功能也就是加载DLL文件。加载DLL文件的方法前面已经介绍过了，是调用LoadLibrary()函数。LoadLibrary()函数的定义如下：

该函数的参数是一个指针，API函数的默认调用方式是WINAPI方式（WINAPI是一个宏定义，前面已经介绍过了），而线程函数的定义恰巧也是这样，线程函数的定义格式如下：

两个函数除了返回值以外，其余的都是相同的，返回值的问题可以忽略不考虑。那么，可以直接把LoadLibrary()函数作为线程函数创建到指定进程中。LoadLibrary()的参数是要加载的DLL文件的路径。这样，使用CreateRemoteThread()就可以创建一个远程线程了。不过别急，还有一个问题没有解释，那就是如何将LoadLibrary()函数的地址放到目标进程空间中，还有如何把DLL文件路径也保存到目标进程空间中。

LoadLibrary()函数在Kernel32.dll这个系统DLL中，而Kernel32.dll这个DLL文件在任何进程中的加载位置都是相同的，也就是说， LoadLibrary()函数的地址在任何进程中的地址都是相同的，因此只要在进程中获取LoadLibrary()函数的地址后，该地址在目标进程中也是可以使用的。至于要注入的DLL文件如何写入目标进程中，那么就要依赖一个API函数了，该函数的定义如下：

该函数的功能是把lpBuffer中的内容写到hProcess指定进程中的lpBaseAddress地址处。

参数说明如下。

（1） hProcess：指定进程的进程句柄。

（2） lpBaseAddress：指定写入内存的起始地址。

（3） lpBuffer：指定要写入的内容的缓冲区。

（4） nSize：指定写入内容的长度。

（5） lpNumberOfBytesWritten：接收实际写入内容的长度。

这个函数的功能非常强大，比如在破解方面，该函数可以实现一个“内存补丁”；在做单击游戏修改器时，该函数可以修改游戏的值。

使用该函数可以把DLL文件的地址写入目标进程中，这样就可以在目标进程中用LoadLibrary()加载该DLL文件了。是不是非常值得庆祝呢？我们解决了所有的问题，注入终于可以实现了。如果这样想的话，就有过于心急了。WriteProcessMemory()第二个参数是指定写入的起始地址，这个地址明显在目标进程中，那么这个地址是哪个地址？目标进程中的哪块内存允许把DLL文件的路径写进去呢？新的问题来了，我们如何确定应该写入目标进程的哪个地址。我们应该先在目标进程中申请一块内存，然后把DLL文件的路径写入进去，这样就可以解决了。在目标进程申请内存的函数如下：

参数说明如下。

（1） hProcess：指定要申请内存的进程句柄。

（2） lpAddress：指定申请的起始位置。

（3） dwSize：指定申请的长度。

（4） flAllcationType：指定申请内存的状态类型。

（5） flProtect：指定申请内存的内存属性。

该函数的返回值返回目标进程申请到的内存块的起始地址。

现在，所有问题都解决了，看看注入DLL程序的界面吧，如图3-32所示。

图3-32　DLL注入/卸载器

这个程序既可以注入DLL，也可以卸载注入的DLL。注入DLL文件的代码如下：

该函数有两个参数，分别是目标进程PID和要注入的DLL文件。在代码中，获取的不是LoadLibrary()函数的地址，而是LoadLibraryA()函数的地址。在系统中其实没有LoadLibrary()这个函数，只有LoadLibraryA()和LoadLibraryW()这两个函数，这两个函数分别针对的是ANSI字符串和UNICODE字符串，而LoadLibrary()函数只是一个宏。在编写程序的时候，直接使用该宏是可以的，如果要获取其地址，那么就要指定获取具体的函数了。LoadLibrary()宏定义如下：

只有在涉及与字符串有关的函数时才会出现ANSI版本和UNICODE版本，其余不涉及字符串的函数没有ANSI版本和UNICODE版本。

现在，就用前面写的DLL程序做练习，看把它注入到一个记事本中，其效果如图3-33所示。

图3-33　DLL文件注入

这个对话框是DLL程序在DLL_PROCESS_ATTACH中弹出的（如果不记得，请看前面写的DLL程序的代码），其所在进程为notepad.exe，这里的4032为目标PID。我们的注入成功了。

注意：如果要对系统进程进行注入的话，由于有一个OpenProcess()的权限问题，可能会导致无法获得系统进程的句柄，想要获得其句柄，需要用到前面介绍的提升权限的代码。对于DLL卸载也是同样的情况。

3.7.2　DLL卸载

DLL注入如果用在木马方面危害很大。下面完成一个如何卸载被注入DLL的程序。卸载被注入DLL的程序的思路和注入的思路是一样，而且代码的改动也非常得小。区别在于一个是卸载，一个是注入。

对于DLL的卸载使用的API函数如下：

该函数的参数是要卸载的模块的句柄。

FreeLibrary()函数使用到的模块句柄可以通过前面介绍的Module32First()/Module32Next()两个函数获取。在介绍进程枚举时使用到了一个PROCESSENTRY32的结构体，在使用Module32First()函数时，我们会使用到一个MODULEENTRY32的结构体，该结构体中保存了模块的句柄。MODULEENTRY32的定义如下：

该结构体中的hModule为模块的句柄，szModule为模块的名称，szExePath是完整的模块的名称（包含路径）。

DLL卸载的代码如下：

DLL卸载的代码比DLL注入的代码还要简单，这里就不做过多介绍了。

大家可以尝试把第2章中介绍的简单的木马服务端写成DLL文件，再将其注入到某个进程中，然后用客户端文件和服务端文件进行通信。这将对我们的所学的知识有一个深入的体会。

3.7.3　无DLL的代码注入

DLL文件的注入与卸载都很成功的完成了，整个注入的与卸载的过程其实就是让远程线程执行一次LoadLibray()或FreeLibrary()。主要的功能代码都在DLL文件中，那么可否直接通过WriteProcessMemory()函数把代码直接写入到目标进程中？答案是可以的。

我们要在目标进程中完成一定的功能，那么就要使用到相关的API函数。Kernel32.dll文件在每个进程中的地址是一样的，但是并不代表其他DLL文件在每个进程中的地址都一样。这样，必须在目标进程中获取想要使用的API函数的地址。获取API函数的地址使用的是LoadLibrary()和GetProcAddress()这两个函数，在目标进程中，通过这两个函数就可以获取任何一个想要使用的API函数的地址了。把想要使用的API函数及API函数所在的DLL都封装到一个结构体中，直接写入到目标进程空间中，也可以直接把要在远程执行的代码写入到目标进程空间中，然后使用CreateRemoteThread()运行即可。

接下来写一个例子，让远程线程弹出一个简单对话框，但是不借助于DLL。这个例子将要使用到的API函数也全都在前面的知识中介绍了。根据上面的步骤，先定义一个结构体，结构体如下：

结构体中保存了LoadLibraryA()、GetProcAddress()、GetModuleHandle()和GetModuleFileName()函数的地址。这4个函数都属于Kernel32.dll文件中的函数，因此可以提前获取。User32Dll保存“User32.dll”字符串，因为MessageBoxA()函数在User32.dll文件中。至于Str，是我们想要用MessageBoxA()弹出的字符串。

看一下注入的代码：

卸载代码和注入代码没什么差别，关键是要把线程函数也写入目标进程中，这样在使用CreateRemoteThread()时，直接给出线程函数在目标进程中的地址即可。

线程函数代码如下：

线程函数显得很乱，但是只要仔细看还是能看明白的，线程函数里面的内容都没有超出前面介绍的知识范围。请大家认真地看一遍。

这就是无DLL注入的全部代码了。编译运行，并用“记事本”程序做测试，发现报错了，如图3-34所示。

竟然出错了，问题出在哪里呢？注入器的编译方式是Debug方式，而以Debug方式编译VC会在程序中加入很多与调试相关的内容，而这些内容的地址是相对当前进程地址，这些代码到了别的进程可能就会出错。这里应该使用Release方式编译，因为以Release方式编译不会由于插入与调试相关的代码而导致注入的代码到别的进程会出错。用Release方式编译并运行，如图3-35所示。

图3-34　无DLL注入错误

图3-35　以Release方式编译运行正常

这样运行就正常了，无DLL的代码注入也就成功了。

3.8　总结

本章学习了Windows下应用程序的编程基础，包括文件操作、注册表操作、服务枚举、进程、线程、DLL等相关内容。在后面的章节中将会经常使用到这些内容，请大家一定要掌握本章的内容。

第4章　加密与解密

本章将学习一些简单的加密与解密的知识,并会接触到PE结构、OD调试器和调试API函数。

4.1　PE文件结构

PE （Portable Executable）,即可移植的执行体。所有的 Windows（包括Win 9x、Win NT、 Win CE……）下的可执行文件（包括EXE、DLL、SYS、OCX、COM……）均使用PE文件结构,这些使用PE文件结构的可执行文件也成为PE文件。

作为一个普通的程序员也许没有必要掌握PE文件结构,因为普通的程序员大多都是开发服务性、决策性、辅助性的软件，比如MIS、HIS等软件。但是对于学习黑客编程,学习安全编程的程序员来说,那么掌握PE文件结构的知识就非常重要了。因此，我们要进行对 PE文件结构地学习。

4.1.1　PE文件结构全貌

Windows下的可执行程序有着复杂的文件结构,同样,PE结构是由若干个复杂的结构体组合而成的，不单单是一个结构体那么简单。就像磁盘结构体一样,磁盘结构同样是由非常多的结构体组成的。PE结构包含的结构体有DOS头、PE标识、文件头、可选头、目录结构、节表等。要掌握PE结构,必须对PE结构有一个整体上的认识，要知道PE结构分为哪些部分，这些部分大概是起什么作用的。有了宏观上的概念以后，就可以深入PE结构的各个结构体进行细致地学习了。下面给出一张图，让大家对PE结构有一个大概的了解,如图4-1所示。

图4-1　PE结构总览图

从图中可以看到,在PE文件中,真正包含代码和数据的部分是节表数据,节表数据以上的部分都是PE文件结构。

下面根据这个图给大家介绍PE文件结构的各个结构体,介绍完以后再写一个PE结构的解释器。这里不会介绍PE结构中的每个结构,只针对常用的和相对重要的结构体进行介绍。

4.1.2　MZ头部

MZ头部其实是一个DOS头部,由于其开始处的字符为“MZ”,因此叫做MZ头,也可以叫做DOS头,看个人习惯了。该部分用于在DOS下加载可执行程序,是用 IMAGE_DOS_HEADER来定义的。

DOS残留是一段简单的程序，主要用于输出“This program cannot be run in DOS mode.”类似的字符串。

为什么PE结构的最开始位置有这样一段DOS头部呢？关键是为了该可执行程序可以兼容DOS系统。通常情况下,Win32下的PE程序只是简单地输出一段字符串，提示不能运行于DOS系统下。但是这个DOS程序是可以在程序连接时修改的，不过由于不常使用也就不进行介绍了。

4.1.3　PE头部

PE头部保存Windows系统加载可执行文件的重要信息。PE头部由 IMAGE_NT_HEADERS来定义,通过该结构体的定义可以看出,IMAGE_NT_HEADERS是由多个结构体组合而成的,具体内容将在后面进行介绍。PE头部在PE文件中的位置是不确定的,PE头部的位置由DOS头部的某个字段给出。

4.1.4　节表

程序的组织按照各属性的不同而被保存在不同的节中,在PE头部之后就是一个数组结构的节表。如果PE文件中有N个节,那么节表就是由N个IMAGE_SECTION_HEADER组成的数组。节表中存储了各个节的属性、位置等相关信息。

4.1.5　节表数据

真正的程序部分就保存在节数据中。节名称最大长度为8个字节,大于8个字节将被截取。节的划分是根据各组数据的属性划分的,每个节拥有共同的属性。

4.2　详解PE文件结构

在PSDK的头文件Winnt.h中,包含了PE文件结构的定义格式。PE头文件分为32位和 64位。64位的PE结构是对32位的PE结构做了扩展,这里主要讨论32位的PE文件结构。对于64位的PE文件结构大家可以自行查阅资料进行学习。

4.2.1　DOS头部详解 IMAGE_D〇S_HEADER

对于一个PE文件来说,最开始的位置就是一个DOS程序。DOS程序包含一个DOS头和一个DOS程序体。DOS头是用来装载DOS程序的,DOS程序也就是图4-1中的那个DOS存根,也就是说DOS头是用来装载DOS存根的。保留这部分内容是为了与DOS系统做兼容。一般在PE结构中的DOS存根,只会输出一个“This program cannot be run in DOS mode.”字符串。

虽然DOS头是为了装载DOS程序的,但是DOS头部中的一个字段保存着指向PE头部的位置。DOS头在Winnt.h头文件中被定义为IMAGE_DOS_HEADER,该结构体的定义如下:

在该结构体中，有两个字段需要注意,分别是第一个字段e_magic,和最后一个字段e_lfanew字段。

e_magic字段是一个DOS可执行文件的标识符,该字段占用两个字节,该位置保存着的字符是“MZ”。该标识符在Winnt.h头文件中有一个宏定义,定义如下所示:

e_lfanew字段中保存着PE文件的起始位置。

在VC下创建一个简单的“Win32 Application”程序（注意,这次创建的不是控制台程序,创建程序的方法类似,这里就不再介绍创建工程的步骤了）,用来学习PE结构。

程序代码如下:

由于创建的工程是“Win32 Application”的程序,而不是控制台的程序,因此主函数是 WinMain(),而不是main()了。WinMain()函数有4个参数,具体参数的含义请参考MSDN,这里就不进行介绍了。

该程序的功能是弹出一个Messagebox对话框,使用“Win32 Release”方式进行编译连接，并把编译好的程序用C32Asm打开。C32Asm是一个反汇编与十六进制编辑于一体的程序，其界面如图4-2所示。

图4-2　C32Asm程序界面

在这里选择“十六进制模式”单选项,单击“确定”按钮，打开十六进制编辑模式，如图4-3所示。

图4-3　十六进制编辑状态下的C32Asm

在图中可以看到，在文件偏移为0×00000000的位置处保存着两个字节的内容0×5A4D,用ASCII表示则是“MZ”。图中明明写着是4D 5A,为什么说的是0×5A4D,大家到上面看 Winnt.h头文件中定义的那个宏，也写着是0×5A4D,这是为什么呢？这就是在前面章节中讲解的字节顺序,高位保存在高地址,地位保存在低地址。这个概念是很重要的,希望大家能够掌握。

在图中0×0000003c的位置处,就是e_lfanew字段,该字段保存着PE结构的起始位置,该字段的值为多少呢？保存的是0×C800000000吗？如果是这样就错了,CPU架构使用的是小尾方式，系统对数据的存放始终是高位存放高字节，低位存放低字节，因此该处保存的是 0×000000C8。大家查看一下0×000000C8这个位置保存的内容是50 45 00 00,与之对应的 ASCII字符为“PE\0\0”。这里就是PE结构开始的位置。

“PE\0\0”和IMAGE_DOS_HEADER之间的内容就是DOS存根，就是一个没什么太大用处的DOS程序。由于这个程序本身没什么利用的价值，因此这里就不做介绍了。选中DOS存根程序，也就是从0×40000000处一直到0×000000C7处的内容。然后单击右键选择“填充”命令，在弹出的“填充数据”的对话框中，选中“使用十六进制填充”单选钮，在其后的编辑框中输入“00”,单击“OK”按钮,该过程如图4-4、图4-5所示。

图4-4　填充数据

图4-5　填充后的数据

把DOS存根部分填充完以后,单击工具栏上的“保存”按钮对修改内容进行保存。保存时会提示是否备份，选择“是”，这样文件就保存了。找到我们的文件，然后运行，对话框依旧弹出了，说明这里的内容是无关紧要的。

注:DOS存根部分经常由于某种需要而保存其他数据，因此该操作较为常见。

4.2.2　PE头部详解IMAGE_NT_HEADERS

DOS头是为了兼容DOS系统而遗留的,在DOS头中的最后一个字段给出了 PE头的位置。PE头部是真正用来装载Win32程序的头部,PE头的定义为IMAGE_NT_HEADERS,该结构体包含PE标识符、文件头与可选头这三部分。IMAGE_NT_HEADERS是一个宏，其定义如下:

该头分为32位与64位,看是否定义了_WIN64。我们只讨论32位的PE文件格式,因此来看一下IMAGE_NT_HEADER32的定义,该定义如下:

该结构体中的Signature就是PE标识符,该标识符标识该文件是否是PE文件。该部分占4个字节,即“50 45 00 00”，该部分可以参看图4-3。这里在Winnt.h中有一个宏定义，定义如下:

该值非常重要，简单地判断一个文件是否是PE文件,首先要判断DOS头部的开始字节是否是“MZ”,然后通过DOS头部找到PE头,接着判断PE头部的前四个字节是否为“PE\0\0”,如果是的话,则可以说明该文件是一个有效的PE文件。

在PE头中,除了IMAGE_NT_SIGNATURE以外,还有两个重要的结构体，分别是 IMAEG_FILE_HEADER（文件头）和IMAGE_OPTIONAL_HEADER32（可选头）。这两个头在PE头中占据重要的位置,因此需要详细介绍这两个结构体。

4.2.3　IAMGE_FILE_HEADER

该结构体是IMAGE_NT_HEADERS中的第一个结构体,该结构紧接在PE标识符的后面。该结构体大小为20个字节,起始位置为0×000000CC,结束位置为0×000000DF,如图4-6所示。

图4-6　IMAGE_FILE_HEADER在PE文件中的位置

该结构包含了PE文件的一些基础信息,其结构体的定义如下:

下面介绍一下该结构各字段。

（1） Machine:该字段是WORD类型,占用两个字节。该字段表示可执行文件的目标CPU类型。该字段的取值如图4-7所示。

图4-7　CPU类型取值

在图4-6中,Machine字段的值为“4C 01”,即0×014C,也就是支持Intel类型的CPU。

（2） NumberOfSection:该字段是WORD类型,占用两个字节。该字段表示PE文件的节区的个数。在图4-6中，该字段值为“03 00”,即为0×0003,也就是说明该PE文件的节区有3个。

（3） TimeDataStamp:该字段表明文件是何时被创建的，这个值是自1970年1月1日以来用格林威治时间计算的秒数。

（4） PointerToSymbolTable:该字段很少被使用,这里不做介绍。

（5） NumberOfSymbols:该字段很少被使用,这里不做介绍。

（6） SizeOfOptionalHeader:该字段为WORD类型，占用两个字节。该字段指定 IMAGE_OPTIONAL_HEADER结构的大小。在图4-6中,该字段的值为“E0 00”,即“0×00E0”,也就是说IMAGE_OPTIONAL_HEADER的大小为 0×E0。注意,在计算 IMAGE_OPTIONAL_HEADER的大小时应该从IMAGE_FILE_HEADER结构中的 SizeOfOptionalHeader获取，而不应该直接使用sizeof（IMAGE_OPTIONAL_HEADER）获取。由该字段可以看出IMAGE_OPTIONAL_HEADER结构体的大小可能是会改变的。

（7） Characteristics:该字段为WORD类型，占用两个字节。该字段指定文件的类型。该字段取值如图4-8所示。

图4-8　文件类型取值

从图4-6可知，该字段的值为“0F 01”，即“0×010F”。该值表示该文件运行的目标平台为32位平台,是一个可执行文件,且不存在重定位信息,行号信息和符号信息已从文件中移除。

4.2.4　IMAGE_OPTIONAL_HEADER

IMAGE_OPTIONAL_HEADER在几乎所有的参考书中都被称作“可选头”。虽然被称作可选头，但是该头不是一个可选头，而是一个必须存在的头，不可以没有。该头被称作“可选头”认为是在该头的数据目录数组中，有的数据目录项是可有可无的，这部分内容是可选的，因此成为可选头。而我觉得如果称之为“选项头”是否会更好一点？不管如何称呼,只要大家能够理解该头是必须存在的,就可以了。

可选头紧挨着文件头，文件头的结束位置为0×000000DF,那么可选头的起始位置为 0×000000E0。可选头的大小在文件头中给出,其大小为0×00E0字节（十进制为224个字节),其结束位置为0×000000E0+ 0×00E0-1=0×000001BF,如图4-9所示。

图4-9　可选头的内容

可选头是文件头的一个补充，其中的字段除了对文件的一些定义外，还为操作系统提供了装载PE文件的相关定义。该头同样有32位与64位版本之分,IMAGE_OPTIONAL_HEADER是一个宏,该宏的定义如下:

32位版本与64位版本的选择是根据是否定义了_WIN64而决定的,这里只讨论其32位的版本,IMAGE_OPTIONAL_HEADER32的定义如下:

该结构体的成员变量非常之多，为了能够很好地掌握该结构体，这里对该结构体的成员变量进行一一介绍。

（1） Magic:该成员变量指定了文件的状态类型,状态类型如图4-10所示。

图4-10　Magic变量取值

（2） MajorLinkerVersion:主链接版本号。

（3） MinorLinkerVersion:次链接版本号。

（4） SizeOfCode:代码节的大小,如果有多个代码节的话，就是它们的总和。该处是指所有包含可执行属性的节的大小。

（5） SizeOfInitializedData:已初始化数据块的大小。

（6） SizeOfUninitializedData:未初始化数据块的大小。

(7) AddressOfEntryPoint:程序执行的入口地址，该地址是一个相对虚拟地址，该地址简称EP。如果加壳后，找到了该地址，就被称作了 OEP。该地址指向的不是main(),也不是 WinMain()的地址，该地址指向了运行库代码的地址。对于DLL这个值的意义不大，因为DLL甚至可以没有DllMain()函数，没有DllMain()是无法捕获DLL的4个消息的。

（8） BaseOfCode:代码段的起始相对虚拟地址。

（9） BaseOfData:数据段的起始相对虚拟地址。

（10） ImageBase:文件被装入内存后的首选建议装载地址。对于EXE文件来说，通常情况下该地址就是装载地址；对于DLL来说，可能就不是其装入内存后的地址了。

（11） SectionAlignment:节被装入内存后的对齐值。节被映射到内存中需要对齐的单位。通常情况下0×1000,也就是4KB大小。Windows操作系统的内存分页一般为4KB。

（12） FileAlignment:节在文件中的对齐值。节在磁盘上是对齐单位。

（13） MajorOperatingSystemVersion:要求最低操作系统的主版本号。

（14） MinorOperatingSystemVersion:要求最低操作系统的次版本号。

（15） MajorImageVersion:可执行文件的主版本号。

（16） MinorImageVersion:可执行文件的次版本号。

（17） MajorSussystemVersion:要求最低子系统的主版本号。

（18） MinorSubsystemVersion:要求最低子系统的次版本号。

（19） Win32VersionValue:该成员变量是被保留的。

（20） SizeOfImage:可执行文件装入内存后的总大小。该大小按内存对齐方式对齐。

（21） SizeOfHeaders: PE头的大小，这个PE头泛指DOS头、PE头、节表的总和大小。

（22） CheckSum:校验和。对于EXE文件通常为0，对于SYS文件则必须有一个校验和。

（23） Subsystem:可执行文件的子系统类型。该值如图4-11所示。

图4-11　Subsystem值的意义

（24） DllCharacteristics:指定DLL文件的属性。该值大部分时候为0。

（25） SizeOfStackReserve:为线程保留的栈大小。

（26） SizeOfStackCommit:为线程已经提交的栈大小。

（27） SizeOfHeapReserve:为线程保留的堆大小。

（28） SizeOfHeapCommit:为线程已经提交的堆大小。

（29） LoaderFlags:被废弃的成员值。MSDN上的原话为“This member is obsolete”。但是该值在某些情况下还是会被用到的，比如针对旧版OD时，修改该值会起到反调试的作用。

（30） NumberOfRvaAndSizes:数据目录项的个数。该个数在PSDK中有一个宏定义，定义如下:

（31）DataDirectory:数据目录表，由NumberOfRvaAndSize个IMAGE_DATA_DIRECTORY结构体组成。该数组中包含了输入表、输出表、资源等数据的RVA。 IMAGE_DATA_DIRECTORY的定义如下：

该结构体的第一个变量为该目录的相对虚拟地址的起始值，第二个是该目录的长度。数据目录中的部分成员在数组中的索引如图4-12所示。

图4-12　数据目录部分成员在数组中的索引

在数据目录中，并不是所有的目录项都会有值，有很多目录项的值都为0。

这个可选头的结构体就介绍完了，希望大家按照对结构体中各成员变量的掌握自行学习可选头中的十六进制的字段，这样有助于我们对PE文件格式的分析，加快对PE文件格式的掌握。

4.2.5　节区详解IMAGE_SECTION_HEADER

节表的位置在IMAGE_OPTIONAL_HEADER的后面，节表中的每个 IMAGE_SECTION_HEADER中都存放着可执行文件被映射到内存中所在位置的信息，节的个数由IMAGE_FILE_HEADER中的NumberOfSections给出，希望大家没有忘记。该内容如图4-13所示。

图4-13　IMAGE_SECTION_HEADER位置的数据内容

IMAGE_SECTION_HEADER的结构体起始位置在0×000001CO处，结束位置在 0×00000237处，IMAGE_SECTION_HEADER的大小为40个字节，该文件有3个节表，因此占用了120个字节。

IMAGE_SECTION_HEADER结构体的定义如下：

这个结构体相对于IMAGE_OPTIONAL_HEADER来说成员变量少很多，下面逐一进行介绍。

（1）Name:该成员变量保存节的名称，节的名称用ASCII编码来保存。节名的长度为 IMAGE_SIZEOF_SHORT_NAME,这是一个宏，该宏的定义如下：

节名的长度为8个字节，多余的字节会被自动截断。通常情况下节名以“.”为开始，当然这是编译器的习惯。我们看一下图4-13的前8个字节的内容为“2E 74 65 78 74 00 00 00”，其对应ASCII字符为“.text”。

（2） VirtualSize:该值为数据实际的节区大小，该值不一定为对齐后的值。

（3） VitualAddress:该节区载入到内存后的相对虚拟地址。这个地址是按内存进行对齐的。

（4） SizeOfRawData:该节区在磁盘上的大小，该值通常是对齐后的值，但是也有例外。

（5） PointerOfRawData:该节区在磁盘文件上的偏移值。

（6） Characteristics:节区属性。该属性的值如图4-14所示。

图4-14　节区属性取值

关于IMAGE_SECTION_HEADER的介绍就到这里了。

4.2.6　与PE结构相关的3种地址

下面介绍一下与PE结构相关的3种地址，分别是VA（虚拟地址）、RVA（相对虚拟地址）和FileOffset（文件偏移地址）。

VA（虚拟地址）：PE文件映射到内存后的地址。

RVA（相对虚拟地址）：相对虚拟地址是内存地址相对于映射基地址的偏移地址。

FileOffset（文件偏移地址）：相对PE文件在磁盘上的文件开头的偏移量。

这3个地址都是和PE文件结构密切相关的，前面简单地引用过这几个地址，但是前面只是个概念。从了解节区开始，这3个地址的概念就非常重要了，否则后面的很多内容都将没法理解。

这3个概念之所以重要，是因为我们要不断地使用它们，而且三者之间的关系也很重要。每个地址之间的转换也很重要，尤其是VA和FileOffset的转换，还有RVA和FileOffset之间的转换。这两个转换不能说是复杂，但是需要一定的公式。而VA和RVA的转换就非常得简单与容易了。

PE文件在磁盘上和在内存中的结构是一样的。所不同的是，在磁盘上文件是按照IMAGE_OPTIONAL_HEADER的FileAlignment值进行对齐的。而在内存中，映像文件是按照IMAGE_OPTINAL_HEADER的SectionAlignment进行对齐的。这两个值前面已经介绍过了，这里再进行一个简单的回顾。FileAlignment是按照磁盘上的扇区为单位的，也就是说FileAlignment最小为512字节，十六进制的0×200字节。而SectionAlignment是按照内存分页为单位来对齐的，其值为4KB,也就是十六进制的0×1000字节。一般情况下，FileAlignment的值会与SectionAlignment的值相同，这样磁盘文件和内存映像的结构是完全一样的。当FileAlignment的值和SectionAlignment的值不相同的时候就存在一些细微的差异了，其差异的主要区别在于，根据对齐的实际情况而多填充了很多0值。PE文件映射如图4-15所示。

图4-15　PE文件映像

4.2.7　3种地址的转换

当FileAlignment和SectionAlignment的值不相同时，磁盘文件与内存映像的同一数据在磁盘和内存中的偏移也不相同，这样两个偏移就发生了一个需要转换的问题。当你知道某数据的RVA的时候，想要在文件中读取同样的数据，就必须将RVA转换为FileOffset。反之，也是同样的情况。

下面用一个例子来学习如何进行转换。还记得前面为了分析PE文件结构而写的那个用 MessageBox()输出“Hello World”的例子程序吧？我们用PEDI打开它，查看它的节表情况，如图4-16所示。

图4-16　节表内容

从图4-16的标题栏可以看到，这里不叫“节表”而叫“区段”，还有别的资料上称之为 “区块”，这个只是叫法不同，内容都是一样的。

从图4-16中可以看到，节表的第一个节区的节名称为“.text”。通常情况下，第一个节区都是代码区，入口点也通常指向这个节区。在早期壳不流行时，通过判断入口点是否在第一个节区来判断该程序是否为病毒，如今这种做法就不可靠了。我们关键要看的是“R.偏移”，这个表明了该节区在文件中的起始位置。PE头部，包括DOS头、PE头和节表，通常不会超过512个字节，也就是说，不会超过0×200的大小。如果这个“R.偏移”为0×00001000,那么通常情况下可以确定该文件的磁盘对齐大小为0×1000。测试验证一下这个程序，看到“V.偏移”与“R.偏移”相同，则说明磁盘对齐与内存对齐是一样的，这样，就没办法完成演示转换的工作了。不过，可以人为地修改磁盘对齐大小，也可以通过工具来修改磁盘对齐的大小。这里，借助LordPE来修改其大小，修改方法很简单。先将要修改的测试文件复制一份，以与修改后的文件做对比。打开LordPE,单击“重建PE”按钮，然后再选择刚才复制的那个测试文件，如图4-17、图4-18所示。

PE重建功能中会压缩文件大小的功能，这里的压缩也就是修改磁盘文件的对齐值，避免过多的因对齐而进行补0,使其少占用磁盘空间。用PEID查看一下这个进行重建的PE文件的节表，如图4-19所示。

现在可以看到“V.偏移”与“R.偏移”的值不相同了，它们的对齐值也不相同了，大家可以自己验证一下FileAlignment和SectionAlignment的值是否相同。

现在我们有两个功能完全一样，而且PE结构也一样的两个文件了，唯一的不同就是其磁盘对齐大小不同。现在在这两个程序中分别寻找相同的数据，来学习不同地址之间的转换。

图4-17　LordPE界面

图4-18　重建PE功能结果

图4-19　重建PE后的节表

先用OD打开未进行重建PE的测试程序，找到MessageBox()处要弹出的两个对话框的地址,如图4-20、图4-21所示。

图4-20　MessageBox0函数中所使用的字符串地址

图4-21　两个字符串的地址

从图4-20和图4-21中可以看到，字符串“hello world!”的虚拟地址（VA）为0×00405030。

相对虚拟地址（RVA）为VA减去ImageBase（映像文件的装载虚拟地址）,则RVA= VA-ImageBase=0×00405030-0×00400000=0×00005030。由于 SectionAlignment与 FileAlignment的值相同，因此其FileOffset的值也为0×00005030,用C32ASM打开该文件查看0×00005030处，如图4-22所示。

图4-22　文件偏移0×00005030处的内容为“hello world!”字符串

从这个例子中可以看出，当SectionAlignment和FileAlignment相同时,同一数据的RVA （相对虚拟地址）和FileOffset（文件偏移地址）相同。RVA的值是使用VA-ImageBase得到的。

再次用OD打开“重建PE”后的测试程序，同样找到MessageBox()函数使用的那个字符串“hello world!”,看其虚拟地址是多少。可以告诉大家，它的虚拟地址仍然是0×00405030。虚拟地址是0×00405030,那么同样的用虚拟地址减去装载地址，相对地址的值仍然为 0×00005030。用C32ASM打开该文件，看一下0×00005030地址处的内容，如图4-23所示。

图4-23　文件的末尾地址

从图中可以看到，用C32ASM打开该文件后，文件的末尾偏移为0000379C,根本没有 0×00005030这个偏移地址。这就是文件对齐与内存对齐的差异而引起的。这个时候，就要通过一些简单的计算把RVA转换FileOffset。

把RVA转换为FileOffset的方法很简单，首先要看一下当前的RVA或者是FileOffset属于哪个节。0×00005030这个RVA属于.data节。0×00005030这个RVA相对于该节的起始RVA地址0×00005000来说偏移0×30个字节。再看.data节在文件中的起始位置为0×00003400,以.data节的文件起始偏移0×00003400加上0×30个字节的值为0×00003430。用C32ASM看一下0×00003430这个地址处的内容，如图4-24所示。

图4-24 0×00003430文件偏移处的数据内容

从图4-24可以看出，该文件偏移处保存着“hello world!”字符串，也就是说将RVA转换为FileOffset是正确的。通过LordPE工具来验证一下，如图4-25所示。

图4-25　用LordPE计算0×00005030的文件偏移

我们再来回顾一下这个过程。

某数据的文件偏移=该数据所在节的起始文件偏移+（某数据的RVA-该数据所在节的起始RVA）。

除了上面的计算方法以外，还有一种计算方法，把节的起始RVA值减去节的起始文件偏移值，得到一个差值。然后再用RVA减去这个得到的差值就可以得到其所对应的FileOffset了。大家可以对这种方法自行验证。

这3种地址相互的转换方法就介绍完了。如果没有理解，那么就反复地按照公式进行学习和计算。只要在头脑中建立了关于磁盘文件和内存映像的结构，那么理解起来就不会太吃力。在后面的例子当中，将会写一个类似LordPE中那样转换3种地址的程序，来加强理解。

4.3　PE查看器

写PE查看器并不是件复杂的事情，只要按照PE结构一步一步地解析就可以了。下面简单地解析其中几个字段内容，显示一下节表的信息。写的PE查看器如图4-26所示。

图4-26　PE查看器解析记事本程序

PE查看器的界面就按照如图4-26所示的那样来设置，不过这个可以按照个人的偏好进行设置。编写该PE查看器的步骤为打开文件并创建文件内存映像，判断文件是否为PE文件并获得PE格式相关结构体的指针，解析基本的PE字段，枚举节表，最后关闭文件。我们需要在类中添加几个成员变量及成员函数，添加的内容如图4-27所示。

图4-27　在类中添加的成员变量及成员函数

按照前面所说的顺序，依次实现添加的各个成员函数。

这个函数的主要功能是打开文件并创建内存文件映像。通常对文件进行连续读写时直接使用ReadFile()和WriteFile()两个函数，当不连续操作文件时，每次在ReadFile()或者WriteFile()后就要使用SetFilePointer()来调整文件指针的位置，这样的操作较为繁琐。内存文件映像的作用是把整个文件映射入进程的虚拟空间中，这样操作文件就像操作内存变量或数据一样的方便。

创建内存文件映像所使用的函数有两个，分别是CreateFileMapping()和MapViewOfFile()。下面分别介绍这两个函数，CreateFileMapping()函数的定义如下：

参数说明如下。

（1） hFile:该参数是CreateFile()函数返回的句柄。

（2） lpAttributes:是安全属性，该值通常是NULL。

（3） flProtect:创建文件映射后的属性，通常设置为可读可写PAGE_READWRITE,如果需要按照装载可执行文件那样把文件映射入内存的话，那么需要使用SEC_IMAGE;

最后3个参数在这里为0。如果创建的映射需要在多进程中共享数据的话，那么最后一个参数设定为一个字符串，以便通过该名称找到该块共享内存。

该函数的返回值为一个内存映射的句柄。

MapViewOfFile()函数的定义如下：

参数说明如下。

（1） hFileMappingObject:该参数为CreateFileMapping()返回的句柄。

（2） dwDesiredAccess:想获得的访问权限，通常情况下也是可读可写FILE_MAP_READ、 FILE_MAP_WRITE。

最后3个参数一般给0值就可以了。

按照编程的规矩，打开要关闭，申请要释放。那么对于CreateFileMapping()的关闭需要使用CloseHandle()函数，对于MapViewOfFile()来说，要使用UnmapViewOfFile,该函数的定义如下：

该函数的参数就是MapViewOfFile()函数的返回值。

接着说PE查看器，文件已经打开，那么就要判断文件是否为有效的PE文件了，如果是有效的PE文件，那么就把解析PE格式的相关结构体的指针也得到。代码如下：

这段代码应该非常容易理解，继续看解析PE格式的部分吧。

对于PE格式的基础信息来说，就是简单地获取结构体的成员变量，没有过多复杂的内容。获取导入表、导出表就要比获取基础信息要复杂了。关于导入表、导出表的内容将在后面介绍。接下来进行节表的枚举，代码如下：

最后的动作就是释放动作了，因为很简单，这里就不给出代码了。整个PE查看器就算是写完了。

4.4　简单的查壳工具

前面的内容介绍了通过编程解析了PE文件格式的基础数据，对于PE文件格式的解析其实并不难，难点在于兼容性。我们从前面的学习中可以看到，在PE文件结构中大多用的都是偏移地址，因此，只要偏移地址和实际的数据相符，那么PE文件格式有可能是嵌套的。也就是说PE文件是可以变形的，只要保证其偏移地址和PE文件格式的结构基本就没多大问题。

对于PE可执行文件来说，为了保护可执行文件或者是压缩可执行文件，通常会对该文件进行加壳。接触过软件破解的人，应该都是清楚壳的概念的。关于壳的概念就不多说了，下面来写一个查壳的工具。

首先，用ASPack给前面写的程序加个壳，打开ASPack加壳工具，如图4-28所示。

图4-28　ASPack加壳工具界面

对测试用的软件进行一次加壳，不过在加壳前先用PEiD查看一下，如图4-29所示。

图4-29　PEiD查壳

从图4-29可以看出，该程序是Visual C++ 5.0 Debug版的程序，其实该程序是用Visual C++6.0写的，这是PEiD识别有误的原因。不过只要用Visual C++6.0进行编译选择Release版时，PEiD是可以正确进行识别的。使用ASPack对该程序进行加壳，加壳后再用PEiD查壳,如图4-30所示。

从图4-30中可以看出,PEiD识别出来文件被加过壳，且是用ASPack进行加壳的。PEiD是如何识别程序被加壳，加了哪种壳呢？在PEiD的目录下，有一个特征码文件，名为 “userdb.txt”。打开这个文件，看一下大概内容，里边保存了壳的特征码。我们的任务就是来做一个这个壳的识别工具。

图4-30　用PEiD查看加壳后的文件

壳的识别是通过特征码来进行的，特征码的提取通常是选择文件的入口处。壳会修改程序的入口处，因此对于壳的特征码来说选择入口处比较合适。我们的工具主要是用来学习和演示用的，因此写的查壳工具要能识别两种类型，第一种类型是可以识别用Visual C++6.0编译出来的文件，第二种类型就是可以识别ASPack加壳后的程序，当然ASPack加壳工具的版本众多，这里只要能识别上面演示的那个版本的ASPack就可以了。

如何提取特征码呢？程序无论是在磁盘上，还是在内存中，都是以二进制的形式存在的。前面也提到过，特征码是从程序的入口处进行提取的，那么可以使用C32ASM以十六进制的形式打开这些文件，在入口处提取特征码，也可以用OD将程序载入内存后提取特征码。这里选择使用OD提取特征码。用OD载入未加壳的程序，如图4-31所示。

图4-31　OD载入未加壳文件的入口处

可以看到这就是未加壳程序的入口处代码，在图4-31中，“十六进制”列中就是代码对应的十六进制编码，我们就是要提取这些十六进制编码。提取结果如下：

根据这个步骤，把ASPack的特征码也提取出来，提取结果如下：

有了这些特征码，就可以开始编程了。先来定义一个数据结构，用来保存特征码，该结构如下：

利用该数据结构定义两个保存特征码的全局变量，定义如下：

程序界面是在PE查看器的基础上完成的，如图4-32所示。

提取了特征码，对于查壳工作就剩特征码匹配了，这个非常简单，只要用文件的入口处代码和特征码进行匹配，匹配相同就会给出相应的信息。查壳的代码如下：

这样查壳的功能就已经完成了。

图4-32　查壳界面

4.5　地址转换器

前面介绍了关于PE文件的3种地址，分别是VA（虚拟地址）、RVA（相对虚拟地址）和FileOffset（文件偏移地址）。3种地址的转换如果始终使用手动来计算那是非常累的，因此通常的做法是借助工具来完成。前面介绍了使用LordPE来计算这3种地址的转换，现在来编写一个对这3种地址进行转换的工具。该工具如图4-33所示。

图4-33　地址转换器

这个工具是在前两个工具的基础上完成的。因此，在进行计算的时候，应该先要进行“查看”，然后再进行“计算”。否则，该获取的指针还没有获取到。

在界面上，左边的3个按钮是“单选框”，单选框的设置方法如图4-34所示。

图4-34　对单选框的设置

3个单选框中只能有一个是选中状态，为了记录哪个单选框是选中状态，在类中定义一个成员变量m_nSelect。分别对3个单选框使m_nSelect为1、2和3三个值。关于界面的编程大家自己参考源代码，这里就不进行过多的介绍了。来看主要的代码。

在单击“计算”按钮后，响应该按钮的代码如下：

分别看一下GetAddr()、GetAddrInSecNum()和 CalcAddr()的实现。

获取在编辑框中输入的地址内容的代码：

获取该地址所属的第几个节的代码：

计算其他地址的代码：

代码都不复杂，关键就是在CalcAddr()中3种地址的转换。如果没能理解代码，那么请参考前面手动转换3种地址的方法。这里就不进行介绍了。

4.6　添加节区

添加节区在很多场合都会用到，比如在加壳中，在免杀中都会经常使用到对PE文件添加一个节区。添加一个节区的方法有4个步骤，第1个步骤是在节表的最后面添加一个 IMAGE_SECTION_HEADER,第 2个步骤是更新 IMAGE_FILE_HEADER中的 NumberOfSections字段，第3步是更新IMAGE_OPTIONAL_HEADER中的SizeOfImage字段，最后一步则是添加文件的数据。当然了，前3个步骤是没有先后顺序的，但是最后一个步骤一定要明确如何改变。

4.6.1　手动添加一个节区

先来进行一次手动添加节区的操作，手动添加节区的过程是一个让我们熟悉上述步骤的过程。在网上有很多现成的添加节区的工具，我们自己编写工具的目的是掌握和了解其实现方法，锻炼我们的编程能力；手动添加节区是为了巩固前面的知识，熟悉添加节区的步骤。

接下来还是使用前面的那个测试程序。使用C32ASM用十六进制编辑方式打开这个程序，并定位到其节表处，如图4-35所示。

图4-35　节表位置信息

从图中可以看到该PE文件有3个节表，直接看十六进制信息可能很不方便，为了直观方便地查看节表中IMAGE_SECTION_HEADER的信息，那么使用LordPE来进行查看，如图4-36所示。

图4-36　使用LordPE查看该节表信息

用LordPE工具查看的确直观多了。对照LordPE显示的节表信息来添加一个节区。回顾一下IMAGE_SECTION_HEADER结构体的定义，定义如下：

IMAGE_SECTION_HEADER结构体的成员很多，但是真正要使用的就只有6个，分别是Name、VirtualSize、VritualAddress、SizeOfRawData、PointerToRawData和 Characteristics。这6项刚好与LordPE显示的6项相同。其实IMAGE_SECTION_HEADER结构体中其余的成员几乎不被使用。下面分别来介绍如何添加这些内容。

IMAGE_SECTION_HEADER的长度为40个字节，是十六进制的0×28。在C32ASM中占用2行半的内容，我们一次把这两行半的内容手动添加进去。回到C32ASM中，在最后一个节表的位置处开始添加内容，首先把光标放到右边的ASCII字符中，输入“.test”，如图4-37所示。

图4-37　添加".test"节名

接下来在00000240位置处添加节的大小，该大小直接是对齐后的大小即可。由于文件对齐是0×1000字节，也就是4096个字节。那么就采用最小值即可，使该值为0×1000。不知道大家是否还记得前面提到的字节顺序的问题，在C32ASM中添加时，正确的添加应当是“00 10 00 00”,以后添加时也要注意字节顺序，在添加后面几个成员时，不再提示注意字节顺序，大家应时刻清楚这点。在添加该值时，应当将光标定位在十六进制编辑处，而不是刚才所在的ASCII字符处。顺便要把VirutalAddress也添加上，VirtualAddress的值是前一个节区的起始位置加上上一个节对齐后的长度的值,上个节区的起始位置为0×5000,上个节区对齐后的长度为0×1000,因此新节区的起始位置为0×6000。添加VirtualSize和VirtualAddress后如图4-38所示。

图4-38　添加VirtualSize和 VirtualAddress

接下来的两个字段分别是SizeOfRawData和PointerToRawData,这两个字段的添加方法类似前面两个字段的添加方法，这里就不细说了，分别要添加“0×6000”和“0×1000”两个值,如图4-39所示。

图4-39　添加SizeOfRawData和PointerToRawData

PointerToRawData后面的12个字节都可以为0,只要修改最后4个字节的内容，也就是 Characteristics的值即可，这个值直接使用上个节区的值即可，实际添加时应根据所要节的属性给值，这里为了省事而直接使用上个节区的属性，如图4-40所示。

图4-40　添加Characteristics

整个节表需要添加的地方就添加完成了，接下来需要修改该PE文件的节区数量了，当前节区数量是3,这里要修改为4。虽然可以通过LordPE等修改工具完成，但是这里仍然使用手动修改。对于修改的位置请大家自行定位找到，修改如图4-41所示。

图4-41　修改节区个数为4

除了节区数量以外还要修改文件映像的大小，也就是前面提到的SizeOfImage的值。由于新添加了节区，那么应该把该节区的大小加上SizeOflmage的大小，即为新的SizeOflmage的大小。现在的SizeOflmage的大小为0×6000,加上新添加节区的大小为0×7000。SizeOflmage的位置请自行查找，修改如图4-42所示。

图4-42　修改SizeOflmage的值为0×70000

对于修改PE结构字段的内容都已经做完了，最后一步就是要添加真实的数据了，由于这个节区不使用，因此填充0值就可以了，文件的起始位置为0×6000,长度为0×1000。把光标移到文件的末尾，单击菜单“编辑”-> “插入数据”命令，在插入数据大小文本框中输入十进制的4096,也就是十六进制的0×1000,如图4-43所示。

图4-43　“插入数据”对话框的设置

单击“确定”按钮，可以看到在刚才的光标处插入了很多0值，这样我们的工作也完成了，单击“保存”按钮进行保存，提示是否备份，选择“是”。然后用LordPE查看一下添加节区的情况，如图4-44所示。

图4-44　添加新的节区信息

对比一下前后两个文件的大小，如图4-45所示。

图4-45　添加节区前后文件的大小

从图4-45中可以看出，添加节区后的文件比原来的文件大了4KB,也就是添加了4096个字节的0值的原因。也许最关心的不是大小问题，而在于软件添加了大小后是否真的可以运行，我们试运行一下，是可以运行的。

上面的整个过程就是手动添加一个新节区的全部过程，除了特有的几个步骤以外，就是要注意新节区的内存起始位置和文件起始位置的值。相信通过上面手动添加节区对此已经非常熟悉了。那么下面就开始通过编程来完成添加节区的任务吧。

4.6.2　通过编程添加节区

通过编程添加一个新的节区无非就是文件相关的操作，只是多了一个对PE文件的解析而已。添加节区的步骤和手动添加节区的步骤是一样的,只要一步一步按照上面的步骤写代码就可以了。在开始写代码前，首先修改一下FileCreate()这个函数中的部分代码，修改代码如下:

在这里，要把SEC_IMAGE这个值注释掉,因为要修改内存文件映射,有这个值会使添加节区失败，因此要将其注释掉或者删除掉。

程序的界面如图4-46所示。

图4-46　添加节区界面

首先来编写“添加”按钮响应事件，代码如下:

按钮事件中最关键的地方是AddSec()这个函数，该函数有两个参数,分别是添加节的名称与添加节的大小。这个大小无论输入多大，最后都会按照对齐方式进行向上对齐。我们看一下AddSec()函数的代码。

代码中每一步都按照相应的步骤来完成，其中用到了两个函数分别是AlignSize()和AddSecData()。前者是用来进行对齐的，后者是在文件中添加实际的数据内容的。这两个函数非常简单，代码如下所示：

整个添加节区的代码就完成了，仍然使用最开始的那个简单程序进行测试，看是否可以添加一个节区，如图4-47所示。

从图4-47中可以看出，添加节区是成功的。试着运行一下添加节区后的文件，可以正常运行，而且添加节区的文件比原文件大了4KB，和前面手动添加的效果是一样的。

至此，对PE文件结构的介绍就结束了。其实PE文件结构还有很多比较重要的内容，但是这里只介绍了一些基础的知识，至于其他的内容请大家自行学习。PE结构查看器最关键的是兼容性，PE结构是可以进行各种变形的，常规的PE结构也许比较好解析，但是经过变形的PE结构解析起来就可能会出错，因此要不断地尝试去解析不同的PE文件结构，PE查看器兼容性才会不断地完善。前面介绍了通过C32ASM进行手动分析PE文件结构，这种方法是有助于完善PE查看器的。这就好比数据恢复一样，数据恢复高手绝对不是通过数据恢复工具来进行的，虽然高手们也在使用工具，但是如果遇到较为复杂的情况，数据恢复工具可能就会显得无力，那么手动分析分区格式将是唯一的方法。

图4-47　添加节区

4.7　破解基础知识及调试API函数的应用

在介绍完PE文件结构以后，接下来要介绍调试API。调试API是系统留给用户进行程序调试的接口，其功能非常强大。在介绍调试API以前，先来介绍一下OD的使用。OD是强大的用来调试应用程序的工具。在第1章中对其进行了简单的介绍，在本章中将通过实例来演示一下OD的强大功能。同样，为了后续的部分较容易理解，这里我们自己写一个简单程序，用OD来进行调试。除了介绍调试API以外，还会介绍一些简单的与破解相关的内容。当然，破解是一个技术的积累，也是要靠多方面技术的综合应用，希望这些简单的基础知识能给大家起到一个抛砖引玉的作用。

4.7.1　CrackMe程序

下面将写一个CrackMe程序，CrackMe的意思就是“来破解我”的意思。这里提到的破解是针对软件方面来说的，不是网络中的破解。对于软件的破解来说，主要是取消软件中的各种限制，比如时间限制、序列号限制……对于破解来说，无疑与逆向工程有着密切的关系，想要突破任何一种限制都要去了解该种限制的保护方式或保护机制。

破解别人的软件属于侵权行为，尽管有很多人在做这样的事情，但是大多人是为了进行学习研究而非用于牟取商业利益。但是，为了尊重他人的劳动成果，也为了避免给自己带来不必要的麻烦，大家尽可能找一些CrackMe来进行学习和研究。

下面来写一个非常简单的CrackMe程序供我们学习研究，并进行“破解”。自己写CrackMe并自己破解，虽然这样省去了很多问题的思考，但是对于初学者来说这仍然是一件非常有乐趣的事情。

这个程序使用MFC来编写，界面如图4-48所示。

图4-48　自己编写的CrackMe程序

从图4-48中可以看出，整个程序只有两个可以输入内容的编辑框，和两个可以单击的按钮，除此之外什么都没有了，更不会有什么提示。基本上这就是一个CrackMe的样子。不过有的人习惯在CrackMe中添加一个美女的照片，让界面显得美观诱惑，有的人喜欢给CrackMe加层壳来增加破解的难度，不过这些对于我们都不重要，关键是要进行学习。在界面上输入一个账号和一个密码，当单击“确定”按钮后，该按钮会执行以下代码：

整个代码非常简单。这段代码是通过输入的账号来生成密码的，而不是有固定的账号和固定的密码进行一一对应的。生成密码的算法非常简单，把输入的账号的每一个ASCⅡ码进行加1运算，但是有几个ASCⅡ值除外。如果该ASCⅡ码是字符大写 “Z”、小写 “z”或者是数字“9”，那么，就不会进行加1运算。除了这点外，要求账号的长度必须大于7位，这也算是一个小小的要求了。

测试一下。输入一个小于7位的账号，再随便输入一个密码，单击“确定”按钮，这时程序不会有任何反应。那么，这次输入一个超过7位的账号，单击“确定”按钮来试试，如图4-49所示。

图4-49　CrackMe提示错误

CrackMe提示“密码错误”。当然了，密码是根据账号算出来的，而且跟账号的长度是相等的。那么，该如何获得这个CrackMe的密码呢？如果这个CrackMe不是我们写的，那我们该怎么办？接下来的工作，就交给OD来完成吧。

4.7.2　用OD破解CrackMe

对于破解来说，总是要找到一个突破点的。而对于这个简单的CrackMe来说，突破点是非常多的。下面会以不同的方式来开始这次破解之旅，不需要有太多的汇编知识，毕竟这里是基础性的知识，要想深入学习破解，对破解有所了解的话，那么学习和掌握汇编是必修课。

1.破解方法一

现在用OD打开所编写的CrackMe，如图4-50所示。

大家还记得OD中各个窗口的作用吧？如果忘记请参考第1章的内容。用OD打开CrackMe以后会看到很多汇编代码，这部分内容大家可以通过学习汇编语言和逆向知识来进行学习。在这里会介绍一些基本的破解技巧，通过这些技巧同样可以完成破解工作。

图4-50　用OD打开CrackMe后的界面

首先来梳理一下思路，梳理思路的时候可以参考上面写的代码来进行梳理。输入了“账号”及“密码”后，首先程序会从编辑框处获得“账号”的字符串及“密码”的字符串，然后进行一系列的比较验证，再通过“账号”来计算出正确的“密码”，最后来匹配正确的“密码”与输入的“密码”是否一致，根据匹配结果，给出相应的提示。

上面是编写代码的流程和思路，我们也可根据这个思路合理地设置断点（设置断点也叫下断点）。“断点”就是产生中断的位置。通过下断点，可以让程序中断在需要调试或分析的地方。下断点在调试中起着非常大的作用，学会在合理的地方下断点也是一个技巧性的知识，合理地下断点有助于对软件进行分析和调试，大家应该学着掌握它。断点的分类很多，有内存断点、硬件断点、INT 3断点……关于断点的知识，将在稍后的内容中进行介绍，这里就不介绍了。

现在就可以选择合适的地方设置断点了。可以在API函数上设置断点，比如在GetDlgItemText()行设置断点，也可以选择在strlen()上设置断点，还可以在strcmp()上设置断点，甚至可以在MessageBox()上设置断上设置断点。上面的这些API函数都是可以设置断点的，但是对于GetDlgItemText()和MessageBox()这两个API函数来说，需要下断点的时候指定是ANSI版本，还是UNICODE版本。也就是说系统中是没有这两个函数的，根据版本的不同存在系统的函数只有GetDlgItemTextA()、GetDlgItemTextW()、MessageBoxA()和MessageBoxW()这样的函数。通常使用ANSI版本的即可。

上面有如此多的API函数可供我们设置断点，那么要选择哪个进行设置呢？最好的选择是strcmp()这个函数，因为在比较函数处肯定会出现正确的“密码”。而在GetDlgItemTextA()和strlen()上设置断点，需要使用F8进行跟踪。如果在MessageBoxA()上设置断点，那么就不容易找到正确的“密码”存放的位置了。所以选择在strcmp()上设置断点。在“命令”窗口中输入“bp strcmp”，然后按回车键，如图4-51所示。

图4-51　在“命令”窗口设置断点

如何知道断点是否设置成功呢？按下Alt + B组合键打开断点窗口可以查看，如图4-52所示。

图4-52　在断点窗口查看所下断点

断点已经设置好了，那么就按F9键来运行程序吧。CrackMe启动了，输入一个长度大于等于7位的“账号”为“testtest”，然后随便输入一个“密码”为“123456”，单击“确定”按钮，OD中断在断点，如图4-53所示。

图4-53　OD断下响应了断点

从图4-53中可以看到，OD断在了strcmp这个函数的首地址处，地址为10217570。断在这里如何找到真正的密码呢？其实在提示的地方已经显示出了正确的密码，我们可以看栈窗口。函数参数的传递是依赖于栈的，对于C语言来说，函数的参数是从右往左依次入栈的。对于strcmp()函数来说，该函数有两个参数，这两个参数分别是要进行比较的字符串。那么在栈窗口中可以看到输入的密码及正确的密码，如图4-54所示。

图4-54　栈窗口中显示出的两个密码

可以看到，在调用strcmp()时，传递的两个参数的值分别是“123456”和“uftuuftu”两个字符串。前面的字符串肯定是输入的密码，那么后面的字符串肯定就是正确的密码了。按F9键运行程序，会出现那个错误对话框，提示密码错误。现在关闭OD，直接打开CrackMe，现在仍然用刚才的账号“testtest”，然后输入密码“uftimftu”，单击“确定”按钮，会提示“密码正确”，如图4-55所示。

图4-55　密码正确

这样就完成了破解，这种方法比较简单。只要在strcmp()函数处设置断点即可。大家可以试着在其他几个API函数处设置断点，然后试着找到正确的注册码。接下来，尝试使用另外的方法来对CrackMe进行破解。

2.破解方法二

在上一种方法中通过对API函数设置断点找到了正确的密码，现在通过提示字符串来完成破解。在不知道正确密码的情况下输入密码，通常会得到的提示字符串是“密码错误”，只要在程序中寻找该字符串，并且查看是何处使用了该字符串，那么就可以对破解起到提示性的作用。

用OD打开CrackMe程序，然后在反汇编界面处单击鼠标右键，在弹出的菜单中依次选择“超级字串参考”–> “查找ASCⅡ”命令，会出现“超级字串参考”窗口，如图4-56所示。

图4-56　超级字串参考

在“超级字串参考”窗口中可以看到两个非常熟悉的字符串，双击“密码正确”这个字符串，来到004024de地址处，该地址内容如图4-57所示。

图4-57　004024DE地址处反汇编代码

从图4-57中可以看到3处比较关键性的内容，第一个是可以看到strcmp()函数，第二个是可以看到字符串“密码正确”，第三个是可以看到字符串“密码”错误。由这3个内容可以让我们联想到这岂不是和C代码基本上是对应的啊。根据strcmp()的比较结果，if……else……会选择不同的流程进行执行。也就是说，只要改变比较的结果，或者更换比较的条件，都可以改变程序的流程。下面主要讲述一下修改比较条件的方法，拿具体的例子来解释一下，在代码中是这么一个情况，代码如下：

strcmp()是字符串比较函数，如果两个字符串相等也就是说输入的密码与正确的密码匹配，匹配则执行“密码正确”流程，否则反之。那么如果修改一下比较的条件，也就是说两个密码匹配不成功，使其执行“密码成功”的流程。这样，输入错误的密码，也会提示“密码正确”的提示。在C语言中的修改很简单，只要修改为如下代码就可以：

但是对于反汇编应该如何做呢？其实非常简单，我们再回过头来看一下图4-57中的那几条反汇编代码。想要修改其判断条件，那么只要修改004024D8处的指令代码JNZ SHORT EasyCrac.004024ED即可，该指令的意思是如果比较结果不为0，则跳转到004024ED处执行。 JNZ结果不为0则跳转，只要把JNZ修改为JZ即可，JZ的意思刚好与JNZ相反。修改方法很简单，选中004024D8地址所在的行，按下空格键即可进行编辑，如图4-58所示。

单击窗口上的“汇编”按钮，然后按F9键运行，随便输入一个长度大于7位的账号，再输入一个密码，然后单击“确定”按钮，会提示“密码正确”，如图4-59所示。

关掉OD和CrackMe，然后直接运行CrackMe，随便输入账号和密码，单击“确定”按钮后提示密码错误。为什么呢？因为刚才只是在内存中进行了修改，需要对文件进行存盘在以后运行时我们的修改才有效。修改后的存盘方法为：选中修改的反汇编代码（可以多选几行，只要修改的那行被选中即可），然后单击右键，在弹出的菜单中选择“复制到可执行文件” –> “选定内容”命令，会出现“文件”对话框，如图4-60所示。

图4-58　在OD中修改反汇编代码

图4-59　修改指令后的流程

图4-60　“文件”对话框

在出现的这个对话框中单击鼠标右键，在弹出的菜单中选择“保存文件”命令，然后进行保存。这样我们的修改就存盘了，下次在执行该程序时，随便输入大于7位的账号和密码都会提示“密码正确”了。如果你输入了正确的密码，那么会提示“密码错误”。

上面就是两种破解该CrackMe的方法，这两种方法都是极其简单的方法，现在可能已经很不实用了。这里是为了学习，为了提高动手能力，而采用了这两种方法。

4.8　文件补丁及内存补丁

4.8.1　文件补丁

用OD修改CrackMe是比较容易的，如果脱离OD该如何修改呢？其实在OD中修改了反汇编的指令以后，对应的在文件中修改的是机器码。只要在文件中能定位到指令对应的机器码的位置，那么直接修改机器码就可以了。JNZ对应的机器码指令为0×75, JZ对应的机器码指令为0×74。也就是说，只要在文件中找到这个要修改的位置，用十六进制编辑器把0×75修改为0×74即可。如何能把这个内存中的地址定位到文件地址呢？这就是前面介绍的PE文件结构中把VA转换FileOffset的知识了。

具体的手动步骤大家自己尝试，我们直接通过写代码进行修改吧。为了简单起见，这里使用控制台来编写，而且直接对文件进行操作，中间的步骤就省略了，想必有了思路以后对于大家来说已经不是难事了。

看一下关于文件补丁的代码：

代码给出了详细的注释，只需要把CrackMe文件拖放到文件补丁上即可，如图4-61所示。

图4-61　对CrackMe进行文件补丁

通常，在做文件补丁以前一定要对打算进行修改的位置进行比较，以免产生错误的修改。程序使用的方法是将要修改的部分读出来，看是否与用OD调试时的值相同，如果相同则打补丁。由于这里只是介绍编程知识，针对的是一个CrackMe。如果对某个软件进行了破解，自己做了一个文件补丁发布出去给别人使用，不进行相应的判断就直接进行修改，很有可能导致软件不能使用的情况，因为对外发布以后你不能确认别人所使用的软件的版本等因素。因此，在进行文件补丁时最好判断一下，或者是用CopyFileO对文件进行备份。

4.8.2　内存补丁

相对于文件补丁来说，还有一种补丁是内存补丁。这种补丁是把程序加载到内存中以后对其进行修改，也就是说本身是不对文件进行修改的。要将CrackMe载入内存中，载入内存可以调用CreateProcess()这个API函数来完成，这个函数参数众多，功能强大。使用CreateProcess()创建一个子进程，并且在创建的过程中将该子进程暂停，那么就可以安全地使用WriteProcessMemory()函数来对CrackMe进行修改了。整个过程也比较简单，下面直接来阅读源代码。

代码中的注释也比较详细，代码的关键是要进行比较，否则会造成程序的运行崩溃。在进行内存补丁前，需要将线程暂停，这样做的好处是有些情况下可能没有机会进行补丁就已经执行完需要打补丁的地方了。当打完补丁以后，再恢复线程继续运行就可以了。

文件补丁与内存补丁就已经介绍完了。对于这两种补丁来说，都是通过前面学到的知识来完成的，可见前面的基础知识的用处还是非常广泛的。用了这么多的篇幅来介绍使用OD破解CrackMe，也介绍了文件补丁和内存补丁，那么，接下来就要开始学习调试API了。掌握了调试API以后，完全可以打造一个类似于OD这样的应用程序调试器。让我们一步一步学习吧。

4.9　调试API函数的使用

在Windows中有这么一些API函数是专门用来进行调试的，这些函数被称作为Debug API，或者是调试API。利用这些函数可以进行调试器的开发，调试器通过创建有调试关系的父子进程来进行调试，被调试进程的底层信息、即时的寄存器、指令等信息都可以被获取，进而用来分析。

上面介绍的OllyDbg调试器的功能非常得强大，虽然有众多的功能，但是其基础的实现就是依赖于调试API。调试API函数的个数虽然不多，但是合理的使用会产生非常大的作用。调试器依赖于调试事件，调试事件有着非常复杂的结构体，调试器有着固定的流程，由于实时需要等待调试事件的发生，其过程是一个调试循环体，非常类似SDK开发程序中的消息循环。无论是调试事件，还是调试循环，对于调试或者说调试器来说，其最根本、最核心的部分是中断，或者说其最核心的部分是可以捕获中断。

4.9.1　常见的3种断点方法

在前面介绍OD的时候提到过，产生中断的方法是设置断点，常见的产生中断的断点方法有3种，一种是中断断点，一种是内存断点，还有一种是硬件断点。下面分别来介绍这3种断点的不同。

中断断点，这里通常指的是汇编语言中的int 3指令，CPU执行该指令时会产生一个断点，因此也常称之为INT3断点。现在演示一下如何使用int 3来产生一个断点。代码如下：

在代码中使用了__asm，在__asm后面可以使用汇编指令，如果想添加一段汇编指令，方法是__asm{}这样的。通过__asm可以在C语言中进行内嵌汇编语言。在__asm后面直接使用的是int 3指令，这样会产生一个异常，称为断点中断异常。对这段简单的代码进行编译连接，并且运行。运行后出现了错误对话框，如图4-62所示。

图4-62　异常对话框

这个对话框可能常常见到，而且见到以后多半很郁闷，通常情况是直接单击“不发送”按钮，然后关闭这个对话框。在这里，这个异常是通过int 3导致的，不要忙着关掉它。通常在写自己的软件时如果出现这样的错误，应该去寻找一些更多的帮助信息来修正错误。单击“请单击此处”链接，出现如图4-63所示的对话框。

图4-63　“异常基本信息”对话框

弹出“异常基本信息”对话框，因为这个对话框给出的信息实在太少了，继续单击“要查看关于错误报告的技术信息”后面的“单击此处”链接，打开如图4-64所示的对话框。

图4-64　“错误报告内容”对话框

通常情况下，在这个报告中只关心两个内容，一个内容是Code，另一个内容是Address。在图4-64中，Code后面的值为0×80000003，在Address后面的值为0×0000000000401028。 Code的值为产生异常的异常代码，Address是产生异常的地址。在Winnt.h中定义了关于Code的值，在这里0×80000003的定义为STATUS_BREAKPOINT，也就是断点中断。在Winnt. h中的定义为：

这里给的Address可以看出是一个VA（虚拟地址），用OD打开这个程序，直接按F9键运行，如图4-65、图4-66所示。

图4-65　在OD中运行后被断下

图4-66　OD状态栏提示

从图4-65所示的地方看到，程序执行停在了00401029的位置处。从图4-66所示的地方可以看到，INT3命令位于00401028的位置处。再看一下图4-64中Address后面的值，值为00401028。这也就证明了在系统的错误报告中可以给出正确的出错地址的。这样在以后写程序的过程中可以很容易地定位到自己程序中有错误的位置。

回到中断断点的话题上，中断断点是由int 3产生的，那么要如何通过调试器（调试进程）在被调试进程中设置中断断点呢？看图4-65中00401028地址处，在地址值的后面，在反汇编代码的前面，中间那一列的内容是汇编指令对应的机器码。可以看出，INT3对应的机器码是0×CC。如果想通过调试器在被调试进程中设置INT3断点的话，那么只需要把要中断的位置的机器码改为0×CC即可，当调试器捕获到该断点异常时，修改为原来的值即可。

内存断点的方法同样是通过异常来产生的。在Win32平台下，内存是按页进行划分的，每页的大小为4KB。每一页内存都有其各自的内存属性，常见的内存属性有只读、可读写、可执行等。内存断点的原理就是通过对内存属性的修改，而导致本该进行的操作无法进行，这样便会引发异常。

在OD中关于内存断点有两种，一种是内存访问，另外一种是内存写入。用OD随便打开一个应用程序，在其“转存窗口”（或者叫“数据窗口”）中随便选中一些数据点后单击右键，在弹出的菜单中选择“断点”命令，在“断点”子命令下会看到“内存访问”和“内存写入”两种断点，如图4-67所示。

图4-67　内存断点类型

下面通过简单例子来看一下如何产生一个内存访问异常，代码如下：

在这个程序中，使用了VirtualProtect()函数，该函数与第3章中介绍的VirtualProtectEx()函数类似，不过VirtualProtect()是用来修改当前进程的内存属性的。如果不记得，可以参考一下MSDN。

对这个程序编译连接，并运行起来。熟悉的出错界面又出现在眼前，如图4-68所示。

图4-68　“异常基本信息”对话框

按照前面介绍的步骤打开“错误报告内容”对话框，如图4-69所示。

图4-69　“错误报告内容”对话框

按照上面的分析方法来看一下Code和Address这两个值。Code后面的值为0×C0000005，这个值在Winnt.h中的定义如下：

这个值的意义表示访问违例。在Address后面的值为0×0000000000402fa3，这个值是地址，但是这里的地址根据程序来考虑，值是用malloc()函数申请的，用于保存数据的堆地址，而不是用来保存代码的地址。这个地址就不进行测试了，因为是动态申请，很可能每次不同，因此大家了解就可以了。

硬件断点是有硬件进行支持的，关于硬件断点的具体原理就不进行介绍了。在OD中使用硬件断点的方法类似于内存断点，同样是在右键菜单中进行设置。由于是由硬件支持的，因此只能设置4个。请大家自行学习使用。

4.9.2　调试API函数及相关结构体介绍

通过前面的内容已经知道，调试器的根本是依靠中断，其核心也是中断。前面也演示了两个产生中断异常的例子。本小节的内容是介绍调试API函数，及其相关的调试结构体。调试API函数的数量非常少，但是其结构体是非常少有的较为复杂的，虽然说是复杂，其实只是嵌套的层级比较多，只要了解了较为常见的，剩下的可以自己对照MSDN进行学习。在介绍完调试API函数及其结构体后，再来简单演示一下，如何通过调试API捕获INT3断点和内存断点。

创建调试关系

既然是调试，那么必然存在调试和被调试。调试和被调试的这种调试关系是如何建立起来的，是我们首先要了解的内容。要使调试和被调试创建调试关系，那么就会用到两个函数中的一个，这两个函数分别是CreateProcess()和DebugActiveProcess()。其中CreateProcess()函数已经介绍过了，那么如何使用CreateProcess()函数来建立一个需要被调试的进程呢？还是来回顾一下CreateProcess()函数吧。CreateProcess()函数的定义如下：

现在要做的是创建一个被调试进程。在CreateProcess()函数中，有一个dwCreationFlags的参数，该参数的取值中有两个重要的常量，分别为DEBUG_PROCESS和DEBUG_ONLY_THIS_PROCESS。DEBUG_PROCESS的作用就是被创建的进程处于调试状态，如果一同指定了DEBUG_ONLY_THIS_PROCESS的话，那么就只能调试被创建的进程，而不能调试被调试进程创建出来的进程。只要在使用CreateProcess()函数时指定这两个常量即可。

除了CreateProcess()函数以外，还有一种创建调试关系的方法，该方法用的函数如下：

这个函数的功能是附加到一个进程上。该函数的参数就一个，该参数指定了被调试进程的进程ID号。从函数名与函数参数可以看出来，这个函数是和一个已经被创建的进程来建立调试关系的，跟CreateProcess()的方法是不一样的。在OD中也同样有这个功能，打开OD，选择菜单中的“文件”> “挂接”命令，就出现“选择要挂接的进程”窗口，如图4-70所示。

图4-70　“选择要挂接到的进程”的窗口

OD的这个功能就是通过DebugActiveProcessO函数来完成的。

调试器与被调试的目标进程可以通过前两个函数建立调试关系，但是如何使调试器与被调试的目标进程断开调试关系呢？有一个很简单的方法，关闭调试器进程，这样调试器进程与被调试的目标进程会同时结束。也可以关闭被调试的目标进程，这样也可以达到断开调试关系。那如何让调试器与被调试的目标进程断开调试关系，又保持被调试目标进程的运行呢？这里介绍一个函数，函数名为DebugActiveProcessStopO，其定义如下：

该函数只有一个参数，就是被调试进程的进程ID号。使用该函数可以在不影响调试器进程和被调试进程的正常运行而将两者的关系进行解除。

4.9.3　判断是否处于被调试状态

很多程序都要检测自己是否处于被调试状态，比如游戏、病毒，或者加壳后的程序。游戏为了防止被做出外挂而进行反调试，病毒为了给反病毒工程师增加分析难度而反调试，加壳程序是专门用来保护软件的，当然也会有反调试的功能（该功能仅限于加密壳，压缩壳是没有反调试功能的）。

本小节不是要介绍反调试，而是要介绍一个简单的函数，这个函数是判断自身是否处于被调试状态，函数名为IsDebuggerPresent()，函数的定义如下：

该函数没有参数，根据返回值来判断是否处于被调试状态。这个函数也可以用来进行反调试。不过由于这个函数的实现过于简单，很容易就能够被分析者突破，因此现在也没有软件再使用该函数来用作反调试了。

下面通过一个简单的例子来演示一下IsDebuggerPresent()函数的使用。代码如下：

这个例子用来检测是否被调试。在进入主函数后，直接调用IsDebuggerPresent()函数，用来判断是否被调试器创建。在自定义线程函数中，一直循环检测是否被附加。只要发现自身处于被调试状态，那么就在控制台中进行输出提示。

现在用OD对这个程序进行测试。首先用OD直接打开这个程序并按F9键运行，如图4-71所示。

图4-71　主函数检测到调试器

按下F9键启动以后，控制台中输出“mian func checked the debuggee”，也就是发现了调试器。

再测试一下检测OD附加的效果。先运行这个程序，然后用OD去挂接它，看其提示，如图4-72所示。

图4-72　线程函数检测到调试器

控制台中输出“thread func checked the debuggee”。可以看出用OD进行附加也能够检测到自身处于被调试状态。

4.9.4　断点异常函数

有时为了调试方便可能会在自己的代码中插入_asm int 3，这样当程序运行到这里时会产生一个断点，就可以用调试器进行调试了。其实微软提供了一个函数，使用该函数可以直接让程序运行到某处的时候产生INT3断点，该函数的定义如下：

修改一下前面的程序，把_asm int 3替换为DebugBreak()，编译连接并运行看一下。同样会因产生异常而出现“异常基本信息”对话框，查看它的“错误报告内容”，如图4-73所示。

图4-73　“错误报告内容”对话框

看一下Code的后面的值，看到值为0×80000003就应该知道是EXCEPTION_BREAKPOINT。再看Address后面的值，值为0×000000007c92120e，从这个地址可以看出，该值在系统的DLL文件中，因为调用的是系统提供的函数。

4.9.5　调试事件

调试器在调试程序的过程中，是通过不断地下断点来完成的，而断点的产生在前面的内容中提到过一部分。通过前面介绍的INT3断点和内存断点可以得知，调试器是在捕获目标进程产生的异常从而作出响应。当然，对于介绍的断点来说是这样的，不过对于调试器来说，除了对异常作出响应以外，还会对其他的一些事件作出响应，异常只是所有调试能进行响应事件的一部分。

调试器的工作主要是依赖调试事件，调试事件在系统中被定义为一个结构体，也是到目前为止要接触的最为复杂的一个结构体，因为这个结构体的嵌套关系很多。这个结构体的定义如下：

这个结构体非常重要，我们有必要详细地介绍一番。

（1） dwDebugEventCode：该字段指定了调试事件的类型编码。在调试的过程中可能产生的调试事件非常多，因此要根据不同的类型码进行不同的响应处理。常见的调试事件如图4-74所示。

图4-74　dwDebugEventCode取值

（2） dwProcessId：该字段指明了引发调试事件的进程ID号。

（3） dwThreadId：该字段指明了引发调试事件的线程ID号。

（4） u：该字段是一个联合体，该联合体的取值由dwDebugEventCode指定。在该联合体中包含了很多个结构体，包括 EXCEPTION_DEBUG_INFO、CREATE_THREAD_ DEBUGJNFO、CREATE_PROCESS_DEBUG_INFO、EXIT_THREAD_DEBUG_INFO、 EXIT_PROCESS_DEBUG_INFO、LOAD_DLL_DEBUG_INFO、UNLOAD_DLL_DEBUG_ INFO和 OUTPUT_DEBUG_STRING_INFO。

在以上众多的结构体中，特别要介绍一下EXCEPTION_DEBUG_INFO,因为这个结构体中包含了关于异常相关的信息，而对于其他的几个结构体，使用比较简单，大家可以参考 MSDN。EXCEPTION_DEBUG_INFO的定义如下：

在EXCEPTION_DEBUG_INFO中包含的EXCEPTION_RECORD结构体中保存着真正的异常信息，对于dwFirstChance里面保存着ExceptionRecord的个数。看一下关于 EXCEPTION_RECORD结构体的定义：

（1） ExceptionCode：异常码。该值在MSDN中的定义非常多，不过我们需要使用的值只有 3个，分别是 EXCEPTION_ACCESS_VIOLATION （访问违例）、EXCEPTION_ BREAKPOINT （断点异常）和EXCEPTION_SINGLE_STEP （单步异常）。这3个值中的前两个值对于我们来说是非常熟悉的，因为在前面已经介绍过了，关于最后一个单步异常想必也是非常熟悉的了。我们在使用OD快捷键的F7键、F8键时就是在使用单步功能，而单步异常就是有EXCEPTION_SINGLE_STEP来表示的。

（2） ExceptionRecord：指向一个EXCEPTION_RECORD的指针，异常记录是一个链表，其中可能保存着很多的异常信息。

（3） ExceptionAddress：异常产生的地址。

调试事件这个结构体DEBUG_EVENT看似非常复杂，其实也只是嵌套得比较深而已。只要大家去体会每个结构体，体会每层嵌套的含义，自然而然就觉得它没有多么复杂了。

4.9.6　调试循环

调试器在不断地对被调试目标进程进行捕获调试信息，有点类似于Win32应用程序的消息循环，但是又有所不同。调试器在捕获到调试信息后，进行相应地处理，然后恢复线程使之继续运行。

用来等待捕获被调试进程调试事件的函数是WaitForDebugEvent()，该函数的定义如下：

（1） lpDebugEvent：该参数用于接收保存调试事件；

（2） dwMillisenconds：该参数用于指定超时的时间，无限制等待使用INFINITE。

在调试器捕获到调试事件后会对被调试的目标进程中产生调试事件的线程进行挂起，在调试器对被调试目标进程进行相应的处理后，需要使用ContinueDebugEvent()对先前被挂起的线程进行恢复。ContinueDebugEvent()函数的定义如下：

（1） dwProcessId：该参数表示被调试进程的进程标识符。

（2） dwThreadId：该参数表示准备恢复挂起线程的线程标识符。

（3 ） dwContinueStatus：该参数指定了该线程以何种方式继续执行‘该参数的取值为 DBG_EXCEPTION_NOT_HANDLED和DBG_CONTINUE。对于这两个值来说，在通常的情况下并没有什么差别。但是当遇到调试事件中的调试码为EXCEPTION_DEBUG_EVENT时这两个常量就会有不同的动作，如果使用DBG_EXCEPTION_NOT_HANDLED,调试器进程将会忽略该异常，Windows会使用被调试进程的异常处理函数对异常进行处理；如果使用 DBG_CONTINUE的话，那么需要调试器进程对异常进行处理，然后继续运行。

由上面两个函数配合调试事件结构体，就可以构成一个完整的调试循环，以下这段调试循环的代码摘自MSDN，代码如下：

以上就是一个完整的调试循环，不过有些调试事件对于我们来说可能是用不到的，那么就把不需要的调试事件所对应的case语句删除掉就可以了。

4.9.7　内存的操作

调试器进程通常要对被调试的目标进程进行内存的读取或写入。对于跨进程的内存读取和写入的函数其实在前面的章节已介绍过，就是ReadProcessMemory()和WriteProcessMemory().

要对被调试的目标进程设置INT3断点时，就需要使用WriteProcessMemory()函数对指定的位置写入0×CC.当INT3被执行后，要在原来的位置上把原来的机器码写回去，原来的机器码需要使用ReadProcessMemory()函数来进行读取。

关于内存操作除了以上两个函数以外，还有一个就是修改内存的页面属性的函数，即 VirtualProtectEx()，同样这个函数前面也介绍过了。

4.9.8　线程环境相关API及结构体

在前面的章节中介绍过，进程是用来向系统申请各种资源的，而真正被分配到CPU并执行代码的是线程。进程中的每个线程都共享着进程的资源，但是每个线程都有不同的线程上下文，或线程环境。Windows是一个多任务的操作系统，在Windows中为每一个线程分配一个时间片，当某个线程执行完其所属的时间片后，Windows会切换到另外的线程去执行。在进行线程切换以前有一步保存线程环境的工作，那就是保存在切换时线程的所有寄存器值、栈信息及描述符等相关的所有信息。只有把线程的上下文保存起来，在下次该线程被CPU再次调度时才能正确地接着上次的工作继续进行。

在Windows系统下，将线程环境定义为CONTEXT结构体，该结构体需要在Winnt.h头文件中找到，在MSDN中并没有给出定义。CONTEXT结构体的定义如下：

这个结构体看似很大，但是也并不大，只要了解汇编语言的话，结构体中的各个字段应该是非常熟悉的。关于各个寄存器的介绍这里就不进行介绍了，这个需要大家自己查看资料了。这里只介绍一下ContextFlags字段的功能，该字段用于控制GetThreadContext()和SetThreadContext()能够获取或写入的环境信息。ContextFlags的取值也只能在Winnt.h头文件中找到，其取值如下：

从这些宏定义的注释来看，能很清楚地知道这些宏可以控制GetThreadContext()和 SetThreadContext()进行何种操作。大家在真正使用时进行相应的赋值就可以了。

线程环境在Windows中定义了一个CONTEXT的结构体,我们要进行获取或设置线程环境的话，需要使用GetThreadContext()和SetThreadContext()。这两个函数的定义分别如下：

这两个函数的参数都基本一样，hThread表示线程句柄,而lpContext表示指向CONTEXT的指针。所不同的是，GetThreadContext()是用来获取线程环境的，SetThreadContext()是用来进行设置线程环境的。

4.10　打造一个密码显示器

关于系统提供的调试API函数我们已经学习了不少了，而且基本上常用到的函数我们也都学习过了。下面用调试API编写一个能够显示密码的程序。大家别以为我们写的程序什么密码都能显示，这是不可能的。下面针对前面的那个CrackMe来编写一个显示密码的程序。

在编写关于CrackMe的密码显示程序以前需要准备两项工作，第一个工作是知道要在什么地方合理地下断点，第二个工作是从哪里能读取到密码。带着这两个问题重新来进行思考一下。在我们的程序中，要对两个字符串进行比较，而比较的函数是strcmp()，该函数有两个参数，分别是输入的密码和真正的密码。也就是说在调用strcmp()这个函数的位置下断点通过查看它的参数是可以获取到正确的密码的。就在调用strcmp()这个函数的位置设置INT3断点，也就是将0×CC机器码写入这个地址。用OD看一下调用strcmp()函数的地址，如图 4-75所示。

图4-75　调用strcmp()函数的地址

从图4-75中可以看出，调用strcmp()函数的地址为004024CE。有了这个地址只要找到该函数的两个参数，就可以找到输入的错误的密码及正确的密码。从图4-75中看出，正确的密码的起始地址保存在EDX中，错误的密码的起始地址保存在ECX中，只要在004024CE这个地址下断，并通过线程环境读取EDX和ECX寄存器值就可以得到两个密码的起始地址。

要进行准备的工作已经做好了，下面来写一个控制台的程序。先定义两个常量，一个是用来设置断点的地址，另一个是INT3指令的机器码。定义如下：

把CrackMe的文件路径及文件名当参数传递给显示密码的程序。显示的程序首先要以调试的方式创建CrackMe,代码如下：

然后就进入调试循环，调试循环中要处理两个调试事件，一个是 CREATE_PROCESS_DEBUG_EVENT,另一个调试事件是 EXCEPTION_DEBUG_EVENT下的EXCEPTION_BREAKPOINT。这两个事件在调试循环中进行处理，对于处理 CREATE_PROCESS_DEBUG_EVENT的代码如下：

在CREATE_PROCESS_DEBUG_EVENT中对调用strcmp()函数的地址处设置INT3断点，在将0×CC写入这里时要把原来的机器码读取出来。读取原机器码使用 ReadProcessMemory()，写入 INT3的机器码使用 WriteProcessMemory()。再来看一下 EXCEPTION_DEBUG_EVENT下的 EXCEPTION_BREAKPOINT是如何进行处理的。

对于调试事件的处理，应该放到调试循环中，上面的代码给的是对调试事件的处理，再来看一下调试循环的大体代码：

只要把调试事件的处理方法放入到调试循环中，程序就完整了。接下来编译连接一下，然后把CrackMe直接拖放到这个密码显示程序上。程序会启动CrackMe进程，并等待我们的输入，当输入账号及密码后单击“确定”按钮，程序会显示出正确的密码和我们输入的密码，如图4-76所示。

图4-76　显示正确密码

根据图4-76显示的结果进行验证，可见我们的获取是成功的。至此程序到此结束了。大家可以把该程序修改一下，把它改成通过附加调试进程来显示密码，以巩固我们的知识。

4.11　总结

本章学习了 PE结构的基础部分，学习了 OD的使用及调试API函数等。相信大家对PE结构解析、OD调试工具使用及调试原理有了一定的了解。本章学习了一些基础的加解密知识，在以后学习了更多的相关知识后会发现，这些基础知识对我们学习加解密的知识是非常有必要的。希望大家多多掌握原理，多多动手实践。

第5章　HOOK编程

有一种技术被称作是HOOK技术，人们常叫钩子。谈到钩子很容易让人联想到是在钓东西，比如鱼钩就用于钓鱼。在编程中钩子技术在DOS时代就已经存在了，在Windows下，钩子按照实现技术的不同，其种类也越来越多，但是设置钩子的本质却是始终不变的。

钩子到底是做什么用的呢？钩子的应用范围非常广泛，比如输入监控、API拦截、消息捕获等方面都在应用它。杀毒软件会用HOOK技术钩住一些API函数，比如钩住RegSetValueExO函数，防止病毒对注册表进行写入；病毒使用HOOK技术有针对性地捕获键盘的输入从而进行记录。这些都是属于HOOK范畴的知识。本章将介绍与HOOK有关的知识。

5.1　HOOK知识前奏

在DOS时代进行编程，那时操作系统提供的编程接口不称为API函数，而称为中断服务向量。也就是说，当时的操作系统提供的编程接口只有中断，要进行写文件要调用系统中断，要进行读文件也要调用系统中断（当然，也不调用操作系统的中断直接调用更底层的中断）……中断服务向量类似于Windows下的API函数，中断服务向量在操作系统的某个地址保存着，它是以数组形式保存着的，我们也称其为中断向量表。DOS时代的HOOK技术也就是修改中断向量表中的中断地址。比如，要捕获写操作，那么就修改中断向量表中关于写文件的地址，将写文件的中断地址保存好，然后替换为我们的地址，这样当程序调用写文件中断时，我们的函数就被执行了；当程序执行完可以继续调用原来的中断地址，从而完成写文件的操作。

在Windows下HOOK技术的方法比较多，使用比较灵活，常见的应用层的HOOK方法有Inline Hook、IATHOOK、Windows钩子……HOOK技术涉及到了DLL相关的知识。因为HOOK其他进程的时候需要访问其他进程的地址空间，使用DLL是必然的。HOOK技术也涉及到了注入的知识，想要把完成HOOK功能的DLL加载到目标进程空间中就要使用注入的知识了。让我们来学习常用的HOOK技术吧。

5.2　内联钩子——Inline Hook

5.2.1　Inline Hook的原理

API函数都保存在操作系统提供的DLL文件中，当在程序中使用某个API函数时，在运行程序后，程序会隐式地将API所在的DLL加载入进程中。这样，程序就会像调用自己的函数一样调用API，大体过程如图5-1所示。

从图5-1中可以看出，在进程中当EXE模块调用CreateFile()函数的时候，会去调用kernel32. dll模块中的CreateFile()函数，因为真正的CreateFile()函数的实现在kernel32. dll模块中。

CreateFile()是API函数，API函数也是由人编写的代码再编译而成的，也有其对应的二进制代码。既然是代码，那么就可以被修改。通过一种“野蛮”的方法来直接修改API函数在内存中的映像，从而对API函数进行HOOK。使用的方法是，直接使用汇编指令的jmp指令将其代码执行流程改变，进而执行我们的代码，这样就使原来的函数的流程改变了。执行完我们的流程以后，可以选择性地执行原来的函数，也可以不继续执行原来的函数。

假设要对某进程的kernel32. dll的CreateFile()函数进行HOOK，首先需要在指定进程中的内存中找到CreateFile()函数的地址，然后修改CreateFile()函数的首地址的代码为jmpMyProc的指令。这样，当指定的进程调用CreateFile()函数时，就会首先跳转到我们的函数当中去执行流程，这样就完成了我们的HOOK了。看一下它的流程图，如图5-2所示。

图5-1　调用API函数的大体过程

图5-2　Inline Hook的流程

由于这种方法是在程序流程中直接进行嵌入jmp指令来改变流程的，所以就把它叫做Inline Hook.

5.2.2　Inline Hook的实现

了解了大体的HOOK流程后，现在来学习它的具体实现。

我们的C程序被编译连接后为一个二进制文件，在二进制文件中，对于代码部分来说，都是CPU可以用来执行的机器码，机器码和汇编指令又是一一对应的。前面讲过了，Inline Hook是在程序中嵌入jmp汇编指令然后跳转到流程处继续执行的，jmp指令的用法是jmp目的地址。jmp在汇编语言中是一个无条件的跳转指令，jmp后面跟随的参数是跳转的目的地址。用OD随便打开一个程序，并且修改它的某条指令为jmp指令。跳转的目的为一个任意地址，如图5-3和图5-4所示。

图5-3　准备修改00401600地址处的代码为jmp指令

图5-4　修改后的代码内容

从图5-3和图5-4的对比可以看出，jmp指令占用了5个字节。原来从00401600到00401605处的机器码为55 8BEC6AFF，当修改为jmp 12345678后，现在的机器码为E9 73 40 F4 11。可以告诉大家，jmp对应的机器码是E9（针对长转移来说的），后面的73 40 F4 11是一个偏移量，这个偏移量是多少呢？这个偏移量是11F44073，请回忆一下前面提到过的字节顺序的问题。偏移量的计算公式如下：

JMP后的偏移量=目标地址-原地址-5

这是一个非常重要的公式，当然对于我们的使用只要记住就可以了，这里的5是JMP的指令长度，也就是说JMP　XXXXXXXX这个指令的机器码长度为5个字节。验证一下这个公式,目标地址是12345678，原地址为00401600，用12345678-00401600-5 = 11F44073，用计算器进行计算，如图5-5所示。

上面地址都是用十六进制进行计算的，大家计算时要注意这一点，以免计算错误。通过上面的例子可以看出来，修改时只需要修改5个字节就可以了。下面来梳理一下Inline Hook的流程吧。流程如下。

（1）构造跳转指令。

（2）在内存中找到欲HOOK函数地址，并保存欲HOOK位置处的前5个字节。

（3）将构造的跳转指令写入需HOOK的位置处。

（4）当被HOOK位置被执行时会转到我们的流程执行。

（5）如果要执行原来的流程，那么取消HOOK，也就是还原被修改的字节。

（6）执行原来的流程。

（7）继续HOOK住原来的位置。

图5-5　偏移量计算

这就是Inline Hook的大概的流程。

由于Inline Hook的实现代码比较简单，关键就是一个HOOK和一个取消HOOK的过程，因此可用C++封装一个Inline Hook的类，在今后Inline Hook编程中，可以始终使用这个封装好的类。

一般情况下封装类都有两个文件，一个是类的头文件；另一个是类的实现文件。在Windows下,类名（Class Name）都是以“C”开头的，我们封装的是Inline Hook类，因此类名是CILHook。为了保持一致性，类的头文件和实现文件分别是ILHook.h文件和 ILHook.cpp文件。先来看一下ILHook.h文件中的类定义部分。

在C++中，类的定义使用关键字“Class”。在类中定义有成员函数和成员变量，通常情况下把成员函数放在上面，把成员变量放在下面。因为对于拿到头文件的人来说，他首先关注的是类实现了哪些功能，因此应该让他第一眼就能看到实现了的成员函数，当然这不是必须的。

回到类定义，在类中除了构造函数和析构函数以外，还定义了3个成员函数，分别是 Hook()、UnHook()和ReHook()函数。它们的功能分别是用来进行HOOK操作、取消HOOK操作和重新进行HOOK操作的。对于3个成员函数来说，这里只是一个定义，实现部分在 ILHook.cpp中。

除了上面的3个成员函数外，还定义了3个成员变量，分别是m_pfnOrig、bO1dBytes[5]和bNewBytes[5]。这3个函数的作用已经在定义中给出了注释，想必大家应该能明白，这里就不具体说了。接着看ILHook.cpp文件中的实现代码吧。

在构造函数中主要是完成对成员变量的初始化工作，在析构函数中主要是取消HOOK。构造函数在C++对象被创建时自动执行，同样析构函数是在C++对象被销毁时自动执行。

该函数是InlineHook类的重要函数，在Hook()成员函数中，我们完成了3项工作，首先是获得了被HOOK函数的函数地址；接下来是保存了函数的前5个字节；最后是用构造好的跳转指令来修改被HOOK函数的前5个字节的内容。

除了上面的函数外，还有两个函数，分别是取消挂钩和重新挂钩两个函数。这两个函数非常简单，就是完成修改内存属性、复制字节的工作。代码如下：

上面两个成员函数就不进行介绍了，只要大家看懂了Hook()函数的实现，这两个函数的功能就肯定理解了。

整个Inline Hook的封装已经完成了，在后面的代码中，可以很容易地实现对函数的HOOK功能了。

5.2.3　HOOK MessageBoxA

本小节将完成一个HOOK本进程MessageBoxA()的程序，这个程序的目的是测试我们的类是否封装成功，以便完成今后的程序。在VC6下创建一个控制台程序，添加好封装过的库，然后键入下面的代码：

在主函数中，调用了两次MessageBox()函数，两次弹出的文本内容是一样的。但是第二次调用MessageBox()函数时，对MessageBox()做了HOOK, HOOK的函数是我们自己写的 MyMessageBoxA()函数。在MyMessageBoxA()函数中首先恢复了对MessageBox()函数的HOOK，然后连续调用了两次MessageBox()函数。那么，这个测试程序应该弹出3次 MessageBox()对话框。大家将其编译连接一下，并运行，结果和我们想的完全一样，弹出了3次MessageBox()对话框。

这里介绍了关于本进程的Inline Hook的例子，接下来要介绍的是其他进程Inline Hook的例子。由于每个进程的地址空间是隔离的，那么对于其他进程的Inline Hook是需要用到 DLL文件的。下面学一些如何使用DLL文件来完成对其他进程的Inline Hook的工作。

5.2.4　HOOK CreateProcessW

在这个例子中，我们先写一个DLL，然后通过DLL来HOOK CreateProcessW()函数。在 Windows下，大部分的应用程序都是由Explorer.exe进程来创建的。我们用“Process Explorer”这个工具来查看一下，如图5-6所示。

从图5-6中可以看出，大部分的应用程序都是由Explorer. exe这个进程创建的，那么只要把Explorer. exe进程的CreateProcessW()函数HOOK住，就可以针对要完成的工作做很多事情了，比如，可以记录哪个应用程序被启动了，也可以对应用程序进程进行拦截了。

图5-6　“Process Explorer”查看应用程序的父进程

我们的例子就是通过HOOK CreateProcessW()函数来显示一下被创建的进程的进程名。还是使用前面给出的ILHook类来进行HOOK工作。代码如下：

代码不是很长，Hook功能是由前面封装过的类来完成的，只要去使用封装好的类进行HOOK，并定义一个HOOK函数就可以了。将这段代码编译连接，然后用第3章中编写的 DLL注入工具将这个DLL文件注入到Explorer.exe中，如图5-7所示。

图5-7　用DLL注入工具注入HOOK DLL

将这个DLL注入到Explorer.exe进程后，运行一下IE浏览器，会弹出一个对话框，如图5-8所示。

图5-8　对话框标题栏上显示了被创建的进程名

单击“确定”按钮后，IE浏览器就被打开了。再打开记事本、画图、计算器等程序，都成功地显示出了其进程名及进程的路径。

把这个程序修改一下，让它可以拦截进程的创建，这样来达到对创建应用程序的管控。修改的方法很简单，在弹出对话框以后，对话框上有两个按钮，分别选择一下相应的按钮就可以了。修改后的代码如下：

编译连接一下这个程序，提示连接错误。原因是刚才编译连接的DLL文件正在被使用，所以无法对其修改。用DLL注入工具将刚才的DLL进行卸载，然后再次编译连接，这次就通过了。把这个DLL文件注入到Explorer.exe进程中，然后启动IE浏览器，如图5-9所示。

单击“是”按钮，那么IE浏览器被创建。如果单击“否”按钮，那么会提示“您启动的程序被拦截”，并且IE浏览器没有被打开。单击“否”按钮看一下效果，如图5-10所示。

图5-9　是否创建进程的提示框图5-10进程创建被拦截的提示

图5-10　进程创建被拦截的提示

提示框出现了，单击“确定”按钮以后，IE浏览器没有被打开。再对记事本、计算器、画图等程序进行测试。测试的结果都和IE浏览器的结果是一样的，那么说明对应用程序创建的拦截功能已经成功了。

5.2.5　7字节Inline Hook

做Inline Hook的时候是通过构造一个jmp指令来修改目标函数入口的。在构造jmp指令时唯一比较不好理解的可能是计算jmp指令后面的偏移量，这是由于CPU机器码要求的。既然是修改目标函数入口指令，可以多修改几条指令，从而达到不计算jmp指令的跳转偏移量。

完成的指令为两条，一条是把目标地址保存入寄存器eax中，然后直接跳转到寄存器eax中保存的地址处。代码如下：

用OD随便打开一个程序，然后修改其入口代码为上述代码，然后提取其机器码，如图 5-11所示。

图5-11　修改入口代码

从图5-11中可以看出mov eax,12345678对应的机器码为B8 78 56 34 12,也就是说B8是mov指令的机器码。再看一下jmp eax,其对应的机器码为FF E0。将其定义为一个字节数组为：

Byte bJmpCode[]={‘\xb8’, ‘\0’, ‘\0’, ‘\’, ‘\0’, ‘\xFF’, ‘\xE0’};

这样定义以后，只要把目标函数的地址保存在从第一个到第四个字节的位置就可以了 （下标是从0开始的）。通过这种方法，就不用再计算jmp要跳转的位置对应的偏移地址了。这也是一种进行Inline Hook的方法，不过同样都是修改目标函数的入口。

5.2.6　Inline Hook的注意事项

在写Hook函数时一定要注意函数的调用约定，函数的调用约定决定了函数调用后负责平衡栈的一个约定，如果在调用函数后栈不恢复到调用前的样子的话，那么程序后续的部分一定会报错。也许程序短可能会不报错，但是千万不要有这样侥幸的心里。

下面用Hook本进程的例子做一个简单的修改，来演示一下调用。

Hook的是MessageBoxA()函数，该函数有4个参数，我们定义的函数也一定要是4个参数。MessageBoxA()函数的调用约定是__stdcall,那么定义函数时也要使用__stdcall。定义时使用的是WINAPI,这是一个宏，该宏的定义如下：

在MSDN中看一下MessageBox()的函数定义，该函数的定义如下：

在MSDN中，并没有看到对MessageBox()函数有关于调用约定方面的修饰。在WinUser.h中看一下关于MessageBoxA()函数的定义，该定义如下：

在WinUser.h这个头文件中可以看到，在定义中使用了WINAPI这个函数调用约定的修饰。现在来修改一下代码，修改的代码如下：

从代码中可以看到，这里把WINAPI函数调用约定的宏注释掉了。将程序进行编译连接，并运行。运行后，看到了MessageBox()的对话框，但是最后却出现了报错，如图5-12和图 5-13所示。

图5-12　错误对话框

图5-13　错误对话框

在出现图5-12后，单击“忽略”按钮，会弹出如图5-13所示的错误提示。从图5-13中可以看到一个提示“File:i386\chkesp.c”。看到这个提示以后首先要知道，这个提示告诉我们是Debug版本在检查栈平衡时报的错误。虽然这个代码是系统的代码，不是自己写的代码，但是在系统检查栈时报错，多半是由于代码破坏了栈的平衡。因此，要检查我们的代码。

出现这个错误时，其实我们是知道错误原因的，是因为我们把WINAPI这个函数调用约定的修饰去掉了。因此，的确是要检查我们的代码。但是应该从哪里开始着手呢？修改了调用约定以后，栈会不平衡。使用—stdcall是在被调用函数内进行平栈，而VC默认的调用约定是——cdcel,而此种调用约定是由调用方进行平栈。那么，我们就要手动进行平栈了。

MessageBoxA()函数有4个参数，每个参数占用4个字节，那么我们自己在函数中进行平栈，只要在返回时调用ret 0×10就可以了。修改的代码如下：

编译连接，并运行，仍然提示有错误。看来，要进行更进一步的调试了。在 MsgHook.UnHook()位置处按F9键设置断点，如图5-14所示。

按F5键执行代码，运行到断点处。单击工具栏上的反汇编按钮，如图5-15所示。

图5-14　在MsgHook.UnHook()位置处设置断点

图5-15　反汇编按钮

单击了反汇编按钮以后，将代码窗口往上移动，到函数的定义处，如图5-16所示。

通过反汇编看到有几条修改栈的操作，分别是push ebp、sub esp,40h、push ebx、push esi、push edi这5条代码。根据这5条代码来修改我们的代码以保证栈的平衡。按F7键停止调试状态的程序，并修改代码，修改后的代码如下：

图5-16　函数定义处的代码

将该代码编译连接并运行，这次运行正常了。

以上演示了一次如何手动平衡栈的过程，是不是很麻烦？其实对汇编熟悉的话就不麻烦了。不过个人觉得即使不麻烦，还是要按照原函数的函数定义来定义HOOK函数，以避免不必要的麻烦。在学习的过程中，为了深入地学习和掌握知识，手动平衡栈是可以的。但是在实际编程的过程中，仍然使用手动进行栈的平衡，那就成了钻牛角尖了。

本节介绍了Inline HOOK的原理，并通过两个例子学习了Inline Hook的用法。一个例子是对本进程的HOOK,另一个例子是对其他进程的HOOK。在对其他进程的HOOK中，演示了如何HOOK CreateProcessW()函数，并且从中学习了如何拦截应用程序进程被创建的过程，又强调了对函数栈平衡的重要性。再接下来的HOOK学习中，我们将要学习的是IATHOOK。

5.3　导入地址表钩子——IATHOOK

导入地址表是PE文件结构中的一个表结构，在学习PE文件结构的时候虽然没有提到导入地址表，但是提到了数据目录。数据目录在IMAGE_OPTIONAL_HEADER中的DataDirectory中，我们回忆一下它的定义：

（1） NumberOfRvaAndSizes:该字段表示数据目录的个数，该个数的定义为16。如下：

（2）DataDirectory:数据目录表，由NumberOfRvaAndSize个IMAGE_DATA_DIRECTORY结构体组成。该数组中包含了输入表、输出表、资源等数据的RVA。 IMAGE_DATA_DIRECTORY的定义如下：

该结构体的第一个变量为该目录的相对虚拟地址的起始值，第二个是该目录的长度。

5.3.1　导入表简介

在可执行文件中使用其他DLL可执行文件的代码或数据时称为导入，或者称为输入。当 PE文件被加载时，Windows加载器会定位所有的导入的函数或数据。这个定位是需要借助于导入表来完成的。导入表中存放了使用的DLL的模块名称，及导入的函数。

在加壳与脱壳的研究中，导入表是非常关键的部分。加壳要尽可能地隐藏导入表，脱壳一定要找到导入表。如果无法还原或修复脱壳后的导入表的话，那么可执行文件仍然是无法运行的。

在免杀中也有与导入表相关的内容，比如移动导入表函数、修改导入表描述信息、隐藏导入表……不过这些操作都是杀毒软件将特征码定位到了导入表上才需要这样做，不过可以看出导入表也同样受到杀毒软件的“关注”。

既然导入表这么重要，我们先来学习一下关于导入表的知识吧。

5.3.2　导入表的数据结构定义

在数据目录中定位第二个目录，即IMAGE_DIRECTORY_ENTRY_IMPORT。该结构体中保存了导入函数的重要信息，每个DLL都对应一个IMAGE_IMPORT_DESCRIPTOR结构，也就是说导入的DLL文件与IMAGE_IMPORT_DESCRIPTOR是一对一的关系。 IMAGE_IMPORT_DESCRIPTOR在文件中是一个数组，但是文件中并没有明确地指出导入表的个数，在导入表中是一个以全“0”的IMAGE_IMPORT_DESCRIPTOR为结束的。导入表对应的结构体定义如下：

（1） OriginalFirstThunk:该字段指向导入名称表的RVA,该表是一个IMAGE_THUNK_DATA的结构体数组。

（2） TimeDataStamp:该字段可以被忽略。

（3） ForwarderChain:该字段一般为0。

（4） Name:该字段为DLL名称的指针，该指针也为一个RVA。

（5） FirstThunk:该字段包含了导入地址表（IAT）的RVA,IAT是一个IMAGE_THUNK_DATA的结构体数组。

IMAGE THUNK_DATA结构体的定义如下：

该结构体的成员是一个联合体，虽然联合体中有若干个变量，但由于该结构体中包含的是一个联合体，那么这个结构体也就相当于只有一个成员变量，只是有时代表的意义不同。看其本质，该结构体实际上是一个DWORD类型。当IMAGE_THUNK_DATA值的最高位为 1时，表示函数以序号方式导入，而这时第31位被看作是一个导入序号。当其最高位为0时，表示函数以函数名字符串的方式导入，这时DWORD的值表示一个RVA,并指向一个 IMAGE_IMPORT_BY_NAME结构。

IMAGE_IMPORT_BY_NAME结构体的定义如下：

（1） Hint:该字段表示该函数在其DLL中的导出表中的序号。

（2） Name:该字段表示导入函数的函数名，导入函数是一个以ASCII编码的字符串，并以NULL来结尾。在IMAGE_IMPORT_BY_NAME中使用Name[l]来定义该字段，表示这是只有1个长度大小的字符串。通过越界访问，来达到访问变长字符串的功能。 IMAGE_THUNK_DATA与IMAGE_IMPORT_DESCRIPTOR类似，同样是以一个全“0”的 IMAGE_THUNK_DATA为结束的。

5.3.3　手动分析导入表

在学习PE文件结构时就借助十六进制编辑器完成了我们的学习，现在仍然通过十六进制编辑器来学习导入表的结构体IMAGE_IMPORT_DESCRIPTOR。在这里随便找个PE（EXE格式）文件来进行分析，大家可以找一个PE文件来进行分析。

用C32ASM打开找来的PE文件，首先定位到数据目录的第二项，如图5-17所示。

在图5-17中看到了数据目录中的第二项的内容,其值分别是0×00024000和0×0000003C。 0×00024000的值表示IMAGE_IMPORT_DESCRIPTOR的RVA,注意这里给出的是RVA。现在使用十六进制编辑器打开，那么就要通过RVA转换为FileOffset,也就是从相对虚拟地址转换为文件偏移地址。使用LordPE来进行转换，如图5-18所示。

图5-17　IMAGE—IMPORT—DESCRIPTOR的RVA及大小

从图5-18中可以看出，0×00024000这个RVA对应的FileOffset为0×00023000。那么在 C32中转移到0×00023000的位置处，按下Ctrl+G组合键，在弹出的对话框中填入“23000”，如图5-19所示。

图5-18　计算IMAGE—IMPORT_DESCRIPTOR的FileOffset

图5-19　C32中的“跳转到”

单击“确定”按钮，来到了文件偏移为00023000的位置处，如图5-20所示。

图5-20　导入表位置

来到文件偏移的00023000处就是IMAGE_IMPORT_DESCRIPTOR的开始位置了。从图 5-20中可以看出，该文件有2个IMAGE_IMPORT_DESCRIPTOR结构体。我们重点分析第一个IMAGE_IMPORT_DESCRIPTOR,同样关于其他几个相关的数据结构也只分析第一个。

在IMAGE_IMPORT_DESCRIPTOR中只看最后两个字段，分别是Name和FirstThunk。看一下第一个IMAGE_IMPORT_DESCRIPTOR的这两个字段的值分别是00024338和 00024190。这两个值都是RVA值。

先来看一下00024338这个值，该值表示DLL名字符串的RVA,将其转换为文件偏移后值为00023338。转到00023338这个文件偏移处查看，如图5-21所示。

图5-210　0023338处的内容

从图5-21中可以看出，这个位置保存的内容是一个字符串，该字符串的内容为 KERNEL32.dll,说明这个Name值的确保存的是DLL名字符串的RVA。

再来看一下00024190这个值，该值表示IMAGE_THUNK_DATA的RVA。将该值转换为文件偏移后的值为00023190。转到00023190这个文件偏移处查看，如图5-22所示。

图5-22　FirstThunk的值

可以看到，000242E4是FristThunk的值。该值的最高位不为1,那么说该值是导入函数 IMAGE_IMPORT_BY_NAME的RVA。将该值转换为文件偏移后的值为000232E4。转到 000232E4这个偏移处查看该处的内容，如图5-23所示。

图5-23　IMAGE—IMPORT—BY—NAME处的内容

从图5-23中可以看出，这里保存的是CloseHandle()这个函数名称的函数字符串。

在IMAGE_IMPORT_DESCRIPTOR中，有两个IMAGE_THUNK_DATA结构体，第一个为导入名字表，第二个为导入地址表。两个结构体在文件当中是没有差别的，但是当PE文件被装载内存后，第二个IMAGE_THUNK_DATA的值会被修正，该值为一个RVA,该RVA加上映像基址后，虚拟地址就保存了真正的导入函数的入口地址。

5.3.4　枚举导入地址表

从上面的分析过程中已经学习了IMAGE_IMPORT_DESCRIPTOR这个结构体。那么，下面就来用代码实现枚举导入地址表的内容。我们知道一个DLL文件会对应一个IMAGE_IMPORT_DESCRIPTOR结构，而一个DLL文件中有多个函数，那么需要使用两个循环来进行枚举。外层循环来枚举所有的DLL，而内层循环来枚举所导入的该DLL的所有的函数名及函数地址。

代码如下：

只要对于手动分析导入表能够理解的话，那么上面这段代码就不难理解了。希望大家可以理解上面的代码。对某个程序进行一个测试，看其输出结果，如图5-24所示。

图5-24　测试程序的导入表信息

用OD进行验证一下，对该测试程序的导入表信息的获取是否正确。用OD载入测试程序，然后在数据窗口中按下Ctrl+G组合键，输入地址“424190”，然后在数据窗口上单击鼠标右键，在弹出的菜单中选择“长型”-> “地址”命令，看数据窗口的内容，如图5-25所示。

图5-25　测试程序在OD中的导入表信息

那么说明程序是正确的。关于导入表的知识就介绍到这里了。接下来，要介绍关于如何对IAT进行HOOK的内容了，请大家务必掌握该小节介绍的内容。

5.3.5　IAT HOOK介绍

在前面的内容中提到这样一个问题，在IMAGE_IMPORT_DESCRIPTOR中，有两个 IMAGE_THUNK_DATA结构体，第一个为导入名字表，第二个为导入地址表（IAT）。两个结构体在文件当中是没有差别的，但是当PE文件被装载内存后，第二个 IMAGE_THUNK_DATA的值会被修正，该值为一个RVA,该RVA加上映像基址后，虚拟地址就保存了真正的导入函数的入口地址。

在这个描述当中我们知道，要对IAT进行HOOK大概分为3个步骤,首先是获得要HOOK函数的地址，第二步是找到该函数所保存的IAT中的地址，最后一步是把IAT中的地址修改为HOOK函数的地址。这样就完成了IATHOOK。也许这样的描述不是很清楚，那么下面就来举例说明一下。

比如要在IAT中HOOK系统模块kernel32.dll中的ReadFile()函数，那么首先是获得 ReadFile()函数的地址，第二步是找到ReadFile()所保存的IAT地址，最后一步是把IAT中的 ReadFile()函数的地址修改为HOOK函数的地址。这样是不是就能够明白了呢？下面通过一个实例来介绍IATHOOK的具体过程和步骤。

5.3.6　IAT HOOK之 CreateFileW ()

上次对Explorer.exe进程的CreateProcessW()函数进行了Inline Hook,这次对记事本进程的CreateFileW()函数进行IAT Hook。对CreateFileW()函数进行HOOK后主要是管控记事本要打开的文件是否允许被打开，我们一步一步地来完成代码。

先建立一个DLL文件，然后定义好DLL文件的主函数，并定义一个 HookNotePadProcessIAT()函数，在DLL被进程加载的时候，让DLL文件去调用 HookNotePadProcessIAT()函数。代码如下：

在遍历某程序的导入表时是通过文件映射来完成的，但是当一个可执行文件已经被 Windows装载器装载入内存后，便可以省去CreateFile()、CreateFileMapping()等诸多繁琐的步骤，取而代之的是通过简单的GetModuleHandle()函数就可以得到EXE文件的模块映像地址，并能够很容易地获取DLL文件导入表的虚拟地址。代码如下：

在获得导入表的位置以后，要在导入表中找寻要HOOK函数的模块名，也就是说，要对CreateFileW()函数进行HOOK，首先要找到该进程中是否有“kernel32.dll”这个模块存在。一般情况下，kernel32.d11模块一定会存在于进程的地址空间内，因为它是Win32子系统的基本模块。当然，我们并不是简单地要找到该模块是否存在，关键是要找到这个模块所对应的IMAGE_IMPORT_DESCRIPTOR结构体，这样才能通过kernel32.d11所对应的IMAGE_IMPORT_DESCRIPTOR结构体去查找保存CreateFileW()函数的地址，并进行修改。看一下代码：

对CreateFileW()函数进行HOOK，目的是为了对其打开的文件进行管控。由于这是演示程序，那么笔者在G盘下建立一个test.txt文件，然后对其进行管控，也就是说，如果用记事本打开这个程序的话，可以选择性的是否允许打开，或者不允许打开。代码如下：

我们HOOK的函数是CreateFileW()，通过函数中的W可以看出，这个函数是一个UNICODE版本的字符串，也就是宽字符串。在CreateFileW()函数的参数中，1pFileName的类型是一个指向宽字符的指针变量。那么，就需要在操作该字符串时使用宽字符集的字符串函数，而不应该再使用操作ANSI字符串的函数。在代码中wcscpy()、wcscmp()、wcslwr()都是针对宽字符集的字符串。WCHAR是定义宽字符集类型的关键字。L“g:\\test.txt”中的“L”表示这个字符串常量是一个宽字符型的。

打开一个记事本程序，然后将编译连接好的DLL文件注入到记事本进程中，当注入并HOOK成功后会用对话框提示“Hook Successfully !”。然后用记事本打开G盘下的test.txt文件，会弹出对话框询问“是否打开文件”，单击“否”按钮，也就是拒绝打开该文件，如图5-26和图5-27所示。

在图5-27中单击“确定”按钮，然后可以看到记事本并没有打开G盘下的test.txt文件，这说明对G盘下的test.txt文件的管控还算是成功的（这个程序并不完善，希望大家可以自行将其完善）。

图5-26　询问是否打开

图5-27　选择“否”后的提示

以上实例演示了如何对IAT进行HOOK。不过上面针对IAT进行HOOK的做法只能针对隐型调用。也就是说，可执行文件是直接调用了DLL的导出函数，用上面的代码可以对 IAT进行HOOK。如果是显式调用的话，以上的例子就无法达到HOOK的作用了。当可执行文件直接通过调用LoadLibrary()函数和GetProcAddress()函数来使用某个函数的话，上面的HOOK代码是无能为力的。如何解决这样的问题，答案是要对LoadLibrary()和 GetProcAddress()函数也进行HOOK，这样就可以避免对DLL的显式加载，和对函数的显式调用了。

5.4　Windows钩子函数

5.4.1　钩子原理

Windows下的应用程序大部分是基于消息模式机制的，一些CUI的程序不是基于消息的。 Windows下的应用程序都有一个消息函数，根据不同的消息来完成不同的功能。Windows操作系统提供的钩子机制的作用是用来截获、监视系统中的消息的。Windows操作系统提供了很多不同种类的钩子，不同的钩子可以处理不同的消息。

钩子分为局部钩子和全局钩子。局部钩子是针对一个线程的，而全局钩子则是针对整个操作系统内基于消息机制的应用程序的。全局钩子需要使用DLL文件，DLL文件里存放了钩子函数的代码。

在操作系统中安装了全局钩子以后，只要进程接收到可以发出钩子的消息后，全局钩子的DLL文件会被操作系统自动或强行地加载到该进程中。由此可见，设置消息钩子也是一种可以进行DLL注入的方法。

5.4.2　钩子函数

上面已经简单地讲述了Windows钩子的基本原理，现在来学习一下Windows下的钩子函数，主要有3个，分别是SetWindowsHookEx()、CallNextHookEx()和UnhookWindowsHookEx()。下面介绍这些函数的使用方法。

SetWindowsHookEx()函数的定义如下：

该函数的返回值为一个钩子句柄。这个函数有4个参数，下面分别进行介绍。

（1） 1pfn：该参数指定为Hook函数的地址。如果dwThreadId参数被赋值为0，或者被设置为一个其他进程中的线程ID，那么1pfn则属于DLL中的函数过程。如果dwThreadId为当前进程中的线程ID，那么1pfn可以是指向当前进程中的函数过程，也可以是属于DLL中的函数过程。

（2） hMod：该参数指定钩子函数所在模块的模块句柄。该模块句柄就是1pfn所在的模块的句柄。如果dwThreadId为当前进程中的线程ID，而且1pfn所指向的函数在当前进程中，那么hMod将被设置为NULL。

（3） dwThreadId：该参数设置为需要被挂钩的线程的线程ID号。如果设置为0，那么表示在所有的线程中挂钩（这里的所有的线程表示基于消息机制的所有的线程）。如果指定为具体的线程的ID号，那么表示要在指定的线程中进行挂钩。该参数影响上面两个参数的取值，该参数的取值决定了该钩子属于全局钩子，还是局部钩子。

（4） idHook：该参数表示钩子的类型。由于钩子的类型非常多，因此放在所有的参数后面进行介绍。钩子的类型非常多，下面介绍几个常用到的，也可能是大家比较关心的几个钩子类型。

1. WH_GETMESSAGE。安装该钩子的作用是监视被投递到消息队列中的消息。也就是当调用GetMessage()或PeekMessage()函数时，函数从程序的消息队列中获取一个消息后则调用该钩子。WH GETMESSAGE钩子函数的定义如下：

2. WH_MOUSE。安装该钩子的作用是监视鼠标消息。该钩子函数的定义如下：

3. WH_KEYBOARD。安装该钩子的作用是监视键盘消息。该钩子函数的定义如下：

4. WH_DEBUG。安装该钩子的作用是调试其他钩子的钩子函数。该钩子函数的定义如下：

其他的钩子类型请大家参考MSDN吧。从上面的这些钩子函数定义可以看出，每个钩子函数的定义都是一样的。是的，的确是这样的，每种类型的钩子监视、截获的消息不同，虽然它们的定义都是相同的，但是函数参数的意义是不同的。由于篇幅所限，每个函数的具体意义请参考MSDN，在MSDN上会给出最为详细的介绍。

接着介绍跟钩子有关的函数，UnhookWindowsHookEx()，该函数的定义如下：

这个函数是用来移除先前用SetWindowsHookEx()安装的钩子。该参数只有一个参数，是钩子句柄。也就是用该函数用通过制定的钩子句柄来移除与其相对应的钩子。

在操作系统中，可以多次反复地使用SetWindowsHookEx()函数来安装钩子，而且可以安装多个同样类型的钩子。这样，钩子就会形成一条钩子链，最后安装的钩子会首先截获到消息，当该钩子对消息处理完毕以后，会选择返回，或者选择把消息继续传递下去。在通常情况下，为了消息可以传达到目标窗口，我们会选择将消息继续传递，使消息能继续传递的函数的定义如下：

该函数有4个参数，第一个参数是钩子句柄，就是SetWindowsHookEx()函数的返回值。后面3个参数是钩子函数的参数，直接抄过来即可。例如：

5.4.3　键盘钩子实例

下面来写一个可以截获键盘消息的钩子程序，这个程序的功能非常简单，就是把按下的键对应的字符显示出来。既然要截获键盘消息，那么肯定是截获系统范围内的键盘消息，因此需要安装全局钩子，这样就需要DLL文件的支持。先来新建一个DLL文件，在该DLL文件中，需要定义两个导出函数和两个全局变量，定义如下：

在DllMain()函数中，需要保存该DLL模块的句柄，以方便安装全局钩子。代码如下：

安装与卸载钩子的函数如下：

对于Windows钩子来说，上面的这些步骤基本上都是必须有的，或者是差别不大的，关键在于钩子函数。这里是为了获取键盘按下的键，钩子函数如下：

关于钩子函数，这里简单地解释一下，首先是进入钩子函数的第一个判断。

如果code的值小于0，则必须调用CallNextHookEx()，将消息继续传递下去，不对该消息进行处理，并返回CallNextHookEx()函数的返回值。这一点是MSDN上要求这么做的。

如果code等于HC_ACTION，表示消息中包含按键消息，如果为WM_KEYDOWN，则显示按键对应的文本。

将该DLL文件编译连接。为了测试该DLL文件，新建一个MFC的Dialog工程，添加两个按钮，如图5-28所示。

图5-28　键盘钩子的测试程序

分别对这两个按钮添加代码，如下：

直接调用DLL文件导出这两个函数，不过在使用之前要先对这两个函数进行声明，否则编译器因无法找到这两个函数的原型而导致连接失败。定义如下：

进行编译连接，提示出错，出错内容如下：

从给出的提示可以看出是连接错误，是找不到外部的符号。将DLL编译连接后生成的DLL文件和LIB文件都复制到测试工程的目录下，并将LIB文件添加到工程中。在代码中添加如下语句：

再次连接，成功！

运行测试程序，并单击“HookOn”按钮，随便按下键盘上的任意一个键，会出现提示对话框，如图5-29所示。

图5-29　截获到的键盘输入

从图5-29中可以看出，当我们按下键盘上的按键时，程序将捕获到我们的按键。到此，键盘钩子的例子程序就完成了。关于钩子更多的使用请大家自行思考研究。

5.4.4　使用钩子进行DLL注入

Windows提供的钩子类型非常多，其中有一种类型的钩子非常实用，那就是WH_GETMESSAGE这个类型的钩子。它可以很方便地将DLL文件注入到所有的基于消息机制的程序中。

在有些情况下，需要DLL文件完成一些功能，但是完成功能时需要DLL在目标进程的空间当中。这时，就需要使用WH_GETMESSAGE这个消息把DLL注入到目标的进程中。代码非常简单，这里直接给出DLL文件的代码，代码如下：

整个代码就是这样，只要大家知道，在需要DLL大范围的注入到基于消息的进程中时可以使用这种方法。

5.5　总结

在Windows操作系统下挂钩的方法非常多，这里只介绍了Inline Hook、IAT Hook和Windows钩子等几种。这些都是较为常见的挂钩方法，希望大家可以掌握。

第6章　黑客编程剖析

通过前面5章学到的知识，本章将完成一个综合性的实例，以帮助我们复习和巩固前面所学知识。本章旨在为大家起到抛砖引玉的作用，希望大家可以在自己感兴趣的方面进行深入地学习和研究。

6.1　恶意程序剖析

恶意程序通常是指带有攻击意图的一段程序，主要包括木马、病毒和蠕虫等。恶意程序的编写是违反道德和法律的，我们这里只是进行学习。在前面的章节中说过，黑客编程和普通编程本质上都是编程，只是侧重点不同。我们为了学习编程，学习防御黑客编程攻击，就必须对恶意程序的编写要有所了解。

6.1.1　恶意程序的自启动

每当黑客们入侵了计算机以后，为了下次的登录都会安装一个后门或者木马。当计算机关机或重启时，所有的进程都将会被关闭。那么后门或者木马是如何在计算机重启以后仍然能继续运行呢？下面先来讨论如何实现恶意程序的自启动。

恶意程序的自启动的实现方法很多，下面只介绍几种常见的方法，至于其他方法大家可自己在网上搜集，然后通过编程实现即可。

一、启动文件夹

在Windows系统下，有一个文件夹是专门用来存放启动文件的。该文件夹的位置如图6-1所示。

在“启动”菜单处单击鼠标右键，然后选择“属性”命令，就可以看到启动文件夹在硬盘上的具体位置了，其位置如：“系统盘：\Documents and Settings\<用户名>\『开始』菜单\程序”。通常情况下<用户名>是当前用户的用户名，Windows为每个用户创建了一个文件夹。如果想要所有的用户在启动时都运行某个程序，需要使用的文件夹为“All Users”。那么，也就是把需要启动的程序放到“系统盘：\Documents and Settings\All Users\『开始』菜单\程序”这个位置下。该程序的实现方法非常的简单，因为基本上在前面已经介绍过该程序的实现方法了。下面给出它的部分代码，代码如下：

图6-1　启动文件夹的位置

打开All Users下的启动目录，也就是上面的“系统盘：\Documents and Settings\All Users\「开始」菜单\程序”这个位置，然后编译连接并运行程序，可以看到在启动目录下多了一个“test.exe”的程序。

二、注册表启动

注册表启动也是一种很常见的启动方法，而且在注册表中可以用来进行启动的位置非常多。这里给大家介绍几个在注册表中可以完成自启动的注册表位置。

1. Run注册表键

HKCU \Software\Microsoft\Windows\CurrentVersion\Run

HKLM \Software\Microsoft\Windows\CurrentVersion\Run

2. Boot Execute

HKLM\System\CurrentControlSet\Control\Session Manager\BootExecute

HKLM\System\CurrentControlSet\Control\Session Manager\SetupExecute

HKLM\System\CurrentControlSet\Control\Session Manager\Execute

HKLM\System\CurrentControlSet\Control\Session Manager\SOInitialCommand

3. Load注册表键

HKCU\Software\Microsoft\Windows NT\CurrentVersion\Windows\Load

当然，在注册表下绝对不只这么几个位置能够使程序跟随系统自动启动，这只是注册表下可以让程序随机启动位置的“冰山一角”。打开一个注册表项看一下，如图6-2所示。

图6-2　Run注册表键中的启动项

同样，下面来完成一个通过写入注册表进行自启动的例子程序，其部分代码如下：

将该代码编译连接并运行，然后打开注册表中写入的位置查看一下，会发现已经把它写入了注册表，当下次开机时，它就会随机启动了。

三、其他启动方法

除了使用上面两种方法外，还有很多启动方法，比如文件关联、创建服务、ActiveX启动、svchost.exe启动等。下面大概地介绍一下。

文件关联启动是通过修改注册表来完成的。比如，默认启动“文本文件”的程序是记事本程序，只要在注册表中把启动“文本文件”的关联程序改掉，也就是把记事本程序改掉，改成我们的木马程序，然后由木马去调用记事本来启动文本文件，这样，就达到了启动木马的效果。来看一下注册表，如图6-3所示。

图6-3　文本文件对应的文件关联

文本文件对应的文件关联的注册表位置为KHEY_CLASS_ROOT\txtfile\shell\ open\command。

创建系统服务的方法是使用CreateService()函数进行创建，然后通过服务来启动恶意程序。CreateService()函数的定义如下：

该函数的参数虽然很多，但是使用起来并不复杂，大部分参数可以使用NULL来表示。该函数的参数不进行说明，请大家自行参考MSDN进行使用。

关于恶意程序的启动就介绍这么多，还有其他更多的方法，大家可以自行查找相关的资料进行学习。最后给大家介绍一款工具，这款工具可以查看电脑上的所有启动项。也许大家会想到360、金山等一些工具，但是这里要介绍的这款工具是微软自己的工具，而且其功能相对来说非常的全面，该工具如图6-4所示。

图6-4　Autoruns工具界面

6.1.2　木马的配置生成与反弹端口

由于黑色产业链的供需关系，木马作为商品在网络上有着大量的交易，因此木马必须具备可配置性。当然了，即使是免费的木马，为了可以让广大网友使用，木马也是一定可以进行配置的。

木马要进行配置后才可以生成真正的服务端，看一下灰鸽子的配置界面，如图6-5所示。

从灰鸽子的配置界面中可以看出，灰鸽子支持的可配置的内容非常多，这里只显示了其中的一部分而已。灰鸽子在进行了服务端配置后会生成服务器端程序。下面就来介绍一下如何对木马进行配置生成，并介绍一下如何进行反弹连接。

一、反弹连接介绍

早期的防火墙只对连入主机的连接进行阻拦，而不对连出的连接进行阻拦。也就是说，当有人试图连接主机时，防火墙会给出提示有人要连接主机。而当主机向外连接其他主机时，防火墙是不会给出提示的。在这样的情况下，反弹木马诞生了。

图6-5　灰鸽子的配置界面

所谓的反弹木马是由攻击者监听一个端口，中木马的被攻击者主动向被攻击者发起连接。由于是被攻击者发起的连接，从而被攻击的防火墙不会给被攻击者任何安全提示。给出一个简单的示意图进行说明，如图6-6所示。

图6-6　反弹木马的示意图

从图6-6中看到了反弹木马的工作原理。通常情况下攻击者的IP地址是变动的，那么“小白”是如何连接到“黑客”的主机的呢？一般情况下黑客要把自己的IP地址动态地保存到某个固定的IP地址下（比如保存到网上FTP空间中），然后木马通过读取该IP地址下保存的黑客的IP地址进行连接，同样用图来说明，如图6-7所示。

从图6-7中可以看出，黑客开启木马客户端后，首先会更新服务器上保存着的自己的IP地址。“小白”会去读取服务器中保存着的黑客的IP地址，然后“小白”去连接“黑客”的主机，主动地让黑客去控制它，这就是木马中的“自动上线”。关于反弹端口的介绍就到这里。有了思路，通过前面学习的Winsock的知识自己可以试着实现一下，这里就不做更多的介绍了。

二、木马的配置生成与配置信息的保护

木马写好以后，通常会发布一个程序，在木马程序中通过配置一些相关的内容和参数后，会生成一个木马的服务器端程序。为什么木马的客户端会生成木马的服务端程序呢？其实木马的客户端和服务端本来就是两个程序，只是通过某种方式使其成为了一个文件而已。让木马的服务端和客户端成为一个文件可以有多种方法，常见的有资源法和文件附加数据法两种。

图6-7　木马动态获取黑客的IP地址

在PE文件结构中有一个数据目录称作资源，资源可以是图片、图标、音频、视频等内容。资源法也就是把服务端以资源的形式连接到客户端的程序中，然后客户端通过一些操作资源的函数将资源读取出来并生成文件。文件附加数据法是将服务端保存到客户端的末尾，然后通过文件操作函数，直接将服务端读取出来并生成新的文件。

反弹端口连接是要访问某个固定的IP地址去读取保存着黑客的动态IP地址的信息，而这个固定的IP地址是保存在木马程序中的。也就是说，我们的客户端在把服务端生成以后，会把一些配置信息写入服务端程序的指定位置中，服务端程序会读取指定位置的信息来进行使用。配置信息的写入与读出必须要一致，否则就没有意义了。

对于配置信息中往往会存在一些比较敏感的信息，比如邮箱账号、密码等内容。比如，我们在分析盗QQ的木马时会发现接收QQ密码的邮箱，由于现在很多邮箱都需要SMTP的验证，因此在配置信息中也会看到邮箱的账号及密码信息。这样配置信息中的这些敏感信息很容易被人获取到，甚至接收QQ密码邮箱的账号和密码也会被别人获取到，真是“偷鸡不成蚀把米”。对于此类情况，正确的做法是对配置信息进行加密。也就是说客户端往服务端中写配置信息前需要加密后再写入，而服务端在使用这些信息前需要先解密再使用。

关于配置生成客户端与配置信息的保护上面已经介绍得差不多了，接下来应该把重点放在代码的实现上了。我们的代码是模拟实现上面的内容，而不是真的去生成木马。

6.1.3　代码实现剖析

我们通过资源来生成木马，首先要写一个简单的被生成的程序，这个程序要去读取被写入的配置信息。客户端把配置信息写入服务端的文件末尾，服务端从文件的末尾将信息读入。下面来写一个简单的程序，来充当我们的服务端程序。需要设置的配置信息有IP地址和端口号，把这两个信息都写入服务器端程序。先来定义一个结构体，结构体如下：

下面写一个模拟的简单的服务端程序，具体代码如下：

上面的代码就是服务端读取配置文件的代码，把文件指针移动到配置信息处，然后直接读取出来，让服务端连接配置信息中的IP地址就可以了。这就是我们模拟的服务端。下面再来写一个模拟的客户端，用来对其进行配置。

创建一个MFC的对话框程序，然后对界面进行布局，界面布局如图6-8所示。

把模拟的服务端编译连接好以后添加入这个模拟客户端的资源里，添加方法是在VC中的资源选项卡中单击鼠标右键，在弹出的菜单中选择“Import…”命令，如图6-9所示。然后在弹出的对话框中选择编译好的模拟服务端程序，如图6-10所示。会出现一个输入自定义资源类型的对话框，输入“IDC_MUMA“，如图6-11所示。

图6-8　模拟客户端窗口布局

图6-9　添加资源

图6-10　选中编译好的程序

图6-11　自定义资源类型对话框

单击“OK”按钮，就将其添加入资源对话框中了，如图6-12所示。

图6-12　资源选项卡

做好这些准备工作以后，就可以开始写代码了。给“生成”按钮添加如下代码：

编译连接并运行这个程序，输入配置程序，单击“生成”按钮，会生成一个muma.exe的程序，然后运行这个程序就会输出配置信息的内容，如图6-13所示。

图6-13　程序运行结果

在这个程序中使用了4个以前没有使用过的函数，下面分别进行介绍。

(1) 查找资源FindResource()函数的定义如下：

①hModule：该参数表示要查找模块的句柄。

②lpName：该参数表示要查找资源的名称。

③lpType：该参数表示了要查找资源的类型。

(2) SizeofResource()函数用来计算被查找资源的大小，该函数的定义如下：

①hModule：该参数同FindResouce()相同。

②hResInfo: FindResouce()的返回值。

(3) LoadResource()函数用来将资源载入全局内存中，该函数的定义如下：

该函数参数的意义与SizeofResouce()的相同。

(4) LockResouce()函数的作用是将资源锁定，并返回其起始位置的指针，该函数定义如下：

上面介绍了关于使用资源来将两个程序合并为一个程序的方法。除此而外还有一种方法是使用附加数据法将两个程序合并为一个程序。何为附加数据法呢？PE文件在被载入内存时是按照节来映射的，没有被映射入内存的部分就是附加数据，虽然该部分占用文件大小，却不占用映像大小。关于配置信息的保护部分，只要使用简单的加密算法将配置信息加密就可以了，比如异或算法，这里就不进行介绍了。

6.2　简单病毒剖析

编写病毒不是一件光彩的事情，而且随时都有让自己惹上官司的可能。我们介绍一个简单的病毒的编写只是为了进行研究，以便编出更好的防范工具。

6.2.1　病毒的感染剖析

大部分病毒都有感染的功能，病毒会把自身当中的或者需要其他程序来完成的指定功能的代码感染给其他的正常的文件。就像人类的流行感冒，办公室中只要有一个人携带感冒病毒，就有可能所有人都会被传染。如果没被传染说明已经预防过了，因此在机器上安装杀毒软件还是非常有必要的。

前面说了，病毒要感染其他文件也就是把病毒本身的攻击代码或者病毒期望其他程序要完成的功能代码写入到其他程序当中，而想要对其他程序写入代码就必须要有写入代码的空间。除了把代码写入到其他程序中以外，还必须让这些代码有机会被执行到。就上面两个问题而言都是比较容易解决的，下面分别来讨论一下。

病毒要对其他程序写入代码，必须确定目标程序有足够的空间让它把代码写入。通常情况下有两种比较容易实现的方法，第一种在前面的章节介绍过，就是添加一个节区，添加一个节区后就有足够的空间让病毒来写入了。第二种方法是缝隙查找，然后写入代码。何为缝隙？在每个节与节之间，必然有没有使用到的空间，这个空间就叫缝隙。只要确定要写入代码的长度，然后根据这个长度来查找是否有满足该长度的缝隙就可以了。由于第一种添加节区的方法在前面的章节中已经给出过了，因此这里主要介绍第二种方法。

6.2.2　缝隙搜索的实现

通常情况下，每个节之间都是有未使用的空间的，搜索这些未使用的空间来把我们的代码写入到这个位置。由于只是一个测试代码，因此不会写具有攻击性的代码，我们写入目标程序的代码的功能是什么都不做，就是汇编中的“NOP”指令，其机器指令是0×90。简单地写入11个0×90就可以了。定义如下：

搜索缝隙的代码如下：

在代码节和紧挨代码节之后的节的中间搜索缝隙，搜索的方向是从代码节的末尾开始，个人认为反方向的搜索速度要快一些。通过该代码可以找到缝隙，但是也可能找不到缝隙，因此在调用完该函数后要做一些判断，以应变各种不同的情况。

6.2.3　感染目标程序文件剖析

我们把代码添加到了目标文件中，但是这些代码如何才能被执行到呢，这就要修改目标可执行文件的入口地址。修改目标入口地址先来执行我们的代码，然后再跳转到原来程序的入口处继续执行，很多病毒都是这样工作的。修改一下机器码，定义如下：

把机器码的后几个字节改为一条mov指令和一条jmp指令，这个过程和前面章节介绍的inline hook有些类似。我们写一个程序，来调用上面的函数，并且将机器码写入到目标程序中，具体代码如下：

编译连接后，找个以前写的VC程序，将其改名为“hello.exe”，并放到同一个目录下，然后运行这个感染程序。用OD打开“hello.exe”程序看一下，如图6-14所示。

图6-14　被感染后的目标程序入口

从图6-14中可以看出，感染是成功的，而且jmp eax指令中的eax保存的是“hello.exe”的入口地址。运行被感染的目标程序，是可以运行的。再次对其进行一次感染，然后用OD打开看一下目标程序“hello.exe”，如图6-15所示。

图6-15　被二次感染的目标程序入口

可以看到目标程序被二次感染了。由于只是添加了一些简单的跳转指令，因此没有太大影响，但是如果是真正的病毒，很有可能导致被感染的目标程序无法正常运行。因此，需要对被感染过的文件写一个标志，这样就可以避免被二次感染了。

6.2.4　添加感染标志

为了避免重复感染目标程序，必须对目标程序写入感染标志以防被二次感染，导致目标程序无法执行。每次在对程序进行感染时都要先判断是否有感染标志，如果有感染标志则不进行感染，如果没有感染标志则进行感染。在PE文件结构中有非常多不实用的字段，可以找一个合适的位置写入感染标志。想必这个非常容易理解，下面直接看代码。写入感染标志的代码如下：

读取的感染标志的代码类似，代码如下：

每次在进行感染前先调用CheckSig()函数，判断是否有感染标志，然后根据是否被感染做出不同的选择，调用以上两个函数的方法如下：

在代码中我们把感染标志写到了IMAGE_DOS_HEADER中的e_cblp这个位置处。 IMAGE_DOS_HEADER中除了e_magic和e_lfanew这两个字段外，其余都是没有用的，大家可以放心写入。代码中的offsetof()是一个宏，该宏的定义如下：

该宏的作用是取得某字段在结构体中的偏移。对于IMAGE_DOS_HEADER结构体中的e_cblp来说，它在结构体中的偏移是2。那么offsetof（IMAGE_DOS_HEADER, e_cblp）返回的值则为2，大家可以调试跟踪一下。

6.2.5　自删除功能的实现

某些程序在首次运行以后就莫名其妙地消失。当人们意识到这是病毒或木马时已经晚了，悔恨自己没装杀毒软件。其实病毒或木马被执行后都把自己复制到系统盘里了，改个看起来很重要的文件名，并且把自己隐藏得很深。

下面介绍一下它是如何自删除的。自删除的方法有很多，最简单的方法就是创建一个“.cmd”的批处理文件。批处理文件中通过DOS命令del来删除可执行文件，再通过del删除自身。我们来看一下它的实现代码：

直接在main()函数中调用CreateBat()函数，编译连接并运行它。可以看到，编译好的程序消失了。其实，是创建的批处理文件将编译好的程序和批处理本身都删除掉了。这样就达到了自删除的功能了。

6.3　隐藏DLL文件

前面的章节介绍了如何隐藏进程，隐藏进程的方法是把要在进程中完成的功能放在DLL文件中完成，然后将DLL文件注入到其他进程当中，从而达到隐藏进程的目的。现在要做的是隐藏进程中的DLL文件，当把DLL文件注入到远程进程后，可以将DLL也隐藏掉。操作系统在进程中维护着一个叫做TEB的结构体，这个结构体是线程环境块。下面就要通过WinDBG这个调试工具来一步一步地学习TEB，并通过TEB来学习如何隐藏DLL文件。

6.3.1　启动WinDBG

启动WinDBG工具，如图6-16所示。

依次单击菜单栏的“File” -> “Symbol File Path…”命令，在里面输入符号文件路径，这里直接填入微软为我们提供的符号服务器，“srv*F:\Program Files\symbolcache*http://msdl.microsoft.com/download/symbols”，如图6-17所示。

设置好符号路径，就可以开始调试了。这里调试的目标就是WinDBG，因为原理是相同的。我们来进行本地调试，依次单击菜单“File” -> “Kernel Debug”命令，出现如图6-18所示的窗口。

图6-16　WinDBG启动界面

图6-17　设置符号文件路径

图6-18　Kernel Debugging窗口

选择“Local”选项卡，也就是进行本地调试，单击“确定”按钮。这样，就可以用WinDBG开始调试了，跟着步骤一步一步做就可以了。

6.3.2　调试步骤

首先获取TEB，也就是线程环境块。在编程的时候，TEB始终保存在寄存器FS中。获取TEB的命令为“!teb”。在WinDBG的命令提示处输入该命令，WinDBG将输出如下内容。

从上面的输出内容可以看出，TEB地址为7ffde000。

获得TEB以后，通过TEB的地址来解析TEB的数据结构，从而获得PEB，也就是进程环境块，命令为“dt_teb 7ffde000”,WinDBG的输出内容如下：

上面的输出只是部分输出，该结构体非常长，这里只查看其中的一部分内容，只要找到PEB在TEB中的偏移就可以了。从该命令的输出可以看出，PEB结构体的地址位于TEB结构体偏移0×30的位置，该位置保存的地址是7ffd5000。也就是说PEB的地址是7ffd5000，通过该地址来解析PEB，并获得LDR。在命令提示符处输入命令“dt nt!_peb 7ffd5000”，输出内容如下：

从输出结果可以看出，LDR在PEB结构体偏移的0×0C处，该地址保存的地址是001ale90。通过该地址来解析LDR结构体。在命令提示符输入命令“dt _peb_ldr_data 001ale90”, WinDBG输出如下内容：

在这个结构体中，可以看到3个相同的数据结构，也就是在偏移0×0c、0×14、0×24处的3个结构体_LIST_ENTRY。该结构体是个链表，定义如下：

上面这个结构体在SDK提供的帮助中是找不到的，需要去WDK的帮助中才可以找到。这3条链表分别保存的是_LDR_DATA_TABLE_ENTRY，也就是LDR_DATA表的入口。

现在来手动遍历一下第一条链表，输入如下命令“dd lalec0”。

在这么多的输出中，在链表偏移0×18的位置是模块的映射地址，即ImageBase，在链表偏移0×28的位置是模块的路径及名称的地址，在0×30的位置是模块名称的地址。查看一下， lalec0偏移0×28的位置中保存的地址是20c64，接下来输入命令“du 20c64”。

可以看到，输出WinDBG的全部路径，来看一下偏移0×18的地址，该进程的映射基址为01000000。再来看一下偏移0×30处的地址保存着20ca6，查看该地址，输入命令“du 20ca6”。

的确是模块的名称。既然是链表，那么看看下一条链表的信息。

按照上面介绍的解析方法自己进行解析。

上面介绍的几个结构体在VC6的头文件中是找不到的，不过在网上还是可以查到的。这里给出MSDN上给出的几个结构体的定义，该MSDN的地址为： http://msdn.microsoft.com/zh-cn/library/aa813708（v=VS. 85） . aspx，方便大家查看。涉及的几个结构体的定义如下：

从这两个结构体中可以看出有非常多的保留字段，这些都是微软不愿意公开的，或不愿意让大家使用的。不过在网上有大量的相关结构体的具体定义，大家可以自行查找进行阅读。

看完上面的各种结构体是不是觉得我们自己都可以实现枚举进程中模块的函数了？是的，我们来写一个吧。

6.3.3　编写枚举进程中模块的函数

枚举进程中的模块的方法就是通过上面介绍的几个结构体来完成，其步骤是。

获得TEB地址->获得PEB地址->得到Ldr->获得第二条链表的地址->遍历该链表并输出其地0×18的值和0×28指向的内容。

只要把上面在WinDBG中找到链表的方法学明白，那么就不是太大的问题了。关键的问题是TEB怎么找到。告诉大家，TEB保存在FS中，有了这个提示就很好解决了吧？看代码吧。

该函数的实现没有太多的技巧，主要在于对C语言中指针的掌握，还有就是对以上介绍的几个结构体之间的关系能够掌握，也就是各结构体之间的数据关系。在main()函数中调用一下这个函数，输出结果如图6-19所示。

6.3.4　指定模块的隐藏

模块的隐藏是把指定模块在链表中的节点断掉，也就是做一个数据结构中链表的删除动作，只不过不进行删除，只是将其节点脱链即可，如图6-20所示。

图6-19　自实现的枚举模块函数

图6-20　链表节点脱链

如果是枚举模块的话，一般情况下，只要枚举第二条链表就可以了，也就是偏移0×14处的那条。如果要做模块隐藏的话，那最好是将3条链表中的指定模块全部都脱链。对于脱链的方法，其实也是对3条链表进行遍历，然后将指定的模块脱链就可以了。和上面枚举的方法差别不大。下面给出代码，如下：

在main()函数中调用这个函数，主函数如下：

接下来隐藏调用“kernel32.dll”这个模块，当然，这里的隐藏只能是这个程序运行时隐藏该进程中的“kernel32.dll”模块，对其余进程中的模块并没有影响。在程序的末尾使用getchar()，其用意是希望该进程可以停留住，否则它如果退出，便没有验证“kernel32.dll”模块是否真的被隐藏的机会了。编译连接并运行，然后用第3章中写的工具来查看一下，如图6-21所示。

图6-21　查看HideModule中Kernel32.dll模块被隐藏

从图6-21中可以看出，在HideModule. exe进程中看不到Kernel32. dll的模块名。当然，就算用我们自己编写的枚举的模块函数也是没用的，因为模块已经不在链表中了。虽然这个程序把进程中“kernel32.dll”隐藏了，但是并没有多大的实际意义。隐藏模块主要是用在被注入的DLL中，也就是一个DLL文件被注入到远程线程中后，为了不被发现而隐藏。

6.4　安全工具开发基础

黑客攻防一直都在共同进步、发展着，一正一邪，而正邪只在一念之差。黑客攻防从技术层面上看完全没有差别，其差别只在于使用者的想法。前面介绍了恶意程序的编写，那么自然也要介绍一些关于安全工具的开发，从而保证了安全。

6.4.1　行为监控工具开发基础

现在有一种流行的防病毒软件被称作HIPS，中文名字叫做主机防御系统，比如EQ。该软件可以在进程创建时、有进程对注册表进行写入时或有驱动被加载时，给用户予以选择，选择是否拦截进程的创建、是否拦截注册表的写入、是否拦截驱动的加载等功能。

HIPS纯粹是以预防为主，比如有陌生的进程在被创建阶段，就可以让用户禁止，这样就避免了特征码查杀的滞后性。对于杀毒软件的特征码查杀而言，如果杀毒软件不更新病毒数据库，那么依赖病毒特征码的杀毒软件就无法查杀新型的病毒了，那么对新型的病毒就成为一个摆设了。

行为监控的原理主要就是对相关的关键API函数进行HOOK，比如前面介绍的进程拦截。当一个木马程序要秘密启动的时候，对CreateProcessW()函数进行了HOOK，在进程被创建前，会询问用户是否启动该进程，那么木马的隐秘启动就被暴露出来了。对于没有安全知识的大众来说，也许仍然会让木马运行，但是木马的运行可以被我们所发现了。对于没有安全知识的大众来说，使用HIPS可能有点困难。因为不是每个使用计算机的人都是对计算机有所了解的，计算机对于他们而言可能只用来打打游戏，或看看电影之类的。这该如何做呢？现在通常使用的方法就是使用白库和黑库，也就是所谓的白名单和黑名单。在进程被创建时，把要创建的进程到黑白库中去匹配，然后做相应的动作，或者放行，或者拦截。

下面就来实现一个应用层下的简单的进程防火墙，注册表防火墙的功能。

一、简单进程防火墙

进程防火墙指的是放行/拦截准备要创建的进程。通过前面章节的介绍我们知道，进程的创建是依靠CreateProcessW()函数完成的。只要HOOK CreateProcessW()函数就可以实现进程防火墙的功能。对于注册表来说，要对非法进程进行删除或写入注册表键值进行管控，因此需要HOOK两个注册表函数，分别是注册表写入函数RegSetValueExW()和注册表删除函数RegDeleteValueW()。由于使用了HOOK，那么就必然要涉及DLL的编写，我们分DLL和EXE两部分来进行详细的介绍。

二、实现HOOK部分的DLL程序的编写

因为要对目标进程进行HOOK，因此要编写DLL程序。我们创建一个DLL程序，并加入前面的已封装的ILHook.h头文件和ILHook.cpp的实现文件。

为了能在所有的基于消息的进程中注入我们的DLL，必须使用Windows钩子，这样就可以将DLL轻易地注入到基于消息的进程中。代码如下：

以上函数用来定义导出函数，用于加载完成HOOK功能的DLL文件。这里利用WH_GETMESSAGE这个钩子类型，在前面的内容中是介绍过的，这里就不做过多的介绍了。

定义3个CILHook类的对象，分别用来对CreateProcessW()函数、RegSetValueExW()函数和RegDeleteValueW()函数进行挂钩。定义如下：

HOOK部分是在DllMain()函数中完成的，代码如下：

对于放行/拦截这部分是给用户选择的，那么就要给出提示让用户进行选择，至少要给出放行/拦截的类型，比如是注册表写入或是进程的创建，还要给出是哪个进程进行的操作。要把这个信息反馈给用户，我们定义一个结构体，将该结构体的信息发送给用于加载DLL的EXE文件，并让EXE给出提示。结构体定义如下：

定义一些常量用来标识放行/拦截的类型，定义如下：

将这些定义好以后，就可以开始完成HOOK函数了，我们主要给出CreateProcessW()函数的HOOK实现，其余两个函数的HOOK实现请大家自行实现。代码如下：

这里使用了一个SendMessage()函数，该函数用来发送一个WM_COPYDATA消息，将结构体传给了加载DLL的EXE程序，使EXE程序把提示显示给用户。

SendMessage()函数的功能非常强大，该函数的定义如下：

该函数的第一个参数是目标窗口的句柄，第二个参数是消息类型，最后两个参数是消息的附加参数，根据消息类型的不同而不同。

以上代码就是DLL程序的全部了，剩下两个对注册表操作的HOOK函数由大家自己完成。

三、行为监控前台程序的编写

先来看一下程序能达到的效果，然后再给大家讲解程序EXE部分的实现代码，如图6-22和图6-23所示。

图6-22　程序主界面

从上面两个图可以看出，程序的确是可以拦截进程的启动的，当单击“允许”按钮后，进程会被正常创建，当单击“取消”按钮后，进程将被阻止创建。这就是我们最终要完成的功能，来看看主要的实现代码。

图6-23　拦截提示框

EXE的部分主要就是如何来启动行为监控功能，还有就是如果接收DLL程序通过SendMessage()函数发出的消息来给用户弹出提示框。进行拦截的部分已经在DLL程序中通过HOOK实现了，所以重点也就在界面上和消息的接收上了。

先看如何启动和停止行为的监控。代码如下：

从代码中不难看出，直接调用了DLL的两个导出函数，就可以开启我们的打开。在关闭时为什么调用了CloseHandle()函数和FreeLibrary()函数呢。大家把FreeLibrary()函数去掉，然后单击“停止”监控行为，但是还是处在被监控的状态下。因为恢复Inline Hook是在DLL被卸载的情况。因此，在卸载时，我们调用GetModuleHandle()获得本进程的DLL句柄后，虽然CloseHandle()了，但是只是减少了对DLL的引用计数，并没有真正的释放，必须再次使用FreeLibrary()函数才可以使DLL被卸载掉，从而恢复Inline Hook。

对于EXE程序接收DLL消息的代码如下：

这部分代码就是对WM_COPYDATA消息的一个响应，整个代码基本是对界面进行了操作。在代码中有一个CTips类的对象，这个类是用来自定义窗口的。该窗口就是用来提示放行和拦截的窗口。带窗口的主要代码如下：

DLL程序中的SendMessage()函数的返回要等待WM_COPYDATA的消息结束，并从中获得返回值来决定下一步是否执行，因此这里只要简单地返回TRUE或FALSE即可。

对于行为监控，就介绍这么多。这个例子演示了如何通过Inline Hook来达到对进程创建、注册表操作的管控。当然，我们的代码并不能管控所有的进程，而且我们的行为监控过于简单，很容易被恶意程序突破。我们主要是通过实例来完成对行为监控原理的学习，希望可以起到抛砖引玉的作用。

6.4.2　专杀工具

现在免费的杀毒软件越来越多了，例如360安全卫士、金山毒霸、瑞星……有很多人不会去安装杀毒软件，如果爆发了传播速度较快，感染规模较大的病毒或蠕虫的话，杀毒厂商会为了阻止这种比较“暴力”的病毒或蠕虫的传播与感染，会推出免费的供网友使用的专杀工具。

专杀工具是针对某一个或某一类的病毒、木马或蠕虫等恶意软件开发的工具。专业的杀毒软件需要专业的反病毒公司来进行开发，而专杀工具可能是由反病毒公司开发，也可能是由个人来进行开发。

一、病毒分析方法简介

对于编写专杀工具，除非是感染型病毒，通常情况下并不需要对病毒做逆向分析，只需要对病毒进行行为分析就可以编写专杀工具。而如果病毒是感染型的，需要修复被病毒感染的文件，那么就不能只是简单地对病毒进行行为分析，必须对病毒进行逆向分析，从而修复被病毒所感染的文件。

（1）行为分析。病毒、木马等恶意程序都有一些比较隐蔽的“小动作”，而这些动作一般情况下是正常程序所没有的。比如，把自己添加进启动项，或把自己的某个DLL文件注入到其他进程中去，或把自己复制到系统目录下……这些行为都不是正常的行为。我们拿到一个病毒样本以后，通常是将病毒复制到虚拟机中，然后打开一系列的监控工具，比如注册表监控、文件监控、进程监控、网络监控等，将各种准备工作做好以后，在虚拟机中把病毒运行起来，看病毒对注册表进行了哪些操作，对文件进行了哪些操作，连接了哪个IP地址、创建了多少进程。通过观察这一系列操作，就可以写一个程序。只要把它创建的进程结束掉，把它写入注册表的内容删除掉，把它新建的文件删除掉，就等于把这个病毒杀掉了。当然整个过程并不会像说起来这么容易。

（2）逆向分析。当病毒感染了可执行文件以后，感染的是什么内容是无法通过行为监控工具发现的。而病毒对可执行文件的感染，有可能是添加一个新节来存放病毒代码，也可能是通过节与节之间的缝隙来存放病毒代码的。无论是哪种方式，都需要通过逆向的手段进行分析。通常的逆向工具有OD、IDA、WinDBG。

二、病毒查杀方法简介绍

病毒的查杀方法有很多种，在网络安全日益普及的今天，在杀毒软件公司大力宣传的今天，想必大部分关心网络安全的人对于病毒查杀的技术有了一些了解。当今常见的主流病毒查杀技术有特征码查杀、启发式查杀、虚拟机查杀和主动防御等。下面简单地说说特征码查杀和启发式查杀两种病毒查杀方法。

（1）特征码查杀。特征码查杀是杀毒软件厂商查杀病毒的较为原始的一种方法。该方法是通过从病毒体内提取病毒特征码，从而能够有效识别出病毒。这种方法只能查杀已知病毒，对于未知病毒则无能为力。

（2）启发式查杀。静态地通过一系列的“带权规则组合”对文件进行判定，如果值高于某个界限则被认定为病毒，否则不为病毒。启发式查杀可以相对有效地识别出病毒，但是往往也会出现误报的情况。

三、编写病毒专杀工具之简单病毒行为分析

我们来编写一个简单的病毒专杀工具，这个病毒专杀工具非常简单，用到的知识都是前面的知识。先准备一个简单的病毒样例，然后在虚拟机中进行一次行为分析，然后写出一个简单的病毒专杀工具。虽然前面的例子都有代码让大家练习，但是这次我们要对病毒进行行为分析，然后编写代码，完成我们的专杀工具。

我们将在虚拟机中进行病毒分析，因此安装虚拟机是一个必须的步骤。虚拟机也是一个软件，它用来模拟计算机的硬件，在虚拟机中可以安装操作系统，安装好操作系统后可以安装各种各样的应用软件，与操作真实的计算机是没有任何区别的。在虚拟机中的操作完全不影响我们真实的系统。除了对病毒进行分析需要安装虚拟机以外，在进行双机调试系统内核时安装虚拟机也是不错的选择。在虚拟机中安装其他种类的操作系统也非常方便，总之使用虚拟机的好处非常多。这里使用VMware这款虚拟机，请大家自行安装。

在安装好虚拟机以后，在虚拟机上放置几个行为分析的工具，包括FileMon、RegMon和Procexp三个工具。分别对几个工具进行设置，对FileMon和RegMon进行字体设置，并设置过滤选项，如图6-24和图6-25所示。

图6-24　对RegMon设置过滤

对于FileMon和RegMon的字体设置在菜单“选项”-> “字体”命令下，通常笔者选择“宋体”、“9号”，大家可以根据自己的喜好进行设置。在设置过滤条件时，“包含”后面输入的是需要监控的文件，这里的“a2.exe”是病毒的名字。也就是说，只监控与该病毒名相关的操作。

图6-25　对FileMon设置过滤

下面对Procexp进行设置，我们需要在进程创建或关闭时持续5秒钟高亮显示进程，以进行观察。设置方法为单击菜单“Options” -> “Difference Highlight Duration”命令，在弹出的对话框中设置“Difference Highlight Duration”为“5”，如图6-26所示。

将上面几个工具都设置完后，运行一下病毒，观察几个工具的反应，如图6-27、图6-28和图6-29所示。

图6-26　对Procexp的设置

图6-27　在Procexp中看到的mirwzntk.exe的病毒进程

图6-28　RegMon对注册表监控的信息

在图6-27中看到的病毒进程名为“mirwzntk.exe”，而不是“a2.exe”。这个进程是病毒“a2.exe”创建的，在病毒做完其相关工作后，将自己删除。图6-28和图6-29不一一进行说明了，下面作一下行为分析的总结。

图6-29　FileMon对文件监控的信息

病毒在注册中写入了一个值，值的内容为 “mirwznt.dll”，写入的位置如下： HKLM\SOFTWARE\MICROSOFT\WINDOWSNT\CURRENTVERSION\WINDOWS\APPINIT _DLLS。病毒在C:\WINDOWS\system32\下创建了两个文件，分别是“mirwznt.dll”和 “mirwzntk.exe,创建病毒进程mirwzntk.exe,并生成了一个.bat的批处理程序用于删除自身，也就是删除“a2.exe”。

下面来写一个专杀工具对该病毒进行查杀，如图6-30所示。

图6-30 mirwzntk病毒的专杀工具

四、编写病毒专杀工具之专杀工具编写

在查杀病毒的技术中有一种方法类似特征码查杀法，这种方法并不从病毒体内提取特征码，而是计算病毒的散列值。也就是对病毒的大小进行散列计算，然后在查杀的过程中计算每个文件的散列，然后进行比较。这种方法简单且易实现，常见的有MD5、CRC32等一些计算散列的算法。

下面选用CRC32算法计算函数的散列值，这里给出一个现成的CRC32函数，只需直接调用就可以了，代码如下：

该函数的参数有两个，一个是指向缓冲区的指针，第二个是缓冲区的长度。将文件全部读入缓冲区内，然后用CRC32函数计算文件的CRC32散列值。看一下查杀的源代码吧。

整个代码的内容都是前面介绍过的知识，这里只是一个综合的应用。对于有些情况，需要对磁盘的文件进行遍历，那么可以使用FindFirstFile0和FindNextFile0这两个函数，或者是用CFileFind类对磁盘文件进行遍历。其具体使用方法请参考MSDN，这里只给出一段简单的参考代码供大家学习使用。

6.4.3　U盘防御软件

在早期互联网还不发达的时候，病毒都是通过软盘、光盘等媒介进行传播的。到后来互联网被普及以后，通过互联网进行传播的病毒大面积地相继出现。虽然软盘已经被淘汰，但是并没有因为软盘的淘汰而使移动磁盘的病毒减少。相反，U盘的普及使得移动磁盘对病毒的传播更加得方便。U盘的数据传输速度和数据存储容量等多方面都比软盘要先进很多，因此，软盘可以传播病毒，U盘当然也可以传播病毒。

通过U盘来传播病毒通常是使用操作系统的自动运行功能，并配合U盘下的Autorun.inf文件来实现的。如果让操作系统不自动运行移动磁盘，或者保证移动磁盘下不存在Autorun.inf文件，那样通过U盘感染病毒的几率就小很多了。

一、通过系统配置禁止自动运行

先介绍一下关于如何通过系统配置来禁止U盘中Autorun.inf的自动运行。通常情况下需要进行两方面的设置，一方面是通过“管理工具”中的“服务”来进行设置，另一方面是通过“组策略”来进行设置。一般这两处都需要进行修改。下面分别介绍一下如何对这两处进行设置。

先来看一下如何在“服务”中进行设置。首先打开控制面板中的“管理工具”，然后找到 “服务”将其双击打开。在服务列表中找到名称为“Shell Hardware Detection”的服务，双击该服务，打开“Shell Hardware Detection的属性”对话框。单击“停止”按钮将该服务停止，再把“启动类型”修改为“已禁用”状态，如图6-31所示。

将服务中的“Shell Hardware Detection”禁用后，再来对“组策略”进行设置。首先在“运行”中输入“gpedit.msc”，然后依次单击左边的树形控件，“计算机配置”-> “管理模板”-> “系统”，然后在右边双击“关闭自动播放”选项，弹出“关闭自动播放属性”对话框，在“设置”选项卡中选择“已启用”单选项，在”关闭自动播放“处选择”所有驱动器“选项，设置完成后单击”确定“按钮。再到左边的树形控件中选择“用户配置”-> “管理模板”-> “系统”，到右边找到“自动关闭播放”选项，设置方法同上，如图6-32所示。

图6-31　禁用“Shell Hardware Detection”服务

图6-32　组策略中的“关闭自动播放”

通过以上设置，的确是可以相对有效地保护计算机不中U盘相关的病毒。不过，我们不能因此而满足，因为我们的目的是打造一个U盘防御的软件。

二、打造一个简易的U盘防御软件

我们将打造一个U盘防火墙，当插入U盘时会有提示，并且自动检查U盘下是否有 Autorun.inf文件，并解析Autorun.inf文件。除此而外，通过U盘防火墙可以打开U盘，从而可以安全地使用U盘。

如何才能知道有U盘被插入电脑呢？可以使用定时器不断地去检查，也可以开启一个线程不断地去检查，也可以通过Windows的消息得到通知。前两种方法笨了些，我们主动不断地去检查是否有U盘的插入，不如被动地等待Windows的消息来通知我们。

三、WM_DEVICECHANGE和OnDeviceChange()

在Windows下有一个消息可以通知应用程序计算机配置发生了变化，这个消息是 WM_DEVICECHANGE。该消息的消息过程定义如下：

该消息通过两个附加参数来进行使用，其中wParam表示设备改变的事件，1Param表示事件对应的数据。我们要得到设备被插入的消息类型，因此wParam的取值为 DBT_DEVICEARRIVAL，而该消息对应的数据的数据类型为DEV_BROADCAST_HDR，该结构体的定义如下：

在该结构体中，主要看的是dbch_devicetype，也就是设备的类型。如果设备类型为 DBT_DEVTYP_VOLUME，则把当前结构体转换为DEV_BROADCAST_VOLUME结构体，该结构体定义如下：

在该结构体中主要是dbcv_unitmask和dbcv_flags。dbcv_unitmask通过位来表示逻辑盘符，第0位表示A盘，第1位表示B盘。dbcv_flags表示受影响的盘符或媒介，该值为0时表示U盘或移动硬盘。

上面介绍了关于WM_DEVICECHANGE这个消息，由于是在MFC下进行开发的，因此可以使用OnDeviceChange()这个消息响应函数来进行代替WM_DEVICECHANGE消息。虽然使用了OnDeviceChange()这个消息响应函数而没有使用WM_DEVICECHANGE，但是响应函数的附加参数与WM_DEVICECHANGE相同。OnDeviceChange()函数定义如下：

四、通过OnDeviceChange()消息来获得被插入U盘的盘符

通过上面对WM_DEVICECHANGE消息的介绍，下面使用MFC下的OnDeviceChange()消息响应函数来编写一个获取被插入U盘的盘符的小程序，为编写U盘防火墙做一个简单的准备工作。首先来添加消息映射，添加代码如下：

在头文件中添加消息响应函数的定义，定义如下：

最后添加消息响应函数的实现，代码如下：

我们将其编译连接并运行，插入一个U盘，得到如图6-33所示的提示。

图6-33　检测到的U盘盘符

上面的这段代码可以将其封装为一个函数，封装后的函数定义如下：

在使用类似DBT_DEVICEARRIVAL这样以DBT_开头的宏时应包含头文件”“dbt.h”文件。

五、U盘防火墙的完善

前面已经获得了被插入U盘的盘符，接下来就可以对U盘上的Autorun.inf文件进行分析，并删除要运行的程序，还可以安全地打开U盘。改写一下OnDeviceChange()函数，以实现要完成的功能，代码如下：

安全打开U盘的实现代码如下：

用DeleteFile()函数删除U盘中要运行的程序可能会失败，因为有时U盘并没有完全准备好。可以通过判断来完成，但这里就不给出代码了，大家可自行修改完成。在代码中，涉及到两个新的API函数，分别是GetPrivateProfileString()和ShellExecute()函数。这两个函数的功能分别是获取配置文件中指定键的键值，和运行指定的文件或文件夹，大家可以通过查询MSDN进行详细地了解。

在上面的程序中，通过提示让用户选择是否要删除掉U盘中要被执行的文件，如果用户对此并没有太多的了解和认识的话很有可能不删除。如果删了本不该删的文件，那么用户对该软件的友好感便会降低，那应该如何做呢？应该建立一个白名单和黑名单，无论是通过散列进行比较，还是通过文件名进行比较，都可以。当然，越精确的匹配方法越好。这样，就可以提早一步为用户进行判断了，然后给出一个安全建议，不但提高了软件的友好感，而且会显得相对较为专业了。

6.4.4　目录监控工具

我们介绍了通过HOOK技术对进程创建的监控，然后介绍了通过CRC32对病毒进行查杀，还介绍了通过使用WM_DEVICECHANGE消息对U盘的防护。接下来，简单讨论一下如何通过ReadDirectoryChangesW()来编写一个监视目录变化的程序。

对目录及目录中的文件实时监控，可以有效地发现文件被改动的情况。就好像，我们在本地安装了IIS服务器，并搭建了一个网站平台，有时候会遭到黑客的篡改，而我们无法及时地恢复被篡改的页面，导致出现了非常不好的影响。如果能及时地发现网页被篡改，并及时地恢复本来的页面就好了，那么该如何做呢？

下面通过一个简单的例子来介绍如何监控某目录及目录下文件的变动情况。首先需要了解的函数为ReadDirectoryChangesW()，该函数的定义如下：

参数说明如下。

（1） hDirectory：该参数指向一个要监视目录的句柄。该目录需要用 FILE_LIST_DIRECTORY的访问权限打开。

（2） lpBuffer:该参数指向一个内存的缓冲区，它用来存放返回的结果。结果为一个 FILE_NOTIFY_INFORMATION的数据结构。

（3） nBufferLength:表示缓冲区的大小。

（4） bWatchSubtree:该参数为TRUE时，表示监视指定目录下的文件及子目录下的文件操作。如果该参数为FALSE，则只监视指定目录下的文件，不包含子目录下的文件。

（5） dwNotifyfilter:该参数指定要返回何种文件变更后的类型，该参数的常量值参见 MSDN。

（6） lpBytesReturned:该参数返回传给lpBuffer结果的字节数。

（7） lpOverlapped:该参数执行一个OVERLAPPED结构体，该结构体用于异步操作，否则该数据为NULL。

目录监控完整源代码

该函数的使用非常简单，下面通过一个例子来学习该函数的使用。该例子是对E盘目录进行监控，我们将程序编写完成后对E盘进行简单的文件操作，以观察程序的输出结构。完整的代码如下：

将程序编译连接并运行，在E盘下进行简单的操作，查看程序对E盘的监视输出记录，如图6-34所示。

图6-34　目录监控输出记录

对于目录监视的这个例子，可以将其改为一个简单的文件防篡改程序。首先将要监视的文件目录进行一次备份，然后对文件目录进行监视，如果有文件发生了修改，那么就使用备份目录下的指定文件恢复被修改的文件。

6.5　引导区解析

很多病毒会感染磁盘的引导区，这导致了病毒在系统启动前就会执行病毒。关于引导型的病毒这里不多作介绍，只简单介绍一下引导区的知识，并写一个程序来简单地解析一下引导区。在整个过程中，编写程序不是难点，难点在于引导区的各个数据结构和各结构之间的数据关系。

为了能写出程序，先进行一次对引导区的手动分析，使用的工具是WinHex。

6.5.1　通过WinHex来手动解析引导区

WinHex是一个强大的十六进制编辑工具，也是一个强大的磁盘编辑工具。打开WinHex，并打开磁盘，如图6-35、图6-36和图6-37所示。

图6-35　WinHex工具栏图6-36选择物理磁盘

图6-36　选择物理磁盘

图6-37　打开后的位置

当打开磁盘后，会看到很多密密麻麻的十六进制数据，这些数据很像学习PE文件结构时的情况。一眼看上去不能理解，当根据其各种不同的结构进行解析后就一目了然了。因此，重要的是学习其各种结构。这里不对硬盘中涉及的全部各个结构都进行介绍（指的是文件格式），主要介绍一下组成引导区的各个结构体。

引导区，也叫主引导记录（Master Boot Record，简称MBR）。MBR位于整个硬盘的0柱面0磁头1扇区的位置处。MBR在计算机引导过程中起着重要的作用。MBR可以分为五部分，分别是引导程序、Windows磁盘签名、保留位、分区表和结束标志。这五部分构成了一个完整的引导区，引导区的大小为512个字节。我们通过WinHex来具体查看一下每一部分的内容，来了解一下这512个字节的作用。

首先来看一下引导记录，如图6-38所示。

图6-38　MBR的引导程序

在图6-38中被选中的地方就是MBR的引导程序。引导程序会判断MBR的有效性，判断磁盘分区的合法性，及把控制权交给操作系统。引导程序占用了MBR的前440字节。

再来看一下Windows的磁盘签名，如图6-39所示。

图6-39　MBR中的Windows磁盘签名

在图6-39中被选中的位置就是Windows的磁盘签名，它的位置在紧接引导程序的第4个字节。Windows磁盘签名对于MBR来说不是必须的，但是对于Windows系统来说是必须的，它是Windows系统在初始化时写入的，Windows依靠磁盘签名来识别硬盘，如果该签名丢失则Windows认为该磁盘没有被初始化。在图6-39中，Windows的磁盘签名为 “0×BF5FBF5F”。

紧接在磁盘签名后的两个字节是保留字节，也就是暂时没有被MBR使用的位置。

在保留的两个字节后的64个字节，则保存了分区表，如图6-40所示。

图6-40　MBR中的分区表

分区表在MBR中占用了64个字节的位置，分区表被称为DPT （Disk Partition Table），它在MBR中是一个非常关键的数据结构。分区表是用来管理硬盘分区的，如果丢失或者破坏的话，硬盘的分区就会丢失。分区表占用了64个字节，用每16个字节来描述一个分区项的数据结构。由于其字节数的限制，一个硬盘最多可以有4个主硬盘分区（注意，是主硬盘分区）。图6-40中框住的部分就是一个分区表项，可以看出，MBR中只有两个分区表项。硬盘中的磁盘可以分为主磁盘分区和扩展分区，使用过DOS命令中的FDISDK的话应该很清楚。通常情况下，主分区是C盘。在系统中除了C盘以外，还可能存在D盘、E盘、F盘等分区，这3个分区都是从扩展分区中分配出来的，而这些分区并不在MBR中保存。右键单击“我的电脑”，在菜单中选择“管理”命令将出现“计算机管理”这个程序，选择“磁盘管理”，显示如图6-41所示的窗口。

图6-41　磁盘管理

从图6-41中可以看出，在磁盘上，D盘、E盘、F盘、G盘的周围有一个绿色的框，这个框就表示为扩展分区。

下面具体地介绍一下。单击WinHex的菜单项“视图”-> “模板管理”命令，出现如图6-42所示的对话框。

图6-42　模板管理器

在“模板管理器”对话框中双击“Master Boot Record”，也就是主引导记录，出现如图 6-43所示的MBR的偏移解析器。

图6-43　MBR偏移解析器

在图6-43中可以清晰地看到两个分区表项的内容，分别是Partition Table Entry #1和 Partition Table Entry #2。对我们有用的字段已经用框选中。下面介绍Partition Table Entry #1中对于我们有用的几个字段。第一个是在MBR中偏移为0×lBE的位置，这个位置的值为 0×80。该值是一个引导标志，表示该分区是一个活动分区。在MBR中偏移0×lC2的位置处保存的值为0×07，这个值表示的是分区的类型，0×07表示该值为NTFS的系统文件格式。在偏移0×lC6的位置处保存的值为63，该值表示在本分区前使用了多少个扇区，这里表示当前分区前使用了63个扇区。最后一个0×lCA处保存的值为16386237，该位置表示本分区的总扇区数。一个扇区有512个字节数，那个16386237个扇区是多大呢？我们计算一下，首先用 16386237×512求出本扇区占多少字节。通过计算获得本分区所占字节数为8389753344字节，那么字节如何转换成GB呢？这是一个简单的公式，1024字节等于1KB，1024KB等于 1MB，1024MB等于1GB，那么做一个简单的除法就可以了。用8389753344/1024/1024/1024就得出了当前分区是多少个GB了，得出的结果如图6-44所示。

图6-44　第一个分区的大小

从图6-44可以看出，当前的分区占用了7.81个GB的大小（C盘大小为7.81GB）。关于 Partition Table Entry #2大家可以自己进行分析，这里就不介绍了。

在MBR中还有最后一个内容，如图6-40所示。在图6-40中紧接着DPT后面的两个字节就是MBR中最后的两个字节。这两个字节是MBR的结束标志，用“55AA”表示。引导程序会判断MBR扇区的最后两个字节是否为”55AA”，如果不是则报错。

到此，关于MBR的部分就介绍完了，相信大家已对MBR有了比较全面的了解。下面来写一个简单的程序将上面的通过WinHex分析的内容解析出来。

6.5.2　通过程序解析MBR

对于我们来说，解析MBR可能不会有太大的问题，因为前面解析过PE文件结构。虽然解析过PE文件，但是还是有一些微小的几点差别，首先造成解析困难的一点是MBR没有给出具体的结构体。当初分析PE文件结构时，各个结构体在WinNt.h头文件中都有给出定义，而MBR的定义是没有给出的。因此上面也没有对照着结构体给大家介绍。再一个问题是，解析PE文件结构时，我们会打开具体的可执行文件去按照PE文件结构的定义进行解析，而硬盘的引导区属于哪个文件？用WinHex打开的是物理硬盘，那么我们如何来打开物理硬盘。这可能是两个比较困惑的地方。不过，这些都不是太大的问题。只要我们解决了这两个问题，我们的解析其实就容易多了。

6.5.3　自定义MBR的各种结构体

下面介绍如何将MBR的信息定义成一个个的结构体。通过前面用WinHex对MBR的手动分析，我们了解到MBR分为五部分，并且知道每部分占用的字节数。因此，可以将MBR定义为如下：

这就是定义的MBR了，引导程序共440个字节，Windows签名共4个字节，保留字节共2个字节，分区表共64个字节，再加上2个结束标志，一共512个字节。不过这样的定义并不好，因为里面的常量比较多，下面修改一下，定义如下：

这样定义后，可以很方便地获得引导程序的大小和分区表的大小。虽然这样定义直观一些，但是还不能算太直观，因为定义的都是unsigned char类型，无法真正反映出每个成员变量的具体含义。下面再次进行修改，定义如下：

这次修改后，可以很容易地从MBR这个结构体中看出主要两个成员变量的含义了。虽然直观了，但还是有问题。Dpt其实是一个有4条记录的表，也就是说它其实是一个数组，这样的定义当解析它的时候并不方便。这样的定义方便我们一次性将DPT读出，只要再定义一个DP的结构体来对DPT进行转换，就可以方便地对DPT进行解析了。下面再次定义一个结构体，定义如下：

有了这个结构体，就可以方便地对DPT进行解析了。最后两个定义就是对MBR各结构体的完整定义（这几个结构体是笔者自己定义的，可以会有很多考虑不周的地方，网上有公开的结构体的定义，大家可以自行参考。之所以如此反复介绍如何进行MBR结构体的定义，是想告诉大家一个在没有相关数据结构定义的情况下如何通过自己的分析来定义数据结构的思路和方法）。

大家思考一下，如果不定义这些结构体是不是就无法对MBR进行解析，定义了这些结构体后对于解析MBR有哪些影响。对于MBR的解析，可以完全不定义这些结构体，定义这些结构体的目的是方便对程序的后期维护，并使程序在整体上有一个良好的格式。定义数据结构可以清晰地表达各个数据结构之间的关系，让我们在写程序的过程中有一个清晰的思路，让看程序的人也可以一目了然。

硬盘设备的符号链接

有了上面的结构体，解析MBR已经不是太大的问题了。不过还有一个问题，那就是如何打开硬盘读取MBR。其实很简单，只要打开硬盘设备提供的设备符号链接就可以了。如何找到硬盘的设备符号链接呢？有一款工具WinObj可以帮助查找到。打开WinObj,再依次打开左边的树形控件，如图6-45所示。

图6-45　WinObj找到的硬盘设备

通过图6-45可以找到硬盘设备的设备名，例如可以通过\Device\Harddisk0\DR0这个设备名再去查找相应的设备符号链接。我们再依次打开WinObj左边的树形控件，如图6-46所示。

图6-46　WinObj找到的硬盘设备符号链接

从图6-46中可以看到硬盘的设备符号链接为PhysicalDrive0,在使用时应该书写为\\.\PhysicalDrive0。

下面简单地介绍一下设备名和设备符号链接。每个设备在Windows的内核中都有对应的驱动模块，在驱动模块中会为设备提供一个名字来对设备进行操作，驱动模块中提供的名字即为“设备名”。设备名只能在内核模块中使用。如果想要在应用程序下对设备进行操作，不能直接使用设备名，应该使用设备符号链接。设备符号链接就是驱动模块为应用程序提供的操作设备的一个符号，通过这个符号可与设备进行对应。

6.5.4　解析MBR的程序实现

到了这里大家可能会觉得通过程序解析MBR已经不是问题了，下面直接提供程序的代码吧。如果代码中有不理解的地方，可以参考一下我们是如何通过WinHex对MBR的解析了。代码如下：

代码非常短，也不复杂，看起来跟读写文件没什么太大的差别，其实就是在读写文件。前面介绍过，Windows将各种设备都当作文件来看待，因此打开硬盘设备的时候直接使用 CreateFile()函数就可以了。关于MBR的部分就介绍到这里了。

6.6　加壳与脱壳

6.6.1　手动加壳

壳是一种较为特殊的软件。壳分为两类，一类是压缩壳，另一类是加密壳。当然，还有介于两者之间的混合壳。下面先来手动为一个可执行文件加一层外壳，需要准备的工具有 C32ASM、LordPE、添加节表工具和OD。

首先用LordPE查看可执行文件，并对需要的几个数据做一个简单的记录，如图6-47所示。

图6-47　PE格式中需要用到的数据

在LordPE中，需要查看几个对于我们需要的数据，包括PE文件的入口RVA、映像地址和代码节的相关数据。有了这些数据以后就可以通过C32ASM对代码进行加密了。用 C32ASM以十六进制的方式打开可执行文件，然后从代码节的文件偏移开始选择，也就是从1000h的位置开始选择，一直选到4fffh的位置。然后单击右键，在弹出的快捷菜单上选择“修改数据”命令，在“修改数据”对话框中选择“异或”算法来对代码节进行加密，如图6-48所示。

使用0×88来对代码节进行异或加密，单击“确定”按钮后代码节被修改。保存以后使用在前面章节编写的添加节表的软件对可执行文件添加一个新的节，如图6-49所示。

图6-48　用“异或”算法对代码节进行加密

图6-49添加新节区

添加新的节区以后使用OD对可执行文件添加一些代码，用OD打开可执行程序，来到00408000地址处，添加如下代码：

以上代码的作用是将上面修改的代码节内容还原，然后进行保存。用LordPE修改该可执行文件的入口和代码节的属性，如图6-50、图6-51和图6-52所示。

图6-50修改入口点

图6-51添加代码节属性

图6-52修改新节属性

运行修改过的可执行文件，可以正常运行。

下面整理一下思路，以方便我们写代码。最开始用LordPE查看了将要用到的一些PE信息，然后用C32ASM对代码节进行了简单的异或加密，接下来新添加了一个节并在新节中写入了还原代码节的解密指令，最后用LordPE修改了文件的入口地址、代码节属性和新添加节的属性。对照一下前后两个文件的不同之处，如图6-53所示。

图6-53加壳前后PE文件的对比

从图中可以看出这两个PE文件的差别，相信大家对此已经没有不理解的地方了。下面开始手动打造一个这样简单的加壳软件。

6.6.2　编写简单的加壳工具

其实不按照上面的步骤进行也可以，只要步骤是合理的就可以了。我们主要有4个函数需要实现，分别是获取PE信息GetPelnfo()、添加新节AddSection()、加密代码节Encode()和写入解密代码WriteDecode()。获取PE信息和添加新节的代码实现前面已经介绍过了，这里就不重复介绍了。我们主要看两个函数的代码，分别是Encode()和WriteDecode()。

Encode()函数的作用是对代码节的内容进行加密，这里选择的加密算法是异或算法。在进行加密以前需要获得代码节在文件中的位置，以及代码节的长度。有了这两个信息就可以进行加密了，代码如下：

WriteDecode()函数的作用是将对代码节的解密代码写入到新添加的节中，这样在运行真正代码之前会先对加密的代码进行还原。同样，解密的代码也使用异或算法进行。解密的代码不能直接写入C代码，而是要写入机器码，机器码可以到OD中取得，代码如下：

在虚拟地址和汇编指令的中间部分就是汇编指令对应的机器码，将其取出并定义为C语言的数组，定义如下：

这个机器码在解密的过程中要根据实际的PE信息进行修改，修改的位置有3处，分别是代码节的起始虚拟地址、代码节的结束虚拟地址和程序的原始入口点，代码如下：

这就是解密代码，大家可以找个用VC写的程序来进行测试。在这里使用Release版的helloworld测试通过。

这个壳属于袖珍版的壳，严格来说，算不上是一个壳，但是这个壳在免杀领域是有用的，也就是把定位到的特征码进行加密，然后再写入解密代码从而隐藏特征码。一个真正的壳会对导入表、导出表、资源、TLS、附加数据等相关的部分进行处理。加密壳会加入很多反调试的功能，而且还会让壳和可执行文件融合在一起，达到“骨肉相连”的程度来增加脱壳的难度。

第7章　最后的旅程——简单

驱动开发及逆向

本章将一起来学习和了解与驱动相关的编程，还有当下倍受欢迎的逆向知识。这两方面的知识无论是从反面的免杀、外挂、破解角度来说，还是从正面的反病毒、反外挂、反调试来说都非常有用。对于学习黑客编程的新手来说，真正要了解这些也许要花一点时间，不过还是要讨论一下，驱动毕竟是比较神秘的知识，而逆向确实是对于开发环境和分析环节的一个精髓。

7.1　驱动版的 “Hello World”

驱动都是要加载入内核的，我们要做的很多事情也需要在内核下完成，要想在内核中实现功能就需要编写驱动模块。提到驱动可能会想到硬件，大家可能会简单地认为驱动程序是控制硬件设备的。在Windows下驱动并不单单是用来控制硬件设备的。Windows操作系统中的驱动程序可以创建虚拟设备，也可以与设备无关。Windows操作系统是一个开放式的操作系统，这个开放式并不是指其开放源代码，而是指通过其提供的接口可以很容易和方便地对其内核进行扩展。

要开发Windows下的驱动程序时，需要下载安装Windows下的驱动开发包，即WDK（Windows Driver Kit），该开发包免费提供下载，里面附带了开发驱动的头文件、帮助文档、工具及大量的文档等内容。笔者当前使用的版本是WDK 7600.16385.0。下面来编写一个简单的、与设备无关的驱动程序，名为HelloWorld。编写驱动不再依赖VC6的开发环境，而是在记事本或任意一款文本编辑器中编写代码，新建一个文件为helloworld.c的C源码文件。代码如下：

在开发驱动时，不再使用main()函数，而是使用DriverEntry()，该函数是驱动程序的入口函数，其定义在WDK自带的帮助文档中查找：

在WDK的帮助文档中查找DriverEntry()时会找到非常多的关于它的定义，这里查看的是DriverEntry[WDK kernel]的定义。该函数有如下两个参数。

（1） DriverObject:该参数是一个指向DRIVER_OBJECT结构体（驱动对象）的指针.

（2） RegistryPath:该参数是一个UNICODE字符串，指向此驱动负责的注册表。在程序中使用到了第一个参数，DRIVER_OBJECT结构体的定义如下：

﻿

该定义在WDK目录下的\inc\ddk\中的wdm.h头文件中，虽然在WDK的帮助文档中有该结构体的介绍，但是笔者并没有找到关于该结构体的具体定义。在该程序中用到了该结构体中的几个成员变量，分别是DriverUnload和DriverName。DriverUnload是一个用来卸载驱动的函数，卸载驱动的工作是由Windows来完成的，因此该函数是一个回调函数，这个函数用来完成对驱动资源的释放工作。该函数的定义格式如下：

DriverName是一个UNICODE_STRING结构体的变量，该变量指向了驱动的名称， UNICODE_STRING结构体的定义如下：

该结构体中的Buffer里保存了驱动的名称，其他两个变量里保存了驱动名称字符串的长度和最大长度。我们的程序到这里基本上介绍的差不多了，还剩下一个KdPrint()函数没有介绍，这个函数的用法类似printf()函数的用法，这里就不做过多的介绍了。下面来准备编译连接写好的第一个驱动程序吧。

要编译驱动程序需要使用WDK提供的编译工具，在“开始”菜单的程序下可以找到安装 WDK的菜单，如：“Windows Driver Kits->WDK 7600.16385.0->Build Environments-> Windows XP->×86 Checked Build Environment”，除了 Windows XP的以外还有 Windows 2003、 Windows 7等相关的编译命令行。在驱动编译的工程中也同样提供两个版本，分别是Checked版和Free版，这两个版本对应的就是Debug版和Release版，只是叫法不同而已（注：在不同平台下应使用不同的编译命令行）。

在命令行下编译需要编译脚本，编译脚本有两个，分别是“makefile”和“sources”。这两个脚本不用自己写，只要找一个改改就行。我们到WDK提供的例子程序中找个编译脚本，例如笔者在“\7600.16385.0\src\filesys\miniFilter\cancelSafe\”目录下找了这两个文件，并复制到我们写的驱动的目录下。下面来修改一下编译脚本，只需要修改“sources”文件即可，另一个文件保持原样即可。Sources文件修改如下：

简单解释一下，第一行是编译连接后的驱动名称，第二行是编译后的类型，第三行是需要编译连接的源代码文件。修改好后保存，就可以进行编译了。输入编译的命令build/g，如图7-1所示。

图7-1　驱动的编译连接过程

在编译成功后会在最下面一行看到一个“ 1 executable built”的输出，到我们的目录下去找一下编译好的驱动程序，其所在目录为“ HelloWorldDriver\objchk_wxp_x86\i386”下，那个扩展名为.sys的helloworld就是编译好的驱动文件了。

.sys的文件是无法直接双击执行的，需要通过工具进行加载，这里使用的工具是一款名为 “KmdManager”的EXE文件。除了要加载以外，还要查看驱动的输出，由于驱动没有界面，因此无法直接查看其输出，需要使用Dbgview工具来查看，下面具体操作一遍。打开KmdManager和Dbgview两个工具，将驱动程序拖放至KmdManager中，然后单击“Register”加载驱动，单击“Run”运行程序，然后查看Dbgview中有一串字符串，那正是在 DriverEntry()中输出的字符串。单击“Stop”按钮停止驱动，并单击“Unregistry”卸载驱动，再次观察Dbgview会看到在DriverUnload()中输出的字符串，如图7-2和图7-3所示。

图7-2　KmdManager的操作

我们逐步完成了 Helloworld驱动的编写、编译连接及运行显示输出的过程。这一节的内容也主要是让大家学会如何编译连接、运行和查看驱动输出。

读累了记得休息一会哦~

网站：https://elib.cc

百万电子书免费下载

7.2　驱动下的进程遍历

在前面的章节中曾经实现过一个枚举进程中被加载DLL文件的函数，接下来要实现一个枚举进程的函数。枚举进程不能在用户态下进行，需要到内核态下进行，这样就必须使用驱动程序来完成。先用WinDbg完成一次手动的枚举过程，再通过代码来完成。

图7-3　Dbgview显示的输出

7.2.1　配置VMware和WinDbg进行驱动调试

使用WinDbg调试驱动程序或内核，需要双机进行调试，所谓双机就是两台电脑。通常情况下，大部分人都没有两台电脑，往往只有一台电脑。那么，解决的方法就是安装虚拟机，然后对虚拟机进行一些设置，也是可以通过WinDbg来进行调试的。虚拟机选择使用VMware，下面讨论一下如何对虚拟机进行配置。

安装好VMware，并在VMware中安装好操作系统，然后对安装好的虚拟机进行一些设置，通过此设置可以达到调试器与虚拟机的连接。单击菜单“VM” → “Settings”命令，弹出“Virtual Machine Settings”窗口，如图7-4所示。

图7-4　“Virtual Machine Settings”对话框

单击“Add”按钮，打开“Add Hardware Wizard”（添加硬件向导）对话框，如图7-5所示。

图7-5　“Add Hardware Wizard”对话框 1

在该对话框中选择“Serial Port”选项，也就是串口，然后单击“Next”按钮，弹出“Add Hardware Wizard”对话框的第二个界面，如图7-6所示。

图7-6　“Add Hardware Wizard ”对话框 2

在该界面中选择“Output to named pipe”单选钮，也就是命名管道，命名管道是Windows下进程通信的一种方法。选中该项后继续单击“Next”按钮进入下一个界面，也是设置的最后一个界面，如图7-7所示。

图7-7　“Add Hardware Wizard ”对话框 3

在这个界面中对命名管道进行设置，然后单击“Finish”按钮即可。至此，已经完成了一半的设置了。接着，启动虚拟机配置Windows的Boot.ini文件。Boot.ini文件原内容如下：

将最后一行复制一下，然后放到最后面，并进行修改，修改后的内容如下：

去掉Boot.ini文件的只读属性，然后保存Boot.ini文件。在下次需要对驱动进行调试，或者对内核进行调试时选择启动Debug模式的Windows。

注：这里只介绍了针对Windows XP系统的配置方法，关于其他版本系统的配置方法请自行参考相关内容。

至此，所有的配置工作就做好了，但是使用WinDbg进行连接时，还是要有连接参数的。我们先在桌面上创建一个WinDbg的快捷方式，然后在WinDbg快捷方式上单击右键，在弹出的快捷菜单中选择“属性”命令，弹出“属性”对话框，将“目标”位置改为：

F:\WinDDK\7600.16385.0\Debuggers\windbg.exe -b -k com:port=\\.\pipe\com_l,baud= 115200，pipe

这样就可以用WinDbg连接虚拟机中调试状态下的Windows XP了。

7.2.2　EPROCESS和手动遍历进程

Windows中有一个非常大的与进程有关的结构体——EPROCESS。每个进程对应一个 EPROCESS结构，但EPROCESS是一个系统未公开的结构体，我们在WDK中只能找到说明，而找不到其结构体的具体定义，因此需要通过WinDbg来查看，这次使用WinDbg和 WMware进行调试。按照前面的方法，使WinDbg和WMware可以连接，当WinDbg出现调试界面时，在其命令处输入dt_eprocess命令来查看一下该结构体，如图7-8所示。

图7-8　WinDbg显示的部分EPROCESS结构体

从图中可以看出，EPROCESS结构体显示出了非常多的内容，从WinDbg调试界面只能看到部分的成员变量，而且偏移已经到了 0×258，非常多。看一下WinDbg的全部内容。

上面就是EPROCESS结构体的全部，对于遍历进程列表来说，有用的只有几个内容，首先是偏移0×84处的进程ID;然后是偏移0×88处的进程链表；最后一个是偏移0×174的进程名。下面手动进行一次遍历。

在WinDbg的命令输入提示处输入！Process 0 0命令，得到进程的列表，如图 7-9所示。

图7-9　进程信息

PROCESS后面给出的值就是当前进程中EPROCESS的地址，选择explorer.exe进程给出的地址0×ff364708来解析EPROCESS。输入命令dt _eprocess ff364708，输出如下：

可以看到，按照EPROCESS结构体解析ff364708这个地址，输出了我们需要的内容。接着，通过ActiveProcessLinks来获取下一个进程的信息。输入命令dd ff364708 + 0×88，输出如下：

在ff364790地址处保存了下一个EPROCESS结构体ActiveProcessLinks的地址，要得到下一个EPROCESS的地址，必须减去0×88才能得到。输入命令dt_eprocess （ff2b44bO-0×88），输出如下：

将输出结果和图7-9中的结果对比，在explorer.exe的下一个进程为vmwaretray.exe，可见遍历方法是正确的。

7.2.3　编程实现进程遍历

上面介绍的手动遍历过程就是指导我们如何编写代码的，只要上面的手动遍历过程能够掌握，那么代码的编写也就不是问题了。下面直接看代码，代码如下：

在代码中用到了一个函数，就是PsGetCurrentProcess()，这个函数是用来获取当前进程的 EPROCESS指针的，该函数的定义如下：

通过PsGetCurrentProcess()函数获得的是system进程的EPROCESS，大多数内核模式系统线程都在system进程中。除了这个函数没有接触过以外，剩下的部分就是对EPROCESS结构体的操作了，这里就不做过多的介绍了。在前面的章节中介绍过如何实现进程内DLL文件的隐藏，方法是将指定DLL在DLL链表中“脱链”。为了隐藏进程，同样可以将指定进程的EPROCESS结构体在进程链表中“脱链”，以达到隐藏的目的。

7.3　HOOK SSDT（系统服务描述表）

7.3.1　SSDT简介

前面介绍了如何编写简单的驱动程序，这节将介绍内核下的一张非常有用的表。在很多游戏保护系统中，或在一些杀毒软件中，都会对该表进行修改，从而改变系统函数调用流程来起到反外挂、反病毒的作用。同样，病毒也在修改该表，从而修改系统函数调用流程来完成其自身的目的。这张非常关键的表叫做SSDT，即System Service Descriptor Table （系统服务描述表）。这张表的作用是把用户层的Win32 API和内核层的API建立一个关联。在该表中维护非常多Native API，或称本地API。下面通过WinDbg来查看一下该表。

依照前面的方法，使用WinDbg连接到虚拟机上，然后在命令提示符处输入dd KeServiceDescriptorTable命令，会得到一些十六进制的输出。KeServiceDescriptorTable是 Ntoskrnl.exe导出的一个指针，用来指向SSDT表的。我们查看一下命令的输出结果：

在该输出中，第一行就是 SSDT表，该表中的 80502b8c是一个函数指针数组，该指针数组保存了所有Native API的函数地址，0000011c是数组的大小，80503000里面保存的是一个参数个数数组，与Native API相对应。将SSDT定义成一个结构体，如下：

要想在驱动中获得该表，需要使用Notokrnl.exe导出的KeServiceDescriptorTable，将其定义如下：

有了上面的SSDT表和KeServiceDescriptorTable的定义，就可以编写与SSDT相关的程序了，不过似乎还少点什么。表里面对应的Native API到底是什么？用WinDbg来看一下，输入dd 80502b8c，输出结果如下：

全都是一些地址值比较接近的函数地址，为什么说是函数地址，因为这是函数指针数组。输入u8059a948命令，输出如下：

从输出可以看出，8059a948是NtAcceptConnectPort()函数的地址。再来看一个地址，输入u 805e7db6命令，输出如下：

这次输出的是NtAccessCheck()函数的反汇编代码。在SSDT表中，第3个参数告诉我们，这个数组的大小是0×11c，也就是数组最后一项的下标是0×11b。再看一下下标为0×11b中保存的地址是多少，输入命令dd 80502b8c + 11b * 4，80502b8c是数组的起始地址，11b是数组下标，那么乘4的是什么原因呢？数组地址的定位是通过数组首地址+下标×数组元素字节数得出的。一个函数的地址占用4个字节，因此要做乘4的操作。该命令输出如下：

再用u命令来查看805c2798处的反汇编代码，输入命令u 805c2798，输出如下：

数组中最后一项保存的是NtQueryPortInformationProcess()函数的地址。

7.3.2　HOOK SSDT

从上一节的介绍中我们知道，SSDT把用户层的Win32 API与内核层的Native API做了一个关联，而整个Native API都保存在SSDT中的一个函数指针数组中，只要修改函数指针数组中的某一项，就相当于HOOK了某个Native API函数。比如，修改SSDT中函数指针数组中的最后一个函数指针，那么就相当于HOOK了 NtQueryPortInformationProcess()函数。

下面 HOOK一个我们比较熟悉的函数，即创建进程函数NtCreateProcessEx()。该函数在指针数组的第 0×30项（该编号根据系统版本的不同而不同，是系统相关的）。通过编程获取 SSDT表，然后找到Native API的函数指针数组，然后修改其中第0×30项的内容为我们的函数地址，为了不影响进程的正常创建，在函数中调用NtCreateProcessEx()函数。代码如下：

在DriverEntry()中，调用了 HookCreateProcess()函数，该函数的作用就是将指针数组中 NtCreateProcessEx()函数的地址替换为 MyNtCreateProcess()函数的地址。而 MyNtCreateProcess()函数是用来取代NtCreateProcess()函数的函数，在我们的函数中调用了一条KdPrint()用于输出的代码。整个Hook的过程非常简单，只要找到指针数组的位置，保存原地址后修改为新的地址即可。在代码中出现了两个函数，分别是UN_PROTECT()和 RE_PROTECT()。这两个函数的作用是，禁止和开启CPU向标志为只读的内存页进行写入的操作。执行了 UN_PROTECT后CPU可以向标志为只读的内存页进行写入操作。当写入完成好后，调用RE_PROTECT()函数恢复到原来的状态。把它放到虚拟机中，打开DebugView然后加载该驱动，加载成功后随便运行一个可执行程序，可以看到在DebugView中显示了在 MyNtCreateProcess()中的输出了，如图7-10所示，说明我们的HOOK成功了。

图7-10　MyCreateProcess()函数的输出

7.3.3　Inline HOOK SSDT

前面介绍了 HOOK SSDT，下面介绍一下Inline HOOK SSDT，为什么要介绍Inline HOOK SSDT呢？在有些情况下，反病毒软件会保护SSDT不受篡改，其保护方式是检查Native API SSDT呢？在有些情况下，反病毒软件会保护SSDT不受篡改，其保护方式是检查Native API的函数指针数组中的值是否正确。如果其中某项或某几项被修改，那么反病毒软件会将被修改的项恢复到原来的值，这样HOOK SSDT就不成功了。还记得前面章节介绍Inline HOOK的原理吗？Inline Hook是修改被HOOK的入口字节为一个跳转，从而跳转入函数中。如果换作是Inline Hook SSDT的话，那么就不会去修改Native API的函数指针数组中的值了，这样可以避免反病毒软件的检测（注：这只是一个简单的说明，现在使用Inline HOOK SSDT同样会被反病毒软件查杀）。

实现Inline Hook SSDT的代码如下：

关于代码的部分就不多介绍了，测试一下，其运行结果如图7-11所示。

图7-11　Inline Hook

7.4　应用程序与驱动程序的通信

虽然应用程序处在操作系统的用户态，而驱动程序处在操作系统的内核态，但是它们之间是必须通信的，应用程序的执行都是依赖于操作系统内核的。

7.4.1　创建设备

在编写驱动程序的时候，主函数的入口函数是DriverEntry()函数，该函数有一个参数为 PDRIVER_OBJECT类型，是指向驱动对象的指针的。为了能够使驱动程序被卸载掉，我们为DRIVER_OBJECT结构体中的DriverUnload成员进行了赋值。DriverUnload是驱动对象中的一个程序，其保存了驱动卸载例程（Routine，也就是函数的意思）的指针，其实保存的就是一个函数指针。在DRIVER_OBJECT中，除了 DriverUnload以外，还有一个数组也存放了函数指针，这个数组名是MajorFunction，该数组的大小为IRP_MJ_MAXIMUM_FUNCTION + 1，IRP_MJ_MAXIMUM_FUNCTION是一个宏，该宏的定义为：

在MajorFunction中可以指定非常多的例程指针，比如IRP_MJ_CREATE、IRP_MJ_READ、IRP_MJ_WRITE、IRP_MJ_CLOSE……这些例程都有一个非常相似的定义，定义如下：

这些是摘自WDK提供的文件中的定义，函数名可以随意修改，但是函数的定义是不能修改的，也就是说函数的返回值和参数是已经被确定的。在这些函数的定义中，第一个参数是PDEVICE_OBJECT的类型，是一个指向设备对象的指针。第二个参数是一个指向IRP结构体的指针，IRP是I/O请求包。应用程序与驱动程序的通信就是依靠IRP进行的。因此，要在DriverEntry()下指定MajorFunction数组中的各种例程。在没有指定MajorFunction的情况下，有默认的例程来执行。

既然在所有的例程中都需要设备对象指针，那么自然要创建一个设备，创建设备的函数为IoCreateDevice()，其定义如下：

该函数用来创建一个设备，参数说明如下。

（1）DriverObject：该参数是一个指向驱动对象的指针，指定设备所属的驱动，使用 DriverEntry()的第一个参数即可。

（2） DeviceExtensionSize：指定设备扩展的大小，如果没有设备扩展的话该参数为0。

（3） DeviceName：指定设备的名称，设备名通常是\Device\xxx。

（4） DeviceType：指定设备的类型，由于创建的设备没有具体对应的设备类型，因此此处指定 FILE_DEVICE_UNKNOWN。

（5） DeviceCharacteristics：设置设备对象的特征，这里赋值为0即可。

（6） Exclusive：该参数指定设备是否为互斥设备，这里赋值为TRUE即可。

（7） DeviceObject：该参数是一个指向设备对象的指针。

为了能与设备对象进行通信，必须为设备对象指定一个符号链接，用来在应用程序中打开。创建符号链接的函数为IoCreateSymbolicLink()，其定义如下：

该函数的第一个参数用来指定符号链接的名称，该名称通常的格式为\??\xxx，第二个参数为设备名称。

7.4.2　应用程序与驱动程序的通信方式

应用程序与驱动程序的通信方式通常有打开、关闭、读、写、控制这5种。这5种方式对应的 Win32 API函数分别为 CreateFile()、CloseHandle()、ReadFile()、WriteFile()和 DeviceIoControl()。而这5个Win32 API函数分别对应着5个派遣例程，分别是 IRP_MJ_CREATE、IRP_MJ_CLOSE、IRP_MJ_READ、IRP_MJ_WRITE和 IRP_MJ_DEVICE_ CONTROL。下面主要来介绍一下 DeviceIoControl()函数与 IRP_MJ_DEVICE_CONTROL派遣例程。

DeviceIoControl()函数是用来向指定设备发送控制码的，当指定的设备接收到 DeviceIoControl()发来的控制码后会调用IRP_MJ_DEVICE_CONTROL对应的派遣例程，针对不同的控制码进行不同的处理。DeviceIoControl()函数的定义如下：

（1） hDevice：该参数指明设备句柄。

（2） dwIoControlCode：该参数是控制码，该值由CTL_CODE生成。

（3） lpInBuffer：该参数是输入的缓冲区。

（4） nInBufferSize：该参数是输入缓冲区大小。

（5） IpOutBuffer：该参数是输出的缓冲区。

（6） nOutBufferSize：该参数是输出缓冲区大小。

（7） lpBytesReturned：该参数是返回字节的个数。

（8） lpOverlapped：该参数针对重叠方式使用，通常为0即可。

7.4.3　应用程序与驱动程序的通信实例

应用程序与驱动程序之间的通信实例以前面写的HOOK NtCreateProcessEx()来进行介绍。在应用程序中通过发送控制码来启动和关闭HOOK，对NtCreateProcessEx()的HOOK进行有效地控制。下面直接给出完整的源代码，代码如下：

下面来看一下应用层的代码，在于驱动进行通信时，首先要打开驱动程序：

在使用CreateFile()函数时，该函数的第一个参数为驱动程序的符号链接。定义驱动的符号链接为：L“\\??\\HookNtCreateProcessEx”，当在应用层中使用时应改为： \\\\.\\HookNtCreateProcessEx。

打开驱动程序后，通过DeviceIoControl()来对驱动程序发送控制码，使驱动程序开启和关闭HOOK。代码如下：

在应用程序中使用CTL_CODE宏时应包含头文件winioctl.h。运行看一下效果，当开启 HOOK时NtCreateProcessEx()会直接返回，也就是不进行进程的创建。当关闭HOOK后， NtCreateProcessEx()又会正常地创建进程。用KmdManager加载驱动，然后用我们编写的应用程序开启HOOK后运行的任意程序，创建进程都会失败，如图7-12所示。

图7-12　开启HOOK后创建进程失败

关于驱动的开发有非常多的东西需要学习，驱动的开发时下也非常流行，很多安全厂商（比如，反病毒、反外挂公司）都对驱动开发人员有大量的需求。

7.5　C语言代码逆向基础

在学习编程的过程中，需要阅读大量的源代码才能提高我们的编程能力。可是通常情况下，在使用软件的过程中会发现某个软件的功能实现得非常不错，希望该功能能够在自己的软件中实现，但又得不到它的源代码进行参考。遇到这种情况，通常能够做的就是通过反汇编工具进行逆向分析，或者借助调试工具进行调试分析。

何为逆向工程？简单说就是将已经编译好的二进制程序通过反汇编分析来还原相应的源代码。本节就来介绍C语言代码的逆向基础知识，主要讲解C语言中流程控制代码对应的反汇编代码。

7.5.1　函数的识别

做逆向分析的第一步是先要对函数进行识别，也就是说确定函数的开始位置、结束位置、函数调用方式及函数的参数个数。在逆向分析的过程中，不会把单个指令当作是最基本的逆向单位，因为一条汇编指令很难说明任何问题，就像在C语言中，很难通过一行代码来说明问题一样。

大多情况下是针对自己比较感兴趣的部分进行重点分析，分析部分功能或者部分关键函数。因此，确定函数的开始位置及结束位置就很重要了。不过通常情况下，函数的起始位置和结束位置都可以通过反汇编工具自动识别，从而省去了我们自己的识别。使用强大的反汇编工具IDA更是如此，不过在某些特殊情况下仍然需要我们自己去识别。简单介绍一下如何识别函数的开始与结束的位置。写一个简单的C语言代码，用VC6进行编译连接，再用IDA打开。C语言代码如下：

在程序中由主函数调用了自定义函数test()，test()函数的返回值为int类型。在test()函数中调用了printf()函数和MessageBox()两个函数。逆向分析是在DEBUG编译方式下进行的 （RELEASE方式留给大家自己去研究）。用IDA打开我们编译连接好的程序文件。用IDA打开后，将直接找到main()函数（多么方便的工具），如果没有找到main()的话，通过简单的操作也能找到。下面先来通过运行时启动函数定位找到main()函数。

首先在IDA上单击选项卡中的“Exports”项，在这里可以看到mainCRTStartup，如图 7-13所示。

图7-13　“Exports”选项卡

双击mainCRTStartup就可以运行启动函数了。记住main()函数不是程序运行的第一个函数，而是程序员编写程序时的第一个函数。main()函数是由运行时启动函数来调用的。启动函数部分反汇编代码如下：

从反汇编代码中可以看到，main()函数在004012B4位置处，启动函数获得版本号、获得命令行参数、获得环境变量字符串……等一系列的操作后才调用了main()函数。由于使用的是调试版且有pdb文件，因此在反汇编代码中能直接显示出程序中的符号。通常情况是没有 pdb文件的，这样_main是一个地址，而不是直接给出的符号了，不过仍然可以按照规律来找到主函数。

我们已经顺利找到了主函数，直接双击_main到达如下反汇编代码处：

我们看到了4行0040100A，其实不是有4行，真正的那一行是jmp main，其余的都是 IDA为了方便查看而生成的。这时发现这并不是我们编写的主函数，这是以Debug方式编译生成的一个跳表，双击main处，就到了真正的主函数处了，代码如下：

观察反汇编代码就可以确定这是我们的主函数了，代码中有一个对test()的调用，也看到了printf()。下面介绍一下这段反汇编代码。

大多数函数的入口处都是push ebp/mov ebp，esp/sub esp，xxx这样一个形式，这几句是用来保存栈帧并开辟栈空间用的。push ebx/push esi/push edi是保存的几个关键寄存器值，以便函数返回后这几个寄存器中的值还能在调用函数处继续使用而没有被破坏。lea edi，[ebp + var_44]/mov ecx， 11h/mov eax， OCCCCCCCCh/rep stosd，这几句是将开辟的内存空间全部初始化为0×cc，0×cc如果当机器码来解释的话，其对应的汇编指令为int 3，也就是断点中断指令。这样做是方便调试，尤其是对指针变量的调试好处非常多。以上代码是一个固定的形式，唯一会发生变化的部分是sub esp，xxx，这里是sub esp，44h。在VC6下的Debug编译方式下，如果当前函数没有变量，那么就是sub esp，40h;如果有一个变量的情况下是sub esp， 44h；两个变量时为sub esp，48h。也就是说，从Debug方式编译时总是在预留局部变量后将栈顶多抬高40h字节。局部变量都在栈空间中，栈空间进入函数后临时开辟的空间。函数入口处的代码基本上就这些，看一下函数返回处的反汇编代码：

这是函数返回时的固定格式，这个格式跟入口的格式基本是对应的。首先是pop edi/pop esi/pop ebx，这里是将上面保存的几个关键寄存器的值进行恢复。然后是add esp，xxx，这里是与sub esp，xxx对应的，将临时开辟的栈空间释放掉（这个释放只是改变寄存器）。Mov esp， ebp/pop ebp是恢复栈帧，retn就返回到上层函数了。在该反汇编代码中还有一步没有讲，这是cmp ebp，esp/call_chkesp，这两句是对_chkesp函数的一个调用，在Debug编译方式下，对几乎所有的函数调用完成后调用一次_chkesp，该函数的功能是用来检查栈是否平衡，以保证程序的正确性。如果栈不平，会给出错误提示。我们做个简单的测试，在主函数后面加一条内联汇编—asm push ebx，然后编译连接运行，在输出过后会看到一个错误的提示，如图 7-14所示。

图7-14　调用_chkesp后对栈平衡进行检查时出错的提示

整个的主函数还有最后一部分，反汇编代码如下：

首先是push 6/push offset aHello/call j_test/add esp，8/mov [ebp + var_4]，eax是对test()。函数的一个调用，push eax/push offset aD/call printf/add esp，8是对printf()函数的调用，xor eax，eax是对eax进行清0。我们重点来看一下对test()函数的调用。双击j_test来到如下反汇编代码处：

这里仍然是个跳表，双击test来到如下反汇编代码处：

函数开头和结尾的部分我们都已经熟悉了，直接看其他部分吧，反汇编代码如下：

另外一段反汇编代码如下：

从这两段代码中可以看出分别是对printf()函数和MessageBoxA()函数的调用，在对printf()函数调用后有一条add esp，OCh，而在MessageBoxA()后面则没有。这是为什么呢？这就是调用约定。在VC下常见到两种调用约定，分别是stdcall和cdecl两种。

stdcall是Windows下的标准调用约定，Windows提供的API函数及WDK中提供的函数都是使用stdcall，当然也有例外，就是API中变参函数是使用cdecl调用约定。C语言默认的是使用cdecl调用约定。

stdcall参数的入栈方式是从右往左，平衡栈是在被调用函数的内部。cdecl参数的入栈方式也是从右往左，平衡栈是在调用函数方。这就是两种函数调用的约定方式，没有好与不好，只是调用方式不同而已，唯一一点是stdcall不能支持变参函数。

7.5.2　if……else……分支结构

下面用一个相对简单的if……else……分支实例来说明其对应的反汇编，C代码如下：

代码很简单，用IDA看其关键的反汇编代码：

以上3句分别是对3个变量进行赋值初始化。

观察上面的反代码发现，代码中使用的比较关联符号是“大于号”，而反汇编代码却使用了相反的比较跳转指令，使用的是“小于等于”则跳转，否则不跳转。请注意观察00401043和0040105E这两个地址，jle会跳过紧接着的下面的部分代码，跳转的目的地址上面是一条无条件跳转指令jmp，也就是说，jle和jmp之间的部分是代码中比较表达式成功后执行的代码。在反汇编代码中，如果比较指令后的跳转指令没有发生跳转的话，则在执行完一系列指令后，会紧跟着一条无条件跳转指令jmp跳到某个地址。请注意观察00401056和00401071这两个地址，这两个跳转指令跳转到了同一个目的地址。在C源代码中，当比较表达式成功后会执行其后面的代码块，当执行完这些代码块后就跳过与之配对的else if或else后面的代码。

上面C源代码中if……else……对应反汇编结构如下：

这基本上就是if……else……分支结构的执行流程了，当反汇编时遇到这样的结构就可以马上知道这是一个if……else……分支结构了，大的流程定下以后就可以具体地分析每个分支要执行的代码了。

7.5.3　switch分支结构

前面讲了if……else……的分支结构，接下来介绍switch分支结构。该结构是一个比较有趣的结构，它的反汇编代码和我们想的反汇编代码不同，而且switch的反汇编形式特别的丰富，这里只介绍它的其中一种反汇编形式。C语言代码如下：

我们来看以上代码对应的反汇编代码：

根据对if……else……的经验判断，分支是由跳转指令完成的。在上面的反汇编代码中发现有两条跳转指令，一条是跳转到default分支的，另一条是一个看不太明白的跳转。我们把那条跳转指令单独看一下：

此时说明eax的值不大于3，也就是在0~3之间的任意一个数，看一下4010bb是什么内容：

4010bb像是一个数组，数组中的内容是40105f、40106e、40107d和40108c，这4个值就是switch中各个分支的目标地址，也就是说该处是根据一个地址表来跳转到各个分支流程的，根据各个取值的不同而不同。比如输入的内容是4，而经过反汇编的一番处理后变成了3，处理的反汇编代码如下：

当输入变为3时，在jmp ds：：off_4010bb[3*4]中得到的地址为4010bb + 3*4 = 4010c7，而4010c7中保存的则是到case 4：分支的地址。

switch分支的反汇编形式比较有用，这里只介绍了其中的一种反汇编形式，其余的反汇编形式就不进行介绍了，大家可以自行学习。

7.5.4　for循环结构

介绍完分支结构后，相应的就要介绍C语言中的循环结构，下面先来介绍一下for循环， C语言代码如下：

这是一个很经典的求1加到100的累加和的例子，我们看其反汇编代码：

这段for循环的反汇编代码很清晰，也很简单，就不对反汇编代码做具体介绍了，下面只对for循环结构做一个反汇编的总结，具体如下所示：

修改循环变量处：

循环变量比较处：

7.5.5　do while与 while循环结构

最后再介绍两种循环结构，分别是do……while和while循环结构，同样使用简单的例子来整理其反汇编对应的结构。先来看do……while循环，C语言代码如下：

再看其反汇编代码：

do……while循环对应的反汇编结构如下：

下面来看一个while循环的代码：

看其反汇编代码：

整个反汇编代码与do……while类似，只是把判断放到前面了，while循环的结构如下：

对于for、do……while和while这3种循环来说，do……while的效率显然更高一些。编程时可以尽量选择do……while循环来使用。

对于C语言的逆向知识就介绍这么多。大家可以写一些C语言代码，然后用IDA来进行分析学习。

参考文献

1.段钢.加密与解密（第三版）.北京：电子工业出版社。

2. 王艳平.Windows程序设计.北京：人民邮电出版社。

3. （美）Jeffrey Richter著.王建华，张焕生，侯丽坤等译.Windows核心编程.北京：机械工业出版社。

4. Eldad Eilam著作.韩琪，杨艳，王玉英，李娜等译.Reversing:逆向工程解密.北京：电子工业出版社。

5. （美）Greg hoglund James Butler著.韩智文译.ROOTKITS-Windows内核的安全防护.北京：清华大学出版社。

Table of Contents

书名页

版权页

目录

前言

第1章　黑客编程入门

1.1　编程语言和开发环境的选择

1.1.1　何为SDK、API和MFC

1.1.2　VC6和SDK的配置

1.2　应用程序的调试

1.2.1　编写我们的第一个程序

1.2.2　用VC6调试第一个程序

1.2.3　专业的应用程序调试工具——OllyDbg

1.3　简单API的介绍

1.3.1　复制自身程序到Windows目录和系统目录下

1.3.2　获得系统的相关信息

1.3.3　Debug和Release的编译方式

1.3.4　查看函数定义

1.4　总结

第2章　木马开发剖析

2.1　网络通信基础

2.1.1　IP地址的作用与分类

2.1.2　端口的作用与分类

2.2　网络编程基础知识

2.2.1　通信模型

2.2.2　Winsock

2.2.3　Winsock的相关函数

2.2.4　字节顺序

2.3　简单的通信程序

2.3.1　基于TCP协议的“Hello World!”

2.3.2　基于UDP协议的“Hello World!”

2.4　实现一个C/S模式的简单木马

2.4.1　木马服务器端的实现

2.4.2　木马客户端的实现

2.5　总结

第3章　Windows应用编程基础

3.1　文件

3.1.1　打开文件

3.1.2　文件操作

3.2　AutoRun免疫程序的编写

3.2.1　AutoRun免疫原理

3.2.2　AutoRun免疫程序的代码实现

3.2.3　界面设置

3.2.4　代码相关部分

3.3　注册表操作

3.3.1　注册表

3.3.2　与注册表操作相关的常用API函数

3.3.3　注册表启动项的管理

3.3.4　程序的界面设置及相关代码

3.3.5　启动项的枚举

3.3.6　添加启动项的代码

3.3.7　删除启动项的代码

3.4　服务相关的编程

3.4.1　如何查看系统服务

3.4.2　服务控制管理器的开发

3.4.3　枚举服务的相关API函数

3.4.4　服务的停止

3.4.5　停止服务的相关API函数

3.4.6　服务的启动

3.5　进程与线程

3.5.1　进程

3.5.2　进程的创建

3.5.3　“下载者”的简单演示

3.5.4　CreateProcess()函数介绍与程序创建

3.5.5　进程的结束

3.5.6　进程的枚举

3.5.7　调整当前进程的权限

3.5.8　进程的暂停与恢复

3.5.9　多线程

3.6　DLL编程

3.6.1　什么是DLL

3.6.2　编写一个简单的DLL程序

3.6.3　对DLL程序的调用方法一

3.6.4　对DLL程序的调用方法二

3.7　远程线程

3.7.1　DLL注入

3.7.2　DLL卸载

3.7.3　无DLL的代码注入

3.8　总结

第4章　加密与解密

4.1　PE文件结构

4.1.1　PE文件结构全貌

4.1.2　MZ头部

4.1.3　PE头部

4.1.4　节表

4.1.5　节表数据

4.2　详解PE文件结构

4.2.1　DOS头部详解 IMAGE_D〇S_HEADER

4.2.2　PE头部详解IMAGE_NT_HEADERS

4.2.3　IAMGE_FILE_HEADER

4.2.4　IMAGE_OPTIONAL_HEADER

4.2.5　节区详解IMAGE_SECTION_HEADER

4.2.6　与PE结构相关的3种地址

4.2.7　3种地址的转换

4.3　PE查看器

4.4　简单的查壳工具

4.5　地址转换器

4.6　添加节区

4.6.1　手动添加一个节区

4.6.2　通过编程添加节区

4.7　破解基础知识及调试API函数的应用

4.7.1　CrackMe程序

4.7.2　用OD破解CrackMe

1.破解方法一

2.破解方法二

4.8　文件补丁及内存补丁

4.8.1　文件补丁

4.8.2　内存补丁

4.9　调试API函数的使用

4.9.1　常见的3种断点方法

4.9.2　调试API函数及相关结构体介绍

4.9.3　判断是否处于被调试状态

4.9.4　断点异常函数

4.9.5　调试事件

4.9.6　调试循环

4.9.7　内存的操作

4.9.8　线程环境相关API及结构体

4.10　打造一个密码显示器

4.11　总结

第5章　HOOK编程

5.1　HOOK知识前奏

5.2　内联钩子——Inline Hook

5.2.1　Inline Hook的原理

5.2.2　Inline Hook的实现

5.2.3　HOOK MessageBoxA

5.2.4　HOOK CreateProcessW

5.2.5　7字节Inline Hook

5.2.6　Inline Hook的注意事项

5.3　导入地址表钩子——IATHOOK

5.3.1　导入表简介

5.3.2　导入表的数据结构定义

5.3.3　手动分析导入表

5.3.4　枚举导入地址表

5.3.5　IAT HOOK介绍

5.3.6　IAT HOOK之 CreateFileW ()

5.4　Windows钩子函数

5.4.1　钩子原理

5.4.2　钩子函数

5.4.3　键盘钩子实例

5.4.4　使用钩子进行DLL注入

5.5　总结

第6章　黑客编程剖析

6.1　恶意程序剖析

6.1.1　恶意程序的自启动

6.1.2　木马的配置生成与反弹端口

6.1.3　代码实现剖析

6.2　简单病毒剖析

6.2.1　病毒的感染剖析

6.2.2　缝隙搜索的实现

6.2.3　感染目标程序文件剖析

6.2.4　添加感染标志

6.2.5　自删除功能的实现

6.3　隐藏DLL文件

6.3.1　启动WinDBG

6.3.2　调试步骤

6.3.3　编写枚举进程中模块的函数

6.3.4　指定模块的隐藏

6.4　安全工具开发基础

6.4.1　行为监控工具开发基础

6.4.2　专杀工具

6.4.3　U盘防御软件

6.4.4　目录监控工具

6.5　引导区解析

6.5.1　通过WinHex来手动解析引导区

6.5.2　通过程序解析MBR

6.5.3　自定义MBR的各种结构体

6.5.4　解析MBR的程序实现

6.6　加壳与脱壳

6.6.1　手动加壳

6.6.2　编写简单的加壳工具

第7章　最后的旅程——简单驱动开发及逆向

7.1　驱动版的 “Hello World”

7.2　驱动下的进程遍历

7.2.1　配置VMware和WinDbg进行驱动调试

7.2.2　EPROCESS和手动遍历进程

7.2.3　编程实现进程遍历

7.3　HOOK SSDT（系统服务描述表）

7.3.1　SSDT简介

7.3.2　HOOK SSDT

7.3.3　Inline HOOK SSDT

7.4　应用程序与驱动程序的通信

7.4.1　创建设备

7.4.2　应用程序与驱动程序的通信方式

7.4.3　应用程序与驱动程序的通信实例

7.5　C语言代码逆向基础

7.5.1　函数的识别

7.5.2　if……else……分支结构

7.5.3　switch分支结构

7.5.4　for循环结构

7.5.5　do while与 while循环结构

参考文献

后封皮