版权信息

书名：黑客攻防从入门到精通（加密与解密篇）
作者：明月工作室；宗立波
出版社：北京大学出版社
出版时间：2016-12
ISBN：9787301277768

版权所有·侵权必究

前言

从2003年起，中国互联网逐渐找到了适合国情的商业模式和发展道路，互联网应用呈现多元化局面，电子商务、网络游戏、视频网站、社交娱乐等百花齐放。计算机技术及通信技术的进一步发展，持续推动着中国互联网新一轮的高速增长。到2008年，中国网民人数已经达到2.53亿人，首次大幅度超过美国，跃居世界首位。

从2009年开始，移动互联网兴起，互联网与移动互联网共同营造了当前双网互联的盛世。网络已经成为个人生活与工作中获取信息的重要手段，网络购物也已经成为民众重要的消费渠道。当前，“互联网+”的战略布局与工业4.0的深度发展，使得国家经济发展、民众工作生活，都与网络安全休戚相关，一个安全的网络环境是必不可少的。

当前最大的一个问题是广大用户对网络相关软硬件技术的掌握程度远远不够，这就为不法分子提供了大量的机会，借助于计算机网络滋生的各种网络病毒、木马、流氓软件、间谍软件，给广大网络用户的个人信息及财产带来了非常大的威胁。

为提升广大民众对于计算机网络安全知识的掌握程度，做好个人信息财产安全的防护，我们编写了这套“黑客攻防从入门到精通”丛书，本书为其中的《黑客攻防从入门到精通（加密与解密篇）》分册。

丛书书目

黑客攻防从入门到精通（全新升级版）

黑客攻防从入门到精通（Web技术实战篇）

黑客攻防从入门到精通（Web脚本编程篇·全新升级版）

黑客攻防从入门到精通（黑客与反黑工具篇·全新升级版）

黑客攻防从入门到精通（加密与解密篇）

黑客攻防从入门到精通（手机安全篇·全新升级版）

黑客攻防从入门到精通（应用大全篇·全新升级版）

黑客攻防从入门到精通（命令实战篇·全新升级版）

黑客攻防从入门到精通（社会工程学篇）

本书特点

· 内容全面：涵盖了从计算机黑客攻防入门，到专业级的Web技术安全知识，适合各个层面、不同基础的读者阅读。

· 与时俱进：本书主要适用于Windows 7及更高版本的操作系统用户阅读。尽管本书中的许多工具、案例等可以在Windows XP等系统下运行或使用，但为了能够顺利学习本书的全部内容，强烈建议广大读者安装Windows 7及更高版本的操作系统。

· 任务驱动：本书理论和实例相结合，在介绍完相关知识点以后，即以案例的形式对该知识点进行介绍，加深读者对该知识点的理解和认知能力，力争彻底掌握该知识点。

· 适合阅读：本书摒弃了大量枯燥文字叙述的编写方式，而是采用了图文并茂的方式进行编排，以大量的插图进行讲解，可以让读者的学习过程更加轻松。

· 深入浅出：本书内容从零起步，通俗易懂，由浅入深地讲解，使初学者和具有一定基础的用户都能逐步提高。

读者对象

· 计算机初、中级用户。

· 网店店主、网店管理及开发人员。

· 计算机爱好者、提高者。

· 各行各业需要网络防护的人员、中小企业的网络管理员。

· Web前、后端的开发及管理人员。

· 无线网络相关行业的从业人员。

· 计算机及网络相关的培训机构。

· 大中专院校相关学生。

本书结构及内容

本书一共有15章。内容由浅入深，循序渐进，前后衔接紧密，逻辑性较强。

第1章 加密基础知识

第2章 软件加密技术

第3章 加密算法

第4章 解密基础知识

第5章 破解技术基础

第6章 静态反汇编工具

第7章 动态跟踪分析工具

第8章 补丁技术

第9章 解密壳技术

第10章 加密与解密的编程技术

第11章 常用软件加密解密技术应用

第12章 其他软件加密解密技术应用

第13章 光盘的加密解密技术

第14章 网络验证技术应用

第15章 自制加密工具

超值赠送资源

1．黑客攻防全能视频

为了读者能全面地了解黑客方面的知识从而有效地防御黑客的不法入侵行为，本书特赠送全能教学视频，视频内容包括社会工程学、黑客攻防入门、信息的扫描与嗅探、木马与病毒的防范、系统漏洞防范、远程控制术、加密与解密、数据备份与恢复、移动网络安全等内容。

2．其他赠送资源

· Windows系统安全与维护手册

· 计算机硬件管理超级手册

· Windows文件管理高级手册

·（140个）Windows系统常用快捷键介绍

·（157个）Linux命令应用大全

·（136个）Linux系统管理与维护命令应用手册

·（58个）Linux网络与服务器命令应用手册

· 黑客攻防命令手册

我们已将赠送内容上传百度网盘，在浏览器中输入下载链接，打开链接后，在如下图所示的文本框中输入提取码便可下载赠送资源。下载链接： http://pan.baidu.com/s/1eSfvxDK，提取码：ez6a。

提示

读者也可加入QQ群，在群文件中下载“资源下载地址列表”文档，直接复制链接和密码，下载多媒体视频。（注意：我们会在群文件中共享一些赠送资源，如百度网盘链接失效，请加入QQ群下载资源。）

后续服务

本书由明月工作室宗立波编著，胡华、栾铭斌、王栋、高翔、马琳、赵玉萍、闫珊珊等老师也参加了本书部分内容的编写和统稿工作，在此一并表示感谢！在本书的编写过程中，我们竭尽所能地为您呈现最好、最全的实用功能，但仍难免有疏漏和不妥之处，敬请广大读者不吝指正。若您在学习过程中产生疑问或有任何建议，可以通过E-mail或QQ群与我们联系。

投稿信箱：pup7@pup.cn

读者信箱：2751801073@qq.com

读者交流群：218192911（办公之家）、99839857

郑重声明

本丛书对大量计算机及移动端的攻击行为进行了曝光，是为了让广大读者做好安全防范工作。

请本丛书广大读者注意：根据国家有关法律规定，任何利用黑客技术攻击他人的行为都是违法的！

第1章
加密基础知识

互联网是一个面向大众的开放系统，其对于信息的保密性和系统的安全性考虑的并不完备，由此引起的网络安全问题也就日益严重。因此计算机信息内容的保密问题就显得越来越重要。

在当今社会，网络信息可以帮助团体或个人得到利益，同样，这些网络信息也可能会造成威胁，成为破坏者。网络信息的安全一直备受人们的关注，经常需要一种措施来保护它们，防止被一些不良用心的人看到或破坏。于是数据加密技术便应运而生，成为保障网络信息安全的有力手段。网络安全技术逐渐成为加密技术中的核心。加密与解密的方法是非常直接的，容易掌握，利用它可以很方便地对机密数据进行加密和解密。

1.1 探究加密技术

1.1.1 理解加密技术

加密技术是一门古老而深奥的学科，它对一般人来说是陌生的，因为长期以来，它只在很小的范围内，如军事、外交、情报等部门使用。计算机加密技术是研究计算机信息加密、解密及其变换的科学，是数学和计算机的交叉学科，也是一门新兴的学科。在国外，它已成为计算机安全主要的研究方向，也是计算机安全课程教学中的主要内容。

一个密码系统的安全性主要在于密钥的保密性，而不在于算法的保密性。

对于数据的加密的确是这样。对于不愿意让别人看到这些数据（数据的明文）的人，用可靠的加密算法，只要破解者不知道被加密数据的密码，他就无法解读这些数据。

但是，软件的加密不同于数据的加密，它只能是“隐藏”。不管是否愿意让别人（合法用户，或Cracker）看见这些数据（软件的明文），软件最终总要在机器上运行，对机器而言，它就必须是明文。既然机器可以“看见”这些明文，那么Cracker通过一些技术也可以看到这些明文。

于是，从理论上，任何软件加密技术都可以破解，只是破解的难度不同而已。所以，要不想被盗版，就只有增加Cracker的破解难度。

加密也可提高终端和网络通信的物理安全，有3种方法加密传输数据。

（1）链接加密

在网络节点间加密，在节点间传输加密，传送到节点后解密，不同节点对间使用不同的密码。

（2）节点加密

与链接加密类似，不同的只是当数据在节点间传送时，不用明码传送，而是用特殊的加密硬件进行解密和重加密。

（3）首尾加密

对进入网络的数据加密之后，再等数据从网络传送出来之后进行解密，网络本身并不会知道正在传送的数据是否为加密数据。这种加密方法的优点是网络上的每个用户可有不同的加密关键词，并且网络本身不需增添任何专门的加密设备。缺点是每个系统必须有一个加密设备和相应的软件或每个系统必须自己完成加密工作。

数据摘要算法是密码学算法中非常重要的一个分支，它通过对所有数据提取指纹信息以实现数据签名、数据完整性校验等功能，由于其不可逆性，有时候会被用作敏感信息的加密。数据摘要算法也称为哈希（Hash）算法、散列算法。

摘要是一种防止改动的方法，其中用到的函数叫作摘要函数。这些函数的输入可以是任意大小的消息，而输出是一个固定长度的摘要。摘要有这样一个性质，如果改变了输入消息中的任何东西，甚至只有一位，输出的摘要将会发生不可预测的改变，也就是说输入消息的每一位对输出摘要都有影响。

总之，摘要算法从给定的文本块中产生一个数字签名（fingerprint或message digest），数字签名可以用于防止有人从一个签名上获取文本信息，或改变文本信息内容和进行身份认证。摘要算法的数字签名原理在很多加密算法中都被使用。

1.1.2 摘要算法的分类

1.CRC8、CRC16、CRC32

循环冗余校验（Cyclic Redundancy Check,CRC）算法出现时间较长，应用也十分广泛，尤其是通信领域，现在应用最多的就是CRC32算法，它产生一个4字节（32位）的校验值，一般是8位十六进制数，如FA 12 CD 45等。CRC算法的优点在于简便、速度快，严格地来说，CRC更应该称为数据校验算法，但其功能与数据摘要算法类似，因此也作为测试的可选算法。

在WinRAR、WinZIP等软件中，也是以CRC32作为文件校验算法的。一般常见的简单文件校验（Simple File Verify,SFV）也是以CRC32算法为基础，它通过生成一个扩展名为．SFV的文本文件，将文件内容CRC32运算的结果与．SFV文件中的值对比来确定此文件的完整性。

与SFV相关的工具软件有很多，如MagicSFV、MooSFV等。

2.MD2、MD4、MD5

这是应用非常广泛的一个算法家族，尤其是MD5（Message-Digest Algorithm 5，消息摘要算法版本5），它由MD2、MD3、MD4发展而来，由Ron Rivest（RSA公司）在1992年提出，被广泛应用于数据完整性校验、数据（消息）摘要、数据加密等。MD2、MD4、MD5都产生16字节（128位）的校验值，一般用32位十六进制数表示。MD2的算法较慢但相对安全，MD4速度很快，但安全性下降，MD5比MD4更安全、速度更快。

在互联网上进行大文件传输时，都需要用MD5算法产生一个与文件匹配的、存储MD5值的文本文件（扩展名为．md5或．md5sum），这样接收者在接收到文件后，就可以利用与SFV类似的方法来检查文件完整性，绝大多数大型软件公司或开源组织都是以这种方式来校验数据完整性，而且部分操作系统也使用此算法来对用户密码进行加密。另外，它也是目前计算机犯罪中数据取证的最常用算法。

与MD5相关的工具有很多，如WinMD5等。

3.SHA1、SHA256、SHA384、SHA512

SHA（Secure Hash Algorithm）是由美国专门制定密码算法的标准机构——美国国家标准技术研究院（NIST）制定的，SHA系列算法的摘要长度分别为：SHA为20字节（160位）、SHA256为32字节（256位）、 SHA384为48字节（384位）、SHA512为64字节（512位）。由于它产生的数据摘要的长度更长，因此更难以发生碰撞，也更为安全，它是未来数据摘要算法的发展方向。由于SHA系列算法的数据摘要长度较长，因此其运算速度与MD5相比，也相对较慢。

SHA1的应用较为广泛，主要应用于CA和数字证书中，另外，在互联网中流行的BT软件也是使用SHA1来进行文件校验的。

4.RIPEMD、TIGER、MAD

RIPEMD是Hans Dobbertin等3人在对MD4、MD5缺陷分析的基础上，于1996年提出来的，有4个标准，分别是128、160、256和320，其对应输出长度分别为16字节、20字节、32字节和40字节。

TIGER由Ross在1995年提出。Tiger号称是最快的Hash算法，专门为64位机器做了优化。

MAD摘要算法的设计是出于利用32位RISC结构来最大其吞吐量，而不需要大量的替换表来考虑的。

MAD算法是以消息给予的长度作为输入，产生一个128位的“指纹”或“消息化”。要产生两个具有相同消息化的文字块或产生任何具有预先给定“指纹”的消息，都被认为在计算上是不可能的。

MAD摘要算法是个数据认证标准。MAD的设计思想是要找出速度更快，比MAD更安全的一种算法，MAD的设计者可以通过使MAD在计算上慢下来，以及对这些计算进行一些基础性的改动来解决安全性这一问题，是MAD算法的一个扩展。

1.1.3 加密技术的演化

数据加密技术源于大约公元前两千年前，埃及人最先使用特别的象形文字作为信息编码来对信息进行加密处理，随着时间的推移，加密技术越来越多地被应用于对数据安全的保护。加密技术首先由恺撒大帝用于战争中的数据加密，也就是非常熟悉的恺撒密码。

数据加密的基本过程就是对原来为明文的文件或数据按某种算法进行处理，使其成为一段不可读的代码，通常称为“密文”，只有在输入相应的密钥之后才能显示出本来内容，通过这样的途径达到保护数据不被人非法窃取、阅读的目的。

数据加密技术是网络中应用的基本的安全技术，通过对网络中传输的信息进行数据加密来保障其安全性，用很小的代价即可为信息提供相当大的安全保护。数据加密技术是为提高信息系统及数据的安全性和保密性，防止秘密数据被外部破析所采用的主要技术手段之一。随着信息技术的发展，网络安全与信息保密日益引起人们的关注。

随着计算机的发展，人们不断地研究出了新的数据加密方式来保障数据安全，如对称密钥算法和非对称密钥算法。

一个加密系统至少包括如下4个组成部分。

· 未加密的报文，也称明文。

· 加密后的报文，也称密文。

· 加密解密设备或算法。

· 加密解密的密钥。

明文用M（消息）或P（明文）表示，它可以是比特流（文本文件、位图、数字化的语音流或数字化的视频图像）。至于涉及计算机，P是简单的二进制数据。明文可被传送或存储，无论在哪种情况，M是指要进行加密的信息。

密文用C表示，它也是二进制数据，有时和M一样大，有时稍大（通过压缩和加密的结合，C有可能比P小些。然而，仅仅加密通常达不到这一点）。

加密函数E作用于M得到密文C，用数学表示为：

E（M）=C

1.1.4 加密技术的必要性

在当今社会，必须要使用加密技术来保护数据的安全。众所周知，在互联网上进行文件传输、发送电子邮件是进行业务往来的重要手段，但互联网是基于TCP/IP协议存在的，TCP/IP协议由于本身固有的不安全性，需要引起人们的高度重视，特别是进行一些机密文件的网络传输。

互联网把全世界连接成一个整体，带来了无限的商机和便利，为了能在安全的基础上享受这些便利，就要使用数据加密技术和基于数据加密技术的数据签名。

加密技术在网络上的应用非常必要，主要就是防止机密文件或有用的内容在网络传输时，被非法截获或破坏。

如果一个公司通过电子邮件给另一个公司发送一份比较机密的文件，说不定此时就会有人准备窃取这份关乎两个公司生死存亡的文件，这样的损失是无法估计的。解决这个难题的方法就是加密，加密后的文件即使被窃取也是不可读的，只是一堆乱码，所以说使用加密技术是当今网络社会的必然趋势。

数字签名基于加密技术，其作用是用来确定用户是否真实存在，一般应用在收发电子邮件上。当用户接收到一封电子邮件，邮件上标有发信人的姓名和邮箱地址，有些人简单地认为邮件标示的发信人的姓名就是真实的，实际上并不完全是这样，这时，就可以利用加密技术基础上的数字签名来确认发信人的真实身份了。

文件加密不只是针对电子邮件和网络上传输的文件，也可以对静态的文件进行加密保护，像PIP（Project Information Portal，项目信息平台）软件就可对磁盘、硬盘中的文件和文件夹进行加密。

加密类型大致可以分为3类。

（1）私用密钥加密技术

即利用一个密钥对数据进行加密，另一方收到数据之后，使用同一个密钥进行解密。这种加密的特点就是运算量小，速度快。缺点是加密解密使用同一个密钥，一方泄露密钥，整个数据信息就会暴露无遗。

（2）公开密钥加密技术

这种加密技术使用一对密钥进行加密，一个是公开密钥，一个是私有密钥。加密时使用公开密钥，接收者收到信息可以使用私有密钥解密。它的安全系数比私用密钥加密技术高，可以广泛用于通信、数字签名上。

（3）不用考虑解密问题

这种加密技术类似于口令加密，用户只需要进行加密后，与以前的加密进行比较就可以了，而不需要考虑解密问题。

1.1.5 加密技术的发展前景

加密技术的渊源距离现代社会甚为久远，最早源于大约公元前两千年前，那时的埃及人就发明了象形文字来保护秘密信息不会轻易被别人发现。它不是现在要讲的加密技术，只是作为一种加密的概念来进行理解。

在现代社会中，加密技术最常用于对军事领域中的机密信息进行加密、破解。随着计算机技术的不断发展，运算能力的不断增强，以往的密码就变得十分不安全，非常容易被破解。于是人们又研究出了新的数据加密方式，如利用ROSA算法产生的私钥和公钥。

加密技术大致有以下几种发展趋势。

（1）私用密钥加密技术与公开密钥加密技术相结合

这两种加密技术各有优势，在实际应用中可以结合使用DES/IDEA和RSA，以DES为“内核”,RSA为“外壳”，对于网络中传输的数据可以用DES或IDEA加密，而加密用的密钥则或能用RSA加密传送，这样既能保证加密信息的安全，又提高了加密和解密的速度，可以看作是加密技术发展的趋势之一。

（2）寻找新的加密算法

现在广泛使用的加密算法，普遍存在这样或那样的缺点和不足，有的加密速度快，却不能保证数据安全，有的算法烦琐，却实现速度极慢。正因为这样，所以跳出常规算法为基础的思路，脱离基于数字问题的构造方法，寻找新的加密方法也是当今加密界的一大趋势。

（3）加密最终要集成到网络和系统中

例如IPv6协议就实现了内置加密的支持，而Intel公司的硬件方面，研制一种加密协处理器，就可以集成到计算机的主机上。

1.2 软件注册保护方式

软件开发者为了维护自身的版权和商业利益，不断地寻找各种有效的加密算法和技术来保护自己的软件，以增加其保护强度，预防软件被破解；而破解者或受盗版所带来的高额利润驱使，或出于纯粹的个人兴趣，而不断制作新的破解工具，并针对新出现的保护方式进行跟踪分析以找到相应的破解方法。

从理论上说，几乎没有破解不了的保护。对软件的保护仅仅靠技术是不够的，而这最终要靠人们的知识产权意识和法制观念的进步。但如果一种保护技术的强度大到让破解者在软件的生命周期内无法将其完全破解，这种保护技术就可以说是非常成功的。

软件保护方式的设计应在一开始就作为软件开发的一部分来考虑，列入开发计划和开发成本中，并在保护强度、成本、易用性之间进行多方位的考虑，争取找到一个平衡点。

要进行解密的软件大部分都是付费软件，所以软件作者会在软件注册保护方式上面来防止软件被解密，在正式注册之前是有一定限制的，比如使用功能上和使用时间上。

现在的软件一般都采用注册码方式进行保护，此外还有注册文件、加密狗、光盘加密、网络校验、NAG窗口等方式。

1．注册码方式

现在的大多数软件为了便于在网上进行交易，都会采用注册码进行保护。一般是对软件的功能和使用时间进行限制。软件作者使用的注册算法有很多种，经常使用如下几种方法。

（1）机器码+注册码

机器码一般是通过机器硬件设备的唯一编号，利用软件构造算法来生成，如利用CPU、硬盘、网卡等设备来生成机器码。由于硬件的编号是唯一的，所以机器码在不同计算机中是不会重复的，从而注册码也就是唯一的。

（2）用户名+注册码

利用输入的用户名构造算法来生成注册码，这种方式的缺点是不同的计算机可以用同一个用户名进行注册成功。

（3）组合方式+注册码

这种方法是结合了以上的两种方式来构造算法，参数增多，算法复杂。

在构造注册算法时，大多数的软件作者都会利用计算机中的数学运算和逻辑运算，保护的效果不是很好。在这里软件作者可以借鉴密码学算法应用到自己的算法中，如MD5、RSA、DES、CRC32、BASE64等，这些算法基本上都是不可逆的。如使用MD5，根据得到的结果是得不出原始数据的，现在常用在后台数据库的密码字段。即使得到后台数据库，也猜不出原始密码。

在构造算法的最后就要对注册码的真假进行比较，最好不要用变量直接进行比较，而是构造一个算法，让机器码（用户名）和注册码作为变量参与计算，利用构造的算法进行变换比较。如a机器码或者用户名，b为假注册码，在比较时，不能使用“if f（a）=b”这样的变量比较，而最好使用if f（a,b）=c这样的函数比较。

变量比较就是解密者所说的明码比较，在程序运行的某一个时期，在内存中会出现正确的注册码，这样，即使算法再复杂，也会很容易做出内存注册机；算法比较也就是暗码比较，不会在内存当中出现，增加了解密的难度。

2．加密狗方式

所谓加密狗就是一个数字电路，根据连接在计算机上的不同接口，可以分为USB狗、并口狗。软件通过读取加密狗内的数据来判断是否注册及软件的版本等信息，达到保护软件的目的。

3．光盘加密

这种加密方式一般应用于游戏软件，可以防止原始光盘被非法复制。

4．网络验证

随着互联网的快速发展，网络验证保护方式被越来越多的人所采用。软件在注册时要到官方网站进行注册或在注册后会不定期连接到官方网站进行校验，如果不通过，则视为盗版或试用版本。

5.NAG窗口

软件没有注册或使用的是软件的试用版本，往往会弹出一些提示窗口，这些提示窗口称为NAG窗口，它在软件试用期过后，就要屏蔽软件的正式窗口或某些重要功能窗口，使软件不能正常使用。

6．将软件与机器硬件信息结合

用户得到软件后，安装软件时会从用户的机器上取得该机器的一些硬件信息（如硬盘序列号、BIOS序列号等），再把这些信息和用户的序列号、用户名等进行计算，从而在一定程度上将软件和硬件部分绑定。用户需要把这一序列号用E-mail、电话或邮寄等方法寄给软件提供商或开发商，软件开发商利用注册机（软件）产生该软件的注册号寄给用户即可。

虽然软件加密的加密强度比硬件方法较弱，但它具有非常廉价的成本、方便的使用方法等优点，非常适合作为采用光盘（CDROM）等方式发授软件的加密方案。

这种加密算法的优点如下。

① 不同机器的注册码不同。用户获得一个密码，只能在一台机器上注册使用软件。不同于目前大多软件采用的注册方法，即只要知道注册码，就可在任何机器上安装注册。

② 不需要任何硬件。

③ 可以选择控制软件运行在什么机器、运行多长时间或次数等。

④ 可让软件在注册前的功能为演示软件，只能运行一段时间或部分功能。注册后就立即变为正式软件。

⑤ 采用特别技术，解密者很难找到产生注册号码的规律。

⑥ 在使用注册号产生软件（注册机）时可采用使用密码、密钥盘、总次数限制等方法。

7．时间限制

有些程序的试用版每次运行时都有时间限制，例如，运行10分钟或20分钟就停止工作，必须重新运行该程序才能正常工作。这些程序里面有个定时器，来限制程序运行的时间，这也是一种对软件进行保护的方法。

1.3 加密技术中的相关概念

学习加密技术的过程中涉及一些常用概念，用户只有理解这些概念的内涵才能进一步学习。下面就详细介绍一下在加密过程中经常涉及的几个概念。

1．断点

所谓断点是指程序被中断执行的地方。程序产生中断就是由于有特殊事件发生，计算机暂停当前的程序运行，转而去执行别的程序，当执行完毕后再返回执行被中断的程序。

解密的过程就是当程序去获取输入的注册码，并准备和正确的注册码相比较时将其中断，通过分析找到正确的注册码。这就需要为被解密的程序设置断点，在适当的时候切入程序内部，追踪到程序的注册码，达到破解的目的。

2．领空

所谓程序的领空，其实就是程序自己的地方，也就是要破解程序的程序码所处的位置。在DOS时代，基本上所有的程序都是工作在中断程序之上的，即几乎所有的DOS程序都会去调用各种中断来完成任务。

但到了Windows时代，程序没有权力直接调用中断，Windows系统提供了一个系统功能调用平台（API），就像DOS程序以中断程序为基础一样，Windows程序以API为基础来和系统打交道，从而实现各种功能，所以Windows下的软件破解其断点设置是以API函数为基础的，即当程序调用某个API函数时中断其正常运行，然后进行解密。

API（Application Programming Interface，应用程序编程接口）是一个系统定义函数的大集合，它提供了访问操作系统特征的方法。

API包含了几百个应用程序调用的函数，这些函数执行所有与操作系统相关的操作，如内存分配、向屏幕输出和创建窗口等，用户的程序通过调用API接口同Windows打交道，无论什么样的应用程序，其底层最终都是通过调用各种API函数来实现各种功能的。

通常情况下，API有两种基本形式：Win16和Win32。

Win16是原来的API的16位版本，用于Windows 3.1;Win32是现在的API的32位版本，用于Windows 9X/NT/ME/2000/XP/2003/7/8/10。Win32包括了Win16，是Win16的超集，大多数函数的名字、用法都是相同的。

16位的API函数和32位的API函数的区别在于最后的一个字母，例如，设置这样的断点Bpx GetDlgItemText、Bpx GetDlgItemTextA和Bpx GetDlgItemTextW，其中GetDlgItemText是16位API函数，GetDlgItemTextA和GetDlgItemTextW是32位API函数，而GetDlgItemTextA表示函数使用单字节，GetDlgItemTextW表示函数使用双字节。

现在破解中常用到的是Win32单字节API函数，就是和GetDlgItemTextA类似的函数，其他的两种（Win16 API和Win32双字节API函数）则比较少见。

Win32 API函数包含在动态链接库（Dynamic Link Libraries,DLLs）中，即包含在kernel32.dll、user32.dll、gdi32.dll和comctl32.dll中，这就是要在SoftICE中用exp=C:\Windows\system\kernel32.dll等命令行将这些动态链接库导入SoftICE中的原因。因为不这样做，就无法拦截到系统Win32 API函数调用了。

3．程序中注册码的存在方式

破解过程中都会去找程序中将输入的注册码和正确的注册码相比较的地方，然后通过对程序的跟踪、分析找到正确的注册码。但正确的注册码通常在程序中以两种形态存在：显式的和隐式的。

对于显式存在的注册码，可以直接在程序所处的内存中看到它，例如，可以直接在SoftICE的数据窗口中看到类似“297500523”这样存在的注册码（这里是随意写的），对于注册码显式存在的软件破解起来比较容易。

但有些软件的程序中并不直接将输入的注册码和正确的注册码进行比较，如有可能将注册码换算成整数或将注册码拆开，然后将每一位注册码分开在不同的地方逐一进行比较，或将输入的注册码进行某种变换，再用某个特殊的程序进行验证等。

总之，应用程序会采取各种不同的复杂运算方式来回避直接的注册码比较，对于这类程序，通常要下功夫去仔细跟踪、分析每个程序功能，找到加密算法，然后才能破解它。当然，这需要一定的8086汇编编程功底和很大的耐心与精力。

1.4 常用的汇编语言命令

在加密中会常用到几个汇编语言命令，所以在这里做一下说明，便于以后的参考和运用。

跳转命令

根据条件做出是否跳转的决定，通常前面会有一个判断语句，例如，

        CMP AX, BX       比较AX与BX
        JZ XX             相减为0则跳转到XX标号行

上面两行语句命令意为用AX减BX，它的值如果为0，则跳转到XX的标号行。

常用的跳转命令有以下几种。

        JZ/JE             相等或为0则跳转（机器码为74或0F84）
        JNZ/JNE          不相等或不为0则跳转（机器码为75或0F85）
        JL/JLE            小于、小于或等于则跳转
        JG/JGE           大于、大于或等于则跳转
        JMP              无条件跳转（机器码为EB）

比较语句

        CMP AX, BX       AX寄存器减去BX寄存器的内容
        AND AX, BX       AX与BX做“与运算”
        OR AX, BX        AX与BX做“或运算”
        TEST AX, BX       与AND AX, BX命令有相同效果
        XOR AX, BX        使AX的内容清零，每个寄存器与自己作异或运算等于清零动作

子程序

一个子程序的例子：CALL 15F:334422

在软件的加密中，子程序是个很重要的概念，它是一个主程序的分支，用来指定某些特定动作，即一个程序不可能就是一条主程序到头，肯定会调用子程序，由子程序来实现注册的功能，并对其进行比较，然后标记是否注册正确。因此，破解的关键在于找准程序作注册判断的部分，进入那个注册子程序。子程序的返回码是RET。

算术运算

        ADD AX, BX        加法运算AX=AX+BX
        SUB AX, BX        减法运算AX=AX-BX
        INC AX           寄存器加AX=AX+1
        DEC AX           寄存器减AX=AX-1
        MUL             乘法运算
        DIV               除法运算

数据操作

        MOV AX, BX       数据传输指令，将BX的值移送到AX中
        XCHG AX, BX      将AX与BX的值互换

1.5 小结

本章主要介绍了关于加密的基础知识，有助于初学者了解什么是加密，加密的由来，以及几种经常用的软件注册保护方式，展现了比较全面的加密技术知识。总之，学习加密重在实践，如果能亲自动手将所学内容多加应用，相信一定会有不小的进步。

技巧与问答

❖ 什么样的加密算法是一个优秀的加密算法？

从理论上说，任何加密算法都可以破解，但是只要这种加密算法的加密强度到达一定程度，破解者可能在一定的时间内无法破解。也就是说，如果这种算法使得破解者在破解时需要花费大量的时间，金钱等，其破解的代价已经远远超出了破解软件所带来的利益，就说明这种算法是一种优秀的加密算法。

❖ 什么是网络验证，以及网络验证的破解思路是什么？

网络验证是目前流行的一种保护技术，其优点是可以将一些关键数据放到服务器上，运行软件时必须从服务器上获取这些数据才能正确运行。

破解的思路是拦截服务器返回的数据包，分析程序是如何处理数据包的。

相关的函数如下。

常用的有sand和recv两个socket函数，另外还有微软扩展的函数WSASend和WSARecv。

（1）send

          int send(
                        socket s,                  //套接字描述符
                        const char FAR ＊buf,      //缓冲区
                        int len,                   //实际要发送的数据的字节数
                        int flags                   //附加标识，一般都为0
                      )；

（2）recv

          int recv(
                      socket s,                  //套接字描述符
                      char FAR ＊buf,            //缓冲区
                      int len,                   //缓冲区buffer的长度
                      int flags                   //附加标识，一般都为0
                      )；

网络验证破解的一般思路如下。

如果网络数据包内容固定，可以将数据包的内容进行抓取，写个本地服务端模拟服务器。

如果验证的数据包内容不固定，则必须分析其结构，找出相应的算法。

❖ 什么是软件破解版和注册版？

破解版（crack）：一般是需要替换文件，达到注册的目的。所替换的文件是通过汇编或其他方法跳过注册码的验证以达到注册的目的。

注册版（keygen）：是指在不改变源程序的情况下通过序列号（正版序列号或通过注册机）得到序列号。

第2章
软件加密技术

软件加密技术和解密技术形影不离，相伴相随。随着加密技术的产生，解密技术应运而生，每当一项新的加密技术出现后，相应的解密技术也会随之兴起，有人可能会问什么时候加密技术和解密技术会停止竞争，答案是永远不会。只有加强人们对版权的认识，提高人们反盗版的意识，在使用软件的过程中使用正版软件，拒绝使用盗版软件，才可以加强对版权的保护。

2.1 认识口令加密技术

口令加密技术作为较早期就出现的加密技术，主要有如下几种：口令加密、加密起始簇号、为COM文件加口令和为EXE文件加口令等（其中的口令可以是字符串，也可以是矩阵口令）。

2.1.1 口令加密技术的基本概念

口令加密技术就是在被加密软件的开始部分编写了一小段程序，这样，当该软件在运行时，就会首先要求用户输入口令。而用户所输入的字符串有两种表现形式，一种是不在屏幕上显示出来，另一种是以星号代替，这样用户输入的口令就不容易被别人窥探到，而口令校验时由程序将接受输入的字符串与程序中事先安排的规定字符串进行比较。如果两者一致，则该程序进入正常运行状态；反之，该程序将转入死循环或中途退出。

这种加密技术可适用于所有程序文件，文件只需要加入一段小程序就可以起到保护作用。如果在加密口令时采用了复杂的加密算法，将口令字作为加密密钥对程序进行加密、解密变换，则解密者一般是很难对其进行解密的，因此效果也较为可靠一些。因此，口令加密一般与其他加密方法结合起来使用，而不单独使用，以提高加密的效果。

如今市场上出售的通用加密软件为了限制未授权用户的使用，加强软件的加密效果，一般都提供有口令加密的功能，软件为了确保用户随时随地的使用或是不使用该功能，将该功能设置为一个可选项。如果用户启用了口令加密功能，则被加密的软件就具有了口令识别和身份验证的功能，只有输入正确的口令，才允许用户使用该软件，如果用户忘记口令，则软件不允许用户使用。

如在最为经典的一些口令加密示例程序中，该程序通常都是利用系统的功能调用07H来接收键盘输入口令的。在口令字输入的过程中，以回车键（0DH）作为结束符，将输入后的口令字与事先安排在程序中的规定值进行比较，如果两者一致，则显示“OK PASS…”等信息，然后运行程序；否则，将提示用户重新输入合法的口令字。并且还规定了该口令最多允许输入3次，如果3次输入均不正确，则屏幕将显示“未经允许不得使用本软件”等信息，使用户无法实现正常使用或直接退出。

这种口令判别的程序比较简单，加密效果不理想，容易被他人破解造成用户的损失。所以在实际应用中，在采用加密算法时先将口令字作为密钥，对文件中的主要程序段进行加密变换，而在程序的开头接收键盘输入的口令字作为密钥，对文件中的密码进行解密。如果用户没有输入正确的口令，则程序主代码无法解密，也就限制了非法用户的使用，而对于解密者来说要想解密这样的软件具有一定的难度。

如果想使加密后的程序正常运行，首先需要修改程序的代码，在程序接收到正确的口令字后，需要将用户输入的口令字与程序数据段中的指令密码进行解密运算，并将解密的结果存放在代码段中原有的位置，这样才能正常使用该软件。

一般为了防止因输入口令字的错误而导致最终的解密错误，可以采取将解密的结果求代码和的方法，以便检查输入口令字的正确性，从而决定是否再一次提示用户输入口令字或者直接退出软件的运行。

2.1.2 口令加密软件的起始簇号

在加密过程中可以将用户输入的口令字与文件的实际起始簇号进行运算，然后用得到的值来替换文件的起始簇号。起始簇号的存取可通过中断调用INT26H（绝对写盘）与INT25H（绝对读盘）来实现。由于起始簇号占2个字节，则它的可能值为164个，因此在不知口令的情况下，如果想要将其破译是需要花费大量时间和精力的。因此，可编制一个汇编程序，它应包括下列几个内容：要求用户输入口令字（为了避免失误，可要求输入2次进行确认）；要求用户输入驱动器号及加密、解密的相关信息；进行口令验证，当口令不符时，显示“YOU ARE ILLEGAL!”等信息；用目录区中此文件目录项的起始簇号与用户所输入的口令字进行运算，并将最终结果存回；在程序中实现加密运算与解密逆运算，然后根据用户加密、解密的要求来进行选用。

实际应用中可设这样的程序名为TEST.EXE，经编译、连接后产生TEST.EXE文件。然后再根据提示分别输入文件名、驱动器号、加/解密要求及口令字等信息，在录入完这些相关的信息之后，就可以对目标文件实施加密了。一般为了增加破译难度，可用TEST.EXE文件进行多次加密。当然，这样所造成的最终结果是为了能运行正常也要进行多次解密程序。

2.1.3 可执行文件的口令加密

不管计算机的操作系统是DOS环境还是WIN32环境，可执行文件的扩展名为．EXE、.BAT和．COM类型，对可执行文件的加密保护仍然是软件加密技术的主要内容之一。由于DOS环境对软件的保护力度不够，导致可执行文件容易被破解。现在WIN7系统允许软件设置口令加密，同时，随着新的复制和调试工具不断涌现，就必须采取一些措施对可执行文件进行加密保护处理，以防止非法用户使用、分析或复制。下面分别介绍对以下5类文件的加密保护。

（1）EXE文件的口令加密

大家都很熟悉扩展名为．EXE的文件，因为其在计算机软件中的数量最多，因此软件加密的重点就是对这类文件的加密保护。EXE文件是由两部分组成的，即文件头与文件本身。因此对其实施加密的情况主要分为如下两种。

① 按口令字对EXE的头文件实施加密，使文件头的信息与程度不匹配。

② 对程序本身用口令字实施加密，这种加密方法与COM文件的加密方法类似，主要也是将附加程序段放在了EXE文件的末尾。

但由于EXE文件存在有复位的问题，因此在对EXE文件加密实现时还需要考虑下列几点。

① 附加程序段使用的堆栈不能采用SS、SP，而需要另设堆栈，因此需要修改文件头中的堆栈指针SP，使其指向新设的堆栈。

② 为了使附加程序在执行完毕之后能顺利回到程序的起始值，还需要将文件头中的代码段偏移值存放到附加程序段中，最后再用CS减去这个相对段值即可求出程序的起始段值。

③ 在文件的最后附加文件头中的“复位位项表”，并且只有在解密后才能对程序中的几个复位位项进行正确的复位。

④ 不能像COM文件那样靠修改程序的第一条为跳转指令来实现过程控制权的获得，而只能通过修改文件头中的入口地址（IP值），来使其指向最终的附加程序段。

⑤ 由于EXE文件增加了复位位项表和附加程序段，因此，其文件的长度也就随之而发生了变化。所以，还需要修改文件头中有关文件长度的几项内容。

实际上，为了减少许多不必要的编程和极易发生的冗余错误，可以不必为那些需要加密的可执行文件全部加上加密和反跟踪功能，而采取直接把一段标准的加密程序加到已有的可执行程序中去修改原文件头和有关指令，使修改后的程序能首先执行加密的反拷贝部分，然后运行原来部分的方法。

（2）COM文件的口令加密

为了对COM文件实施加密，可以采用对被保护的COM文件设置口令，通过口令对文件加密，并在程序末尾增加一个附加程序段，然后再进行存盘的加密思想，如图所示。

原文件与新文件的关系

下图所示即为对COM文件设置口令的流程图。其中，附加程序段用于负责输入用户口令及对代码进行变换，并最终将程序还原，然后转入程序入口正常执行，如下图所示。

为COM文件设置口令

附加程序段的工作示意图

通过检索用户可以发现，在PC上COM类文件其实已经占据了相当大的比重，且很多EXE类的文件都可以通过EXE2BIN来转换成COM类文件（大小要小于64KB），并且由于COM类文件比EXE文件的结构更为紧凑，而且执行更快，比EXE文件更好。因此，对COM类文件用设置口令的办法来实施加密保护有很好的应用前景。

（3）EXE和COM文件的异同

由于EXE和COM程序在运行结束之后，通常都会释放其占用的内存块（常驻内存的除外），因此，该类程序也称为“临时程序”。

EXE和COM文件的异同点主要如下。

① 文件大小不同：COM文件的全部程序与数据的总长度不可超过64KB，而EXE文件可存在任意大小。

② 执行速度不同：COM文件不需要进行复位，因此调入速度很快；而EXE文件则由于存在复位的过程，而相对较慢。

③ 存放位置不同：对COM文件而言，程序本身从CS:100H开始释放，其CS、DS、SS和ES都指向PSP所在的段。而对EXE文件，头文件则从CS:100H起存放，以后才是待复位的程序，尽管ES和DS还都指向PSP所在的段，但CS却不一定指向PSP所在的段。

④ 段的数目不同：COM文件的程序和数据都存在于同一个段中（不具有一个以上的段）；而EXE文件的CS,DS,HE和SS却可以都不相同，可具有多个段。

如表所示即为在DOS下EXE文件与COM文件进行加载后的位置比较。

COM文件与EXE文件异同点

（4）口令加密可执行文件的矩阵方法

在加密过程中采用把口令设置为 n × n 的矩阵的方法，可以增加可执行文件加密的口令变化，如取（ n =3），可以通过取矩阵中某两个字符为口令，如X和G，用户需要输入平行的两个字符，在这里要求输入A和Z即可。

使用该加密方法的优势是定期实现矩阵口令的更改，增加破解者的破译难度。可以很好地保护可执行文件，防止他人对可执行文件的修改。

（5）.BAT文件的口令加密

扩展名为．BAT的批处理文件主要是由系统准许DOS命令的批量操作而生成的文件（该文件包含若干条DOS内部命令、外部命令以及有关的批处理命令），所以在根目录或任何一级目录上，都可以建立该文件。当系统在指定的路径上找到要执行的该批处理文件之后，就开始逐条解释执行该文件中所包含的各条命令，并且在执行完毕之后能够再返回到DOS的命令处理程序中。

虽然目前人们鉴于批处理文件中的各命令语句是由DOS逐条解释执行的，且对其内容进行加密保护的方法很少，对批处理文件源程序的保护，不能采用将其编译成二进制代码进行存储的方法，但人们仍然采取各种加密手段对其源程序进行了加密保护。对批处理文件的内容进行加密保护的方法如下。

如果能够采用DEBUG/PCTOOLS对批处理文件进行分析的话，就可以发现其实该文件中的每条命令都是以0DH,0AH作为该命令结束符的，而整个批处理文件则是以0DH,0AH, 1AH作为批处理程序结束符的。于是，就可以考虑采用所谓的“覆盖法”（是指在不破坏批处理程序组织结构的基础上，将其所有的内容迭加到一行上，进行层层覆盖，从而达到保护批处理的方法）来对批处理文件进行加密保护。

应用“覆盖法”对批处理文件进行加密之后，当用一些编辑软件对其进行显示或编辑时，该批处理程序的内容将逐条迭加到一行在屏幕上进行显示。这样一来，破解者就无法看到该批处理程序内容的整体了，但程序仍能够保证正常运行，从而也就达到了良好的对批处理文件内容加密保护的目的。

具体实施办法是将换行符0AH改为固定返回符00H，下面给出了一个用Turbo C语言编制的加密/解密保护批处理文件程序的源代码。

        #include <dos.h>
        #include <stdio.h>
        #include <string.h>
        /＊ Begin main function ＊/
        main()
        {
          char errormsg[34], filename[15], ＊ ptr, ＊data;
          static char readerror[]='READ failed';
          File ＊ stream;
          int length;
          printf('Please input decipher filename:') ;
          gets(fi lename);
          stream=fopen(filename, 'rb++');
          if(stream=NULL)
          {
              printf('\07') ;
              strcpy(errormsg, readerror) ;
              pritf('\07');
              pritf('%s', erroemsg) ;
              pritf('\07') ;
        }
        else
        {
          fseek(stream,2, SEEK_END) ;
          length=ftell(stream) ;
          data=(char ＊)calloc (unsigned)length, sizeof(char) ;
          if(! data)
          {
                printf('alloction failure_aborting') ;
                exit(1);
          }
          rewild(stream) ;
          while(! feof(stream))
          {
              fscanf(stream, '%～\x00', data) ;
              while(ptr=strrchr(data, '\n'))
              strnset(ptr, '\x00',1) ;
          }
          rewind(stream);
          fwrite(data,1, length, stream);
          fclose(stream);
        }
        }

2.2 软件狗加密技术概述

软件狗主要有国外狗和国产狗，国外狗主要是指美国的彩虹和以色列的HASP；国产狗主要有圣天地、深思、坚石。总的来说，“国外狗”在软件接口、加壳、防跟踪等“软”方面做得相对没有“国产狗”好，但在硬件上破解难度非常大。而“国产狗”在“软”的方面做得比较好，许多技术，如噪声、自检测、算法可变、码表变换等，可以说都很先进，而在硬件上比国外稍弱，只要稍微有些单片机基础的人，都可对其进行复制。

2.2.1 软件狗加密技术的基本概念

软件开发商为了保护版权，要求用户只有在计算机的打印接口（并行口）或者USB口安装相应的硬件时，软件才会正常工作。这种安装在并行口或者USB口的防盗版硬件，就是我们所说的“加密狗”或“软件狗”。

软件狗是插在微机并行口上的一个软件保护装置，它包括主机检查程序和密钥（也称加密盒）两部分。主机检查程序就是前面说的加密代码的一部分，加密盒是用来存放密码的。一般来说，软件狗插在并行口上，不会影响打印机的正常工作。

常见的软件狗加密盒外形，如两个一公一母的D行25针连接器倒接在一起，公头（DB25/M）插在并行口上，母头（DB25/F）可接打印机，相当于原来的并行口。整个软件狗的硬件电路板就在这约5厘米的加密盒子里。

电路板上的公头（DB25/M）之间的管脚是逐一对应、直接相连接的，以保证并行口的作用不变。存储密码或起信号加密变换作用的器件及其他辅助元件就跨接在这25根线上面，应用程序以特定的方式跟它们沟通、核对。除了某些设计不良的情况之外，一般不会影响打印机的正常工作，打印机工作时也不会影响它们。

为了防止程序被非法复制，所做的加密保护措施一般都包括两部分。首先是要有保存密码数据的载体，即密钥；其次是夹杂在应用程序中的主机检查程序，即加密代码。密钥应该能保证不易被解密、复制；如通常采用磁盘进行加密时，加密部分无法用一般的工具复制。

另外，当检查程序用特殊方法去读密码时，密码应该能很容易地被读出，而不致影响应用程序的正常执行。当发现密码不对或密钥不存在时，就通过让主机挂起、重新启动或采用退出等措施。

现在软件狗的技术发展很快，针对不同的应用场合有不同的类型。

· 强劲狗：自由定义算法的高强度加密方案。

· 微狗： 面向单片机环境的高强度加密方案。

· USB狗：USB接口的微狗全兼容产品。

· 软件狗：面向单机环境的低成本加密方案。

· 网络狗：面向网络环境的加密方案。

· 卡式狗：面向网络环境的加密方案。

软件狗采取了各种加密技术，目前较先进的加密技术有以下几种。

1）口令：可由软件开发商设置32位口令，口令错误将不能对存储区进行读写。

2）存储器：提供20字节掉电保持存储器供开发商存放关键数据、配置参数等信息。

3）抗共享：可从硬件对抗并口共享器，由开发商选择是否共享狗。

4）AS技术：API函数调用与Shell外壳加密结合，即使外壳被破坏，加密程序依然不能正常运行。

5）时间闸：某些狗内部设有时间闸，各种操作必须在规定的时间内完成，狗正常操作用时很短，但跟踪时用时较长，超过规定时间狗将返回错误结果。

6）单片机：硬件内置单片机，由于固化的单片机软件保证外部不可读，从而保证了狗不可仿制。

7）反跟踪：① 数据交换随机噪声技术，能有效地对抗逻辑分析仪分析及各种调试工具的攻击；② 迷宫技术，在程序入口和出口之间包含大量判断跳转干扰，动态改变执行次序，提升狗的抗跟踪能力。

现在有许多公司都在制作和出售软件狗，各种软件狗的核心技术都一样，为了吸引用户而附加了一些功能，主要是一些工具软件，可以帮助用户方便地使用软件狗，提升用户的使用体验。

2.2.2 软件狗的功能和特点

软件狗的加密锁通常都很小巧，但包含的技术内容却十分丰富，一般可从3个方面分析加密锁的性能。

（1）加密锁的易学性、易用性

加密锁的一般特点如下。

① 不占用并行口，因为它虽然插在并行口上，但却又提供了一个跟原来一致的并行口。

② 软件狗具有防解密功能，可对抗各种调试工具的跟踪。

③ 一狗一密码或一种线路，软硬件不可互换，就像一把锁一把钥匙那样。

④ 提供各种语言的编程接口以及一套实用工具，方便用户在自己开发的程序中嵌入加密模块。

⑤ 提供对可执行文件的加密工具，以便用户对已有的产品进行加密。

（2）加密锁的可靠性、稳定性、兼容性、透明性等

这些是对锁的基本要求，但是做完整并不容易，尤其是兼容性、透明性问题，加密厂家在这方面有着丰富的经验。

透明性是锁的一个比较特殊的指标，由于锁是工作在并口（打印口），并口还会有其他设备，例如，打印机、绘图仪、硬盘、光驱等，锁如果影响到原来设备的正常工作，就是透明性不够好，完全透明是难以做到的，一般的加密厂家不保证锁对并口硬盘、光驱等设备的透明性。

（3）加密原理，或者说加密锁都有些什么功能

这往往是针对一些解密方法而发展起来的，最需要创造性的地方就是这里。国内的产品在这个方面一直与国外产品具备足够的竞争能力。

2.2.3 软件狗加密的缺点

对软件加密保护产品而言，使用者最关心的是加密的有效性、产品的兼容性和稳定性。目前市场上主要的软件加密锁在硬件内部均含有单片机，即所谓内置CPU，软件厂商主要是利用算法功能进行加密。加密锁通常还增加了一些辅助功能，如倒计数器、远程升级等。这种类型的加密锁主要产品有彩虹天地的“微狗”“SuperPro”、深思洛克的“深思Ⅰ、Ⅲ型”、ALADDIN的HASP3、HASP4等。

通过对这些软件锁进行分析，发现从安全性上讲它们至少有3方面薄弱点。

（1）设计原理有很大缺陷

目前主流的加密锁硬件提供了读、写和算法变换功能，且算法变换关系难以破解和穷举。但这类加密锁最大的缺陷是算法不向软件厂商公开，锁内的变换算法在出厂时已经固定，软件加密者只能设置算法的参数。这样就限制了厂商对算法的使用，要么预先记录算法结果。然后在软件运行时核对（使用码表），要么在软件中至少变换2次，然后比较结果是否一致；如果解密者截获这些数据，通过统计、分析就有可能达到解密目的。

（2）加密锁受处理能力的限制，无法为软件提供强有力的保护

市场上曾先后推出了几款“可编程”加密锁。这类型加密锁最大的特点就是可以让用户自行设计专用算法。“可编程”加密锁的出现的确是软件加密技术的一次进步。深思洛克的“深思Ⅲ”、飞天诚信的“Rockey4”均属此类产品。

但由于成本限制，此类型加密锁往往只能采用10～20元人民币的通用8位单片机或同档次的ASIC芯片作为核心微处理器。这种低档单片机的处理运算能力是相当弱的，这就给 “可编程”加密锁造成了很大的局限性，主要表现在以下几个方面。

① 算法变换的复杂度不够高。

② 指令编码空间较小。

③ 程序区的空间较小。

这些局限性使得用户根本不可能利用“可编程”加密锁实现理想的高强度加密方案。

（3）硬件本身抵抗恶意攻击的能力较弱

随着集成电路设计、生产技术的发展，安全产品的核心芯片硬件本身受到攻击的可能性越来越大。典型的硬件攻击手段有电子探测攻击（如SPA和DPA）和物理攻击（探测，如采用SiShell技术）。

下面我们就这方面来进行一些简要的分析。

电子探测（SPA和DPA）攻击技术的原理是：单片机芯片是一个活动的电子元器件，当它执行不同的指令时，对应的电功率消耗也产生相应的变化。通过使用特殊的电子测量仪和数学统计技术，来检测和分析这些变化，从中得到单片机中的特定关键信息。

物理攻击的方法有：通过扫描电子显微镜对芯片内部存储器或其他逻辑直接进行分析读取；通过测试探头读取存储器内容；通过从外部无法获取的接口（例如，厂家测试点）对存储器或处理器进行直接数据存取；再激活单片机的测试功能等。

由于通用低档单片机并非定位于制作安全类产品，没有提供有针对性的防范物理攻击手段，因此比较容易通过电子探测（SPA和DPA）攻击直接读出存储器内的数据。虽然大多数普通单片机都具有熔丝烧断保护单片机内代码的功能，但此类芯片应用场合广、发行批量大，随着厂商间委托加工与频繁技术转让，使得利用该类芯片出现下载程序的设计漏洞，利用厂商的芯片测试接口，通过特殊的烧写时序和数据读出信息成为比较容易的事情。

ASIC芯片是完全根据用户需求而特别定做的，属于小批量生产。由于其采用特殊的逻辑电路且不会轻易公开测试功能接口，因此，只要以其为基础开发的系统不是保存重要的信息或者不用于高级别的安全场合，还是可以防范一般情况下的物理攻击。

综上所述，解密一个软件狗主要从两方面入手。

① 软件，只要把检查软件狗的那部分代码解除，那么软件狗就成了摆设。

② 从硬件入手的解密方法，其含义是要仿制一个加密盒，不管机密做得如何好，如果加密盒被仿制了，那么软件狗的加密作用也就不存在了。因此，从硬件入手的解密跟从软件入手的解密其效果是一样的，只不过后者需要付出一定的额外代价——硬件成本而已。

那么，既然从软件、硬件着手都能解密，用它来保护软件还有什么作用？不用着急，下面就来谈谈如何更好地保护自己的软件。

首先可以把要保护软件关键部分的程序加以编码，运行时再译码出来，这样就增加了解密的难度，因为解密者必须懂得编码、译码规则才能修改可执行文件，而这些规则加密者一般是不会泄露的。

还可以采取多种软件加密方法的综合，以及几种加密技术交叉使用，足以让许多解密者知难而退（例如，有人就把软件狗加密技术和磁盘加密技术结合起来做）。因为必须了解所有的加密技术原理才有可能解密，这就增加了难度，毕竟能够精通各种加密方法的人不多。

2.3 探究激光孔加密技术

激光具有方向性好、高亮度及高光子简并度的特点，激光加密技术是指用激光在磁盘的数据区或扇区标识符上烧出若干个痕迹，但由于所产生的激光孔极小，不仔细看几乎不容易发现。磁盘上的某些位置可能会失去磁性，产生不可恢复的标志，以此达到加密的效果。

磁道上的磁性介质记录数据的正常功能被磁盘在激光处理过的区域中所产生的激光孔破坏了，于是必然会产生CRC校验错误。

一般加密磁盘通常都有自己的识别程序，也就是说，只有当该识别程序判断出现CRC校验错误时，才认为此盘是原盘，然后进一步执行被保护的用户程序；而如果没有检测到CRC校验错误，则认为该盘为复制盘，拒绝执行被保护的用户程序，这样使复制工具无法实现复制的目的，对用户的程序起到了很好的保护作用。

由于激光加密盘上的激光孔硬标志无法复制，因此，当使用高级复制软件复制激光加密盘时，经过复制后所产生的复制盘几乎都无法正常使用。即使使用具有激光孔的磁盘去复制激光加密盘，但由于每个激光加密盘上的激光孔个数不可能相同，或者即使激光孔的个数相同，位置也不可能相同，因此无法完成复制。如果能够在加密的时候再使用密码法将所保护的软件重新进行编码，这样一来，即使破解者利用反汇编方法将程序的汇编代码打印出来，也是读不懂程序的。

如在某个有激光孔的扇区写入全“33”，则其结果如下。

根据如图所示的激光孔检测示意图可以看出：该扇区上有2个激光孔，记下扇区号L及激光孔的位置，把它们保留在指定的单元中以备检测。

激光孔检测示意图

以较为著名的PROLOCK加密程序为例，其主要采取了如下几种措施来阻止解密者对其进行分析和跟踪。

1．隐蔽转移

为了不让解密者发现程序的走向，PROLOCK采用了隐式调用（如INT 0H调用）。这样做的好处是：当程序出现运算溢出时，将转向溢出处理程序，而这时的溢出处理程序已由加密程序所取代。

另外，还有一种隐式调用为单步中断调用。在中断处理返回时，要恢复标志寄存器，如在程序中把将要恢复的标志字中的单步标志置位，程序在返回断点前就要首先进入单步中断处理程序，这时的单步中断处理程序已被加密程序取代（这种隐式调用可以迷惑解密者）。

2．多循环多出口

PROLOCK充分利用了多循环多出口技术来增加解密者的工作量，以拖垮解密者。

较为典型的做法是：在PROLOCK程序块中有时为一段程序有意设置多次循环，并且在这些循环中设置有多个出口，而且许多出口的转向均由程序来实现动态设置。这样，就可以很容易使得跟踪者无法事先决定程序的走向，也就无法对程序正确设置断点了。

3．密文处理

当用户需要保护的程序被PROLOCK加密之后，通常是对其部分代码进行了密文处理，即使得该程序在DEBUG下用反汇编命令“U”无法实现静态分析，而是在执行过程中进行逐块解密，执行后再进行加密。这样，就可以实现在任何时刻内存中都不存在完整的明文程序，从而起到很好的保护作用。

4．反跟踪

为了防止解密者对自己的程序进行跟踪分析，PROLOCK还采用了多种防跟踪措施，主要如下。

（1）坏断点中断和单步中断的向量。

（2）PROLOCK程序把中断向量表中的0004-0007和000C-000FH用作程序的数据区。

由于程序在执行过程中需要经常对这几个单元进行存取操作，因此，当PROLOCK采用了这些防跟踪措施之后，就破坏了单步和断点中断向量。同时，为了能够防止解密者将该数据区转向别处，还在程序中大量地进行了该类操作，这就使得解密者在进行跟踪时改不胜改。

5．设置堆栈

在实施加密时，PROLOCK程序还有意把堆栈设置在内存低端。这样做的好处是一方面可以破坏中断向量区；另一方面可以破坏跟踪。

在没有跟踪的情况下，堆栈大小可以满足栈操作的要求。但当有用户对程序实施跟踪时，由于有额外的保存断点等操作，因此，就使得栈空间不够，从而导致跟踪者的返回地址丢失，出现系统混乱。

2.4 伪随机数加密技术概述

伪随机数加密技术其实质就是通过伪随机数序列使文件的字节值发生变化而产生密文。由于相同的初值能得到相同的随机数序列，因此，可以采用同样的伪随机数序列来对密文进行解密并将其恢复为明文。

而其加密算法则为：当取定一个初值时，在加密过程中将产生伪随机数序列 r 1 , r 2 , …, r n 。对应密文 C i 有（其中所得的 a 1 , a 2 , …, a n 即为原明文）。

a i = C i - r i ，当 C i ≥ r i 时

a i =（ C i +256）- r i ，当 C i ＜ r i 时

下面是伪随机数加密法的加密和解密程序。

加密程序的C语言源代码如下。

        define KEY 10
        #define SIZE 160
        #define NULL 0
        #include <stdio.h>
        #include <math.h>
    main()
    {
      FILE ＊fp
      int k, j, i, count, rj, t;
      char fbuf[SIZE], sbuf[SIZE], filename[10], ch;
      j=KEY;
      printf("input locked filename :\n");
      scanf("%s, fi lename);
      if((fp=fopen(fi lename, "r"))==NULL)
      {
        printf("Cannot open file! \n");
        exit(0);
      }
      for (k=0; (ch=fgetc(fp))! =EOF; k++)
      {
        fbuk[k]=ch;
        putchar(fbuf[k]);
      }
      putchar('\n');
      fclose(fp);
      count=k;
      for(i=0; i<count; i++)
      {
        rj=rand(j);
        sbuf[i]=fbuf[i]+rj)%256;
        j++;
      }
      if((fp=fopen(filename, "w"))= =NULL)
      {
        printf("Cannot open 1.ss! \n");
        exit(0);
      }
      for(i=0; i<count; i++)
      {
        fputc(sbuf[i], fp);
        putchar(subf[i]);
      }
      putchar('\n');
      fclose(fp);
    }

解密程序的C语言源代码如下。

    #define KEY 10
    #define SIZE 160
    #define NULL 0
    #include <stdio.h>
    #include <math.h>
    main()
    {
      FILE ＊fp;
      int  k, j, i, count, rj;
      char fbuf[SIZE], sbuf[SIZE], ch, filename[10];
      j=KEY;
      printf("input unlocked filename:\n");
      scanf("%s", fi lename);
      if((fp=fopen(fi lename, "r"))==NULL)
      {
        printf("Cannot open file! \n");
        exit(0);
      }
      for(k=0; (ch=fgetc(fp))! =EOF; k++)
      {
        fbuf[k]=ch;
        putchar(fbuf[k];
      }
      putchar('\n');
      fclose(fp);
      count=k;
      for(i=0; i<count; i++)
      {
        rj=rand(j);
        if (fbuf[i]<rj)
          sbuf[i]=fbuf[i]+256-rj;
        else
          sbuf[i]=fbuf[i]-rj;
        j++;
      }
      if((fp=fopen(fi lename, "w"))==NULL)
      {
        printf("Cannot open u.ss! \n");
        exit(0);
      }
      for(i=0; i<count; i++)
      {
      fputc(sbuf[i], fp);
          putchar(subf[i]);
          }
          putchar('\n');
          fclose(fp);
        }

设其可执行文件名为TEST.EXE，说明文件的内容为：

        garden plants need different amounts of sunlight cucumbers, for
      example, are very hard to please

加密/解密均覆盖操作如下。

加密操作：

        C:\>TEST
        输入说明文件名
            显示原文件内容（garden…please）
            显示加密后的内容（乱码）

解密操作：

        C:\>TEST
        显示input unlocked filename：
        说明文件名
        显示加密后的内容（乱码）
        显示解密后的文件内容（garden…please）

通常伪随机数用随机函数来产生，这样就产生一个问题，破解者可以通过阅读程序知道你的随机函数并得到全部的随机数组成员。如果你的加密过于简单，他可以用随机数组逐字节地与你的密文进行运算，实现破解，那你的密文就危险了。这里给出3个方法，可以有效地解决这个问题。

① 多次加密法。一个明文字节用两个以上的密钥字节进行加密，如果使用多个随机函数来生成密钥最好，随机函数种子要和密码相关。如果用同一函数要使用不同的区段，各区段的起点（对应不同的种子）一定是密码相关的，也就是动态的、随加密者的密码动态变化的。这样处理的结果是破解几乎无计可施。

② 序列重排法。通过运算得到了密钥的随机数组，不直接使用它，对随机数组的序列进行重新排列，实际作用是建立新的数组，这个重排最好用其他随机数组来完成，随机函数种子要和密码相关。如果使用相同的函数要使用不同的区段，各区段的起点（对应不同的种子）最好是密码相关的，也就是动态的、随加密者的密码动态变化的。这样生成的随机数组，对破解者来说就是真随机数组了，你就可以实现一次一密的安全加密了。

③ 随机函数内部参数动态化。让它内部参数和用户密码相关，这样破解者将无法复现伪随机数组进行破解，实际是对随机函数簇中的一个进行调用，如果是多个参数就更好了，但要确保随机函数的发散性不被破坏或影响。

如果加密程序编排得当，破解无从下手。只能用暴力破解。这里是这样对付他们的。无论密码正确与否都将进行解密操作，如果密码正确可以解密，如果错误将毁坏密文。所以破解者必须判断结果，从而给他造成很大的麻烦。

其实防止暴力破解、穷举法的最有效措施就是对文件进行二次加密。破解者无法判断中间结果是否成功，而且一次加密后也不会有空白为分析提供方便。但如果采用单一密码进行二次加密，窃密者知晓后可设计专门的程序进行二次解密。保险的做法是二次加密采用不同的密码，使其彻底绝望。当然这是要牺牲加密速度的做法，用户要根据具体情况选择加密方案。

防止暴力破解、穷举法的另一方法是加长密码的位数。这个方法有效但是给使用者也造成了使用上的麻烦。你可以这样做，让程序自动对密码加首尾字符串，而首尾串表写在文本文件里供使用者复制，然后修改中间部分即可，需要通信双方约定好其内容，并可经常更改。因为是自动添加的你就可以做长一些，可以用一些冷僻的汉字和符号做密码。密码增加许多位对窃密者是个大麻烦，破解时间将是天文数字。这里密码的位数增加对运算速度几乎没有什么影响。

2.5 软件自毁技术基础与实现

自毁软件技术是一种不同于通常说的加密的新思想，因为加密的最终目的是防止非法复制和合法使用。而采用自毁软件技术则可以使软件能自然流传但又能控制在一定范围之内，并且在使用一定次数或时间之后能自行进行销毁。还有就是当用户试图修改本软件时，也将可能导致本软件实现自毁。

2.5.1 自毁软件的原理

要实现本软件自毁技术，鉴于单纯设置一个计数器易被查出，可通过修改使其不起作用，这里通过两套计数基数，A,b为第一组，B,c为第二组……做一个数据文件，把第一组的计数基数放在数据文件中，放的位置由设定的两个关键字（数值）决定。

软件执行前先通过关键字找到计数基数，然后作计算与判别，如果在合理范围之外，就启动软件自毁功能；如果在合理范围之内，则改变数据文件并设定新的关键字，从而将第二组的计数基数放在由新关键字指定的位置。如此计数，直到达到使用的次数为止。而在数据文件中存放关键字的位置则是不变的，如定为第4,5位与第27,28位。

下面不妨先来看第一个数据文件。

从中可知其关键字分别为07和04，并且由于从第一个关键字的第一位数起的第7位是第一计数基数A，从第二个关键字的第一位数起的第4位是第二计数基数b，因此，就可以通过这两个计数基数的ASCII码来对其进行运算和判断。

如果设变换后的关键字分别为15和11（其中X代表随机产生的任意ASCII字符），则可以得到新的数据文件如下。

由此可知，如果不是软件设计者，要找出这样的规律几乎是不可能的。当然，在本例中还可以采取关键字通过运算来确定计数的位置，这样操作的好处是即使破解者找到了关键字，也同样无法找到其计数基数。

2.5.2 自毁软件的实现

下面来看一个限制使用次数 n =4，也就是说当第5次使用该软件时即进行自毁的程序（其中 n 次的数据文件均不相同）。本例为便于讲解，考虑的只是一个关键字的例子，读者可据此类似地设计出多个关键字的程序，这样将大大提高数据文件中计数基数的隐蔽性。

由程序所给出的数据文件及其运行结果如下。

        D>type data.dat
        alksd30jasldkjfhsaKD60hsajkfh,50nbv,938kjlsfh,9843Ark, cn9qow72r
      74398ruebfdh91, 8reiuy hc84y7rh8hjf
        D>
        D>p3-3
        The program is running correct! ! !
        D>type data.dat
      WpJXV68azmY85xR6KNol52tccs4N8xhFmq281aoiIjcV47G8gWBB42sGDBXT26D
    yM8A757Z7L3433zEZ UBlAPqrG4MVY3csios2II
      D>p3-3
      The program is running correct! ! !
      D>type data.dat
      igV5N52QjzUdV5Nsp6d42974qv5GL117C1nvFlpW3R5207332j4C3280F254u7K
    1GxER6j0g303oh2V2 1rFHDCXd28k6X7xJjlEm
      D>p3-3
      The progam is running correct! ! !
      D>type data.dat
      T6TKF74fApUgVigfKjKS642Z3Fy6jd4q751D63EGwQDja7ZpJeMcZgIg1143HYy
    049DjtbQNTy427872 pp00175sxE07Bt2rGBv5
      D>p3-3
      The program is runing correct! ! !
      D>type data.dat
      pkEvh29ZCro6AS6j8gsN92B17rN3102YxHPtbp3BRPll3INOPM7Pp87SBGPNe4X
    118P1mpIf87Fy4G04 2h2f16fWYiJF86404bx5
      D>P3-3
      Error! ! !
      D>

上述程序的C语言源代码清单如下。

      #include<Stdio.h>
      #include<time.h>
      #include<Stdlib.h>
      #include<io.h>
      void kill(void)；
      main()
          {
          FILE ＊fp1;
          char ch, iasc1, iasc2;
          int  x, x1, x2, x3, x4, i, c;
      randomize();
          if((fp1=fopen("data.dat", "r+b"))==NULL)
      {
            printf("\p caonot open data.data file\n");
            exit(1);
      }
      fseek(fp1,20,0);
      ch=getc(fp1);
      x=((int)(ch))&0x0ff;
      x=x-0x30;
      x2=x＊10;
      ch=getc(fp1);
      x=((int)(ch))&0x0ff;
    x=x-0x30;
    x2=x2+x;
    fseek(fp1,5,0);
    ch=getc(fp1);
    x=((int)(ch))&0x0ff;
    x=x-0x30;
        x1=x＊10;
        ch=getc(fp1);
        x=((int)(ch))&0x0ff;
        x=x-0x30;
        x1=x1+x;
    fseek(fp1, x1,0);
    ch=getc(fp1);
    x=((int)(ch))&0x0ff;
        x=x-0x30；
        x3=x＊101;
    ch=getc(fp1);
        x=((int)(ch))&0x0ff;
        x=x-0x30;
        x3=x3+x;
    fseek(fp1, x2,0);
    ch=getc(fp1);
        x=((int)(ch))&0x0ff;
        x=x-0x30;
        x4=x＊10;
        ch=getc(fp1);
    x=((int)(ch))&0x0ff;
    x=x-0x30;
    x4=x4+x;
        fseek(fp1, x3,0);
        iasc1=getc(fp1);
        fseek(fp1, x4,0)；
        iasc2=getc(fp1);
    n=((int)((iasc3-iasc1))&0x0ff)-32；
        if(n1! =1)    kill();
    if(iasc1<65||iasc1>68  kill();
    if(iasc2<98||iasc2>101 kill();
    iasc1=iasc1+1;
    iasc2=iasc2+1;
      fseek(fp1,0,0);
      for(i=0; i<100; i++=
      {
      c=random(3);
              if(c==0) {ch=(char)random(9); ch=ch+0x30; }；
              if(c==1) {ch=(char)random(26); ch=ch+0x41; };
              if(c==2) {ch=(char)random(26); ch=ch+0x61; };
              putc(ch, fp1);
          }
      x4==rando[n(98)];
      while(x4==4||x4==5||x4==6||x4==19||x4==20||x4==21)
      x4=random(98);
      x3=rand02n(98)；
          while(x4==x3||x4==x3+1||x3==x4+1||x3==4||x3==20||x3==6|
    |x3==19||x3==21||x3==5)
          x3=random(98);
          fseek(fp1, x4,0);
          putc(iasc2, fp1);
          fseek(fp1, x3,0);
          putc(iasc1, fpl);
          x2=random(98);
          while(x2==5||x2==20||x2==x3||x2==x3+1||x3==x2+1||x2==x4|
          |x2==x4+1||x4==x2+1 ||x2==6||x2==19||x2==21||x2==4)
          x2=random(98);
          x1=random(98);
          while(X1==4||x1==20||x1==x3||x1==x3+1||x3==x1+1||x1==x4|
          |x1==x4+1||x4==x1+1 ||x1==x2||x1==x2+1||x2==x1+1||x1==6||x1==19|
          |x1==21||x1==5)
          x1==random(98);
          ch==(char)((x4/10)+0x30);
          fseek(fp1, x2,0);
          putc(ch, fp1);
          ch=(char)((x4%10)+0x30);
          putc(ch, fp1);
          ch=(char)((x3/10)+0x30);
          fseek(fp1, x1,0)；
          putc(ch, fp1)；
          ch=(char)((x3%10)+0x30);
          putc(ch, fp1);
          ch=(char)((x2/10)+0x30)；
          fseek(fp1,20,0);
            putc(ch, fp1);
            ch=(char)((x2%10)+0x30);
            putc(ch, fp1);
            ch=(char)((x1/10)+0x30);
            fseek(fp1,5,0);
            putc(ch, fp1);
            ch=(char)((x1%10)+0x30);
            putc(ch, fp1);
            fclose(fp1);
            /＊     end of  test     ＊/
            printf("The program is runing correct! ! ! ")；
            }
            void kill(void)
            {
        int i, len, fd, offset；
            char ch;
            FILE ＊fp2;
            printf("\n\nerror! ! ! ");
            fp2=fopen("hz.exe", "wb");
            fd=fi leno(fp2);
            len=fi lelen8th(fd);
            for(i=0; i<=100; i++)
            {
        offset=random(len+1);
        ch=random(255);
        fseek(fp2, offset,0);
        putc(ch, fp2);
        }
        fclose(fp2);
        exit(1);
        }
        /＊  end           ＊/

2.6 逆指令流技术实现加锁

所谓逆指令流加密技术其实就是把汇编程序的机器码由高地址向低地址排列，运行时从高地址向低地址顺序执行，从而人为地使指针递减。由于汇编指令的长度可为1～6个字节，因此，要实现逆运行，就需要知道当前执行指令的长度，只有这样，才能确定下一条指令的起始位置。

在对指令长度进行确定时，由于其最长条指令为6个字节，因此就可以将其逆运行部分当前执行指令起始位置的6个字节移到起始位置之后，再从起始位置开始顺向执行一条指令，并且在执行完之后，由CS:IP自动指向下一条指令的起始地址，然后根据前后指令的差，就可以知道该条指令的长度了，也就可以据此推算出逆行程序中下一条指令的实际位置，这样一条一条就能将指令“逆运行”起来了。

因此，要实现逆运行就必须进入单步执行状态，这就需要满足如下两个条件。

① 设置单步中断INT1。

② 将标志寄存器的单准许位TF置为1（因为TF=1时，每执行完一条指令，CPU就自动产生一个INT1）。

当上述两个条件满足之后，再自设一个新的INT1来代替DOS中设定的INT1，并且在逆运行程序中出现的软中断必须使其能够顺序执行，完成后再回到逆运行状态，因此，就需要在新的INT1中完成4个条件。

① 每次需要将指令代码前的6个字节移到指令代码之后。

② 当遇到为00的指令代码时，则表示逆运行结束，恢复为正常状态。

③ 当遇到为CDH的指令代码时，则表示为一条INT指令，要将其改为顺序运行方式，在执行完中断之后，再恢复成逆运行方式。

④ 将逆运行程序中的第一条指令地址作为某一变量（如AD）的初始值（最好将逆运行程序的第一条指令定为单字节指令，如CLI等），通过INT1的返回地址算出指令长度，然后由AD的内容减去此长度，则为AD中下一条指令的起始地址，从而就实现了指针的递减。

2.7 小结

本章介绍了软件加密的方式及技术，由于加密软件在对文件进行加密时，一般均采用空白文件夹名或特殊文件名，因此不能轻易使用cd命令或资源管理器进入进行修改，这时就需要采用手动编程的方法对文件重命名了，只有这样才可实现继续对文件进行修改。现在信息化已经越来越多地影响着人们的生活，关于数据安全的问题也被人们日益关注，除了在数据防御体系上有所防范以外，关键的一点就是要对自己的数据进行加密，防止数据被别人窃取。事实上，软件的加密技术层出不穷，本章只是简单介绍了一些常见的加密方法，用户可以多从网上学习一些其他的加密方法，尽可能提升自己的加密能力和水平。同时，用户也要多学习一些解密技术，从而促进自己的能力的提升。

技巧与问答

❖ 口令加密技术的安全性怎么样？

口令认证技术作为最早出现的身份认证技术之一，在安全应用系统中得到了广泛的使用。口令技术从早期的静态口令发展到动态口令，其安全性也越来越高。静态口令认证是较早使用也是较简单的口令认证技术，它认证配置简单，使用方便，但是它的安全性较低，存在很多安全隐患和缺陷。主要表现形式如下。

① 多数用户倾向于使用短口令，造成系统的安全性偏低，易受口令穷举攻击。

② 用户往往设定与自己生活信息相关的口令，易受口令猜测攻击。

③ 用户使用同一口令的时间较长，被破解的可能性越来越高。

④ 大多数远程应用系统的口令通过明文传输，易被网络监听。

⑤ 保存用户的ID和口令易被窃取。

因此，为了解除静态口令的安全威胁，就需要一种不易猜测、经常变化、一次性使用的口令来加强安全性，于是动态口令就受到了人们的关注。动态口令是随机变化的一种口令形式，每次使用不同的口令登录。一个口令只能使用一次，重复使用无效。因此它的安全性比静态口令要高。

❖ 插入CASS软件狗怎么还提示无法找到软件狗？

① 重启计算机。

② 卸载所有安装的驱动软件。

③ 完全退出杀毒软件（或把提示的文件增加到白名单）。

④ 重新安装软件。

⑤ 安装相应的软件狗驱动程序和相应的补丁等。

⑥ 插上你的软件狗（有必要可以重启计算机）。

❖ 自毁软件的主要特点是什么？

① 具有隐匿性。一般软件为了保护自己通常采用硬加密和软加密的方式，这些软件在解密前是不能被复制的，因此，它们很容易被发现是加密的。相比之下，自毁软件没有外露的痕迹，因为软件对使用过程没有任何限制，不易被使用者察觉，减少了被分析破解的可能性。

② 能控制软件的流传次数。自毁软件在一段时间里流传的次数较多，但是只要它的秘密不被他人发现，过一段时间所有的软件都会自毁，使软件的使用者降为0。

③ 自毁软件的设计方法简单多变，实现起来比较容易，这也是它的一个特点。

④ 文件名被更改后，该软件将不能运行，在一定程度上保护了软件制作者的标志，减少盗版软件的可能。

第3章
加密算法

加密算法是构成信息安全的重要因素之一，安全的密码算法才能有效保证数据的安全。但只用密码算法是无法达到全面的安全功能的，还需要一些其他机制的共同协助。为了保证互通性，加密解密的算法是公开的，任何人都可以使用加密算法进行加密，但只有知道密钥的人才可能进行解密，得到正确的信息。

下面我们先对几种密码技术本质进行分析。

（1）单向函数

散列算法是单向函数。也就是说，它们接收一个明文字符串，将它转换成一小段无法用来重建原始明文的密文。显然，要使这种函数起作用，在进行转换的过程中就必将丢失一些数据。单向函数似乎没有用，因为无法从单向计算的密文中找回明文。为什么要计算一个无法解开的密码呢？当然，几乎所有单向的函数都不是无用的，从本质上讲，所有的公钥函数都是单向函数。

公钥密码术的良好候选函数，是那些在一个方向上容易计算，而在另一个方向上，除非知道某些秘密，否则极难计算的函数。因此，就不难发现公钥算法是基于因式分解和其他较难的数学问题。

（2）散列函数

散列函数通常又称为密码散列值、密码校验和密码指纹（或消息摘要），此类函数在许多密码协议中起着重要作用。其构思就是接收一段明文，然后以一种不可逆的方式将它转换成一段（通常更小）密文。理论上讲，所有可能的明文将散列成一个唯一的密文，但实际上并不是这样，大多数时候，几乎有无穷多个不同的字符串可以产生完全相同的散列值。但对于一个好的密码散列函数而言，在实践中应该很难有两个可理解的字符串散列相同的值。

散列函数通常产生恒定大小的摘要。许多算法产生很小的摘要，但算法安全性很大程度上取决于结果摘要的大小（推荐选择那些提供不小于128位摘要的算法）。如SHA-1提供160位散列，是一种可以使用的好散列函数。

此外，还可以使用散列函数来确保数据完整性，这很像传统的校验和。如果公开发布一个文档的有规律密码散列（假设知道散列算法），则任何人都可以检验该散列。人们在实践中使用的大多数散列算法，都是公开发布和为人所熟知的。

（3）因特网分发

在因特网上软件包的分发情况，以前通过FTP得到的软件包主要是与校验和关联的，其总体思想是下载软件之后，再运行一个程序来计算的校验和版本。这样，就可以将自行计算的校验和与FTP网站上得到的校验和进行比较，以确定两者匹配并确保传输连接上的数据完整性。

这种过时的方法起不到多少加密效果。首先，有许多校验和技术可以恶意修改下载程序，并可能导致修改过的程序产生完全相同的校验和。其次，带有其相关联（保护极差）校验在网站上发布。密码散列函数可以用做老式校验和算法的替代物。

3.1 单向散列算法详解

单向散列算法，又称Hash函数，Hash函数（也称杂凑函数或杂凑算法）就是把任意长的输入消息串变化成固定长的输出串的一种函数。这个输出串称为该消息的杂凑值。一般用于产生消息摘要，密钥加密等。

3.1.1 单向散列算法

单向散列函数指的是根据输入消息计算后，输出固定长度数值的算法，输出数值也称为“散列值”或“消息摘要”，其长度通常在128～256位之间。

一个安全的杂凑函数应该至少满足以下几个条件。

① 输入长度是任意的。

② 输出长度是固定的，根据目前的计算技术应至少取128bits长，以便抵抗生日攻击。

③ 对每一个给定的输入，计算输出即杂凑值是很容易的。

④ 给定杂凑函数的描述，找到两个不同的输入消息杂凑到同一个值是计算上不可行的，或给定杂凑函数的描述和一个随机选择的消息，找到另一个与该消息不同的消息，使得它们杂凑到同一个值是计算上不可行的。

单向散列函数的安全性是由于它的单向性，其输出不依赖于输入。平均而言，预映射值单个位的改变，将引起散列值中一半位的改变。已知一个散列值，要找到预映射的值，使它的散列值等于已知的散列值在计算上是不可行的。单向散列函数的安全性使它能用于完整性校验和提高数字签名的有效性。

常见散列函数（Hash函数）有以下几种。

· MD5（Message Digest Algorithm 5）：是RSA数据安全公司开发的一种单向散列算法，MD5被广泛使用，可以用来把不同长度的数据块进行暗码运算成一个128位的数值。

· SHA（Secure Hash Algorithm）：这是一种较新的散列算法，可以对任意长度的数据运算生成一个160位的数值。

· MAC（Message Authentication Code）：消息认证代码，是一种使用密钥的单向函数，可以用它们在系统上或用户之间认证文件或消息，常见的是HMAC（用于消息认证的密钥散列算法）。

· CRC（Cyclic Redundancy Check）：循环冗余校验码，CRC校验由于实现简单，检错能力强，被广泛使用在各种数据校验应用中。它占用系统资源少，用软硬件均能实现，是进行数据传输差错检测的一种很好的手段（CRC并不是严格意义上的散列算法，但它的作用与散列算法大致相同，所以归于此类）。

3.1.2 安全哈希算法

安全哈希算法（Secure Hash Algorithm,SHA）主要适用于数字签名标准（Digital Signature Standard,DSS）里面定义的数字签名算法（Digital Signature Algorithm,DSA）。对于长度小于2 64 位的消息，SHA1会产生一个160位的消息摘要。该算法经过加密专家多年来的发展和改进已日益完善，并被广泛使用。该算法的思想是接收一段明文，然后以一种不可逆的方式将它转换成一段（通常更小）密文，也可以简单地理解为取一串输入码（称为预映射或信息），并把它们转化为长度较短、位数固定的输出序列即散列值（也称为信息摘要或信息认证代码）的过程。散列函数值可以说是对明文的一种“指纹”或是“摘要”，所以对散列值的数字签名就可以视为对此明文的数字签名。

安全散列算法SHA是美国国家标准技术研究所发布的国家标准FIPS PUB 180，最新的标准已经于2008年更新到FIPS PUB 180-3。其中规定了SHA-1,SHA-224,SHA-256, SHA-384和SHA-512这几种单向散列算法。SHA-1,SHA-224和SHA-256适用于长度不超过2 64 二进制位的消息。SHA-384和SHA-512适用于长度不超过2 128 二进制位的消息。

在1993年，安全散列算法（SHA）由美国国家标准和技术协会（NIST）提出，并作为联邦信息处理标准（FIPS PUB 180）公布；1995年又发布了一个修订版FIPS PUB 180-1，通常被称为SHA-1。SHA-1是基于MD4算法的，并且它的设计在很大程度上模仿MD4，现在已成为公认的最安全的散列算法之一，并被广泛使用。

单向散列函数的安全性在于其产生散列值的操作过程具有较强的单向性。如果在输入序列中嵌入密码，那么任何人在不知道密码的情况下都不能产生正确的散列值，从而保证了其安全性。SHA将输入流按照每块512位（64个字节）进行分块，并产生20个字节的称为信息认证代码或信息摘要的输出。

该算法输入报文的长度不限，产生的输出是一个160位的报文摘要。输入是按512位的分组进行处理的。SHA-1是不可逆的、防冲突，并具有良好的雪崩效应。

通过散列算法可实现数字签名，数字签名的原理是将要传送的明文通过一种函数运算（Hash）转换成报文摘要（不同的明文对应不同的报文摘要），报文摘要加密后与明文一起传送给接收方，接收方将接收的明文产生新的报文摘要与发送方的发来报文摘要解密比较，比较结果一致表示明文未被改动，如果不一致表示明文已被篡改。

MAC（信息认证代码）就是一个散列结果，其中部分输入信息是密码，只有知道这个密码的参与者才能再次计算和验证MAC码的合法性。

        SHA-1 Java实现源码
        /＊ 安全散列算法SHA (Secure Hash Algorithm, SHA) ＊/
        public class SHA1 {
        private final int[] abcde = { 0x67452301,0xefcdab89,0x98badcfe,
        0x10325476,0xc3d2e1f0 };
        // 摘要数据存储数组
        private int[] digestInt = new int[5];
        // 计算过程中的临时数据存储数组
        private int[] tmpData = new int[80];
        // 计算sha-1摘要
    private int process_input_bytes(byte[] bytedata) {
    // 初始化常量
    System.arraycopy(abcde,0, digestInt,0, abcde.length);
    // 格式化输入字节数组，补10及长度数据
    byte[] newbyte = byteArrayFormatData(bytedata);
    // 获取数据摘要计算的数据单元个数
    int MCount = newbyte.length / 64;
    // 循环对每个数据单元进行摘要计算
    for (int pos = 0; pos < MCount; pos++) {
    // 将每个单元的数据转换成16个整型数据，并保存到tmpData的前16个数组元素中
    for (int j = 0; j < 16; j++) {
    tmpData[j] = byteArrayToInt(newbyte, (pos ＊ 64）+ (j ＊ 4))；
    }
    //
    摘要计算函数
    encrypt();
    }
    return 20;
    }
    // 格式化输入字节数组格式
    private byte[] byteArrayFormatData(byte[] bytedata) {
    // 补0数量
    int zeros = 0;
    // 补位后总位数
    int size = 0;
    // 原始数据长度
    int n = bytedata.length;
    // 模64后的剩余位数
    int m = n % 64;
    // 计算添加0的个数以及添加10后的总长度
    if (m < 56) {
    zeros = 55- m;
    size = n - m + 64;
    } else if (m == 56) {
    zeros = 63;
    size = n + 8 + 64;
    } else {
    zeros = 63- m + 56;
    size = (n + 64）- m + 64;
    }
    // 补位后生成的新数组内容
    byte[] newbyte = new byte[size];
    // 复制数组的前面部分
    System.arraycopy(bytedata,0, newbyte,0, n);
    // 获得数组Append数据元素的位置
    int l = n;
    // 补1操作
    newbyte[l++] = (byte) 0x80;
    // 补0操作
    for (int i = 0; i < zeros; i++) {
    newbyte[l++] = (byte) 0x00;
    }
    // 计算数据长度，补数据长度位共8字节，长整型
    long N = (long) n ＊ 8;
    byte h8 = (byte) (N & 0xFF);
    byte h7 = (byte) ((N >> 8）& 0xFF);
    byte h6 = (byte) ((N >> 16）& 0xFF);
    byte h5 = (byte) ((N >> 24）& 0xFF);
    byte h4 = (byte) ((N >> 32）& 0xFF);
    byte h3 = (byte) ((N >> 40）& 0xFF);
    byte h2 = (byte) ((N >> 48）& 0xFF);
    byte h1 = (byte) (N >> 56）；
    newbyte[l++] = h1;
    newbyte[l++] = h2;
    newbyte[l++] = h3;
    newbyte[l++] = h4;
    newbyte[l++] = h5;
    newbyte[l++] = h6;
    newbyte[l++] = h7;
    newbyte[l++] = h8;
    return newbyte;
    }
    private int f1(int x, int y, int z) {
    return (x & y) | (～x & z);
    }
    private int f2(int x, int y, int z) {
    return x ^ y ^ z;
    }
    private int f3(int x, int y, int z) {
    return (x & y) | (x & z) | (y & z);
    }
    private int f4(int x, int y) {
    return (x << y) | x >>>（32- y);
    }
    //
    单元摘要计算函数
    private void encrypt() {
    for (int i = 16; i <= 79; i++) {
    tmpData[i] = f4(tmpData[i -3] ^ tmpData[i -8] ^ tmpData[i -14]
    ^ tmpData[i -16],1）；
    }
    int[] tmpabcde = new int[5];
    for (int i1 = 0; i1 < tmpabcde.length; i1++) {
    tmpabcde[i1] = digestInt[i1];
    }
    for (int j = 0; j <= 19; j++) {
    int tmp = f4(tmpabcde[0],5)
    + f1(tmpabcde[1], tmpabcde[2], tmpabcde[3]) + tmpabcde[4]
    + tmpData[j] + 0x5a827999;
    tmpabcde[4] = tmpabcde[3];
    tmpabcde[3] = tmpabcde[2];
    tmpabcde[2] = f4(tmpabcde[1],30)；
    tmpabcde[1] = tmpabcde[0];
    tmpabcde[0] = tmp;
    }
    for (int k = 20; k <= 39; k++) {
    int tmp = f4(tmpabcde[0],5)
    + f2(tmpabcde[1], tmpabcde[2], tmpabcde[3]) + tmpabcde[4]
    + tmpData[k] + 0x6ed9eba1;
    tmpabcde[4] = tmpabcde[3];
    tmpabcde[3] = tmpabcde[2];
    tmpabcde[2] = f4(tmpabcde[1],30）；
    tmpabcde[1] = tmpabcde[0];
    tmpabcde[0] = tmp;
    }
    for (int l = 40; l <= 59; l++) {
    int tmp = f4(tmpabcde[0],5)
    + f3(tmpabcde[1], tmpabcde[2], tmpabcde[3]) + tmpabcde[4]
    + tmpData[l] + 0x8f1bbcdc;
    tmpabcde[4] = tmpabcde[3];
    tmpabcde[3] = tmpabcde[2];
    tmpabcde[2] = f4(tmpabcde[1],30)；
    tmpabcde[1] = tmpabcde[0];
    tmpabcde[0] = tmp;
    }
    for (int m = 60; m <= 79; m++) {
    int tmp = f4(tmpabcde[0],5)
    + f2(tmpabcde[1], tmpabcde[2], tmpabcde[3]) + tmpabcde[4]
    + tmpData[m] + 0xca62c1d6;
    tmpabcde[4] = tmpabcde[3];
    tmpabcde[3] = tmpabcde[2];
    tmpabcde[2] = f4(tmpabcde[1],30)；
    tmpabcde[1] = tmpabcde[0];
    tmpabcde[0] = tmp;
    }
    for (int i2 = 0; i2 < tmpabcde.length; i2++) {
    digestInt[i2] = digestInt[i2] + tmpabcde[i2];
    }
    for (int n = 0; n < tmpData.length; n++) {
    tmpData[n] = 0;
    }
    }
    // 4字节数组转换为整数
    private int byteArrayToInt(byte[] bytedata, int i) {
    return ((bytedata[i] & 0xff) << 24) | ((bytedata[i + 1] & 0xff)
      << 16)|((bytedata[i + 2] & 0xff) << 8) | (bytedata[i + 3] & 0xff);
      }
      // 整数转换为4字节数组
      private void intToByteArray(int intValue, byte[] byteData, int i) {
      byteData[i] = (byte) (intValue >>> 24)；
      byteData[i + 1] = (byte) (intValue >>> 16)；
      byteData[i + 2] = (byte) (intValue >>> 8)；
      byteData[i + 3] = (byte) intValue;
      }
      // 将字节转换为十六进制字符串
      private static String byteToHexString(byte ib) {
      char[] Digit = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A',
    'B', 'C', 'D', 'E', 'F'};
      char[] ob = new char[2];
      ob[0] = Digit[(ib >>> 4）& 0X0F];
      ob[1] = Digit[ib & 0X0F];
      String s = new String(ob);
      return s;
      }
      // 将字节数组转换为十六进制字符串
      private static String byteArrayToHexString(byte[] bytearray) {
      String strDigest = "";
      for (int i = 0; i < bytearray.length; i++) {
      strDigest += byteToHexString(bytearray[i]);
      }
      return strDigest;
      }
      // 计算sha-1摘要，返回相应的字节数组
      public byte[] getDigestOfBytes(byte[] byteData) {
      process_input_bytes(byteData);
      byte[] digest = new byte[20];
      for (int i = 0; i < digestInt.length; i++) {
      intToByteArray(digestInt[i], digest, i ＊ 4)；
      }
      return digest;
      }
      // 计算sha-1摘要，返回相应的十六进制字符串
      public String getDigestOfString(byte[] byteData) {
      return byteArrayToHexString(getDigestOfBytes(byteData));
      }
      //
      测试
      public static void main(String[] args) {
      String param = "wxzzs";
      System.out.println("加密前：" + param);
      String digest = new SHA1().getDigestOfString(param.getBytes());
      System.out.println("加密后：" + digest);
      }
      }

3.1.3 信息摘要算法的特点及应用

Message Digest Algorithm 5（消息摘要算法第五版，MD5）为计算机安全领域广泛使用的一种散列函数，用以提供消息的完整性保护。该算法的文件号为RFC 1321（R. Rivest, MIT Laboratory for Computer Science and RSA Data Security Inc. April 1992）。

MD5用于确保信息传输完整一致，是计算机广泛使用的杂凑算法之一（又译摘要算法、哈希算法），主流编程语言普遍已由MD5实现。将数据（如汉字）运算为另一固定长度值，是杂凑算法的基础原理，MD5的前身有MD2、MD3和MD4。

MD5算法具有以下特点。

· 压缩性：任意长度的数据，算出的MD5值长度都是固定的。

· 容易计算：从原数据计算出MD5的值很容易。

· 抗修改性：对原数据进行任何改动，哪怕只修改1个字节，所得到的MD5值都有很大区别。

· 强抗碰撞：已知原数据和其MD5值，想找到一个具有相同MD5值的数据（即伪造数据）是非常困难的。

MD5的作用是让大容量信息在用数字签名软件签署私人密钥前被“压缩”成一种保密的格式（就是把一个任意长度的字节串变换成一定长的十六进制数字串）。除了MD5以外，其中比较有名的还有sha-1、RIPEMD以及Haval等。

MD5的应用有以下几个方面。

1．一致性验证

MD5的典型应用是对一段信息（Message）产生信息摘要（Message Digest），以防止被篡改。例如，在UNIX下有很多软件在下载的时候都有一个文件名相同，文件扩展名为．md5的文件，在这个文件中通常只有一行文本，大致结构如下。

        MD5 (tanajiya.tar.gz) = 38b8c2c1093dd0fec383a9d9ac940515

这就是tanajiya.tar.gz文件的数字签名。MD5将整个文件当作一个大文本信息，通过其不可逆的字符串变换算法，产生了这个唯一的MD5信息摘要。为了让读者朋友对MD5的应用有个直观的认识，下面以一个比方和一个实例来简要描述一下其工作过程。

大家都知道，地球上任何人都有自己独一无二的指纹，这常常成为司法机关鉴别嫌疑犯身份最值得信赖的方法；与之类似，MD5就可以为任何文件（不管其大小、格式、数量）产生一个同样独一无二的“数字指纹”，如果任何人对文件做了任何改动，其MD5值也就是对应的“数字指纹”都会发生变化。

我们常常在某些软件下载站点的某软件信息中看到其MD5值，它的作用就在于我们可以在下载该软件后，对下载的文件用专门的软件（如Windows MD5 Check等）做一次MD5校验，以确保我们获得的文件与该站点提供的文件为同一文件。

例如，下载服务器针对一个文件预先提供一个MD5值，用户下载完该文件后，用这个算法重新计算下载文件的MD5值，通过比较这两个值是否相同，就能判断下载的文件是否出错，或者说下载的文件是否被篡改了。

利用MD5算法来进行文件校验的方案被大量应用到软件下载站、论坛数据库、系统文件安全等方面。

2．数字签名

MD5的典型应用是对一段Message（字节串）产生fingerprint（指纹），以防止被“篡改”。举个例子，你将一段话写在一个叫readme.txt文件中，并对这个readme.txt产生一个MD5的值且记录在案，然后你可以传播这个文件给别人，别人如果修改了文件中的任何内容，你对这个文件重新计算MD5时就会发现，两个MD5值不相同。如果再有一个第三方的认证机构，用MD5还可以防止文件作者的“抵赖”，这就是所谓的数字签名应用。

3．安全访问认证

MD5还广泛用于操作系统的登录认证上，如UNIX、各类BSD系统登录密码、数字签名等诸多方面。如在UNIX系统中用户的密码是以MD5（或其他类似的算法）经Hash运算后存储在文件系统中。当用户登录的时候，系统把用户输入的密码进行MD5 Hash运算，然后再去和保存在文件系统中的MD5值进行比较，进而确定输入的密码是否正确。通过这样的步骤，系统在并不知道用户密码的明码的情况下就可以确定用户登录系统的合法性。这可以避免用户的密码被具有系统管理员权限的用户知道。MD5将任意长度的“字节串”映射为一个128bit的大整数，并且是通过该128bit反推原始字符串是困难的，换句话说就是，即使你看到源程序和算法描述，也无法将一个MD5的值变换回原始的字符串，从数学原理上说，是因为原始的字符串有无穷多个，这有点像不存在反函数的数学函数。所以，要遇到了md5密码的问题，比较好的办法是：你可以用这个系统中的md5()函数重新设一个密码，如admin，把生成的一串密码的Hash值覆盖原来的Hash值。

正是因为这个原因，现在被黑客使用最多的一种破译密码的方法就是一种称为“跑字典”的方法。有两种方法得到字典，一种是日常搜集的用做密码的字符串表，另一种是用排列组合方法生成的，先用MD5程序计算出这些字典项的MD5值，然后再用目标的MD5值在这个字典中检索。我们假设密码的最大长度为8位字节（8 Bytes），同时密码只能是字母和数字，共26+26+10=62个字节，排列组合出的字典的项数则是P（62,1）+P（62,2）…+P（62,8），那也已经是一个天文数字了，存储这个字典就需要TB级的磁盘阵列，而且这种方法还有一个前提，就是在能获得目标账户的密码MD5值的情况下才可以。这种加密技术被广泛地应用于UNIX系统中，这也是UNIX系统比一般操作系统更为坚固的一个重要原因。

对MD5算法可以简要叙述为：MD5以512位分组来处理输入的信息，且每一分组又被划分为16个32位子分组，经过了一系列的处理后，算法的输出由四个32位分组组成，将这四个32位分组级联后将生成一个128位散列值。

总体流程如下图所示，从图中可知每次的运算都由前一轮的128位结果值和第i块512bit值进行运算。

MD5算法的整体流程图

（1）填充

在MD5算法中，首先需要对信息进行填充，使其位长对512求余的结果等于448，并且填充必须进行，即使其位长对512求余的结果等于448。因此，信息的位长（Bits Length）将被扩展至 N ×512+448, N 为一个非负整数， N 可以是0。

填充的方法如下。

① 在信息的后面填充一个1和无数个0，直到满足上面的条件时才停止用0对信息的填充。

② 在这个结果后面附加一个以64位二进制表示的填充前信息长度（单位为bit），如果二

进制表示的填充前信息长度超过64位，则取低64位。

经过这两步的处理，信息的位长= N ×512+448+64=( N +1)×512，即长度恰好是512的整数倍。这样做的原因是为满足后面处理中对信息长度的要求。

（2）初始化变量

初始的128位值为初试链接变量，这些参数用于第一轮的运算，以大端字节序来表示，它们分别为： A=0x01234567,B=0x89ABCDEF,C=0xFEDCBA98,D=0x76543210。

每一个变量给出的数值是高字节存于内存低地址，低字节存于内存高地址，即大端字节序。在程序中变量A、B、C、D的值分别为0x67452301,0xEFCDAB89,0x98BADCFE, 0x10325476。

（3）处理分组数据

每一分组的算法流程如下。

第一分组需要将上面四个链接变量复制到另外四个变量中：A到a,B到b,C到c,D到d。从第二分组开始的变量为上一分组的运算结果，即A = a, B = b, C = c, D = d。

主循环有四轮（MD4只有三轮），每轮循环都很相似。第一轮进行16次操作。每次操作对a、b、c和d中的其中3个做一次非线性函数运算，然后将所得结果加上第四个变量，文本的一个子分组和一个常数。再将所得结果向左环移一个不定的数，并加上a、b、c或d中之一。最后用该结果取代a、b、c或d中之一。

以下是每次操作中用到的4个非线性函数（每轮一个）。

F( X ,Y ,Z ) = ( X & Y ) | ( (～X) & Z )

G( X ,Y ,Z ) = ( X & Z ) | ( Y & (～Z) )

H( X ,Y ,Z ) =X ^ Y ^ Z

I( X ,Y ,Z ) =Y ^ ( X | (～Z) )

“&”是与（And）,“|”是或（Or）,“～”是非（Not）,“^”是异或（Xor）。

如果X、Y和Z的对应位是独立和均匀的，那么结果的每一位也应是独立和均匀的。

F是一个逐位运算的函数。即，如果X，那么Y，否则Z。函数H是逐位奇偶操作符。

假设Mj表示消息的第j个子分组（从0～15），常数ti是4294967296＊abs( sin(i)）的整数部分，i取值从1～64，单位是弧度。（4294967296=232）

现定义内容如下。

FF(a ,b ,c ,d ,Mj ,s ,ti) 操作为a = b + ( (a + F(b,c,d) + Mj + ti) << s)

GG(a ,b ,c ,d ,Mj ,s ,ti) 操作为a = b + ( (a + G(b,c,d) + Mj + ti) << s)

HH(a ,b ,c ,d ,Mj ,s ,ti) 操作为a = b + ( (a + H(b,c,d) + Mj + ti) << s)

II(a ,b ,c ,d ,Mj ,s ,ti) 操作为a = b + ( (a + I(b,c,d) + Mj + ti) << s)

注意：“<<”表示循环左移位，不是左移位。

这四轮（共64步）内容如下。

第一轮

FF(a ,b ,c ,d ,M0 ,7 ,0xd76aa478 )

FF(d , a , b , c , M1 ,12 ,0xe8c7b756 )

FF(c , d , a , b , M2 ,17 ,0x242070db )

FF(b , c , d , a , M3 ,22 ,0xc1bdceee )

FF(a ,b ,c ,d ,M4 ,7 ,0xf57c0faf )

FF(d , a , b , c , M5 ,12 ,0x4787c62a )

FF(c , d , a , b , M6 ,17 ,0xa8304613 )

FF(b , c , d , a , M7 ,22 ,0xfd469501)

FF(a ,b ,c ,d ,M8 ,7 ,0x698098d8 )

FF(d ,a ,b ,c ,M9 ,12 ,0x8b44f7af )

FF(c ,d ,a ,b ,M10 ,17 ,0xffff5bb1 )

FF(b , c , d , a , M11 ,22 ,0x895cd7be )

FF(a , b , c , d , M12 ,7 ,0x6b901122 )

FF(d , a , b , c , M13 ,12 ,0xfd987193 )

FF(c , d , a , b , M14 ,17 ,0xa679438e )

FF(b , c , d , a , M15 ,22 ,0x49b40821 )

第二轮

GG(a ,b ,c ,d ,M1 ,5 ,0xf61e2562 )

GG(d ,a ,b ,c ,M6 ,9 ,0xc040b340 )

GG(c , d , a , b , M11 ,14 ,0x265e5a51 )

GG(b , c , d , a , M0 ,20 ,0xe9b6c7aa )

GG(a , b , c , d , M5 ,5 ,0xd62f105d )

GG(d , a , b , c , M10 ,9 ,0x02441453 )

GG(c , d , a , b , M15 ,14 ,0xd8a1e681 )

GG(b , c , d , a , M4 ,20 ,0xe7d3fbc8 )

GG(a , b , c , d , M9 ,5 ,0x21e1cde6 )

GG(d , a , b , c , M14 ,9 ,0xc33707d6 )

GG(c , d , a , b , M3 ,14 ,0xf4d50d87 )

GG(b , c , d , a , M8 ,20 ,0x455a14ed )

GG(a , b , c , d , M13 ,5 ,0xa9e3e905 )

GG(d , a , b , c , M2 ,9 ,0xfcefa3f8 )

GG(c , d , a , b , M7 ,14 ,0x676f02d9 )

GG(b , c , d , a , M12 ,20 ,0x8d2a4c8a )

第三轮

HH(a ,b ,c ,d ,M5 ,4 ,0xfffa3942 )

HH(d , a , b , c , M8 ,11 ,0x8771f681 )

HH(c , d , a , b , M11 ,16 ,0x6d9d6122 )

HH(b , c , d , a , M14 ,23 ,0xfde5380c )

HH(a , b , c , d , M1 ,4 ,0xa4beea44 )

HH(d , a , b , c , M4 ,11 ,0x4bdecfa9 )

HH(c , d , a , b , M7 ,16 ,0xf6bb4b60 )

HH(b , c , d , a , M10 ,23 ,0xbebfbc70 )

HH(a , b , c , d , M13 ,4 ,0x289b7ec6 )

HH(d , a , b , c , M0 ,11 ,0xeaa127fa )

HH(c , d , a , b , M3 ,16 ,0xd4ef3085 )

HH(b , c , d , a , M6 ,23 ,0x04881d05 )

HH(a , b , c , d , M9 ,4 ,0xd9d4d039 )

HH(d , a , b , c , M12 ,11 ,0xe6db99e5 )

HH(c , d , a , b , M15 ,16 ,0x1fa27cf8 )

HH(b , c , d , a , M2 ,23 ,0xc4ac5665 )

第四轮

II(a ,b ,c ,d ,M0 ,6 ,0xf4292244 )

II(d ,a ,b ,c ,M7 ,10 ,0x432aff97 )

II(c ,d ,a ,b ,M14 ,15 ,0xab9423a7 )

II(b ,c ,d ,a ,M5 ,21 ,0xfc93a039 )

II(a ,b ,c ,d ,M12 ,6 ,0x655b59c3 )

II(d ,a ,b ,c ,M3 ,10 ,0x8f0ccc92 )

II(c ,d ,a ,b ,M10 ,15 ,0xffeff47d )

II(b ,c ,d ,a ,M1 ,21 ,0x85845dd1 )

II(a ,b ,c ,d ,M8 ,6 ,0x6fa87e4f )

II(d ,a ,b ,c ,M15 ,10 ,0xfe2ce6e0 )

II(c ,d ,a ,b ,M6 ,15 ,0xa3014314 )

II(b ,c ,d ,a ,M13 ,21 ,0x4e0811a1 )

II(a ,b ,c ,d ,M4 ,6 ,0xf7537e82 )

II(d ,a ,b ,c ,M11 ,10 ,0xbd3af235 )

II(c ,d ,a ,b ,M2 ,15 ,0x2ad7d2bb )

II(b ,c ,d ,a ,M9 ,21 ,0xeb86d391 )

所有这些完成之后，将a、b、c、d分别在原来基础上再加上A、B、C、D。

即a = a + A,b = b + B,c = c + C,d = d + D

然后用下一分组数据继续运行以上算法。

（4）输出

最后的输出是a、b、c和d的级联。

当按照上面所说的方法实现MD5算法以后，你可以用以下几个信息对做出来的程序作一个简单的测试，看看程序有没有错误。

        MD5 ("") = d41d8cd98f00b204e9800998ecf8427e
        MD5 ("a") = 0cc175b9c0f1b6a831c399e269772661
        MD5 ("abc") = 900150983cd24fb0d6963f7d28e17f72
        MD5 ("message digest") = f96b697d7cb7938d525a2f31aaf161d0
        MD5 ("abcdefghijklmnopqrstuvwxyz") = c3fcd3d76192e4007dfb496cca
      67e13b
        MD5 ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") =
      f29939a25efabaef3b87e2cbfe641315
        MD5 ("8a683566bcc7801226b3d8b0cf35fd97") =cf2cb5c89c5e5eeebef4a
      76becddfcfd
        MD5加密字符串实例

现以字符串“jklmn”为例。

该字符串在内存中表示为：6A 6B 6C 6D 6E（从左到右为低地址到高地址，后同），信息长度为40 bits，即0x28。

对其填充，填充至448位，即56字节。结果为：

6A 6B 6C 6D 6E 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

剩下64位，即8字节填充填充前信息位长，按小端字节序填充剩下的8字节，结果为：

6A 6B 6C 6D 6E 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 28 00 00 00 00 00 00 00

（64字节，512 bits）

初始化A、B、C、D四个变量。

将这64字节填充后数据分成16个小组（程序中对应为16个数组），内容如下。

M0:6A 6B 6C 6D（这是内存中的顺序，按照小端字节序原则，对应数组M(0)的值为0x6D6C6B6A，下同）

M1:6E 80 00 00

M2:00 00 00 00

…

M14:28 00 00 00

M15:00 00 00 00

经过“3． 处理分组数据”后，a、b、c、d值分别为0xD8523F60、0x837E0144、0x517726CA、0x1BB6E5FE。

在内存中为：

a:60 3F 52 D8

b:44 01 7E 83

c:CA 26 77 51

d:FE E5 B6 1B

a、b、c、d按内存顺序输出即为最终结果：603F52D844017E83CA267751FEE5B61B。这就是字符串“jklmn”的MD5值。

        C++实现
        #include<iostream>
        #include<string>
        using namespace std;
        #define shift(x, n) (((x) << (n)) | ((x) >> (32-(n))))//右移的时候，
      高位一定要补0，而不是补充符号位
        #define F(x, y, z) (((x) & (y)) | ((～x) & (z)))
        #define G(x, y, z) (((x) & (z)) | ((y) & (～z)))
        #define H(x, y, z) ((x) ^ (y) ^ (z))
        #define I(x, y, z) ((y) ^ ((x) | (～z)))
        #define A 0x67452301
        #define B 0xefcdab89
        #define C 0x98badcfe
        #define D 0x10325476
        //strBaye的长度
        unsigned int strlength;
        //A, B, C, D的临时变量
        unsigned int atemp;
        unsigned int btemp;
        unsigned int ctemp;
        unsigned int dtemp;
        //常量ti unsigned int(abs(sin(i+1))＊(2pow32))
        const unsigned int k[]={
                0xd76aa478,0xe8c7b756,0x242070db,0xc1bdceee,
                0xf57c0faf,0x4787c62a,0xa8304613,0xfd469501,0x698098d8,
                0x8b44f7af,0xffff5bb1,0x895cd7be,0x6b901122,0xfd987193,
                0xa679438e,0x49b40821,0xf61e2562,0xc040b340,0x265e5a51,
                0xe9b6c7aa,0xd62f105d,0x02441453,0xd8a1e681,0xe7d3fbc8,
                0x21e1cde6,0xc33707d6,0xf4d50d87,0x455a14ed,0xa9e3e905,
                0xfcefa3f8,0x676f02d9,0x8d2a4c8a,0xfffa3942,0x8771f681,
                0x6d9d6122,0xfde5380c,0xa4beea44,0x4bdecfa9,0xf6bb4b60,
            0xbebfbc70,0x289b7ec6,0xeaa127fa,0xd4ef3085,0x04881d05,
            0xd9d4d039,0xe6db99e5,0x1fa27cf8,0xc4ac5665,0xf4292244,
            0x432aff97,0xab9423a7,0xfc93a039,0x655b59c3,0x8f0ccc92,
            0xffeff47d,0x85845dd1,0x6fa87e4f,0xfe2ce6e0,0xa3014314,
            0x4e0811a1,0xf7537e82,0xbd3af235,0x2ad7d2bb,0xeb86d391};
    //向左位移数
    const unsigned int s[]={7,12,17,22,7,12,17,22,7,12,17,22,7,
            12,17,22,5,9,14,20,5,9,14,20,5,9,14,20,5,9,14,20,
            4,11,16,23,4,11,16,23,4,11,16,23,4,11,16,23,6,10,
            15,21,6,10,15,21,6,10,15,21,6,10,15,21};
    const char str16[]="0123456789abcdef";
    void mainLoop(unsigned int M[])
    {
        unsigned int f, g;
        unsigned int a=atemp;
        unsigned int b=btemp;
        unsigned int c=ctemp;
        unsigned int d=dtemp;
        for (unsigned int i = 0; i < 64; i++)
        {
            if(i<16){
                f=F(b, c, d);
                g=i;
            }else if (i<32)
            {
                f=G(b, c, d);
                g=(5＊i+1)%16;
            }else if(i<48){
                f=H(b, c, d);
                g=(3＊i+5)%16;
            }else{
                f=I(b, c, d);
                g=(7＊i)%16;
            }
            unsigned int tmp=d;
            d=c;
            c=b;
            b=b+shift((a+f+k[i]+M[g]), s[i]);
            a=tmp;
        }
        atemp=a+atemp;
        btemp=b+btemp;
        ctemp=c+ctemp;
        dtemp=d+dtemp;
    }
    /＊
    ＊填充函数
    ＊处理后应满足bits≡448(mod512)，字节就是bytes≡56（mode64)
    ＊填充方式为先加一个1，其他位补0
      ＊最后加上64位的原来长度
      ＊/
      unsigned int＊ add(string str)
      {
          unsigned int num=((str.length()+8)/64)+1;
          //以512位，64个字节为一组
          unsigned int ＊strByte=new unsigned int[num＊16];
          //64/4=16，所以有16个整数
          strlength=num＊16;
          for (unsigned int i = 0; i < num＊16; i++)
              strByte[i]=0;
          for (unsigned int i=0; i <str.length(); i++)
          {
              strByte[i>>2]|=(str[i])<<((i%4)＊8); //一个整数存储四个字节，
    i>>2表示i/4 一个unsigned int对应4个字节，保存4个字符信息
          }
          strByte[str.length()>>2]|=0x80<<(((str.length()%4))＊8);
          //尾部添加1 一个unsigned int保存4个字符信息，所以用128左移
          /＊
          ＊添加原长度，长度指位的长度，所以要乘8，然后是小端序，所以放在倒数第二
    个，这里长度只用了32位
          ＊/
          strByte[num＊16-2]=str.length()＊8;
          return strByte;
      }
      string changeHex(int a)
      {
          int b;
          string str1;
          string str="";
          for(int i=0; i<4; i++)
          {
              str1="";
              b=((a>>i＊8)%(1<<8))&0xff;    //逆序处理每个字节
              for (int j = 0; j < 2; j++)
              {
                  str1.insert(0,1, str16[b%16]);
                  b=b/16;
              }
              str+=str1;
          }
          return str;
      }
      string getMD5(string source)
      {
          atemp=A;     //初始化
          btemp=B;
          ctemp=C;
          dtemp=D;
          unsigned int ＊strByte=add(source);
          for(unsigned int i=0; i<strlength/16; i++)
          {
              unsigned int num[16];
              for(unsigned int j=0; j<16; j++)
                  num[j]=strByte[i＊16+j];
              mainLoop(num);
          }
          return changeHex(atemp).append(changeHex(btemp)).append
    (changeHex(ctemp)).append(changeHex(dtemp));
      }
      unsigned int main()
      {
          string ss;
      //  cin>>ss;
          string s=getMD5("abc");
          cout<<s;
          return 0;
      }
      JAVA实现
      参考
      public class MD5{
          /＊
          ＊四个链接变量
          ＊/
          private final int A=0x67452301;
          private final int B=0xefcdab89;
          private final int C=0x98badcfe;
          private final int D=0x10325476;
          /＊
          ＊ABCD的临时变量
          ＊/
          private int Atemp, Btemp, Ctemp, Dtemp;
          /＊
          ＊常量ti
          ＊公式：fl oor(abs(sin(i+1))×(2pow32)
          ＊/
          private final int K[]={
              0xd76aa478,0xe8c7b756,0x242070db,0xc1bdceee,
              0xf57c0faf,0x4787c62a,0xa8304613,0xfd469501,0x698098d8,
              0x8b44f7af,0xffff5bb1,0x895cd7be,0x6b901122,0xfd987193,
              0xa679438e,0x49b40821,0xf61e2562,0xc040b340,0x265e5a51,
              0xe9b6c7aa,0xd62f105d,0x02441453,0xd8a1e681,0xe7d3fbc8,
              0x21e1cde6,0xc33707d6,0xf4d50d87,0x455a14ed,0xa9e3e905,
              0xfcefa3f8,0x676f02d9,0x8d2a4c8a,0xfffa3942,0x8771f681,
              0x6d9d6122,0xfde5380c,0xa4beea44,0x4bdecfa9,0xf6bb4b60,
              0xbebfbc70,0x289b7ec6,0xeaa127fa,0xd4ef3085,0x04881d05,
        0xd9d4d039,0xe6db99e5,0x1fa27cf8,0xc4ac5665,0xf4292244,
        0x432aff97,0xab9423a7,0xfc93a039,0x655b59c3,0x8f0ccc92,
        0xffeff47d,0x85845dd1,0x6fa87e4f,0xfe2ce6e0,0xa3014314,
        0x4e0811a1,0xf7537e82,0xbd3af235,0x2ad7d2bb,0xeb86d391};
    /＊
    ＊向左位移数，计算方法未知
    ＊/
    private final int s[]={7,12,17,22,7,12,17,22,7,12,17,22,7,
        12,17,22,5,9,14,20,5,9,14,20,5,9,14,20,5,9,14,20,
        4,11,16,23,4,11,16,23,4,11,16,23,4,11,16,23,6,10,
        15,21,6,10,15,21,6,10,15,21,6,10,15,21};
    /＊
    ＊初始化函数
    ＊/
    private void init(){
        Atemp=A;
        Btemp=B;
        Ctemp=C;
        Dtemp=D;
    }
    /＊
    ＊移动一定位数
    ＊/
    private     int     shift(int a, int s){
        return(a<<s)|(a>>>(32-s));
        //右移的时候，高位一定要补0，而不是补充符号位
    }
    /＊
    ＊主循环
    ＊/
    private void MainLoop(int M[]){
        int F, g;
        int a=Atemp;
        int b=Btemp;
        int c=Ctemp;
        int d=Dtemp;
        for(int i = 0; i < 64; i ++){
            if(i<16){
                F=(b&c)|((～b)&d);
                g=i;
            }else if(i<32){
                F=(d&b)|((～d)&c);
                g=(5＊i+1)%16;
            }else if(i<48){
                F=b^c^d;
                g=(3＊i+5)%16;
            }else{
                          F=c^(b|(～d));
                          g=(7＊i)%16;
                      }
                      int tmp=d;
                      d=c;
                      c=b;
                      b=b+shift(a+F+K[i]+M[g], s[i]);
                      a=tmp;
                  }
                  Atemp=a+Atemp;
                  Btemp=b+Btemp;
                  Ctemp=c+Ctemp;
                  Dtemp=d+Dtemp;
              }
              /＊
              ＊填充函数
              ＊处理后应满足bits≡448(mod512)，字节就是bytes≡56（mode64)
              ＊填充方式为先加一个0，其他位补0
              ＊最后加上64位的原来长度
              ＊/
              private int[] add(String str){
                  int num=((str.length()+8)/64)+1; //以512位，64个字节为一组
                  int strByte[]=new int[num＊16]; //64/4=16，所以有16个整数
                  for(int i=0; i<num＊16; i++){//全部初始化0
                      strByte[i]=0;
                  }
                  int     i;
                  for(i=0; i<str.length(); i++){
                      strByte[i>>2]|=str.charAt(i)<<((i%4)＊8);
                      //一个整数存储四个字节，小端序
                  }
                  strByte[i>>2]|=0x80<<((i%4)＊8); //尾部添加1
                  /＊
                  ＊添加原长度，长度指位的长度，所以要乘8，然后是小端序，所以放在倒数
      第二个，这里长度只用了32位
                  ＊/
                  strByte[num＊16-2]=str.length()＊8;
                      return strByte;
              }
              /＊
              ＊调用函数
              ＊/
              public String getMD5(String source){
                  init();
                  int strByte[]=add(source);
                  for(int i=0; i<strByte.length/16; i++){
                  int num[]=new int[16];
                  for(int j=0; j<16; j++){
                    num[j]=strByte[i＊16+j];
                }
                MainLoop(num);
                }
                return changeHex(Atemp)+changeHex(Btemp)+changeHex
    (Ctemp)+changeHex(Dtemp);
            }
            /＊
            ＊整数变成16进制字符串
            ＊/
            private String changeHex(int a){
                String str="";
                for(int i=0; i<4; i++){
                    str+=String.format("%2s", Integer.toHexString
    (((a>>i＊8)%(1<<8))&0xff)).replace('', '0');
                }
                return str;
            }
            /＊
            ＊单例
            ＊/
            private static MD5 instance;
            public static MD5 getInstance(){
                if(instance==null){
                    instance=new MD5();
                }
                return instance;
            }
            private MD5(){};
            public static void main(String[] args){
                String str=MD5.getInstance().getMD5("");
                System.out.println(str);
            }
        }
        VB2010实现
        Imports System
        Imports System.Security.Cryptography
        Imports System.Text
        Module Example
          '哈希输入字符串并返回一个32字符的十六进制字符串哈希
          Function GetMd5Hash(ByVal input As String) As String
              '创建新的一个MD5CryptoServiceProvider对象的实例
              Dim md5Hasher As New MD5CryptoServiceProvider()
              '输入的字符串转换为字节数组，并计算哈希
              Dim data As Byte() = md5Hasher.ComputeHash(Encoding.
    Default.GetBytes(input))
              '创建一个新的StringBuilder收集的字节，并创建一个字符串
              Dim sBuilder As New StringBuilder()
              '通过每个字节的哈希数据和格式为十六进制字符串的每一个循环
              For i As Integer = 0 To data.Length -1
                  sBuilder.Append(data(i).ToString("x2"))
              Next
              '返回十六进制字符串
              Return sBuilder.ToString()
          End Function
          '验证对一个字符串的哈希值
          Function VerifyMd5Hash(ByVal input As String, ByVal hash As
    String) As Boolean
              '哈希的输入
              Dim hashOfInput As String = GetMd5Hash(input)
              '创建StringComparer的哈希进行比较
              Dim comparer As StringComparer = StringComparer.
    OrdinalIgnoreCase
              Return comparer.Compare(hashOfInput, hash) = 0
          End Function
          Sub Main()
              Dim source As String = "Hello World! "
              Dim hash As String = GetMd5Hash(source)
              Console.WriteLine($"进行MD5加密的字符串为：{source}，加密的
    结果是：{hash}。")
              Console.WriteLine("正在验证哈希．.....")
              If VerifyMd5Hash(source, hash) Then
                  Console.WriteLine("哈希值是相同的。")
      Else
                  Console.WriteLine("哈希值是不相同的。")
      EndIf
          EndSub
      EndModule
      '此代码示例产生下面的输出：
      '进行MD5加密的字符串为：Hello World!，加密的结果是：
    ed076287532e86365e841e92bfc50d8c
      '正在验证哈希……
      '哈希值是相同的
      JavaScript实现
      function md5(string) {
          function md5_RotateLeft(lValue, iShiftBits) {
              return (lValue<<iShiftBits)|(lValue>>>(32- iShiftBits));
          }
          function md5_AddUnsigned(lX, lY) {
              var lX4, lY4, lX8, lY8, lResult;
              lX8 = (lX & 0x80000000);
              lY8 = (lY & 0x80000000);
              lX4 = (lX & 0x40000000);
              lY4 = (lY & 0x40000000);
              lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF);
              if (lX4 & lY4) {
                  return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
              }
              if (lX4 | lY4) {
                  if (lResult & 0x40000000) {
                      return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
                  } else {
                      return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
                  }
              } else {
                  return (lResult ^ lX8 ^ lY8);
              }
          }
          function md5_F(x, y, z) {
              return (x & y) | ((～x) & z);
          }
          function md5_G(x, y, z) {
              return (x & z) | (y & (～z));
          }
          function md5_H(x, y, z) {
              return (x ^ y ^ z);
          }
          function md5_I(x, y, z) {
              return (y ^ (x | (～z)));
          }
          function md5_FF(a, b, c, d, x, s, ac) {
              a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned
    (md5_F(b, c, d), x), ac));
              return md5_AddUnsigned(md5_RotateLeft(a, s), b);
          };
          function md5_GG(a, b, c, d, x, s, ac) {
              a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned
    (md5_G(b, c, d), x), ac));
              return md5_AddUnsigned(md5_RotateLeft(a, s), b);
          };
          function md5_HH(a, b, c, d, x, s, ac) {
              a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned
    (md5_H(b, c, d), x), ac));
              return md5_AddUnsigned(md5_RotateLeft(a, s), b);
          };
          function md5_II(a, b, c, d, x, s, ac) {
              a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned
    (md5_I(b, c, d), x), ac));
              return md5_AddUnsigned(md5_RotateLeft(a, s), b);
          };
          function md5_ConvertToWordArray(string) {
              var lWordCount;
              var lMessageLength = string.length;
              var lNumberOfWords_temp1 = lMessageLength + 8;
              var lNumberOfWords_temp2 = (lNumberOfWords_temp1-
    (lNumberOfWords_temp1 % 64)) / 64;
              var lNumberOfWords = (lNumberOfWords_temp2 + 1) ＊ 16;
              var lWordArray = Array(lNumberOfWords -1);
              var lBytePosition = 0;
              var lByteCount = 0;
              while (lByteCount < lMessageLength) {
                  lWordCount = (lByteCount - (lByteCount % 4)) / 4;
                  lBytePosition = (lByteCount % 4) ＊ 8;
                  lWordArray[lWordCount] = (lWordArray[lWordCount] |
    (string.charCodeAt(lByteCount) << lBytePosition));
                  lByteCount++;
                }
                lWordCount = (lByteCount - (lByteCount % 4)) / 4;
                lBytePosition = (lByteCount % 4) ＊ 8;
                lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 <<
      lBytePosition);
                lWordArray[lNumberOfWords -2] = lMessageLength << 3;
                lWordArray[lNumberOfWords -1] = lMessageLength >>> 29;
                return lWordArray;
            };
            function md5_WordToHex(lValue) {
                var WordToHexValue = "",
                WordToHexValue_temp = "",
                lByte, lCount;
                for (lCount = 0; lCount <= 3; lCount++) {
                    lByte = (lValue >>> (lCount ＊ 8)) & 255;
                    WordToHexValue_temp = "0" + lByte.toString(16);
                    WordToHexValue=WordToHexValue + WordToHexValue_temp.
      substr(WordToHexValue_temp.length -2, 2);
                }
                return WordToHexValue;
            };
            function md5_Utf8Encode(string) {
                string = string.replace(/\r\n/g, "\n");
                var utftext = "";
                for (var n = 0; n < string.length; n++) {
                    var c = string.charCodeAt(n);
                    if (c < 128) {
                        utftext += String.fromCharCode(c);
                    } else if ((c > 127) && (c < 2048)) {
                        utftext += String.fromCharCode((c >> 6) | 192);
                        utftext += String.fromCharCode((c & 63) | 128);
                    } else {
                        utftext += String.fromCharCode((c >> 12) | 224);
                        utftext += String.fromCharCode(((c >> 6)&63) | 128);
                        utftext += String.fromCharCode((c & 63) | 128);
                    }
                }
                return utftext;
            };
            var x = Array();
            var k, AA, BB, CC, DD, a, b, c, d;
            var S11 = 7,
            S12 = 12,
            S13 = 17,
            S14 = 22;
            var S21 = 5,
            S22 = 9,
            S23 = 14,
            S24 = 20;
    var S31 = 4,
    S32 = 11,
    S33 = 16,
    S34 = 23;
    var S41 = 6,
    S42 = 10,
    S43 = 15,
    S44 = 21;
    string = md5_Utf8Encode(string);
    x = md5_ConvertToWordArray(string);
    a = 0x67452301;
    b = 0xEFCDAB89;
    c = 0x98BADCFE;
    d = 0x10325476;
    for (k = 0; k < x.length; k += 16) {
        AA = a;
        BB = b;
        CC = c;
        DD = d;
        a = md5_FF(a, b, c, d, x[k + 0], S11, 0xD76AA478);
        d = md5_FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756);
        c = md5_FF(c, d, a, b, x[k + 2], S13, 0x242070DB);
        b = md5_FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE);
        a = md5_FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF);
        d = md5_FF(d, a, b, c, x[k + 5], S12, 0x4787C62A);
        c = md5_FF(c, d, a, b, x[k + 6], S13, 0xA8304613);
        b = md5_FF(b, c, d, a, x[k + 7], S14, 0xFD469501);
        a = md5_FF(a, b, c, d, x[k + 8], S11, 0x698098D8);
        d = md5_FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF);
        c = md5_FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1);
        b = md5_FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE);
        a = md5_FF(a, b, c, d, x[k + 12], S11, 0x6B901122);
        d = md5_FF(d, a, b, c, x[k + 13], S12, 0xFD987193);
        c = md5_FF(c, d, a, b, x[k + 14], S13, 0xA679438E);
        b = md5_FF(b, c, d, a, x[k + 15], S14, 0x49B40821);
        a = md5_GG(a, b, c, d, x[k + 1], S21, 0xF61E2562);
        d = md5_GG(d, a, b, c, x[k + 6], S22, 0xC040B340);
        c = md5_GG(c, d, a, b, x[k + 11], S23, 0x265E5A51);
        b = md5_GG(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA);
        a = md5_GG(a, b, c, d, x[k + 5], S21, 0xD62F105D);
        d = md5_GG(d, a, b, c, x[k + 10], S22, 0x2441453);
        c = md5_GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681);
        b = md5_GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8);
        a = md5_GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6);
        d = md5_GG(d, a, b, c, x[k + 14], S22, 0xC33707D6);
        c = md5_GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87);
        b = md5_GG(b, c, d, a, x[k + 8], S24, 0x455A14ED);
        a = md5_GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905);
        d = md5_GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8);
                c = md5_GG(c, d, a, b, x[k + 7], S23, 0x676F02D9);
                b = md5_GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A);
                a = md5_HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942);
                d = md5_HH(d, a, b, c, x[k + 8], S32, 0x8771F681);
                c = md5_HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122);
                b = md5_HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C);
                a = md5_HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44);
                d = md5_HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9);
                c = md5_HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60);
                b = md5_HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70);
                a = md5_HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6);
                d = md5_HH(d, a, b, c, x[k + 0], S32, 0xEAA127FA);
                c = md5_HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085);
                b = md5_HH(b, c, d, a, x[k + 6], S34, 0x4881D05);
                a = md5_HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039);
                d = md5_HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5);
                c = md5_HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8);
                b = md5_HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665);
                a = md5_II(a, b, c, d, x[k + 0], S41, 0xF4292244);
                d = md5_II(d, a, b, c, x[k + 7], S42, 0x432AFF97);
                c = md5_II(c, d, a, b, x[k + 14], S43, 0xAB9423A7);
                b = md5_II(b, c, d, a, x[k + 5], S44, 0xFC93A039);
                a = md5_II(a, b, c, d, x[k + 12], S41, 0x655B59C3);
                d = md5_II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92);
                c = md5_II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D);
                b = md5_II(b, c, d, a, x[k + 1], S44, 0x85845DD1);
                a = md5_II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F);
                d = md5_II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0);
                c = md5_II(c, d, a, b, x[k + 6], S43, 0xA3014314);
                b = md5_II(b, c, d, a, x[k + 13], S44, 0x4E0811A1);
                a = md5_II(a, b, c, d, x[k + 4], S41, 0xF7537E82);
                d = md5_II(d, a, b, c, x[k + 11], S42, 0xBD3AF235);
                c = md5_II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB);
                b = md5_II(b, c, d, a, x[k + 9], S44, 0xEB86D391);
                a = md5_AddUnsigned(a, AA);
                b = md5_AddUnsigned(b, BB);
                c = md5_AddUnsigned(c, CC);
                d = md5_AddUnsigned(d, DD);
            }
            return (md5_WordToHex(a) + md5_WordToHex(b) + md5_
      WordToHex(c) + md5_WordToHex(d)).toLowerCase();
        }
        伪代码实现
        //Note:Allvariablesareunsigned32bitsandwrapmodulo2^32whencalcul
      atingvarint[64]r, k//rspecifi estheper-roundshiftamountsr[0..15]:=
      {7,12,17,22,7,12,17,22,7,12,17,22,7,12,17,22}r[16..31]:={5,9,14,20,
      5,9,14,20,5,9,14,20,5,9,14,20}r[32..47]:={4,11,16,23,4,11,16,23,4,11,
      16,23,4,11,16,23}r[48..63]:={6,10,15,21,6,10,15,21,6,10,15,21,6,10,1
      5,21}//Usebinaryintegerpartofthesinesofi ntegersasconstants:forifrom
      0to63k[i]:=fl oor(abs(sin(i+1））×2^32）//Initializevariables:varinth
      0:=0x67452301varinth1:=0xEFCDAB89varinth2:=0x98BADCFEvarinth3:=
      0x10325476//Pre-processing:append"1"bittomessageappend"0"bitsuntilm
      essagelengthinbits≡448(mod512）appendbitlengthofmessageas64-
      bitlittle-endianintegertomessage//Processthemessageinsuccessive512-
      bitchunks:foreach512-bitchunkofmessagebreakchunkintosixteen32-
      bitlittle-endianwordsw[i],0≤i≤15//Initializehashvalueforthischunk:v
      arinta:=h0varintb:=h1varintc:=h2varintd:=h3//Mainloop:forifrom0to
      63if0≤i≤15thenf:=(bandc)or((notb)andd)g:=ielseif16≤i≤31f:=(dandb)
      or((notd)andc)g:=（5×i+1）mod16elseif32≤i≤47f:=bxorcxordg:=（3×i+5）
      mod16elseif48≤i≤63f:=cxor(bor(notd))g:=（7×i)mod16temp:=dd:=cc:=bb:
      =((a+f+k[i]+w[g])leftrotater[i])+ba:=temp//Addthischunk'shashtoresu
      ltsofar:h0:=h0+ah1:=h1+bh2:=h2+ch3:=h3+dvarintdigest:=h0appendh1ap
      pendh2appendh3//(expressedaslittle-endian)MD5加密工具

利用MD5的算法原理，可以使用各种计算机语言进行实现，形成各种各样的MD5加密校验工具。有很多的在线工具可以实现这一点，这些在线工具一般是采用JavaScript语言实现，使用非常方便快捷。

3.1.4 MD5的优势

Van oorschot和Wiener曾经考虑过一个在散列中暴力搜寻冲突的函数（brute-force hash function），而且他们猜测一个被设计专门用来搜索MD5冲突的机器（这台机器在1994年的制造成本大约是100万美元）可以平均每24天就找到一个冲突。但从1991年到2001年这10年间，竟没有出现替代MD5算法的MD6或被叫作其他什么名字的新算法这一点，我们就可以看出这个瑕疵并没有太多地影响MD5的安全性。上面所有这些都不足以成为MD5的在实际应用中的问题。并且，由于MD5算法的使用不需要支付任何版权费用，所以在一般的情况下（非绝密应用领域，但即便是应用在绝密领域内，MD5也不失为一种非常优秀的中间技术）,MD5怎么都应该算得上是非常安全的了。

SHA-1与MD5的比较

因为二者均由MD4导出，SHA-1和MD5彼此很相似。相应地，它们的强度和其他特性也是相似的，但还有以下几点不同。

① 对强行攻击的安全性：较显著和较重要的区别是SHA-1摘要比MD5摘要长32位。使用强行技术，产生任何一个报文使其摘要等于给定报文摘要的难度对MD5是2 128 数量级的操作，而对SHA-1则是2 160 数量级的操作。这样，SHA-1对强行攻击有更大的强度。

② 对密码分析的安全性：MD5的设计易受密码分析的攻击，SHA-1显得不易受这样的攻击。

③ 速度：在相同的硬件上，SHA-1的运行速度比MD5慢。

3.2 对称密钥算法详解

对称密钥加密就是使用对称密码编码技术的加密算法，特点是文件加密和解密使用相同的密钥，即加密密钥也就是解密密钥。对称加密算法使用起来简单快捷，密钥较短，而且破解起来也有一定的难度。对称密钥算法的速度非常快，非常适于加密大型的数据流。这些算法可以加密数据，也可以解密数据。对称加密技术数据的两个交换方（即加密方和解密方）必须使用一个保密的私有密钥。

3.2.1 对称密钥算法

对称密钥算法有时又叫传统密码算法，就是解密密钥能够从加密密钥中推算出来，反过来也成立。在大多数对称算法中，加密密钥和解密密钥是相同的，这些算法也叫单密钥算法，它要求发送者和接收者在安全通信之前，商定一个共同的密钥。对称算法的安全性完全依赖于密钥，泄露密钥就意味着任何人都能对消息进行加密解密（密钥必须保密）。

对称算法可分为两类：一类是只对明文中的单个位（有时对字节）运算的算法称为序列算法或序列密码；另一类算法是对明文的一组位进行运算，这些位称为分组，相应的算法称为分组算法或分组密码算法。现代计算机密码算法的典型分组长度为64位，这个长度大到足以防止分析破译，但又小到便于作用。

采用对称密钥算法的常用加密方案有5个组成部分。

① 明文：原始信息。

② 加密算法：以密钥为参数，对明文进行多种置换和转换的规则和步骤，变换结果为密文。

③ 密钥：加密与解密算法的参数，直接影响对明文进行变换的结果。

④ 密文：对明文进行变换的结果。

⑤ 解密算法：加密算法的逆变换，以密文为输入、密钥为参数，变换结果为明文。

对称密码术的优点在于效率高，算法简单，系统开销小，适合加密大量数据。尽管对称密码学有一些很好的特性，但其仍然存在着明显的缺陷，主要表现如下。

· 进行安全通信前，密钥交换的交换必须是在安全的方式下进行。虽然在某种情况下是可行的，但有时却非常困难，甚至无法实现。

· 密钥的规模庞大。举例来说，A与B两人之间的密钥必须不同于A和C两人之间的密钥，否则对B的消息的安全性就会受到威胁。在有1000个用户的团体中，A需要保持至少999个密钥。对于该团体中的其他用户，此种情况同样存在。推而广之， n 个用户的团体需要N2/2个不同的密钥。

目前比较著名的对称密钥密码算法有DES、RC4、IDEA、FEAL及AES等，都是常用的对称密钥算法。

3.2.2 对称密钥的加密模式

在现代对称加密算法中，主要有4种加密处理模式，分别如下：电子密码本模式（ECB）、加密块链模式（CBC）、加密反馈模式（CFB）和输出反馈模式（OFB），这4种加密处理模式一般是针对块加密算法而言的（如DES加密算法）。

1．电子密码本模式

这是一种最古老的加密模式，也是最简单的加密模式，它将加密的数据分成若干组，每组的大小跟加密密钥长度相同，进行加密的密钥也相同。如DES算法，一个64位的密钥，如果采用该模式加密，就是将要加密的数据分成每组64位的数据，如果最后一组不够64位，那么就补齐为64位，然后每组数据都采用DES算法的64位密钥进行加密。每8个字符（64位）作为一块，再使用一个相同的64位密钥对每个块进行加密，最后一块补足64位，补齐后再进行加密。

这种模式存在明显的缺陷，因为ECB方式每64位使用的密钥都是相同的，所以非常容易获得密文进行密码破解。此外，因为每64位是相互独立的，甚至不用破解密码，只要简单地将其中一块替换就可以达到目的。

2．加密块链模式

如下图所示即为加密块链模式加密过程和解密过程。

从这两个图中可以看到，CBC模式的加密也是将明文分成固定长度（64位）的块（P0, P1...）之后，再将前面一个加密块输出的密文与下一个要加密的明文块进行XOR（异或）操作计算，将计算结果用密钥进行加密得到密文。

加密块链模式加密过程

加密块链模式解密过程

在对第一明文块加密时，因为前面没有加密的密文，所以需要一个初始化向量（IV）。跟ECB方式不一样，通过链接关系，使得密文和明文不再是一一对应的关系，增加了破解的难度，而且克服了只要简单调换密文块就可能达到攻击的目的。

该加密模式的最明显缺点是不能实时解密，即必须要等到每8个字节全部接收到之后，才能开始加密，否则不能得到正确的结果。这在要求实时性比较高时就显得不合适了。

3．加密反馈模式

加密反馈模式为了克服必须等待8个字节全部得到才能进行解密的缺点，采用了一个64位（8个字节）的位移寄存器来获得密文，如图所示即为加密反馈模式加密过程和解密过程。

加密反馈模式加密过程

加密反馈模式解密过程

上面两个图中，C2、C3以及P10等都是一个字节（8位）的数据，所以能够实现字符的实时加密和解密，不用再等到8个字节都接收到之后再进行解密。如在进行第10个字节数据的加密和解密过程中，先从移位寄存器取8个字节的数据（C2到C9）用密钥进行加密之后，再取加密数据最左边的一个字节与输入的明文P10进行XOR（异或）操作，将得到的值作为输出密文C10，同时将C10送入到移位寄存器中。

4．输出反馈模式

输出反馈模式OFB跟加密反馈模式CFB大致是一样的，每一个区块的明文与之前区块加密后的密文作异或运算后产生密文。不同之处在于，加密反馈模式区块加密后的密文为独立产生，每一个区块的加密结果不受之前所有区块内容的影响，即使有区块在传输过程中发生错误或丢失，不至于无法完全解密。下图所示即为输出反馈模式的加密过程，下图所示即为输出反馈模式的解密过程。

输出反馈模式加密过程

输出反馈模式解密过程

可以看到，这种输出反馈模式因为没有采用密文作为加密的数据，所以克服了传输过程中由于单个比特导致64个相关比特解密失败的情况，在这种模式下，如果一个比特发生了错误，那么只会影响对应的一个比特，而不会影响别的。但相对于其他模式，因为数据之间相关性小，这种加密模式仍然存在一定的危险性，所以一般不使用OFB加密模式。

5．三重加密

只有56位的密钥长度的DES算法，就目前看来有些简单，而且加密速度快。但是随着计算机技术的快速发展，DES加密算法的密钥长度破解起来并不困难，在还没有找到更加安全的加密算法以前，使用重复加密方式提高DES的密钥长度是必要的解决方法。经过研究发现，DES有效位达到112位就可以增加其安全度，所以说，同时利用三把不同的密钥的三重加密，是比较安全的方法。

使用DES进行三重加密时，其效率只有原来DES的1/3。因此，RSA公司提出了一种DESX，它的结构如下。

DESX k1,k2,k3 （M）=K2⊕DES加密 k （k1⊕M）

明文M的每一个数据块，在使用密钥k加密前，必须先与密钥k1做一次异或运算，加密之后的结果再与密钥k2做一次异或，经过三个步骤完成DESX运算，才输出相应密文。由此运算可以将DESX的密钥长度扩展为184位。

3.2.3 RC4流密码

RC4加密算法是大名鼎鼎的RSA三人组中的人物Ronald Rivest在1987年设计的密钥长度可变的流加密算法簇。之所以称其为簇，是由于其核心部分的S-box长度可为任意，但一般为256字节。该算法的速度可以达到DES加密的10倍左右，且具有很高级别的非线性。RC4起初是用于保护商业机密的，但是在1994年9月，它的算法被发布在互联网上，也就不再有什么商业机密了。RC4也被叫作ARC4（Alleged RC4，所谓的RC4），因为RSA从来就没有正式发布过这个算法。

RC4算法的原理很简单，包括初始化算法（KSA）和伪随机子密码生成算法（PRGA）两大部分。假设S-box的长度为256，密钥长度为Len。先来看看算法的初始化部分（用C代码表示）。

        其中，参数"s"是一个256长度的char型数组，定义为： unsigned char sBox[256];
        参数"key"是密钥，其内容可以随便定义：char key[256];
        参数"Len"是密钥的长度，Len = strlen(key);
        /＊初始化函数＊/
        void rc4_init(unsigned char＊s, unsigned char＊key, unsigned long Len)
        {
            int i=0, j=0;
            char k[256]={0};
            unsigned char tmp=0;
            for(i=0; i<256; i++) {
                s[i]=i;
                k[i]=key[i%Len];
            }
            for(i=0; i<256; i++) {
                j=(j+s[i]+k[i])%256;
                tmp=s[i];
                s[i]=s[j]; //交换s[i]和s[j]
                s[j]=tmp;
            }
        }

在初始化的过程中，密钥的主要功能是将S-box搅乱，i确保S-box的每个元素都得到处理，j保证S-box的搅乱是随机的。而不同的S-box在经过伪随机子密码生成算法的处理后可以得到不同的子密钥序列，将S-box和明文进行XOR运算，得到密文，解密过程也完全相同。

再来看看算法的加密部分（用C代码表示）。

        其中，参数"s"是上边rc4_init函数中，被搅乱的S-box;
        参数"Data"是需要加密的数据data;
        参数"Len"是data的长度．
        /＊加解密＊/
        void rc4_crypt(unsigned char＊s, unsigned char＊Data, unsigned long Len)
        {
            int i=0, j=0, t=0;
            unsigned long k=0;
            unsigned char tmp;
            for(k=0; k<Len; k++)
            {
                i=(i+1)%256;
                j=(j+s[i])%256;
                tmp=s[i];
                s[i]=s[j]; //交换s[x]和s[y]
                s[j]=tmp;
                t=(s[i]+s[j])%256;
                Data[k]^=s[t];
            }
        }

最后，在main函数中，调用顺序如下。

        int main()
        {
          unsigned char s[256]={0}, s2[256]={0}; //S-box
          char key[256]={"justfortest"};
          char pData[512]="这是一个用来加密的数据Data";
          unsigned long len=strlen(pData);
          int i;
          printf("pData=%s\n", pData);
          printf("key=%s, length=%d\n\n", key, strlen(key));
          rc4_init(s, (unsigned char＊)key, strlen(key)); //已经完成了初始化
          printf("完成对S[i]的初始化，如下：\n\n");
      for(i=0; i<256; i++)
      {
          printf("%02X", s[i]);
          if(i&&(i+1)%16==0)putchar('\n');
      }
      printf("\n\n");
      for(i=0; i<256; i++)//用s2[i]暂时保留经过初始化的s[i]，这一点很重要
      {
          s2[i]=s[i];
      }
      printf("已经初始化，现在加密：\n\n");
      rc4_crypt(s, (unsigned char＊)pData, len); //加密
      printf("pData=%s\n\n", pData);
      printf("已经加密，现在解密：\n\n");
      //rc4_init(s, (unsigned char＊)key, strlen(key)); //初始化密钥
      rc4_crypt(s2, (unsigned char＊)pData, len); //解密
      printf("pData=%s\n\n", pData);
      return0;
    }

因此最终的完整程序如下。

    //程序开始
    #include<stdio.h>
    #include<string.h>
    typedef unsigned longULONG;
    /＊初始化函数＊/
    void rc4_init(unsigned char＊s, unsigned char＊key, unsigned long Len)
    {
        int i = 0, j = 0;
        char k[256] = { 0 };
        unsigned char tmp = 0;
        for (i = 0; i<256; i++)
        {
            s[i] = i;
            k[i] = key[i%Len];
        }
        for (i = 0; i<256; i++)
        {
            j = (j + s[i] + k[i]) % 256;
            tmp = s[i];
            s[i] = s[j]; //交换s[i]和s[j]
            s[j] = tmp;
        }
    }
    /＊加解密＊/
    void rc4_crypt(unsigned char＊s, unsigned char＊Data, unsigned long Len)
    {
        int i = 0, j = 0, t = 0;
        unsigned long k = 0;
        unsigned char tmp;
        for (k = 0; k<Len; k++)
        {
            i = (i + 1) % 256;
            j = (j + s[i]) % 256;
            tmp = s[i];
            s[i] = s[j]; //交换s[x]和s[y]
            s[j] = tmp;
            t = (s[i] + s[j]) % 256;
            Data[k] ^= s[t];
        }
    }
    int main()
    {
        unsigned char s[256] = { 0 }, s2[256] = { 0 }; //S-box
        char key[256] = { "justfortest" };
        char pData[512] = "这是一个用来加密的数据Data";
        unsigned long len = strlen(pData);
        int i;
        printf("pData=%s\n", pData);
        printf("key=%s, length=%d\n\n", key, strlen(key));
        rc4_init(s, (unsigned char＊)key, strlen(key)); //已经完成了初始化
        printf("完成对S[i]的初始化，如下：\n\n");
        for (i = 0; i<256; i++)
        {
            printf("%02X", s[i]);
            if (i && (i + 1) % 16 == 0)putchar('\n');
        }
        printf("\n\n");
        for(i = 0; i<256; i++)//用s2[i]暂时保留经过初始化的s[i]，这一点很重要
        {
            s2[i] = s[i];
        }
        printf("已经初始化，现在加密：\n\n");
        rc4_crypt(s, (unsigned char＊)pData, len); //加密
        printf("pData=%s\n\n", pData);
        printf("已经加密，现在解密：\n\n");
        //rc4_init(s, (unsignedchar＊)key, strlen(key)); //初始化密钥
        rc4_crypt(s2, (unsigned char＊)pData, len); //解密
        printf("pData=%s\n\n", pData);
        return 0;
    }
    //程序结束

但是，RC4算法有一定的漏洞，由于RC4算法加密是采用的XOR，所以，一旦子密钥序列出现了重复，密文就有可能被破解。那么，RC4算法生成的子密钥序列是否会出现重复呢？由于存在部分弱密钥，使得子密钥序列在不到100万字节内就发生了完全的重复，如果是部分重复，则可能在不到10万字节内就能发生重复，因此，推荐在使用RC4算法时，必须对加密密钥进行测试，判断其是否为弱密钥。其不足主要体现于，在无线网络中IV（初始化向量）不变性漏洞。

根据目前的分析结果，没有任何的分析对于密钥长度达到128位的RC4有效，所以，RC4是目前最安全的加密算法之一，大家可以放心使用。

3.2.4 TEA算法

在安全学领域，TEA（Tiny Encryption Algorithm）是一种分组加密算法，它的实现非常简单，通常只需要很精短的几行代码。TEA算法最初是由剑桥计算机实验室的David Wheeler和Roger Needham在1994 年设计的。

TEA算法使用64位的明文分组和128位的密钥，它使用Feistel分组加密框架，需要进行64 轮迭代，尽管作者认为32 轮已经足够了。该算法使用了一个神秘常数δ作为倍数，它来源于黄金比率，以保证每一轮加密都不相同。但δ的精确值似乎并不重要，这里TEA把它定义为 δ=「( √5-1)231」（也就是程序中的0×9E3779B9）。

之后TEA算法被发现存在缺陷，作为回应，设计者提出了一个TEA的升级版本——XTEA（有时也称为“tean”）。XTEA跟TEA使用了相同的简单运算，但它采用了截然不同的顺序，为了阻止密钥表攻击，4个子密钥（在加密过程中，原128 位的密钥被拆分为4 个32位的子密钥）采用了一种不太正规的方式进行混合，但速度更慢了。

C语言的Tea算法（原始）如下。

        void encrypt(uint32_t＊v, uint32_t＊k) {
            uint32_t v0 = v[0], v1 = v[1], sum = 0, i; /＊setup＊/
            uint32_t delta = 0x9e3779b9; /＊akeyscheduleconstant＊/
            uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3]; /＊cachekey＊/
            for(i=0; i<32; i++) {
                /＊basiccyclestart＊/
                sum += delta;
                v0 += ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
                v1 += ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
            } /＊endcycle＊/
            v[0] = v0;
            v[1] = v1;
        }
        void decrypt(uint32_t＊v, uint32_t＊k) {
            uint32_t v0 = v[0], v1 = v[1], sum = 0xC6EF3720, i; /＊setup＊/
            uint32_t delta = 0x9e3779b9; /＊akeyscheduleconstant＊/
            uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3]; /＊cachekey＊/
            for(i = 0; i < 32; i++) {
                /＊basiccyclestart＊/
                v1-= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
                v0-= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
                sum -= delta;
            } /＊endcycle＊/
            v[0] = v0;
            v[1] = v1;
        }

3.2.5 数据加密算法

数据加密算法（DES）为密码体制中的对称密码体制，又称为美国数据加密标准，是1972年美国IBM公司研制的对称密码体制加密算法。明文按64位进行分组，密钥长64位，密钥事实上是56位参与DES运算（第8、16、24、32、40、48、56、64位是校验位，使得每个密钥都有奇数个1）分组后的明文组和56位的密钥按位替代或交换的方法形成密文组的加密方法。

因为DES算法只有56位的密钥容量，因此，针对其不具备足够安全性的弱点，后来又提出了三重DES或3DES系统，使用了3个不同的密钥对数据块进行2次或3次加密，该方法比进行普通加密的3次要快许多。

在DES算法中，用到了8个S-BOX，列出如下。

S1：

14,4,13,1,2,15,11,8,3,10,6,12,5,9,0,7,

0,15,7,4,14,2,13,1,10,6,12,11,9,5,3,8,

4,1,14,8,13,6,2,11,15,12,9,7,3,10,5,0,

15,12,8,2,4,9,1,7,5,11,3,14,10,0,6,13,

S2：

15,1,8,14,6,11,3,4,9,7,2,13,12,0,5,10,

3,13,4,7,15,2,8,14,12,0,1,10,6,9,11,5,

0,14,7,11,10,4,13,1,5,8,12,6,9,3,2,15,

13,8,10,1,3,15,4,2,11,6,7,12,0,5,14,9,

S3：

10,0,9,14,6,3,15,5,1,13,12,7,11,4,2,8,

13,7,0,9,3,4,6,10,2,8,5,14,12,11,15,1,

13,6,4,9,8,15,3,0,11,1,2,12,5,10,14,7,

1,10,13,0,6,9,8,7,4,15,14,3,11,5,2,12,

S4：

7,13,14,3,0,6,9,10,1,2,8,5,11,12,4,15,

13,8,11,5,6,15,0,3,4,7,2,12,1,10,14,9,

10,6,9,0,12,11,7,13,15,1,3,14,5,2,8,4,

3,15,0,6,10,1,13,8,9,4,5,11,12,7,2,14,

S5：

2,12,4,1,7,10,11,6,8,5,3,15,13,0,14,9,

14,11,2,12,4,7,13,1,5,0,15,10,3,9,8,6,

11,8,12,7,1,14,2,13,6,15,0,9,10,4,5,3,

S6：

12,1,10,15,9,2,6,8,0,13,3,4,14,7,5,11,

10,15,4,2,7,12,9,5,6,1,13,14,0,11,3,8,

9,14,15,5,2,8,12,3,7,0,4,10,1,13,11,6,

4,3,2,12,9,5,15,10,11,14,1,7,6,0,8,13,

S7：

4,11,2,14,15,0,8,13,3,12,9,7,5,10,6,1,

13,0,11,7,4,9,1,10,14,3,5,12,2,15,8,6,

1,4,11,13,12,3,7,14,10,15,6,8,0,5,9,2,

6,11,13,8,1,4,10,7,9,5,0,15,14,2,3,12,

S8：

13,2,8,4,6,15,11,1,10,9,3,14,5,0,12,7,

1,15,13,8,10,3,7,4,12,5,6,11,0,14,9,2,

7,11,4,1,9,12,14,2,0,6,10,13,15,3,5,8,

2,1,14,7,4,10,8,13,15,12,9,0,3,5,6,11,

在此以S1为例说明其功能，可以看到，在S1中，共有4行数据，命名为0,1,2,3行；每行有16列，命名为0,1,2,3,...,14,15列。

现设输入为：D=D1D2D3D4D5D6

假设：列=D2D3D4D5行=D1D6

然后在S1表中查得对应的数，以4位二进制表示，此即为选择函数S1的输出。

DES加密算法加密的单位是64位，因此在执行DES加密时，必须先将明文分成每64位一个区块段。DES加密算法在解密时和加密时完全相同，解密时输入密钥的位顺序与加密时正好相反。

DES算法具有极高的安全性，到目前为止，除了用穷举搜索法对DES算法进行攻击外，还没有发现更有效的办法。而56位长的密钥的穷举空间为256，这意味着如果一台计算机的速度是每一秒钟检测一百万个密钥，则它搜索完全部密钥就需要将近2285年的时间，可见，这是难以实现的。然而，这并不等于说DES是不可破解的。而实际上，随着硬件技术和Internet的发展，其破解的可能性越来越大，而且，所需要的时间越来越少。使用经过特殊设计的硬件并行处理要几个小时。

3.2.6 高级加密标准算法概述

高级加密标准（Advanced Encryption Standard,AES），在密码学中又称Rijndael加密法，是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES，已经被多方分析且广为使用。经过五年的甄选流程，高级加密标准由美国国家标准与技术研究院（NIST）于2001年11月26日发布于FIPS PUB 197，并在2002年5月26日成为有效的标准。2006年，高级加密标准已经成为对称密钥加密中最流行的算法之一。

Rijndael是由Daemen和Rijmen早期所设计的Square改良而来，而Square则是由SHARK发展而来。

不同于它的前任标准DES,Rijndael使用的是代换——置换网络，而非Feistel架构。AES在软件及硬件上都能快速地加解密，相对来说较易于实作，且只需要很少的存储器。作为一个新的加密标准，目前正被部署应用到更广大的范围。

Rijndael密码的设计力求满足以下3条标准。

· 抵抗所有已知的攻击。

· 在多个平台上速度快，编码紧凑。

· 设计简单。

当前的大多数分组密码，其轮函数是Feistel结构。Rijndael没有这种结构。Rijndael轮函数是由3个不同的可逆均匀变换。

可逆均匀变换

        Rijndael (State, ExpandedKey)
        {
          AddRoundKey (State, ExpandedKey);
          for (i=1; i <Nr; i ++)
          Round (State, ExpandedKey+Nb＊ i);
          FinalRound (State, ExpandedKey+Nb＊Nr)
        }
        Round (State, RoundKey)
        {
          ByteSub (State);
          ShiftRow (State);
          MixColumn (State);
          AddRoundKey (State, RoundKey)
        }
        FinalRound (State, RoundKey)
        {
          ByteSub (State);
          ShiftRow (State);
          AddRoundKey (State, RoundKey)
        }

严格地说，AES和Rijndael加密法并不完全一样（虽然在实际应用中二者可以互换），因为Rijndael加密法可以支持更大范围的区块和密钥长度，AES的区块长度固定为128位，密钥长度则可以是128,192或256位；而Rijndael使用的密钥和区块长度可以是32位的整数倍，以128位为下限，256位为上限。加密过程中使用的密钥是由Rijndael密钥生成方案产生。

大多数AES计算是在一个特别的有限域完成的。

AES加密过程是在一个4×4的字节矩阵上运作，这个矩阵又称为“状态（state）”，其初值就是一个明文区块（矩阵中一个元素大小就是明文区块中的一个Byte）。Rijndael加密法因支持更大的区块，其矩阵行数可视情况增加。加密时，各轮AES加密循环（除最后一轮外）均包含4个步骤。

① AddRoundKey：矩阵中的每一个字节都与该次轮秘钥（Round Key）做XOR运算；每个子密钥由密钥生成方案产生。

② SubBytes：通过一个非线性的替换函数，用查找表的方式把每个字节替换成对应的字节。

③ ShiftRows：将矩阵中的每个横列进行循环式移位。

④ MixColumns：为了充分混合矩阵中各个直行的操作。这个步骤使用线性转换来混合每列的4个字节。

最后一个加密循环中省略MixColumns步骤，而以另一个AddRoundKey取代。

AES的基本要求是，采用对称分组密码体制，密钥长度的最少支持为128、192、256，分组长度128位，算法应易于各种硬件和软件实现。1998年NIST开始AES第一轮分析、测试和征集，共产生了15个候选算法。1999年3月完成了第二轮AES2的分析、测试。2000年10月2日美国政府正式宣布选中比利时密码学家Joan Daemen和Vincent Rijmen提出的一种密码算法Rijndael作为AES。

在应用方面，尽管DES在安全上是脆弱的，但由于快速DES芯片的大量生产，使得DES仍能暂时继续使用，为提高安全强度，通常使用独立密钥的三级DES。但是DES迟早要被AES代替。流密码体制较之分组密码在理论上成熟且安全，但未被列入下一代加密标准。

AES加密数据块分组长度必须为128位，密钥长度可以是128位、192位、256位中的任意一个（如果数据块及密钥长度不足时，会补齐）。AES加密有很多轮的重复和变换。大致步骤如下：① 密钥扩展（Key Expansion）;② 初始轮（Initial Round）;③ 重复轮（Rounds），每一轮又包括SubBytes、ShiftRows、MixColumns、AddRoundKey;④ 最终轮（Final Round），最终轮没有MixColumns。

3.2.7 IDEA加密算法

国际数据加密算法（International Data Encryption Algorithm,IDEA）是由研究员Xuejia Lai和James L. Massey在苏黎世的ETH开发的，一家瑞士公司Ascom Systec拥有其专利权。IDEA是作为迭代的分组密码实现的，使用128位的密钥和8个循环。通过支付专利使用费，可以在全世界广泛使用IDEA。这些费用是在某些区域中适用，而其他区域并不适用。IDEA被认为是极为安全的。使用128位的密钥，蛮力攻击中需要进行的测试次数与DES相比会明显增大，甚至允许对弱密钥测试。而且，它本身也显示了尤其能抵抗专业形式的分析性攻击。

IDEA加密算法简介

IDEA在密码学中属于数据块加密算法（Block Cipher）类。IDEA使用长度为128bit的密钥，数据块大小为64bit。从理论上讲，IDEA属于“强”加密算法，至今还没有出现对该算法的有效攻击算法。

早在1990年，Xuejia Lai等人在EuroCrypt'90年会上提出了分组密码建议PES（Porposed Encryption Standard）。在EuroCrypt'91年会上，Xuejia Lai等人又提出了PES的修正版IPES（Improved PES）。目前IPES已经商品化，并改名为IDEA。IDEA已由瑞士的Ascom公司注册专利，以商业目的使用IDEA算法必须向该公司申请许可。

IDEA是一种由8个相似圈（Round）和一个输出变换（Output Transformation）组成的迭代算法。IDEA的每个圈都由三种函数：模（216+1）乘法、模216加法和按位XOR组成。

在加密之前，IDEA通过密钥扩展将128bit的密钥扩展为52Byte的加密密钥EK（Encryption Key），然后由EK计算出解密密钥DK（Decryption Key）。EK和DK分为8组半密钥，每组长度为6Byte，前8组密钥用于8圈加密，最后半组密钥（4Byte）用于输出变换。IDEA的加密过程和解密过程是一样的，只不过使用不同的密钥（加密时用EK，解密时用DK）。

密钥扩展的过程如下。

① 将128bit的密钥作为EK的前8byte。

② 将前8byte循环左移25bit，得到下一8byte，将这个过程循环7次。

③ 在第7次循环时，取前4byte作为EK的最后4byte。

④ 至此52byte的EK生成完毕。

IDEA算法相对来说是一个比较新的算法，其安全性研究也在不断进行之中。在IDEA算法公布后不久，就有学者指出：IDEA的密钥扩展算法存在缺陷，导致在IDEA算法中存在大量弱密钥类，但这个弱点通过简单的修改密钥扩展算法（加入异或算子）即可克服。在1997年的EuroCrypt'97年会上，John Borst等人提出了对圈数减少的IDEA的两种攻击算法：对3.5圈IDEA的截短差分攻击（Truncate Diffrential Attack）和对3圈IDEA的差分线性攻击（Diffrential Linear Attack）。但作者也同时指出，这两种攻击算法对整8.5圈的IDEA算法不可能取得实质性的攻击效果。目前尚未出现新的攻击算法，一般认为攻击整8.5圈IDEA算法唯一有效的方法是穷尽搜索128bit的密钥空间。

目前IDEA在工程中已有大量应用实例，PGP（Pretty Good Privacy）就使用IDEA作为其分组加密算法；安全套接字层（Secure Socket Layer,SSL）也将IDEA包含在其加密算法库SSLRef中；IDEA算法专利的所有者Ascom公司也推出了一系列基于IDEA算法的安全产品，包括基于IDEA的Exchange安全插件、IDEA加密芯片、IDEA加密软件包等。IDEA算法的应用和研究正在不断走向成熟。

3.3 非对称密钥加密算法详解

非对称加密算法是一种密钥的保密方法，它使用了两个具有数学关系的不同密钥来加密和解密数据，这两个密钥分别称为私钥和公钥，合称为密钥对。在现在实际应用中，由于非对称加密使用的算法比对称加密更复杂，并且还使用了密钥对，因此在使用非对称加密时，在加密速度上就比不上对称加密算法了。

3.3.1 非对称密钥算法

非对称加密算法需要两个密钥：公开密钥（publickey）和私有密钥（privatekey）。公开密钥与私有密钥是一对，如果用公开密钥对数据进行加密，只有用对应的私有密钥才能解密；如果用私有密钥对数据进行加密，那么只有用对应的公开密钥才能解密。因为加密和解密使用的是两个不同的密钥，所以这种算法叫作非对称加密算法。非对称加密算法实现机密信息交换的基本过程是：甲方生成一对密钥并将其中的一把作为公用密钥向其他方公开；得到该公用密钥的乙方使用该密钥对机密信息进行加密后再发送给甲方；甲方再用自己保存的另一把专用密钥对加密后的信息进行解密。

另外，甲方可以使用乙方的公钥对机密信息进行签名后再发送给乙方；乙方再用自己的私钥对数据进行验签。

甲方只能用其专用密钥解密由其公用密钥加密后的任何信息。非对称加密算法的保密性比较好，它消除了最终用户交换密钥的需要。

非对称密码体制的特点是：算法强度复杂、安全性依赖于算法与密钥，但是由于其算法复杂，而使得加密解密，速度没有对称加密解密的速度快。对称密码体制中只有一种密钥，并且是非公开的，如果要解密，就得让对方知道密钥。所以保证其安全性就是保证密钥的安全，而非对称密钥体制有两种密钥，其中一个是公开的，这样就可以不需要像对称密码那样传输对方的密钥了。这样安全性就大了很多。

非对称密钥算法不太适用于加密大量数据，常用于将对称密钥和初始化向量加密并传输给对方，再在双方之间来回发送的消息中，使用对称算法加密和解密数据。非对称算法主要有RSA、DSA等，主要用于网络数据的加密。

非对称加密算法的保密性比较好，它不像对称加密那样，每次都需要安全通道才能交换密钥，但加密和解密速度很慢，而且同样的密钥长度安全性更低一些。使用它对于大型文件加密就显得很不合适，而只适用于对少量数据进行加密。

另外，非对称加密也同样面临着公钥的交换问题，也同样需要安全通道才能彼此交换公钥，只是适当运用的话只需要交换一次就可以。非对称加密技术一般不单独使用，往往作为辅助对称加密技术的一种手段，可以用来传递对称加密的密钥。

3.3.2 RSA公钥加密算法

所谓的公开密钥密码体制就是使用不同的加密密钥与解密密钥，是一种“由已知加密密钥推导出解密密钥在计算上是不可行的”密码体制。

在公开密钥密码体制中，加密密钥（即公开密钥）PK是公开信息，而解密密钥（即秘密密钥）SK是需要保密的。加密算法E和解密算法D也都是公开的。虽然解密密钥SK是由公开密钥PK决定的，但却不能根据PK计算出SK。

正是基于这种理论，1978年出现了著名的RSA算法，它通常是先生成一对RSA密钥，其中之一是保密密钥，由用户保存；另一个为公开密钥，可对外公开，甚至可在网络服务器中注册。为提高保密强度，RSA密钥长至少为500位，一般推荐使用1024位。这就使加密的计算量很大。为减少计算量，在传送信息时，常采用传统加密方法与公开密钥加密方法相结合的方式，即信息采用改进的DES或IDEA对话密钥加密，然后使用RSA密钥加密对话密钥和信息摘要。对方收到信息后，用不同的密钥解密并可核对信息摘要。

RSA算法是第一个能同时用于加密和数字签名的算法，也易于理解和操作。RSA是被研究得最广泛的公钥算法，从提出到现今的30多年里，经历了各种攻击的考验，逐渐为人们接受，普遍认为是目前最优秀的公钥方案之一。

SET（Secure Electronic Transaction）协议中要求CA采用2048比特长的密钥，其他实体使用1024比特的密钥。RSA密钥长度随着保密级别提高，增加很快。

这种算法1978年就出现了，它是第一个既能用于数据加密也能用于数字签名的算法。它易于理解和操作，也很流行。算法的名字以发明者的名字命名：Ron Rivest, Adi Shamir和Leonard Adleman。早在1973年，英国国家通信总局的数学家Clifford Cocks就发现了类似的算法。但是他的发现被列为绝密，直到1998年才公之于世。

RSA算法是一种非对称密码算法，所谓非对称，就是指该算法需要一对密钥，使用其中一个加密，则需要用另一个才能解密。

RSA的算法涉及3个参数： N 、e、d。

1．产生所需密钥

① 选取两个素数 p 、 q 。

② 计算 N = p ＊ q 。

③ 随机选取加密密钥e，并使其满足1<e<ϕ（ n ）, GCD（e, ϕ（ n ））=1，那么公钥就是（e, n ）。

④ 计算解密密钥d，需要满足条件e＊d=1（modϕ（ n ））。

自己保存好私钥（d, n ）和公开公钥，其中ϕ是Euler函数：ϕ（ n ）=（ p -1）（ q -1）。

2．加密过程

在要加密明文M之前，需要先得到公钥e和N，并进行一次计算，以获得密文C，具体可表示为C=M e mod N。

3．解密过程

收到密文C后，用自己的密钥d去解密出明文M，具体可表示为M=C d mod N

RSA系统执行数字签名的运算正好与加密过程是互逆的。

RSA的安全性依赖于大数分解，但是否等同于大数分解一直未能得到理论上的证明，因为没有证明破解RSA就一定需要作大数分解。假设存在一种无须分解大数的算法，那它肯定可以修改成为大数分解算法。RSA的一些变种算法已被证明等价于大数分解。不管怎样，分解 n 是最显然的攻击方法。人们已能分解多个十进制位的大素数。因此，模数 n 必须选大一些，因具体适用情况而定。

RSA的缺点如下。

① 产生密钥很麻烦，受到素数产生技术的限制，因而难以做到一次一密。

② 安全性，RSA的安全性依赖于大数的因子分解，但并没有从理论上证明破译RSA的难度与大数分解难度等价，而且密码学界多数人士倾向于因子分解不是NP问题。现今，人们已能分解140多个十进制位的大素数，这就要求使用更长的密钥，速度更慢；另外，人们正在积极寻找攻击RSA的方法，如选择密文攻击，一般攻击者是将某一信息做一下伪装（Blind），让拥有私钥的实体签署。然后，经过计算就可得到它所想要的信息。实际上，攻击利用的都是同一个弱点，即存在这样一个事实：乘幂保留了输入的乘法结构：

(XM)d = Xd ＊ Md mod n

前面已经提到，这个固有的问题来自于公钥密码系统的最有用的特征——每个人都能使用公钥。但从算法上无法解决这一问题，主要措施有两条：一条是采用好的公钥协议，保证工作过程中实体不对其他实体任意产生的信息解密，不对自己一无所知的信息签名；另一条是决不对陌生人送来的随机文档签名，签名时首先使用One-Way Hash Function对文档作HASH处理，或同时使用不同的签名算法。除了利用公共模数，人们还尝试一些利用解密指数或ϕ（ n ）等攻击。

③速度太慢，由于RSA的分组长度太大，为保证安全性， n 至少也要在600比特以上，使运算代价很高，尤其是速度较慢，较对称密码算法慢几个数量级；且随着大数分解技术的发展，这个长度还在增加，不利于数据格式的标准化。SET协议中要求CA采用2048比特长的密钥，其他实体使用1024比特的密钥。为了速度问题，人们广泛使用单钥，公钥密码结合使用的方法，优缺点互补，单钥密码加密速度快，人们用它来加密较长的文件，然后用RSA来给文件密钥加密，极好地解决了单钥密码的密钥分发问题。

RSA算法的优点是密钥空间大，如果RSA加密算法和DES加密算法结合使用，则正好弥补RSA的缺点。即DES用于明文加密，RSA用于DES密钥的加密。由于DES加密速度快，适合加密较长的报文，而RSA可解决DES密钥分配的问题。

伴随网络技术的发展，电子商务的广泛开展，电子邮件也被普遍采用，数字签名技术也就显得越来越重要。RSA算法虽然有点复杂，速度慢，但目前还是被广泛地应用于数字签名中。随着对RSA的深入研究，目前RSA正在走向实用化、商业化的道路，可以看到，在网络安全中基于RSA网络安全系统的设计将会被人们广泛接受。

3.3.3 ElGamal公钥算法

ElGamal算法是一种较为常见的加密算法，它是基于1984年提出的公钥密码体制和椭圆曲线加密体系，既能用于数据加密，也能用于数字签名，其安全性依赖于计算有限域上离散对数这一难题。在加密过程中，生成的密文长度是明文的两倍，且每次加密后都会在密文中生成一个随机数 K ，在密码中主要应用离散对数问题的几个性质：求解离散对数（可能）是困难的，而其逆运算指数运算可以应用平方—乘的方法有效地计算。也就是说，在适当的群G中，指数函数是单向函数。

密钥对产生办法：首先选择一个素数 p ，获取一个素数 p 的一个原根 g （若 g 模 p 的阶等于ϕ（ p ），则称 g 为模 p 的一个原根。（其中ϕ（ p ）表示 p 的欧拉函数，即所有<= p 的正整数中和 p 互质的整数的个数）和一个随机数 x ，且 g 和 x 均小于 p ，计算 y = g ^ x ( mod p )，则其公钥为 y , g 和 p 。私钥是 x 。 g 和 p 可由一组用户共享。

ElGamal用于数字签名。被签信息为M，首先选择一个随机数 k , k 与 p -1互质，计算

a = g ^ k (mod p )

再用扩展Euclidean算法对下面方程求解 b ：

M= xa + kb (mod p -1)

签名就是( a, b )。随机数 k 须丢弃。

验证时要验证下式：

y ^ a ＊ a ^ b (mod p )= g ^M(mod p )

同时一定要检验是否满足1<= a < p 。否则签名容易伪造。

ElGamal用于加密。被加密信息为M，首先选择一个随机数 k , k 与 p -1互质，计算

a = g ^ k ( mod p )

b = y ^ k M ( mod p )

( a, b )为密文，是明文的两倍长。解密时计算：

M = b / a^ x ( mod p )

ElGamal签名的安全性依赖于乘法群(IF p ) ＊上的离散对数计算。素数 p 必须足够大，且 p -1至少包含一个大素数，因子以抵抗Pohlig & Hellman算法的攻击。M一般都应采用信息的HASH值（如SHA算法）。ElGamal的安全性主要依赖于 p 和 g ，若选取不当则签名容易伪造，应保证 g 对于 p -1的大素数因子不可约。

ElGamal数字签名方案：在系统中有两个用户A和B, A要发送消息到B，并对发送的消息进行签名。B收到A发送的消息和签名后进行验证。

1．系统初始化

选取一个大的素数 p , g 是GF( p )的本原元。h:GF( p )→GF( p )，是一个单向Hash函数。系统将参数 p 、 g 和 h 存放于公用的文件中，在系统中的每一个用户都可以从公开的文件中获得上述参数。

2．对发送的消息进行数字签名的过程

假定用户A要向B发送消息m [1, p -1]，并对消息m签字。第一步是用户A选取一个 x [1, p -1]作为秘密密钥，计算 y = (mod p )作为公钥。将公钥y存放于公用的文件中。第二步是随机选取 k [1, p -1]且gcd( k , ( p -1))=1，计算 r = (mod p )。对一般的ElGamal型数字签名方案有签名方程（Signature Equation）:a x =b k +c(mod( p -1))。

其中（a, b, c）是（h(m), r , s ）数学组合的一个置换。由签名方程可以解出 s 。那么(m, ( r , s ))就是A对消息m的数字签名。第三步是A将（m, ( r , s )）发送到B。

3．数字签名的验证过程

当B接收到A发送的消息（m, ( r , s )），再从系统公开文件和A的公开文件中获得系统公用参数 p , g , h 和A的公钥y。由（m, ( r , s )）计算出（a, b, c）验证等式： = (mod p )是否成立。

D.Bleichenbache“GeneratingElGamal Signatures Without Knowing the Secret Key”中提到了一些攻击方法和对策。ElGamal的一个不足之处是它的密文成倍扩张。

美国的DSS（Digital Signature Standard）的DSA（Digital Signature Algorithm）算法是经ElGamal算法演变而来。

3.3.4 Diffi e-Hellman密钥交换系统

Diffie-Hellman是一种确保共享KEY安全穿越不安全网络的方法，它是OAKLEY的一个组成部分。Whitefield与Martin Hellman在1976年提出了一个奇妙的密钥交换协议，称为Diffie-Hellman密钥交换协议/算法（Diffie-Hellman Key Exchange/Agreement Algorithm）。这个机制的巧妙在于需要安全通信的双方可以用这个方法确定对称密钥，然后可以用这个密钥进行加密和解密。但是注意，这个密钥交换协议/算法只能用于密钥的交换，而不能进行消息的加密和解密。双方确定要用的密钥后，要使用其他对称密钥操作加密算法实际加密和解密消息。

由于该算法本身限于密钥交换的用途，被许多商用产品用作密钥交换技术，因此该算法通常称为Diffie-Hellman密钥交换（简写为DH算法，基于DH算法的密钥交换通常也称为DH交换）。这种密钥交换技术的目的在于使得两个用户安全地交换一个秘密密钥以便用于以后的报文加密。Diffie-Hellman密钥交换算法的有效性依赖于计算离散对数的难度。简言之，可以如下定义离散对数：首先定义一个素数 p 的原根，为其各次幂产生从1到 p -1的所有整数根，也就是说，如果 a 是素数 p 的一个原根，那么数值 a mod p , a 2 mod p , ..., a （ p -1）mod p 是各不相同的整数，并且以某种排列方式组成了从1到 p -1的所有整数。对于一个整数 b 和素数 p 的一个原根 a ，可以找到唯一的指数 i ，使得 b = a ^ i mod p，其中0 ≤ i ≤（ p -1），指数 i 称为 b 的以 a 为基数的模 p 的离散对数或者指数，该值被记为inda, p(b)。

基于原根的定义及性质，可以定义Diffie-Hellman密钥交换算法，该算法描述如下。

① 有两个全局公开的参数，一个素数 q 和一个整数 a , a 是 q 的一个原根。

② 假设用户A和B希望交换一个密钥，用户A选择一个作为私有密钥的随机数XA(XA< q )，并计算公开密钥YA= a ^XA mod q 。A对XA的值保密存放而使YA能被B公开获得。类似地，用户B选择一个私有的随机数XB< q ，并计算公开密钥YB= a ^XB mod q 。B对XB的值保密存放而使YB能被A公开获得。

③ 用户A产生共享秘密密钥的计算方式是 K = (YB)^XA mod q 。同样，用户B产生共享秘密密钥的计算是 K = (YA)^XB mod q 。这两个计算产生相同的结果。 K = (YB)^XA mod q =( a ^XB mod q )^XA mod q = ( a ^XB)^XA mod q （根据取模运算规则得到）= a ^(XBXA) mod q =(a^XA)^XB mod q = ( a ^XA mod q )^XB mod q = (YA)^XB mod q ，因此相当于双方已经交换了一个相同的秘密密钥。

④ 因为XA和XB是保密的，一个敌对方可以利用的参数只有 q , a , YA和YB。因而敌对方被迫取离散对数来确定密钥。例如，要获取用户B的秘密密钥，敌对方必须先计算XB =inda, q (YB)，然后再使用用户B采用的同样方法计算其秘密密钥K。Diffie-Hellman密钥交换算法的安全性依赖于这样一个事实：虽然计算以一个素数为模的指数相对容易，但计算离散对数却很困难。对于大的素数，计算出离散对数几乎是不可能的。下面给出例子。密钥交换基于素数 q = 97和97的一个原根 a = 5。A和B分别选择私有密钥XA = 36和XB = 58。每人计算其公开密钥YA = 5^36 = 50 mod 97 YB = 5^58 = 44 mod 97。在他们相互获取了公开密钥之后，各自通过计算得到双方共享的秘密密钥如下：K = (YB)^XA mod 97 = 44^36 = 75 mod 97, K = (YA)^XB mod 97 = 50^58 = 75 mod 97，从|50,44|出发，攻击者要计算出75很不容易。下面给出了一个利用Diffie-Hellman计算的简单协议。

假设用户A希望与用户B建立一个连接，并用一个共享的秘密密钥加密在该连接上传输的报文。用户A产生一个一次性的私有密钥XA，计算出公开密钥YA并将其发送给用户B。用户B产生一个私有密钥XB，计算出公开密钥YB并将它发送给用户A作为响应。必要的公开数值 q 和 a 都需要提前知道。另一种方法是用户A选择 q 和 a 的值，并将这些数值包含在第一个报文中。下面再举一个使用Diffie-Hellman算法的例子。假设有一组用户（例如一个局域网上的所有用户），每个人都产生一个长期的私有密钥XA，并计算一个公开密钥YA。这些公开密钥数值，连同全局公开数值 q 和 a 都存储在某个中央目录中。在任何时刻，用户B都可以访问用户A的公开数值，计算一个秘密密钥，并使用这个密钥发送一个加密报文给A。如果中央目录是可信任的，那么这种形式的通信就提供了保密性和一定程度的鉴别功能。因为只有A和B可以确定这个密钥，其他用户都无法解读报文（保密性）。接收方A知道只有用户B才能使用此密钥生成这个报文（鉴别）。Diffie-Hellman算法具有两个吸引力的特征：仅当需要时才生成密钥，减小了将密钥存储很长一段时间而致使遭受攻击的机会。除对全局参数的约定外，密钥交换不需要事先存在的基础结构。

然而，该技术也存在许多不足：没有提供双方身份的任何信息。它是计算密集性的，因此容易遭受阻塞性攻击，即对手请求大量的密钥。受攻击者花费了相对多的计算资源来求解无用的幂系数而不是在做真正的工作，没办法防止重演攻击，容易遭受中间人的攻击，第三方C在和A通信时扮演B；和B通信时扮演A。A和B都与C协商了一个密钥，然后C就可以监听和传递通信量。中间人的攻击按如下进行：B在给A的报文中发送他的公开密钥，C截获并解析该报文，C将B的公开密钥保存下来并给A发送报文，该报文具有B的用户ID但使用C的公开密钥YC，仍按照好像是来自B的样子被发送出去。A收到C的报文后，将YC和B的用户ID存储在一块。类似地，C使用YC向B发送好像来自A的报文。B基于私有密钥XB和YC计算秘密密钥K1。A基于私有密钥XA和YC计算秘密密钥K2。C使用私有密钥XC和YB计算K1，并使用XC和YA计算K2。从现在开始，C就可以转发A发给B的报文或转发B发给A的报文，在途中根据需要修改它们的密文，使得A和B都不知道他们在和C共享通信。

3.3.5 DSA数字签名算法

数字签名是保证电子商务安全性的关键技术之一，在保证数据的完整性、私有性、不可抵赖性等方面起着极其重要的作用。本节探讨了基于DSA（Digital Signature Algorithm）的数字签名技术，包括DSA的产生与验证过程及算法的应用、改进与发展。

算法中应用了下述参数。

p : L bits长的素数。 L 是64的倍数，范围是512～1024。

q : p -1的160bits的素因子。

g : g = h ^(( p -1)/ q )mod p , h 满足 h < p -1, h ^(( p -1)/ q )mod p >1。

x : x < q , x 为私钥。

y : y = g ^ x mod p , ( p , q , g , y )为公钥。

H( x ):One-Way Hash函数。DSS中选用SHA（Secure Hash Algorithm）。

p , q , g 可由一组用户共享，但在实际应用中，使用公共模数可能会带来一定的威胁。签名及验证协议如下。

（1） P 产生随机数 k , k < q 。

（2） P 计算 r = ( g ^ k mod p ) mod q 。

s =( k ^(-1)(H( m )+ xr ))mod q。

签名结果是( m , r , s )。

（3）验证时计算 w = s ^(-1)mod q 。

u 1=(H( m )＊ w )mod q 。

u 2=( r ＊ w )mod q 。

v =(( g ^ u 1＊ y ^ u 2)mod p )mod q 。

若 v = r ，则认为签名有效。

DSA是基于整数有限域离散对数难题，所以它的安全性与RSA差不多。DSA的一个重要特点就是两个素数公开，这样，当使用别人的 p 和 q 时，即使不知道私钥，也能确认它们是否是随机产生的，但使用RSA算法就做不到这一点。

3.4 邮件加密软件PGP详解

PGP（Pretty Good Privacy，更好地保护隐私）是一个基于RSA公钥和私钥及AES等加密算法的加密软件系列。常用的版本是PGP Desktop和Professional（PGP专业桌面版），它包含邮件加密与身份确认，资料公钥和私钥加密，硬盘及移动盘全盘密码保护，网络共享资料加密，PGP自解压文档创建，资料安全擦除等众多功能。由于赛门铁克公司的收购影响，PGP从10.0.2以后，将不再单独放出PGP版本的独立安装包形式，将会以安全插件等形式集成于诺顿等赛门铁克公司安全产品里。

3.4.1 认识PGP

PGP是一个基于RSA公钥加密体系的邮件加密软件，可以用它对邮件保密以防止非授权者阅读，它还能对邮件加上数字签名从而使收信人可以确认邮件的发送者，并能确信邮件没有被篡改。它可以提供一种安全的通信方式，而事先并不需要任何保密的渠道用来传递密钥。它采用了一种RSA和传统加密的杂合算法，用于数字签名的邮件文摘算法，加密前压缩等，还有一个良好的人机工程设计。它的功能强大，有很快的速度。而且它的源代码是免费的。

使用PGP加密算法可以安全地和从未见过的网友彼此进行电子邮件通信，并不需要任何保密的通道用来传递密钥。它采用了审慎的密钥管理，即一种RSA和传统加密的杂合算法，用于数字签名的邮件文摘算法、加密前压缩等。实际上，使用PGP还可以用来加密文件，也可以用PGP生成RADIX 64格式的编码文件。

邮件本身的安全，先要保证邮件来自非法用户窃取或更改，同时接收者也必须能确定该邮件是由合法发送者发出的。可以使用公用密钥系统来达到这个目的。

在实际使用中，用户自己持有一把钥匙，称为私钥，将另一把钥匙公开，这把钥匙称为公钥。当用户向外发送邮件时，先使用一种单向分解函数从邮件中得到固定长度的分解值，该值与邮件的内容相关，称为该邮件的指纹，然后使用自己的密钥对指纹进行加密。

接受者能使用自己的公钥进行解密，然后重新生成指纹进行比较，这保证邮件是由本人发送而非假冒别人的名义，同时也保证邮件在发送过程中没有被更改，这个过程称为数字签名和核实。也可以使用接收者的公钥进行加密，这就保证了只有拥有对应密钥的接收者，才能通过进行解密来得到电子邮件的明文信息。

在现代社会里，电子邮件和网络上的文件传输已成为生活中不可缺少的重要内容，由于在Internet上传输的数据都是公开的，如果自己不保护自己的信息，第三者就会轻易获得私密的信息。还有一个问题就是信息认证，如何让收信人确信邮件没有被第三者篡改，就需要数字签名技术。RSA公钥体系的特点使它非常满足上述两个要求：保密性和认证性。

RSA（Rivest Shamir Adleman）算法是一种基于大数不可能质因数分解假设的公钥体系，简单地说就是找两个很大的质数，一个公开，一个不公开。一个称为“公钥”，另一个称为“私钥”。这两个密钥是互补的，就是说用公钥加密的密文可以用私钥解密，反过来也一样。

假设甲要寄信给乙，互相知道对方的公钥。甲就用乙的公钥加密邮件寄出，乙在收到邮件之后，就可以用自己的私钥解密出甲的原文。由于乙的私钥是保密的，所以即使是甲本人，也无法解密那封信，这就解决了信件保密的问题。另一方面，由于每个人都知道乙的公钥，都可以给乙发信，那么乙就无法确信是不是甲的来信。这时候认证的问题就出现了，就需要用到数字签名了。

在说明数字签名前先要解释一下“邮件文摘”（Message Digest）的问题，即对一封邮件用某种算法算出一个最能体现这封邮件特征的数，一旦邮件有任何改变这个数都会变化，那么这个数加上作者的名字，还有日期等，就可以作为一个签名了。

确切地说，PGP是用一个128位的二进制数作为“邮件文摘”的，用来产生它的算法叫MD5（MD5本身是公用软件）。MD5是一种单向散列算法，它不像CRC校验码，很难找到一份替代的邮件与原件具有同样的MD5特征值。

再来讲一下数字签名，当甲在用自己的私钥将上述128位的特征值加密，并附加在邮件之后，再用乙的公钥将整个邮件加密。这样，当这份密文被乙收到之后，乙就需要自己的私钥将邮件解密，来得到甲的原文和签名。此时，乙的PGP也从原文计算出一个128位的特征值，来和用甲的公钥解密签名所得到的数比较，如果符合就说明这份邮件确实是甲寄来的。这样两个安全性要求都得到了满足。

PGP还可以只签名而不加密，这适用于公开发表声明时，声明人为了证实自己的身份，可以用自己的私钥签名。这样就可以让收件人能确认发信人的身份，也可以防止发信人抵赖自己的声明。这一点在商业领域有很大的应用前途，可以防止发信人抵赖和信件被途中篡改。

3.4.2 PGP邮件加密软件的原理

PGP加密系统是采用公开密钥加密与传统密钥加密相结合的一种加密技术。它使用一对数学上相关的钥匙，其中一个（公钥）用来加密信息，另一个（私钥）用来解密信息。

PGP采用的传统加密技术部分所使用的密钥称为“会话密钥”（sek）。每次使用时，PGP都随机产生一个128位的IDEA会话密钥，用来加密报文。公开密钥加密技术中的公钥和私钥则用来加密会话密钥，并通过它间接地保护报文内容。

PGP中的每个公钥和私钥都伴随着一个密钥证书。它一般包含以下内容。

① 密钥内容（用长达百位的大数字表示的密钥）。

② 密钥类型（表示该密钥为公钥还是私钥）。

③ 密钥长度（密钥的长度，以二进制位表示）。

④ 密钥编号（用以唯一标识该密钥）。

⑤ 创建时间。

⑥ 用户标识（密钥创建人的信息，如姓名、电子邮件等）。

⑦ 密钥指纹（为128位的数字，是密钥内容的提要，表示密钥唯一的特征）。

⑧ 中介人签名（中介人的数字签名，声明该密钥及其所有者的真实性，包括中介人的密钥编号和标识信息）。

⑨ PGP把公钥和私钥存放在密钥环（KEYR）文件中。PGP提供有效的算法查找用户需要的密钥。

⑩ PGP在多处需要用到口令，它主要起到保护私钥的作用。由于私钥太长且无规律，所以难以记忆。PGP把它用口令加密后存入密钥环，这样用户可以用易记的口令间接使用私钥。

⑪ PGP的每个私钥都由一个相应的口令加密。PGP主要在3处需要用户输入口令。

· 需要解开收到的加密信息时，PGP需要用户输入口令，取出私钥解密信息。

· 当用户需要为文件或信息签字时，用户输入口令，取出私钥加密。

· 对磁盘上的文件进行传统加密时，需要用户输入口令。

PGP安全的核心是公钥的安全性，和传统单密钥体系一样，私钥的保密也是具有决定性的。相对公钥而言，私钥不存在被篡改的问题，但存在泄露的问题。

RSA的私钥是一个很长的数字，用户不可能将其记住，PGP的办法是让用户为随机生成的RSA私钥指定一个口令。只有通过给出口令才能将私钥释放出来使用，由于用口令加密私钥方法的保密程度和PGP本身一样，所以私钥的安全性问题，实际上就是对用户口令的保密。当然，私钥文件本身失密也很危险，因为破译者只需要使用穷举法就可以试探出口令了。

PGP在安全性问题上的审慎考虑体现在PGP的各个环节。例如，每次加密的实际密钥是个随机数，大家都知道计算机是无法产生真正的随机数的。PGP程序对随机数的产生是很审慎的，像产生RSA密钥就是从用户敲键盘的时间间隔上取得随机数种子的，而对于磁盘上的Randseed.bin文件，则是采用和邮件同样强度的加密的，这样就有效地防止了他人从Randseed.bin文件中分析出加密实际密钥的规律来。

PGP的加密前预压缩处理，PGP内核使用了PKZIP算法来压缩加密前的明文。一方面，对电子邮件而言，压缩后加密再经过7bits编码密文有可能比明文更短，这就节省了网络传输的时间；另一方面，明文经过压缩，实际上相当于经过一次变换，信息更加杂乱，明文抵御更强。

3.4.3 PGP的安全问题

安全程序的使用不能保证你的通信就是安全的。就算你在房子前门安装一个最安全的锁，小偷仍然可以从开着的窗户爬进来。同样，即使使用了PGP，你的计算机也仍可能很脆弱。

有许多有名的对PGP的攻击，下面的部分就介绍这些攻击。然而，这些绝不是一个完整的清单。将来的攻击很可能会攻破所有的公钥加密技术。这份清单只是让你了解一下保护通信时需要做些什么。

1．蛮力攻击

对PGP最直接的攻击是蛮力攻击使用的密钥。因为PGP 2.6.2使用了两个加密算法，所以要看看这两个算法的安全性。对于公钥加密技术，PGP使用RSA算法；对于私钥加密技术，它使用IDEA。

（1）蛮力攻击RSA密钥

对于RSA密钥，已知最好的蛮力攻击是分解它们。RSA密钥产生时就使得它们难于分解。而且，分解大的数仍然是一门很新的艺术。

最近，最大的一个被分解的RSA密钥是RSA-129，它于1994年4月被分解。RSA-129是在设计RSA算法时于1977年创建的原始RSA提问。它是一个129位的十进制RSA密钥，相当于425位。分解这个数动员了全世界范围的力量，使用了1600台计算机，实际耗费时间超过8个月。

它处理了4600MIPS年的数据，1MIPS年是1MIPS的机器一年所能处理的数据量。例如，一个Pentium 100大约是125MIPS（根据Intel）。如果一个Pentium 100机器为解决一个问题一年时间不停地运行，它就贡献了125MIPS年。按照这种速度，一台机器要花37年才能破解RSA-129。如果使用100台机器，只需要4个月就能破译这个代码，只是实际项目一半的时间。

当前，PGP 2.6.2版使用从512到2048位的密钥。密钥越大，分解越困难。同时，增加密钥长度也会增加使用密钥的时间。从时间来说，据说一个512位的密钥能够给予1年的安全性；访问100台Pentium 100机器要花至少一年才能破译出512位的RSA密钥。如果这是真的，那么一个1024位的密钥，若使用今天最新的算法，假设在技术上没有提高的话，在以后10000年都是安全的。如果技术上有所提高，需要的时间就会少些。然而，看起来技术可能一直在进展。

（2）蛮力攻击IDEA密钥

现在还没听说有人攻击IDEA密钥。最好是尝试2 128 或3.4×10 38 个密钥。由于完成这一测试的困难性，尝试破译PGP中用于加密IDEA密钥的RSA密钥更容易。据估计破译IDEA的困难与分解3000位RSA密钥的困难相当。

2．私钥和通过短语

PGP私钥环的安全性基于两件事：对私钥环数据的访问和对用于加密每个私钥的通过短语的了解。使用私钥要拥有这两部分。然而，这也引起了许多攻击。

如果PGP用于多用户系统中，就可能访问私钥环。通过缓存文件，网络窥视或者许多其他的攻击，可以利用监视网络或者读取磁盘得到私钥环。这样就只剩下通过短语用来保护私钥环中的数据了，这就意味着只要获得通过短语就能攻破PGP的安全。

而且，在一个多用户系统中，键盘和CPU之间的链路可能是不安全的。如果有人能够物理上访问连接用户键盘和主机的网络，监视击键是很容易的。例如，用户可能从一群公共的客户终端上登录，这时就可以窥探连接网络得到通过短语。另外，用户还可能通过调制解调器拨叫，在这种情况下窃贼就可以监听键盘击键。在任何一种情况下，在一个多用户的机器上运行PGP都是不安全的。

当然，运行PGP最安全的方法是在一台没有其他人使用而且不联网的个人机上运行；也就是说，一台笔记本或家庭计算机。用户必须在安全环境的代价和安全通信的代价之间找到一种平衡。使用PGP的推荐方法是：总是在安全环境下的安全机器上动用，这样用户就可以控制整个机器。

最好的安全性关键在于键盘和CPU之间的连接是安全的。这既可以通过加密来完成，或者更好一点通过直接、无中断的连接实现。工作站、PC、Mac、膝上型机器都属于安全的机器。

安全的环境更难于显示，这里没有加以探讨。

3．对公钥环的攻击

由于公钥环（公有密钥环）的重要性和对它的依赖性，PGP受到许多针对密钥环的攻击。首先，只有当密钥环改变时才被检查。当添加新的密钥或签名时，PGP验证它们。然而，它会标记密钥环中已检查过的签名，这样就不会再去验证它们。如果有人修改了密钥环，并且设置了签名中相应的位，就不会被检查出来。

对PGP密钥环的另一种攻击集中于PGP使用的进程，它对密钥有效性设置1位。当到达一个密钥的新签名时，PGP就使用前面描述过的信任网络值计算该密钥的有效位。然后PGP在公有密钥环中缓存这个有效位。一个攻击者可能会在密钥环中修改这位，从而迫使用户相信一个无效密钥是有效的。例如，通过设置这个标志，攻击者能够使得用户相信一个密钥属于Alice，尽管没有足够的签名证明这个密钥的有效性。

也可能发生对PGP公钥环的另一种攻击，因为作为介绍人的密钥信任也缓存在公有密钥环中。这个值定义密钥的签名有多少信任度，因此如果使用带有无效参数的密钥签名，就可能使PGP把无效密钥作为有效密钥接受。如果一个密钥被修改为完全受托的介绍人，那么用这个密钥签名的任何密钥都被信任为有效的。因此，一个攻击者如果用一个修改过的密钥为另一个密钥签名，就会使得用户相信它是有效的。

公钥环最大的问题是所有这些位不仅在公钥环中缓存，而且密钥环中没有任何保护。

读过PGP源代码而且能够访问公钥环的任何人都可以使用一个二进制文件编辑器修改任何一位，而密钥环的所有者却无法注意到这个修改。幸运的是，PGP提供了一种方法重新检查密钥环中的密钥。通过联合 -kc和 -km选项，用户可告诉PGP执行对整个密钥环的密钥维护。

前一个选项告诉PGP检查密钥和签名。PGP会查看整个密钥环，重新检查每一个签名。当检查了所有签名之后，PGP将执行一个维护性检查（-km），重新计算所有密钥的有效性。

不幸的是，没有一种办法能够完全重新检查密钥中的所有信任字节，这是一个漏洞。应当有一个命令告诉PGP忽略所有信任字节，从终极密钥，即私钥环中的密钥，向用户询问信任性。

或许PGP将来的版本会改正这个问题。如果一个密钥被改成是可信任的介绍人，你没有办法找到修改之处并改正它。运行密钥和维护检查只能恢复密钥的有效性，但不是信任值。只有对一个密钥运行PGP -ke才能编辑信任参数，而这不能自动完成。

4．程序的安全性

如果有人能够访问PGP程序的二进制文件，他就可以改变它，让它做他想做的任何事。

如果这个介入者能够从你的鼻子底下替换你的PGP二进制文件，你对PGP的信任就建立在你对这个人的信任和你实际验证这个程序的能力上。例如，一个能够进行这种访问的攻击者可能会改变PGP，使得它总是验证签名，甚至签名是无效的。PGP可能被修改，总是向NSA发送所有消息的纯文本复制。这些攻击很难检测，而且难于对付。PGP需要成为受托代码基础的一部分；如果你不信任你的PGP二进制文件，那就不要信任它的输出。

相信PGP二进制文件最好的办法就是自己从源代码建立它。然而，这并不总是可能的。

其他办法包括在它建立时监视它或者从一个受托的来源得到它。查看二进制文件的大小和日期很有帮助。使用其他受托的程序，像md5sum能有所帮助。但这只是把问题压到了另一层。如果你不信任PGP程序，就没有太多做的了。

5．对PGP的其他攻击

对PGP可能还有其他攻击，但是这里不作讨论。从来没有证明过PGP使用的加密算法是安全的。PGP所使用的数学虽然被认为是安全的，但是有可能很容易攻破。对RSA的分解攻击可能得到改进，或者有人可能在IDEA中找到一个漏洞。

6.MD5的安全性问题

MD5算法在PGP中被用来单向变换用户口令和对信息签名的单向散列算法。一种单向散列的强度，主要体现在它能把任意的输入数随机化到什么程度，并且能产生唯一的输出。对单向散列的直接攻击可以分为普通直接攻击和生日攻击。

（1）对MD5的普通直接攻击

普通直接攻击又常常称为野蛮攻击。攻击者为了找到一份和原始明文m散列结果相同的明文m，就是H（m）=H（m）。普通直接攻击就是穷举可能的明文去产生一个和H（m）相同的散列结果。对MD5而言，如果散列结果为128bits，则表明攻击者使用一台每秒尝试1000 000000条明文的机器，理论上也要计算大约10 22 年，才可能会同时发现m本身。

（2）对MD5的生日攻击

这里要提到的就是有名的概率生日问题。在 N 个人中至少有两个人生日相同的概率是多少？MD5生日攻击实际上只是用概率来指导散列冲突的发现，对于MD5而言，如果尝试2 64 条明文，则在它们中间至少有一对发生冲突的概率就是50%。

生日攻击只是为了找到两条能产生同样散列结果的明文。对当今的科技能力而言，它也是不可能的。有研究表明，一台计算机平均需要运行585年，才有可能找到一对同样散列结果的明文，而且还不能马上变成实际的攻击成果。

（3）其他对MD5的攻击

微分攻击被证明是对MD5的一次循环是有效的，但对全部4次循环却毫无效果。

还有一种成功的MD5攻击，不过它是对MD5代码本身做了变化，准确点说是一种破解而不是攻击。如果做了PGP发行包的签名校验，则很容易就发现代码已经被替换掉了。

（4）口令长度和信息论

根据传统信息论，英语中每个8bits字母的信息熵（信息熵是信息论中用于度量信息量的一个概念。一个系统越是有序，信息熵就越低；反之，一个系统越是混乱，信息熵就越高。一般用符号H表示，单位是比特）为1.3bits。如果口令足够长，MD5的结果就会更加随机。

对128bits的MD5输出而言，一个长达98个字符的口令将给出一个随机如下密钥。

     （8/1.3）＊（128/8）= 98.46 chars

但没有人会使用一个像下面这样长的口令：

        12345678901234567890123456789012345678901235678901234567890
      1234567890123456789012345678

1.3 bits的信息熵来自于英语语法的规律性这个事实，字母出现概率的不等造成了熵的减小。如果26个拉丁字母出现的概率均等，信息熵将会增加到：Log(26)/Log(2) = 4.7 bits。

这样，一个随机密钥所需口令长度就减为27.23 chars了，如果再加上大小写和几个符号，还会更少。

7．随机数的安全性问题

PGP有两个伪随机数发生器，分别是ANSI X9.17发生器和从用户击键的时间与序列中计算熵值从而引入随机性。ANSI X9.17 PRNG使用IDEA加密算法来产生随机数种子。利用用户击键信息产生的Randseed.bin文件，在每次加密前后都会使用新的、本身加密保存的随机数。

（1）ANSI X9.17 PRNG

官方发布的ANSI X9.17标准使用了Triple DES作为内核，这样，还可以改用IDEA实现。ANSI X9.17标准需要Randseed.bin中的24 bytes的随机数，PGP把其他384 bytes用来存放其他信息。

ANSI X9.17标准工作过程大致如下。

E（）= IDEA加密函数，使用一个可复用的密钥（使用明文产生）。

T = 从Randseed.bin文件中来的时间

V = 初始化向量

R = 生成的随机密钥（用来加密一次PGP明文）

R = E[E（T）XOR V]

下一次的初始化向量计算如下。

V = E[E（T）XOR R]

（2）用户击键引入随机性

这产生的是真正的随机数，只是在敲击键盘保持无规则可循就可以了。输入的熵越大，输出的随机数的熵就越大。

（3）X9.17用MD5进行预洗

“洗”类似于平时生活中说的洗牌一样，把数据打乱，加密前叫预洗，加密后为下一次加密的准备叫后洗。PGP的日常随机数产生器X19.7是用明文的MD5值来预洗的，它基于攻击者不知道明文这样一个假设。

如果攻击者知道明文，就没有必要去攻击了，当然也有这种可能，只是会削弱一点PRNG的随机性罢了。

（4）Randseed.bin的后洗操作

后洗操作是更安全的操作。更多的随机字节被用来重新初始化Randseed.bin文件，它们被用当前的随机临时PGP密钥来加密。同样，如果攻击者知道了这个密钥，就不用攻击Randseed.bin文件了，而只需关注Randseed.bin文件当前的状态即可，因为里面可能存放着下次加密的部分信息。因此，对Randseed.bin文件的保护和公钥环及私钥环文件同样重要。当然，如果遇到的不是专门研究密码的专家，这些文件即使全部泄露，也是无关紧要的。

8.PGP的密钥和口令的安全性问题

最简单的泄密方式就是把口令写在某地方，又不能保证除自己之外没有其他人能看到。如果让别人得到口令和私钥文件，整个加密体系就无密可言了。

还有就是口令不要太简单，注意PGP用的是“口令”passphase，而不是“密码”password（即口令中可以包含多个词和空格）。一个经验老练的攻击者可能会用一本名言录来破解口令，因此，为了得到好记又难猜的口令，可以生造一些句子或者找些非常生僻的句子。推荐采用一句话中首字母的序列组合，然后在其中加入几个符号，如“.”“-”“;”等，长度最好大于等于8个字符，同时也可夹杂大小写。

例如，从 “We can get it without your passphase”一句话中可以得到“wCgi.wyp”这样一个口令，就是用穷举法试探出这个口令也有一定难度，因为它用到了大小写字母和符号，平均要试探约50 8 次才有可能成功，以IDEA的速度，这在一般大型计算机上也不是轻而易举的事。因此，短的口令只要随机性够大，一样可以做到很安全，而且输入口令使用时间越短，被窥探的可能也越小。

因此，PGP的最大威胁可以认为就是公钥的篡改和冒充，要点就是：当用别人的公钥时，确信它是直接从对方处得来或由另一个可信的人签名认证过的；确信没有人可以篡改自己的公钥环文件；保持对自己密钥环文件的物理控制权，尽量存放在自己的个人电脑里而不是一个远程的分时系统里；备份自己的密钥环文件。

9．没有完全删除的文件

一般的操作系统在删除文件时，都并没有彻底删除文件的数据，当加密明文后虽然将明文删除了，但并没有从物理上把明文的数据清除。

一些有经验的攻击者仍然可能从磁盘数据块中恢复明文。当然，像碎纸机一样，也可以使用一些工具软件从物理上销毁文件的办法。不过，最简单的办法还是通过添加一些无用的信息将明文文件覆盖。值得注意的是，即使覆盖了所有明文曾占用的磁盘空间，仍然会有微小的剩余空间留在磁盘上，专用的设备可以恢复这些数据，只是一般人达不到这个条件。

对于使用的密钥环文件同样存在这个问题，特别是私钥环文件，直接关系到私钥的安全。因此，除了专用的个人计算机，最好不要将密钥环放到其他机器上，让它们留在自己的软盘上是个安全的办法。

10．物理安全性

物理安全性是PGP不能赋予的，这种攻击比密码学分析要直接得多。PGP无法在一个不保密的环境中保护未加密的明文。

当然，物理安全性也包括对PGP数据的物理安全保护，像防火、防水、防雷等。

11．多用户系统下的泄密

PGP最初是为MS-DOS系统设计的，它认为系统在用户的直接物理控制下。但随着PGP的普及，多用户系统上也出现了PGP，这就在很大程度上提高了暴露明文、密钥或口令的可能性。但也有安全的多用户系统，禁得起所有入侵者所能获得的手段攻击，或其用户都是可以信赖的，要不就是根本没有人对这一系统进行攻击。

正如现实的PGP攻击中，在多用户系统中泄密的风险要大得多。所以尽量在单用户系统中使用PGP，而且保证系统处于自己的直接物理控制之下。

12.PGP的时间标戳可靠性

PGP签名上的时间标戳也可能是错误的，因为任何想伪造一个“错误”时间标戳的人，都可以通过修改系统时间达到目的。

而在商业上，又有这种利用PGP签名时间来确认责任的需要，这样，第三方的时间公证体系就被建立了。很明显，只要公证方在邮件上签上标准的时间，就解决了这个问题。实际上，这个问题对于手写的签名也存在，签字时需要一个公证人，来证明签名的时间（数字签名也一样）。

PGP设计模式是让第三方提供公证服务，服务器对每个送来的签名，自动加上自己的签名后返回，并同时留下一份记录，这份记录是公开的，需要进行时间验证的人可以去查看这个记录。

13．流量攻击

流量分析是指攻击者阅读不到密文的真实内容，但却可以通过观察邮件从哪儿来、到哪儿去、邮件大小以及邮件发送的时间等，从而获得一些有用的信息，就像可以查阅长途电话费单，但却不可能知道谈话的内容。

单独靠PGP是阻止不了攻击者进行流量分析的，借助一些网络通信协议可以防止这些信息的暴露，最好可以采用另一些加密通信体系的协助。

14．现实的PGP攻击揭秘

上面所说的攻击都是一些一般攻击者可能做不到或太费事的攻击方法，实际上，还有一些“可行的”PGP攻击，它们不是攻击PGP密码体系本身，而是攻击PGP的实现系统。

（1）击键窥探

这是一种非常有效的攻击方法，简单地说就是记录用户的击键信息，从中获得口令。攻击者通过键盘记录器窥探用户的击键序列，具体方法会由于系统不同而不同。

目前已经出现了至少一种Windows下的记录器，这就对基于Windows系统的PGP外壳产生了威胁。如果被攻击者是在X-Windows系统环境下输入了口令，则X-Windows系统环境下的记录器将不用root权限。要防止这种攻击，最好还是对工作环境进行仔细检查，同时做好私钥环文件的保存。

（2）电磁泄露窥探

任何计算机设备（尤其是显示器）都存在电磁泄露的问题，通过合适的设备可以收到目标显示器上的信息，那么明文显示时就无密可言了。

这里就是通过类似装置监听到一个间谍的显示器和键盘信号的案例。

在要窥探的计算机里偷偷设置发射器，远程接收信号，然后通过专用的FFT芯片去除噪音，完成了取证工作。射频信号大约22MHz，在接收端加上27KHz的水平同步信号和59.94Hz的垂直同步信号，就可以得到清晰的图像。由于键盘用的是串行单片机通信接口，则信号将更容易稳定。加装一个射频信号干扰器可以有效防止显示器信号泄露。键盘信号传不远，只要没人在计算机里安“耳朵”，就不怕泄露。

（3）磁盘缓存窥探

在Windows这样的多任务操作系统中，系统会把内存中的内容交换到磁盘上，而且这些交换文件对用户是透明的。令人担忧的是，这些内容并不会很快被清除，有可能一直在磁盘上保留。如果在网络环境中，可能会在连用户自己都感觉不到的情况下，信息就被人窃取了。

（4）报文嗅探

在网络环境下，信息是以报文的形式在线路中传输的。如果是通过网络远程使用PGP，就有可能被人从报文传输途中监听到。

要想知道什么安全、什么不安全，对密码技术所运用的数学知识了解得还不够。实际上，据知任何事都不是完全安全的，如果有足够的计算能力，任何形式的密码技术都可以攻破。

问题是破译代码所花费的时间和精力与被保护的数据价值相比是否值得。注意破译代码所花费的力量将随着时间不断地减少，因为计算机的能力不断增强，而价格不断地下降。到现在为止，密码专家仍然超前于破译者。

3.5 小结

本章对数据加密的几类加密算法做了详细、系统的分析，并且对目前流行的PGP技术进行了详细的剖析，对其使用的算法以及安全性进行了详解，其实，PGP只是一个由很多种算法组合的杂合算法。但不可避免的是，由于其中的每种算法都有不同方面的安全问题，因此，PGP作为集成多种算法的一个杂合算法，也仍然是无法做到绝对安全，牢不可破的，破解高手仍然有可能通过使用各种方法，对PGP中的每一种加密算法进行攻击。希望通过本章的介绍，能够使广大读者认识到计算机数据安全的重要性，弄懂多种数据加密算法的基本原理，并掌握一定的数据加密技术来解决数据加密方面的问题。由于数据加密算法都是基于深奥的数学难题发展而来的，因此，用户要想深入理解运用加密算法，最好还是有必要补充一些高等数学知识。

技巧与问答

❖ 信息安全领域中应用的Hash算法有什么关键特性？

① 单向性（one-way），从预映射，能够简单迅速地得到散列值，而在计算上不可能构造一个预映射，使其散列结果等于某个特定的散列值，即构造相应的M=H-1(h)不可行。这样，散列值就能在统计上唯一地表征输入值，因此，密码学上的Hash又称为“消息摘要”（message digest），就是要求能方便地将“消息”进行“摘要”，但在“摘要”中无法得到比“摘要”本身更多的关于“消息”的信息。

② 抗冲突性（collision-resistant），即在统计上无法产生2个散列值相同的预映射。给定M，计算上无法找到M'，满足H(M)=H(M')，此谓弱抗冲突性；计算上也难以寻找一对任意的M和M'，使满足H(M)=H(M')，此谓强抗冲突性。要求“强抗冲突性”主要是为了防范所谓的“生日攻击”（birthday attack），在一个10人的团体中，你能找到和你生日相同的人的概率是2.4%，而在同一团体中，有2人生日相同的概率是11.7%。类似的，当预映射的空间很大的情况下，算法必须有足够的强度来保证不能轻易找到“相同生日”的人。

③ 映射分布均匀性和差分分布均匀性，散列结果中，为0的bit和为1的bit，其总数应该大致相等；输入一个bit的变化，散列结果中将有一半以上的bit改变，这又叫作“雪崩效应”（avalanche effect）；要实现使散列结果中出现1bit的变化，则输入中至少有一半以上的bit必须发生变化。其实质是必须使输入中每一个bit的信息，尽量均匀地反映到输出的每一个bit上去；输出中的每一个bit，都是输入中尽可能多bit的信息一起作用的结果。

❖ 数字签名和加密的区别是什么？

① 数字签名采用公开密钥算法实现，数字签名与通常的数据加密算法作用是不同的，它们的实现过程与使用的密钥不同。数字签名使用的是发送方的密钥对，发送方用自己的私有密钥进行加密，接收方用发送方的公开密钥进行解密。数字签名是一个一对多关系：任何拥有发送方公开密钥的人都可验证数字签名的正确性。数字签名是为了证实信息确实是由某个用户发送，对网络中是否有人看到该信息并不关心。

② 数据加密使用的是接受方的密钥对，发送方用接收方的公开密钥进行加密，接受方用自己的私有密钥进行解密。加密是一个多对一的关系：任何知道接受方公开密钥的人都可以向接收方发送加密信息，只有拥有接收方私有密钥的人才能对信息解密。一个用户通常有两个密钥对，一个用来对数字签名进行加密解密，一个用来对私密密钥进行加密解密。

❖ PGP使用之前的设置有哪些？

① 需要对用户自己的密钥进行一些设置。

② 可以把用户生成的一对密钥中的公钥传到PGP公司的公钥服务器上，以后收件人如果想接收用此公钥加密的邮件，需要直接到PGP钥服务器上下载该公钥并进行验证，而不需要把公钥放在电子邮件上一起发送，提高PGP加密的安全性。

第4章
解密基础知识

用户经常使用到的软件都是由软件作者事先编译好，再供用户通过机器代码程序运行的。因此，必须使用静态或动态调试工具，才能分析并跟踪其汇编代码。

软件的破解技术与保护技术这两者之间本身就是矛与盾的关系，它们是在相互斗争中发展进化的，这种技术上的较量归根结底是一种利益的冲突。从理论上说，几乎没有破解不了的加密保护。用户要想掌握软件的破解技术，必须了解解密技术基础，并且熟练掌握解密基础知识。

4.1 探究解密技术

解密技术既是一门古老的学科，也是一门新兴的学科。它对于一般人来说是陌生的，普通人只是使用而不了解它的基础知识。它因加密技术应运而生，与加密技术相生相克，已成为信息安全的主要研究方向。

4.1.1 解密技术的基本概念

所谓解密技术是最常用的安全保密手段，利用技术手段把重要的数据变为乱码传送，到达目的地后再用相同或不同的手段还原。这种还原技术就是解密技术。解密技术包括两个元素：算法和密钥。算法是将加密的信息分析结合，产生所使用的密钥的步骤，密钥是用来对数据进行编码和解密的一种算法。通过数据解密技术，可以在一定程度上提高数据传输的安全性，保证传输数据的完整性。

明文用M（消息）或P（明文）表示，它可以是比特流（文本文件、位图、数字化的语音流或数字化的视频图像）。至于涉及计算机，P是简单的二进制数据。明文可被传送或存储，无论是哪种情况，M都是指要进行加密的信息。

密文用C表示，它也是二进制数据，有时和M一样大，有时稍大（通过压缩和加密的结合，C有可能比P小些。然而，仅仅加密通常达不到这一点）。

解密函数D作用于C产生M：

D（C）=M

先加密后再解密消息，原始的明文将恢复出来，下面的等式必须成立：

D[E（M）]=M

4.1.2 解密技术的必要性

在当今社会必须使用解密技术来破解数据，众所周知，使用互联网进行文件传输、发送电子邮件是进行业务往来的重要手段，但互联网基于TCP/IP协议存在，TCP/IP协议由于本身固有的不安全性，就需要引起人们的高度重视了，特别是进行一些机密文件的网络传输。互联网把全世界连接成一个整体，带来了无限的商机和便利。为了能在安全的基础上享受这些便利，就要使用数据加密技术和基于数据加密技术的数据签名。解密技术在网络上的应用非常必要，主要就是机密文件或有用的内容在网络传输时，被非法截获或破坏。到达接收方时，可以解密成正常的数据或消息。

如果甲通过电子邮件给乙发送一份比较机密的文件，甲为了防止别人窃取文件采用加密技术，加密后的文件即使被窃取也是不可读的，而乙在收到文件后解决这个难题的方法就是解密，将一堆乱码解密成可读数据，所以说使用解密技术是当今网络社会的必然趋势。

4.1.3 解密技术的发展前景

在现代社会中，解密技术最常用于对军事领域中机密信息进行破解。随着计算机技术的不断发展，运算能力的不断增强，以往的密码就变得十分不安全，非常容易被破解，于是人们又研究出了新的数据加密方式，随着新的加密技术的出现，解密技术也进一步发展。

4.2 软件解密方式概述

关于软件解密和注册保护方式有很多种，比如最常用到的爆破方式解密，不同的解密方式可以显示解密者不同的解密水平。软件的破解技术与保护技术这两者之间本身就是矛与盾的关系，它们是在相互斗争中发展进化的，这种技术上的较量归根结底是一种利益的冲突。

在软件解密中有三种常用的解密方式：爆破方式、跟踪注册过程及写出注册机。

爆破方式就是通过修改程序流程来达到解密注册保护方式的目的，即通过静态分析的过程。常见的软件如W32SAS等静态反汇编或者反编译软件。

跟踪注册过程就是通过使用解密工具找出软件注册码的计算过程，了解软件注册码的计算方法，再利用编程语言来实现，做出软件的算法注册机。这是一个动态分析的过程，常见的软件如OLLYDBG等动态调试软件。

写出注册机就是通过对软件注册码进行对比分析，在求出软件注册码的换算方法之后，再利用编程语言将其编写实现，制作出一个可供用户实现计算注册码的小程序（注册机）。

对于不同的加密方式，可以采用不同的解密方式。

1．加密狗方式

破解加密狗一般有如下几种方式。

（1）爆破

通过修改加密狗驱动程序中的值或修改程序中读取加密狗的操作，达到解密的目的。

（2）硬件复制

利用同型号的加密狗进行加密狗数据复制，这就要对底层的各类硬件功能非常熟悉。

（3）软狗模拟

通过自己重新编写加密狗驱动程序，来模拟加密狗及加密狗内的算法和值。

2．光盘加密

这种加密方式一般应用于游戏软件，可以防止原始光盘被非法复制。要破解这种加密方式，只能进行爆破或虚拟光盘。

3．网络验证

破解网络验证一般也是采用爆破或模拟校验的方式。软件连接到官方网站进行校验其实就是向官方网站发送一些数据包，模拟校验就是通过截取和分析软件发送的数据包，尽可能地分析出官方网站验证数据包的过程，再编写相应的源代码或程序等模块来进行模拟校验，跳过官方网站的校验程序进行注册。

4.NAG窗口

只能用爆破的方式破解NAG窗口，通过修改软件的窗体资源或修改窗体所在代码，来屏蔽窗体或强行跳过之后，才可使NAG窗口不再运行。

4.3 解密技术中的相关问题

学习解密技术的过程中涉及一些常见问题，用户只有理解这些问题的内涵才能更进一步地学习。下面就详细介绍一下在解密过程中经常涉及的几个问题。

4.3.1 软件的破解方式

通常将软件破解方式分为两种：完全破解和暴力破解。

所谓完全破解主要是针对那些需要输入注册码或密码等的软件来说的，如果能通过对程序的跟踪找到正确的注册码，通过软件本身的注册功能正常注册了软件，这样的破解称为完全破解。

如果有些软件本身没有提供注册功能，只是提供试用（DEMO）或注册不能通过软件本身进行（例如需要获取另外一个专用的注册程序，通过Internet的注册等），或软件本身的加密技术比较复杂，软件破解者的能力、精力、时间有限，不能直接得到正确的注册码，此时需要去修改软件本身的程序码，即人为改变软件的运行方向，这样的破解称为暴力破解。

4.3.2 破解教程中程序代码地址问题

破解教程中通常都会放上一部分程序代码，以帮助讲解程序的分析方法，例如，下面的一段程序代码。

        ...
        0167:00408033  PUSH  00
        0167:00408035  PUSH  EBX
        0167:00408036  CALL  [USER32! EndDialog]
        0167:0040803C  JMP  0040812C
        ...

在这里程序中的代码地址如0167:00408033，其代码段的值（即0167）有可能根据不同的计算机而有所区别，不一定会一模一样，但偏移值应该是固定的（即00408033不变）。因此，如果看到破解文章中程序代码的地址值和自己计算机里的不一样，千万不要以为自己搞错地方了，只要程序代码正确就不会有问题。

4.3.3 如何设置断点的问题

正确恰当地设置好断点对于快速有效地解密非常重要，好的断点设置可以迅速找到关键的程序段，而不恰当的断点则会对解密造成不必要的精力消耗，甚至根本就不能拦截到程序的运行。

但具体什么时候用什么断点比较合适很难说，这需要自己用经验去累积。总的说来，Bpx Hmemcpy这个万能断点对大多数注册码方式的软件都有用。

对于那些需要暴力破解的非注册码方式的软件，通常应该拦截对话框（如Bpx DialogBox）和消息框（如Bpx MessageBox(A)）等。不论对于哪一类软件，当设置的断点均没有效果时，可以试一下Bpx Lockmytask，这个断点的作用是拦截任何一个按键的动作。

另外，在注册码的破解中通常需要输入用户名和注册码，一般说来用户名和密码都可以随意输入。但根据经验，很多软件对于注册码都会逐位地进行处理，假如输入“78787878”这串数字，那么在跟踪程序时就无法知道当时所看到的“78”到底是哪一个“78”，所以比较喜欢用“12345678”这样的注册码输入方式，这样就能知道程序是在对注册码的哪一位进行运算。

同样，对于那些需要输入较长序列号的软件，输入类似“12345-67890-ABCDEF”这样的序列号较好。

不过有一点需要特别注意。

注册码输入方式“12345678”是针对拦截Win32 API函数而言的，假如有些时候直接拦截Win32 API函数难以找到程序的突破口，而要借助于“S”指令在内存中寻找输入的用户名或注册码时，就最好不要采用“12345678”作为注册码，因为内存中很可能有许多的“12345678”字符串，这样就没有办法知道到底要破解的程序使用的是哪一个“12345678”，所以应该选择一个不易和内存数据相同的注册码。

具体用什么样的输入形式可以根据本人的爱好、习惯来定，不必拘泥于某一固定的模式。

4.3.4 如何跟踪程序的问题

初学者在开始学习解密时往往不知道怎样去跟踪程序，当面对长长的一堆程序代码时往往显得不知所措。

通常，软件的程序内部都会利用一个子程序（即CALL ＊＊＊＊＊＊＊＊）去验证输入的注册码正确与否，对于注册码显式存在的程序，一般都会将所输入的注册码和正确的注册码放进寄存器，然后调用验证子程序进行判断，将结果返回，应用程序根据子程序返回的结果决定是否注册成功，这样的程序经常具有如下形式。

        ＊＊＊＊:＊＊＊＊＊＊＊＊  MOV  EAX, [＊＊＊＊＊＊＊＊]（或PUSH EAX等形式）
        ＊＊＊＊:＊＊＊＊＊＊＊＊  MOV  EDX, [＊＊＊＊＊＊＊＊]（或PUSH EDX等形式）
        ＊＊＊＊:＊＊＊＊＊＊＊＊  CALL  ＊＊＊＊＊＊＊＊
        ＊＊＊＊:＊＊＊＊＊＊＊＊  TEST  EAX, EAX（或TEST AL, AL，或是没有这一句等形式）
        ＊＊＊＊:＊＊＊＊＊＊＊＊  JNZ  ＊＊＊＊＊＊＊＊（或JZ ＊＊＊＊＊＊＊＊等形式）

其中EAX和EDX指向的内存区域就是输入的注册码和正确的注册码，这里的寄存器EAX和EDX是随意写的，也可以是ECX、EBX、EDI、ESI等。

对于注册码隐式存在的程序，虽然不能直接看到正确的注册码，但通常也是先将所输入的注册码地址放进某个寄存器，然后调用子程序去验证，破解时就需要进入子程序去分析注册算法。

总之，看到子程序（call ＊＊＊＊＊＊＊＊）后面跟着跳转指令（JNZ ＊＊＊＊＊＊＊＊或JZ ＊＊＊＊＊＊＊＊）的地方就应该提高警惕，多用D EAX（或EBX、ECX、EDX、EDI、ESI等）去看看寄存器指向的内存区域藏着什么东西。

看见程序中使用GetDlgItenInt函数时要注意，这个API函数的作用是将输入的文本转化为整数，所以这类程序中是不会显示存在注册码的，因为注册码被转换为整数了，程序通常会用CMP ECX、EDX这种类型的指令去验证注册码的正确性。这里ECX和EDX中存储的就是所输入注册码和正确注册码的整数形式，此时可以用 ？ edx和 ？ ecx查看其十进制形式，即输入的形式。

4.3.5 软件的反安装问题

经常使用某些软件时都会遇到一个问题，就是共享软件过期之后，即使删掉原程序重新安装，程序依然不能用，还是一样提醒试用期已过请注册；或者是已经破解了某个软件，但还想继续研究它，却因为软件已经注册好而没有了注册选项，这时即使彻底删掉程序再重新安装软件，结果程序运行后还是注册过的。

之所以会出现这样的情况，其实原因很简单，因为程序将注册或过期信息存在了系统注册表中，所以简单地重新安装软件是无济于事的。

解决的办法就是自己删掉注册表中有关的信息，但因为注册表是Windows系统工作的基础，如果不小心很可能会因损坏它而引起系统异常。因此，如果对注册表不是很熟，最好在修改之前备份一下。

不论是修改还是备份注册表，都可以使用Windows系统下的注册表管理工具“REGEDIT”来进行，大部分的应用软件都会将自己的信息存在如下的路径中：HKEY_LOCAL_ MACH INE\Software、HKEY_LOCAL_MACHINE\Software\Microsoft、HKEY_CURRENT_USER\Soft ware\HKEY_CURRENT_USER\Software\Microsoft或HKEY_USERS\.DEFAULT\Software分支下，具体是哪个地方依据不同的程序而有所不同，只要按上面的顺序，肯定能找到有关应用程序的键，然后将和用户名及注册码有关的键值删掉就可以了。

4.4 小结

解密技术涉及的方面非常广泛，要想真正理解运用它也不是一天两天的事情，必须深入到程序的内部，汲取更多更全面的计算机知识，深入到解密的第一线，经常进行解密实战，才能真正锻炼自己，解密技术才会有所提高，同时在解密的过程中感受到学以致用的乐趣。

本章就解密技术基础进行了详细的讲述，主要包括软件解密方式、解密技术中的相关问题。要想熟练地进行解密，更深入、透彻地了解解密的精华，则需要付出加倍的努力。

技巧与问答

❖ 软件破解方法有哪几种？

（1）算法注册机

算法注册机是在破解者分析了软件的注册码算法后，制作出来的一种可以自动生成软件注册码的程序（一般软件作者自己也编写有算法注册机，用于销售软件）。由于用这种方式破解的软件和正版注册的一模一样，功能上没有任何区别，所以它是比较完美的软件破解方式。

（2）内存破解法

内存破解法是先加载破解程序（内存注册机），再通过它去读取内存中软件的注册码，或修改内存中软件相关的某些数据，来达到破解软件的目的。

（3）补丁破解法

补丁破解法是指用相关的补丁程序来修改软件，以达到破解软件的目的。此方法一般是破解软件的验证注册码或时间，基本上都是修改原程序的判断语句。

（4）文件注册法

文件注册法是把软件的注册内容放到一个文件里，以方便自己或别人用它来注册。

❖ 在进行软件破解时经常用到的断点有哪些？

（1）VB程序专用断点

        bpx msvbvm60! rtcMsgBox
        bpx msvbvm60! __vbaStrCmp
        bpx msvbvm60! __vbaStrComp
        bpx msvbvm60! __vbaStrCompVar
        bpx msvbvm60! __vbaStrTextCmp
        bpx msvbvm60! __vbaFileOpen
        bpx msvbvm60! __vbaInputFile
        bpx msvbvm60! __vbaFileSeek
        bpx msvbvm60! __vbaWriteFile
        bpx msvbvm60! __vbaFileClose
        bpx msvbvm60! rtcFileAttributes
        bpx msvbvm60! rtcFileDateTime
        bpx msvbvm60! rtcFileLen
        bpx msvbvm60! rtcFileLength
        bpx msvbvm60! __vbaVarInt
        bpx msvbvm60! __vbaVarCmpGe
        bpx msvbvm60! __vbaVarCmpGt
        bpx msvbvm60! __vbaVarCmpLe
        bpx msvbvm60! __vbaVarCmpLt
        bpx msvbvm60! __vbaVarCmpNe

（2）常用断点设置

        bpx hmemcpy、bpx Lockmytask

第5章
破解技术基础

破解技术是破解者或受各种利益的驱使，或出于纯粹的个人兴趣，而不断制作新的破解工具并针对新出现的保护方式进行跟踪分析，以找到相应的破解方法。

5.1 认识PE格式文件

PE（Portable Executable）格式是微软Win32环境可移植可执行文件（如exe、dll、vxd、sys和vdm等）的标准文件格式。PE格式衍生于早期建立在VAX(R)VMS(R)上的COFF（Common Object File Format）文件格式。Portable是指对于不同的Windows版本和不同的CPU类型上PE文件的格式是一样的，当然CPU指令的二进制编码是不一样的，只是文件中各种东西的布局是一样的。

PE意味着此文件格式是跨Win32平台的，即使Windows系统运行在非Intel的CPU上，任何Win32平台的PE装载器都能识别和使用该文件格式。

当然，移植到不同的CPU上后，PE执行体必然得有一些改变。所有Win32执行体（除了VxD和16位的Dll）都使用PE文件格式，包括NT的内核模式驱动程序（Kernel Mode Drivers），因而研究PE文件格式非常重要。

这里要介绍的是目前较为流行的PE格式，希望大家能够对其有一个总体的认识。

由于PE格式文件使用了一个平面地址空间，因此它将所有代码和数据合并在一起，组成一个很大的结构。在该文件格式中，文件内容被分割成不同的区块（Section，又称为区段、节等），块中包含代码或数据，各个块按页边界来对齐，块没有大小限制，是一个连续结构。每个块都有它自己在内存中的一套属性。

PE文件的真正内容划分成块，称为Section（节）。每节是一块拥有共同属性的数据，比如代码/数据、读/写等。可以把PE文件想象成一个逻辑磁盘，PE Header是磁盘的Boot扇区，而Sections就是各种文件，每种文件自然就有不同属性，如只读、系统、隐藏、文档等。

节的属性设置决定了节的特性和功能。如果想把数据设置为只读属性，就可以将该块数据放入只读的节中，当PE装载器映射节内容时，它会检查相关属性并设置对应内存块为指定属性。其中每个区块都有不同的名字，这个名字用来表示区块的功能。例如，一个叫．data的区块，表明它是一个初始化的数据块（常见的区块有．text、.rada、.data、.idata、.rsrc等）。

常见块的含义如表所示。

常见块的含义

下面来看一下PE文件结构的总体层次分布概要，如图所示。

下图是PE文件结构的总体层次分布。所有PE文件（甚至32位的DLLs）必须以一个简单的DOS MZ header开始。有了它，一旦程序在DOS下执行，DOS就能识别出这是有效的执行体，然后运行紧随MZ header之后的DOS stub。通常情况下，简单调用中断21h服务9来显示字符串“This program cannot run in DOS mode”，紧跟DOS stub的是PE header。

PE文件结构的总体层次分布

PE header是PE相关结构IMAGE_NT_HEADERS的简称，其中包含了许多PE装载器用到的重要域。执行体在支持PE文件结构的操作系统中执行时，PE装载器将从DOS MZ header中找到PE header的起始偏移量。因而跳过了DOS stub直接定位到真正的文件头PE header。

PE文件不是作为单一内存映射文件被装入内存的。Windows加载器遍历PE文件并决定映射文件的哪一部分，这种映射方式是将文件较高的偏移位置映射到较高的内存地址中。磁盘文件一旦被装入内存中，偏移地址就可能区别于原始的偏移位置。

以上就是PE文件格式的物理分布，下面将介绍一下装载PE文件的主要步骤。

① 当PE文件被执行时，PE装载器检查DOS MZ header里的PE header偏移量。如果找到，则跳转到PE header。

② PE装载器检查PE header的有效性。如果有效，就跳转到PE header的尾部。

③ 紧跟PE header的是节表。PE装载器读取其中的节信息，并采用文件映射方法将这些节映射到内存，同时附上节表里指定的节属性。

④ PE文件映射入内存后，PE装载器将处理PE文件中类似Import Table（引入表）逻辑部分。

上述步骤的讲述虽然还有一些不够精确的地方，但也基本指明了执行体被处理的整个流程。下面介绍几个与PE格式相关的名词。

（1）入口点（Entry Point）

PE文件在执行时都要有一个入口点，这个值通常是程序在执行时的第一行代码的地址。

（2）文件偏移地址（File Offset）

文件偏移地址也叫物理地址，就是把PE文件存储在磁盘上时各数据的地址，它从PE文件第一个字节开始计数，初始值为0。

（3）虚拟地址（Virtual Address,VA）

虚拟地址的产生主要是因为Windows程序运行在386保护模式下，因此，程序访问存储器所使用的逻辑地址就称为虚拟地址，也就是常讲的内存偏移地址（Memory Offset）。

虚拟地址可写成“段：偏移量”的形式。例如，“0248:01204000”就是这种形式，其中，0248表示段选择子，其数据集中保存在CS段选择器中，不过不同系统环境下就可能会出现不同的情况。01204000表示内存的虚拟地址，通常情况下，同一程序的同一条指令在不同系统环境不会有什么变化。

（4）基地址（Image Base）

基地址是指文件执行时将被映射到指定内存地址中的初始内存地址，这个值由PE文件本身设定。

（5）相对虚拟地址（Relative Virtual Address,RVA）

相对虚拟地址是内存中相对于PE文件装入地址（基地址）的偏移量，也就是指到虚拟地址空间中一个地址的相对距离，即偏移量。

一般情况下，相对虚拟地址总是相对于模块的起始虚拟地址，它们之间的关系如下。

相对虚拟地址（RVA）=虚拟地址（VA）-基地址（ImageBase）

5.2 掌握代码分析技术

5.2.1 文件偏移地址与虚拟地址

用户有时会遇到这样的情况，当PE文件被映射到内存后，由于同一数据相对于文件头的偏移在内存中和磁盘文件中可能不同，这样就存在文件偏移地址与虚拟地址转换的问题。

LordPE是一款功能强大的PE文件分析、修改、脱壳软件。PE编辑器的功能更加强大，加入了各项目的16进制编辑和列表，首先支持新的PE文件格式——pe+，但是只是支持编辑，还不支持脱壳等其他操作。

下面用LordPE来查看一个Hello Word.exe文件的块表结构。具体操作步骤如下。

1 运行LordPE主程序或双击图标，等待一段时间即可打开【LordPE】程序窗口，如图所示。

【LordPE】程序窗口

2 在【LordPE】程序窗口中单击【PE编辑器】按钮，即可打开【打开】对话框，选择Debug文件夹中的Hello Word.exe文件，单击【打开】按钮即可，如图所示。

【打开】对话框

3 单击【打开】按钮后，即可打开【PE编辑器】对话框。

4 在【PE编辑器】对话框中单击【区段】按钮，即可打开区块表编辑器查看区块的信息，如图所示。

区块表编辑器

磁盘文件中每个区块的大小必定等于磁盘对齐值的整数倍，面区块的实际代码或数据的大小不一定就是那么多，不足的地方就要用0X00来补充，就是区块的间隙。还可以创建一个在文件中的区块起始位置与在内存中的装入地址相同的PE文件，以提高文件的装入速度。

由于磁盘文件中．text块的起始端与文件头的偏移量为add1，因此映射到内存之后，.text文件起始端与文件头（基地址）的偏移量为add2。在这里，add1的值就是文件偏移地址add2的值，就是相对虚拟地址。

假设它们的差值为Δ k ，则文件偏移地址与虚拟地址关系如下。

File Offset=RVA-Δ k

File Offset=VA-ImageBase-Δ k

在上述公式中：

File Offset：文件偏移地址。

ImageBase：基地址。

RVA：相对虚拟地址。

VA：虚拟地址。

在同一区块中，各地址的偏移量是相等的，可用上面的公式对区块中任一文件偏移地址与虚拟地址进行转换。但并不是在整个文件夹里文件偏移地址与虚拟地址的差值是Δ k ，因为在内存中各区块是以一个页边界为开始的，所以不同区块在磁盘与内存中的差值不一样。

例如，某实例中某一虚拟地址（VA）=00401234，要求计算其文件偏移地址。

00401234在．text块中，此时 Δk =00000000，因此可以得出：

File Offset= VA- ImageBase- Δk =00401234-00400000-00000000=00001234

再看一下虚拟地址004220B3的转换。

004220B3在．rdata块中，此时 Δk =00000000，因此可以得出：

File Offset= VA-ImageBase- Δk =004220B3-0X400000-00000000=000220B3

在实际操作时，建议使用RVA-Offest之类的转换工具来进行文件偏移地址和虚拟地址之间的转换。具体的操作步骤如下。

1 在【PE编辑器】对话框中单击【位置计算器】按钮，即可打开【文件位置计算器】对话框，如下图所示。

【位置计算器】按钮

2 在【VA】文本框中输入虚拟地址，如输入“00401234”，单击【执行】按钮，即可在其中完成虚拟地址、相对虚拟地址和偏移三者之间的相互换算，如下图所示。

在【文件位置计算器】对话框输入虚拟地址

5.2.2 搜索程序的入口点

用户要想找到程序的入口点（Original Entry Point,OEP），可以通过相关的工具和命令来实现，下面就来具体介绍一下这些工具和命令。

1.PEiD的使用

现在有很多软件都加了壳，给破解汉化带来非常大的不便。PEiD是一款著名的查壳工具，其功能强大，几乎可以侦测出所有的壳，其数量已超过470种PE文档的加壳类型和签名；另外还可识别出EXE文件是用什么语言编写的，例如VC++、Delphi、VB或Delphi等，也可以查看程序的入口点。

1 打开PEiD程序，待程序完全启动后，PEiD程序的主界面如下图所示。

PEiD程序的主界面

2 单击浏览按钮，弹出【Choose the fi le to open】对话框，选取要检查的文件，单击【打开】按钮，如下图所示。

【打开】按钮

3 单击【打开】按钮后，程序的界面如下图所示，在【Entrypoint】文本框中显示的是程序的入口点，入口点为“00001130”。

【Entrypoint】文本框

2.Exeinfo PE的使用

Exeinfo PE是一款免费的Win32可执行程序检查器，它可以检查程序的打包方式、EXE保护等，帮助开发人员对程序进行破解。Exeinfo PE绿色版是一款查看PE文件信息的工具，可以查看EXE/DLL文件的编译器信息、是否加壳、入口点地址、输出表/输入表等PE信息，帮助开发人员对程序进行分析和逆向。

Exeinfo PE的具体使用步骤如下。

1 双击打开Exeinfo PE程序，等待一段时间程序完全启动后，程序运行主界面如图所示。

Exeinfo PE程序运行主界面

2 在Exeinfo PE程序运行主界面单击浏览文件按钮，弹出【Open EXE, DLL fi le】对话框，选取要检查的EXE文件，单击【打开】按钮，如图所示。

【Open EXE,DLL file】对话框

3 可以在程序运行主界面的【Entry Point】文本框中查看入口点的首地址，这里显示的入口地址为“00001130”，如图所示。

【Entry Point】文本框

3.OllyDbg的使用

OllyDbg（简称OD）是一个新的动态追踪工具，将ida与softice结合起来，ring 3级调试器，非常容易上手，已代替softice成为当今流行的调试解密工具了。同时还支持插件扩展功能，可以使用该软件查看入口点。

1 打开OllyDbg程序，等待一段时间后，OllyDbg程序的主界面如图所示。

OllyDbg程序的主界面

2 单击【文件】→【打开】菜单项，弹出【选择32位可执行文件，并指定参数】对话框，选择Hello Word.exe文件，单击【打开】按钮即可，如图所示。

【选择32位可执行文件，并指定参数】对话框

3 文件打开后，在OllyDbg程序主窗口中显示，可以看到程序的入口点为“00401130”，如图所示。

OllyDbg程序主窗口

5.2.3 如何修复输入表

通常情况下，在对某个加壳文件进行破解之后，都要对输入表进行一番修复才可以正常使用。下面介绍一下修复表的具体方法，这里要用的是输入表修复软件Import REConstructor。Import REConstructor可以从杂乱的IAT中重建一个新的Import表（如加壳软件等），它可以重建Import表的描述符、IAT和所有的ASC I函数名，还可手动重建Import表。具体的操作步骤如下。

1 打开加壳程序，在Import REConstructor窗口中选择主运行文件的进程，如图所示。

Import REConstructor窗口

2 在窗口下面的OEP（入口点）文本框中输入OEP的值之后，单击【自动搜索】按钮，再单击【获取输入表】按钮，即可出现DLL的成功识别标志，如图所示。DLL成功识别的标志是出现“有效：有”字样，显示“有效：无”字样则表示无效地址，即还没有修复成功。

【获取输入表】按钮

5.2.4 转储程序概述

在原数据库里的对象属于不同的用户，那么转储的文件会指示其以各个受影响的用户轮流链接再创建相关的对象保留原来所有权属性。不过，这也意味着所有这些用户必须已经存在且能够以每个用户的身份链接，所以可能需要临时放松对客户认证的设置。

下面介绍几个工具转储程序的简单使用方法。

1.ProcDump

ProcDump是一个轻量级的Sysinternal团队开发的命令行工具，它的主要目的是监控应用程序的CPU异常动向，并在其异常时生成crash dump文件，供研发人员和管理员确定问题发生的原因。你还可以把它作为生成dump的工具使用在其他的脚本中。它是一款功能强大的脱壳工具，支持几十种加壳工具生成的压缩加密文件，在修改文件资源前进行脱壳处理。具体的操作步骤如下。

1 用指令suspend挂起调试器之后，即可打开ProcDump程序主窗口，如图下图所示。

打开ProcDump的程序主窗口

2 在命令行中输入“procdump -ma -c 50-s 3-n 23668”，它会在指定的时间内监控目标进程的CPU，并在那个点抓取一个内存快照（dump），如下图所示。命令的含义是：在端口3668上的进程占用CPU资源超过50%并能维持3秒钟的情形下抓取两组dump。dump文件存储位置为“D:\Procdump\”。

抓取内存快照（dump）

-ma：生成full dump，即包括进程的所有内存，默认的dump格式包括线程和句柄信息。

-c：在CPU使用率达到这个阈值的时候，生成dump文件。

-s:CPU阈值必须持续多少秒才抓取dump文件。

-n：在该工具退出之前要抓取多少个dump文件。

2.Windows任务管理器的使用

Windows任务管理器提供了有关计算机性能的信息，并显示了计算机上所运行的程序和进程的详细信息；如果连接到网络，那么还可以查看网络状态并迅速了解网络是如何工作的。它的用户界面提供了文件、选项、查看、窗口、关机、帮助六大菜单项，其下还有应用程序、进程、性能、联网、用户5个标签页，窗口底部则是状态栏，从这里可以查看到当前系统的进程数、CPU使用率等数据，默认设置下系统每隔两秒钟对数据进行1次自动更新，也可以单击“查看→更新速度”菜单重新设置。用户可以使用它创建转储文件，具体的操作步骤如下。

1 可以在Windows 7及Windows 8系统中使用<Ctrl+Shift+Esc>组合键调出，也可以用鼠标右键点击任务栏，选择“任务管理器”。另外<Ctrl+Alt+Delete>组合键也可以出现，只不过还要回到锁定界面。Windows任务管理器主界面如下图所示。

Windows任务管理器主界面

2 可以在【进程】界面中，右键单击某一映像名称，弹出下拉菜单，在下拉菜单中单击【创建转储文件】菜单项，即可创建转储文件，如下图所示。

【创建转储文件】菜单项

3 用户单击【转储文件】菜单项后，弹出【转储进程】对话框，对话框中显示“请稍候，正在向文件写入进程．..”，如下图所示。等待一段时间后，对话框中会提示成功创建文件，并且注明文件的存放位置，如下图所示。

【转储进程】对话框

成功创建文件

5.3 了解静态分析技术及其工具

用户如果想尝试对某个软件加密或者解密，必须首先学会使用各种软件，在对软件的功能进行充分了解的基础上实现对软件的加密，而软件的说明书和使用手册可以帮助用户熟悉软件的注册部分，只有准确、透彻地分析某个软件，才能更好地对软件进行加密和解密。

5.3.1 静态分析的概念

静态分析就是指从汇编出来的程序清单上分析该程序的流程，从提示信息入手进行分析，了解模块完成的功能，阅读静态反汇编出来的程序清单，可以了解软件的编程思路，以便顺利解密。静态分析工具主要由程序类型分析工具、资源编辑器工具和反汇编工具3部分构成。进行静态分析时，为了使静态的分析更具有目的性，首先就是对程序类型的分析，查看程序是否被加壳，了解程序编写的语言或编译使用的编译器。

5.3.2 资源编辑器工具

Windows程序在Windows系统中称为资源，它们一般存储在PE文件的．rsrc区块中，并且不能通过由程序源代码定义的变量直接访问。Windows系统会将其提供的函数直接或间接地加载到内存中，以供广大用户使用。通过本节的学习，用户可以使用资源编辑器工具对资源进行编辑。

下面介绍两款资源编辑修改工具ResourceHacker和eXeScope，利用它们可以直接编辑修改使用Visual C++、Delphi编写的程序资源，包括EXE、DLL和OCX等。

1.ResourceHacker的使用

Resource Hacker是著名的EXE文件编辑器，是一个免费软件，内置了一个内部资源编译器和反编译器，可以查看、修改、添加、重命名与删除Windows可执行文件和资源文件中的资源。该工具虽然可保存资源，但事实上只是直接修改图标和光标而已，对于其他资源就不行了，不过可以作为一个资源管理器来使用。使用ResourceHacker可以很轻松地提取（保存）资源到＊.rc文件中，并且还可以对这些可执行文件的资源进行修改、删除。ResourceHacker工具的主要功能如下。

1 查看Win 32可执行文件和相关文件的资源（*.exe、*.dll、*.cpl、*.ocx），在已编译和反编译的格式下都可以。用户单击【文件】→【打开】菜单项，弹出【打开包含资源的文件】对话框，选中文件后单击【打开】按钮，即可查看文件，如图所示。

【打开包含资源的文件】对话框

2 提取（保存）资源到文件（.res）格式。作为二进制、反编译过的资源脚本、图像、图标、位图、指针、菜单、对话框、字符串表、消息表、加速器、Borland窗体和版本信息资源等，都可以被完整地反编译为它们各自的格式，不论是作为图像或*.rc文本文件。用户可以单击【操作】→【将所有资源保存到一个*.res文件】菜单项，弹出【保存资源】对话框，输入文件名，单击【保存】按钮即可提取资源到文件（.res）格式，如图所示。

【保存资源】对话框

3 修改（替换）可执行文件的资源。图像资源（图标、指针和位图）可以被相应的图像文件或一个．exe文件替换，甚至被一个*.res文件替换，只要替换的资源与其名称和类型相同。用户单击【操作】→【替换图标】菜单项，弹出【替换图标】对话框，单击【打开文件与新图标】按钮，弹出【替换】对话框，选择新图标，单击【打开】按钮即可替换，如图所示。

【打开】对话框

4 添加新的资源到可执行文件。允许一个程序支持多种语言，或添加一个自定义图标（或位图）到程序中。用户单击【操作】→【从资源文件添加（*.res,*.dll,*.exe）】菜单项，弹出【打开包含资源的文件】对话框，选中资源文件，单击【打开】按钮即可添加新资源到可执行文件，如图所示。

【打开包含资源的文件】对话框

5 删除一个资源。大多数编译器会在应用程序中添加用不到的资源。删除这些用不到的资源可以减少应用程序的大小，比如用ResourceHacker打开一个Win32 PE文件，将会显示一个完整的树状文件资源的列表。

用户单击ResourceHacker主窗口中的【查看】→【展开树查看】菜单项，即可打开【展开树查看】子菜单项，表明资源树可被展开，如图所示。

【展开树查看】子菜单项

用户单击ResourceHacker主窗口中的【查看】→【折叠树查看】菜单项即可打开【折叠树查看】子菜单项，表明资源树可被折叠，如图所示。

【折叠树查看】菜单项

2.eXeScope的使用

eXeScope能在没有资源文件的情况下分析，显示不同的信息，重写可执行文件的资源，包括（EXE、DLL、OCX）等。eXeScope是方便强大的汉化工具。打开的eXeScope窗口，如图所示。

打开的eXeScope窗口

eXeScope工具的主要功能如下。

· 可执行文件（EXE、DLL等）的解析与显示。

· 提取资源到外部文件：可以把Icon、Cursor、Bitmap、WAVE、MIDI、AVI、GIF、HTML及Delphi表格提取出来作为外部文件存储，也能提取其他的二进制资源。

· 重新写入资源：可以把Windows标准资源（Icon、Cursor、Bitmap等）重新写入EXE和DLL文件中。

5.3.4 掌握反汇编分析工具

反汇编分析工具中比较出名的就是W32Dasm、C32asm和IDA Pro。下面对其具体用法进行介绍。

1.W32Dasm

W32Dasm是一款URSoft开发的反汇编和调试工具，远程攻击者可以利用这个漏洞构建恶意的可执行文件，诱使用户处理，其能以用户进程权限在系统上执行任意指令。W32Dasm静态分析软件操作简单，使用方便，通常被程序员使用，也可以被用来作Crack软件。用它能够把应用程序反汇编为汇编源程序，还提供很多相关信息，是解密的重要工具。运行W32Dasm 8.93的程序窗口，如图所示。

W32Dasm 8.93的程序窗口

W32Dasm 8.93静态解密的具体步骤如下。

1 查看要解密的软件的使用说明书或者用户手册，看一下软件注册与不注册的区别，以及如何进行注册。

2 运行要解密的软件，试着用任意的编码去注册，当出现错误提示信息之后，将错误提示信息记录下来。

3 测试该软件有没有加壳，如果加壳应该先脱壳。

4 打开W32Dasm 8.93反汇编程序。

5 在串式参考中找到错误提示信息或可能是正确的提示信息后单击鼠标左键。

6 在W32Dasm 8.93的主窗口中分析相应汇编程序，找到关键跳转和call。

7 绿色光条停在关键跳转，在pw32dasmgold主窗口底部找到关键跳转的偏移地址。

8 十六进制编辑器找到偏移地址修改机器码并保存。

9 利用W32Dasm 8.93解密成功。

2.C32asm

C32asm是款非常好用的反汇编程序，它的功能已强于Winhex 16、W32Dasm，具有反汇编模式和十六进制编辑模式，能跟踪EXE文件的断点，也可直接修改软件内部代码。远程攻击者可以利用该软件进行反汇编，伪装成安全程序，获取用户权限。运行C32asm的程序窗口，如图所示。

C32asm的程序窗口

3.IDA Pro

IDA Pro 6.6（简称IDA）可以说是目前功能较为强大、丰富，支持文件种类、CPU指令集较多，交互性较强的一款交互式静态反汇编工具。破解高手们几乎都喜欢用这个软件，它的交互性可以让用户很方便地得到符合自己要求的反汇编代码。IDA Pro 6.6工具运行后的主窗口，如图所示。

IDA Pro的主窗口

我们将在6.2.3小节详细介绍此工具，此处不再赘述。

5.4 动态分析技术及OllyDbg工具

上一节讲述了静态分析技术及其工具，这里将再来介绍一下动态分析技术及其流行工具。现在支持Windows 7系统的只有动态分析软件OllyDbg。

OllyDbg是由德国人编写的一款ring3免费汇编调试器，具有强大的功能、可视化的友好界面，支持对反汇编译后的代码加上自己的注释，而且可以定义复杂的断点条件，使众多解密者用它来调试程序。

OllyDbg将静态分析和动态调试功能完美地结合在一起，还可以调试很多线程的应用程序，从一个线程切换到另一个线程、挂起、恢复和终止，以及改变其优先级。还可附加正在运行的应用程序，并支持DLL动态链接库的调试。OllyDbg运行后的主窗口，如图所示。

OllyDbg的主窗口

5.5 流行注册表分析技术及工具

注册表是Windows操作系统中的一个核心数据库，其中存放着各种参数，直接控制着Windows的启动、硬件驱动程序的装载以及一些Windows应用程序的运行，从而在整个系统中起着核心作用。注册表一旦出错，整个系统都可能出现紊乱。用户要注意保护注册表，不要随便安装来源不明的软件。

5.5.1 注册表监视工具RegMonitor

注册表监视精灵工具（RegMonitor）使用HOOK API技术，监控指定的软件对系统注册表的修改、删除、新增等操作，让您了解被监控的软件在系统背后做了些什么，记录修改注册表的详细日志。监控的软件对系统注册表相关的一切操作（如读取、修改、出错信息等）全部记录下来供用户参考，并允许用户对记录的信息进行保存、过滤、查找等处理，这就为用户对系统的维护提供了极大的便利。

RegMonitor运行后的主窗口，如图所示。

RegMonitor的主窗口

1 单击【视图】→【界面字体】菜单项，弹出【字体】对话框，用户可以设置显示的字体，设置完成后单击【确定】按钮即可，如图所示。

【字体】对话框

2 右键单击要Hook的进程，弹出下拉菜单，在下拉菜单中单击【Hook进程】菜单项，程序就会进入Hook，一旦程序运行，监测器就可以监视到程序在注册表中的运行，如图所示。

【Hook进程】菜单项

3 单击【另存监控日志】按钮，弹出【另存为】对话框，在【文件名】文本框中输入文件名，单击【保存】按钮即可，如图所示。

【另存为】对话框

4 单击【进程】→【定位文件】菜单项，可以打开监控文件的位置，这里显示的是baiduyunguanjia存放位置为：“E:\工具\baiduyunguanjia”，如图所示。

文件存放位置

5.5.2 注册表静态比较工具Regshot

Regshot是个小巧的注册表静态比较工具，能快速地帮助用户发现注册表的变化，甚至通过扫描硬盘来让用户掌握硬盘上某些文件夹，在最新的版本中它还可以通过动态监控注册表[Windows 9x平台]来辅助分析。所有的比较结果都输出为详细的纯文本格式或HTML格式的文档。Regshot的具体使用方法如下。

1 运行Regshot程序，待程序完全启动后，Regshot程序运行的主界面，如图所示。

Regshot的主界面

2 在对注册表进行监视前，可以在Regshot运行的主窗口中单击【建立快照A】按钮，弹出下拉菜单，选中“HKEY_LOCAL_MACHINE”选项，就可以实现自动做记录，如图所示。

建立快照A

3 在单击【建立快照A】按钮之后，可以单击【停止操作】按钮停止快照A的建立，如下图所示。

【停止操作】按钮

4 待被监视对象运行完毕之后，单击【建立快照B】按钮，Regshot即可自动进行记录，如下图所示。

【建立快照B】按钮

5 单击【建立快照B】按钮，待程序执行完成后，再单击【文件】→【比较快照】菜单项，Regshot将自动分析注册表变化，并输出比较结果，如图所示。

【比较快照】菜单项

5.5.3 注册表编辑工具Regedit

注册表编辑器可以用来查看和编辑注册表中的数据。在注册表编辑器窗口的目录中可以查看到键和子键，而在注册表编辑器窗口中可以对配置信息进行修改和处理。Regedit的启动方法如下。

1 直接按<Windows+R>组合键打开“运行”对话框，在【运行】对话框的【打开】文本框中运行“regedit”命令，单击【确定】按钮，如图所示。

【运行】对话框

2 在打开的注册表编辑器窗口中，整个注册表分为5个大项，每项对应一个类型的配置信息，系统在这5个大项的基础上，把配置信息划分为更小的子键，如图所示。

注册表编辑器

注册表的5大项分别如下。

（1）HKEY_CLASSES_ROOT

该主关键字包含了OLE信息和文件类别关联数据，以便在系统工作过程中实现对各种文件和文档信息的访问。其内容有注册的文件扩展名、类型、图标等。

（2）HKEY_CURRENT_USER

HKEY_CURRENT_USER是一个指向HKEY_USERS结构中某个分支的指针，它包含当前用户的登录信息。实际上就是HKEY_USERS\Default下面的一部分内容，如果在HKEY_USERS\Default下面没有用户登录的其他内容，则这两个主关键字包含的内容完全相同。

（3）HKEY_LOCAL_MACHINE

该关键字包含了本地计算机的硬件和软件的全部信息。当系统的配置和设置发生变化时，本关键字下面的登录项也将随之改变。

（4）HKEY_USERS

用户可以在这里设置关键字和子关键字。根据当前的用户不同，这个关键字又可以指向不同的分支部分。这个分支部分将映射到HKEY_CURRENT_USER关键字中。个人设置的诸如桌面、背景、环境、网络连接、显示字体等信息可以在这个关键字中找到。关键字中的大部分设置都可以通过控制面板来修改。

（5）HKEY_CURRENT_CONFIG

这个关键字实际上也是指向HKEY_LOCAL_MACHINE\Config结构中某个分支的指针。HKEY_CURRENT_CONFIG下面的子关键字及内容与HKEY_LOCAL_MACHINE\Config\0001分支下面的子关键字及内容是完全相同的。本关键字包含的主要是计算机的当前配置情况，如显示器、打印机等可选外部设备及其设置信息等，而且这个设置信息均将根据当前连接的网络类型、硬件配置及应用软件的安装不同而有所变化。

使用注册表编辑器可以对注册表进行重命名/删除主键、键值，局部导入，导出等操作。

1．重命名主键、键值

在注册表中，用户为了更好地识别某些主键或键值，可以对主键或子键进行重命名，具体操作步骤如下。

1 打开注册表编辑器。

2 选择已创建的注册表项HKEY_LOCAL_MACHINE\BCD00000000\Description。

3 选中主键之后，选择【编辑】→【重命名】菜单项，或在需要重命名的主键上右击，在弹出的快捷菜单中选择【重命名】选项，输入新的键名即可，如图所示。

【重命名】菜单项

2．删除主键或键值

为了不让没用的主键或键值占用空间，就要将其删除。

删除键值项的具体操作步骤如下。

1 打开注册表编辑器。

2 选择已创建的注册表项HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\xxx。

3 选定键值项UserName并右击，在弹出的快捷菜单中选择【删除】选项，即可弹出【确认数值删除】对话框，单击【是】按钮，如图所示。

【确认数值删除】对话框

删除主键或子键的具体操作步骤如下。

1 打开注册表编辑器。

2 选择已创建的注册表项HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\xxx。

3 选择【编辑】→【删除】菜单项或在需要删除的键名上右击，在弹出的快捷菜单中选择【删除】选项之后，在弹出的【确认项删除】对话框中单击【是】按钮，如图所示。

【确认项删除】对话框

3．导出注册表信息

将注册表导出，作为单独的文件存放，可以作为备份使用；另外，我们还可以利用专门的软件打开导出的注册表进行编辑。

具体操作步骤如下。

1 打开注册表编辑器。

2 在注册表编辑器窗口中，如果要备份整个注册表则选择目录；如果要备份某个子键或子树，则选定该键或子树。

3 选择【文件】→【导出】菜单项，即可打开【导出注册表文件】对话框，在【文件名】文本框中输入文件名（默认文件扩展名为．reg），单击【保存】按钮，如下图所示。

4 默认情况下，注册表编辑器会将管理员选择的注册表子树或整个树作为导出对象，如果要选择导出范围，则可在“导出范围”选项组中执行如下操作。

· 选择“全部”单选项，将备份整个注册表。

· 选择“所选分支”单选项，将在只备份注册表树的某一分支之后，输入需要导出的注册表的分支名称。

导出注册表文件

5 由于备份注册表可以有效地防止系统配置信息的丢失和破坏，建议在每次对系统进行大的修改前，最好都对注册表进行备份。

4．导入注册表信息

当Windows系统出现错误时，就可以使用已经备份的注册表文件来恢复文件（在备份注册表文件后，安装的硬件、软件和其他系统设置信息不能恢复），所以需要经常对注册表进行备份操作，以减少不必要的麻烦。

导入注册表文件的具体操作步骤如下。

1 打开注册表编辑器。

2 选择【文件】→【导入】菜单项，即可打开【导入注册表文件】对话框，如图所示。

导入注册表文件

3 在本机或网络中的计算机上指定备份注册表文件路径之后，选择已经备份的注册表文件，单击【打开】按钮，即可用备份的注册表信息覆盖现在的注册表。

5.5.4 高级系统注册表编辑工具Registry Workshop

高级系统注册表编辑工具Registry Workshop是一款高级的注册表编辑软件，能够完全替代Windows系统自带的RegEdit注册表编辑器。除了RegEdit的特性外，Registry Workshop还提供了许多其他功能以提高注册表编辑操作效率：能够剪切、复制和粘贴注册项和键值名，还可以进行撤销和重做操作；能够快速地查找和替换所需注册项、键值名和字符串；允许编辑注册表文件，同系统自带的注册表编辑器一样；并且提供容易使用和灵活的收藏夹功能。其使用步骤如下。

1 双击高级系统注册表编辑工具Registry Workshop程序，待程序完全启动后，程序主界面如图所示。

Registry Workshop的程序主界面

2 右键单击选中项，弹出下拉菜单，在下拉菜单中单击【新建值】→【String值】菜单项，即可新建值，如图所示。

【String值】菜单项

3 右键单击新建值，弹出下拉菜单，在下拉菜单中单击【剪切】菜单项，即可将新建值剪切后粘贴到指定位置，如图所示。

【剪切】菜单项

4 单击【搜索】→【查找】菜单项，弹出【查找】对话框，在【查找内容】文本框中输入“新建值 #1”，单击【查找】按钮即可，如下图所示。

【查找】对话框

5.5.5 注册表照相机RegSnap

RegSnap是一款注册表辅助工具，可以详细地报告注册表及其他与系统有关项目的修改变化情况。RegSnap对系统的比较报告非常具体。对注册表可报告修改了哪些键，修改前、后的值各是多少；增加和删除了哪些键以及这些键的值。报告结果既可以纯文本的方式，也可以html网页的方式显示，非常便于查看。除系统注册表以外，RegSnap还可以报告系统的其他情况：Windows的系统目录和系统的system子目录下文件的变化情况，包括删除、替换、增加了哪些文件；Windows的系统配置文件win.ini和system.ini的变化情况，包括删除、修改和增加了哪些内容；自动批处理文件au- toexec.bat是否被修改过。该软件可以在需要的时候方便地恢复注册表，可以直接调用regedit程序查看或修改注册表，还可以查看当前机器的机器名和用户名。具体的操作步骤如下。

1 安装软件前先运行RegSnap软件，即可记录安装前注册表的原始情况。即为RegSnap运行后的主窗口，如图所示。

RegSnap的主窗口

2 选择【文件】→【新建】菜单项，或者单击工具栏中的按钮，即可弹出一个【保存快照】对话框，如图所示。

【保存快照】对话框

可以对它进行一些设置以满足需要，先是确定RegSnap记录的内容，有如下几个选择。

· “制作本地注册表快照”选项。

在本地计算机上注册和存储，该选项除记录注册表的数据外，还记录系统目录、系统子目录、系统配置文件、系统自动批处理文件及引导文件的情况。

· “远程电脑的注册表”选项。

连接远程计算机注册和制作快照，这个选项只有专业版可用。

· “架构”单选框。

勾选该32位注册表单选框之后，可以创建32位注册表快照。

勾选该64位注册表单选框之后，可以创建64位注册表快照。

3 设置完成之后，单击【确定】按钮，就可以对系统注册表照相了。RegSnap会给出具体的进程情况，随时向用户报告已经记录了多少个键值。当照相完成之后，RegSnap将给出提示框，报告工作成果，如下图所示。

RegSnap提示框报告工作成果

4 拍照完成后必须保存，选择【文件】→【保存】菜单项，弹出【保存为】对话框，输入文件名后，单击【保存】按钮将记录文件保存为后缀为rsnp的文件，如图所示。

【保存为】对话框

5 进行软件安装，安装完成后，再对注册表及重要系统文件进行拍照。

6 在此拍照完成之后，就可以进行对比了。单击工具栏上的按钮，即可弹出一个【比较快照】对话框，如图所示。

【比较快照】对话框

7 RegSnap提供了“显示已删除键、已修改键和新建键”类型，在比较结束之后，RegSnap将会输出一个比较报告。通过RegSnap对注册表的监视，可以掌握注册表的变动情况，这对用户进行一些复杂的操作很有帮助。

5.6 小结

本章就破解技术进行了详细的讲述，主要包括代码分析、静态分析、动态分析、注册表分析及各类分析的流行工具使用方法的讲解。在以后的章节中我们会进一步讲解各种破解工具以及破解技术。破解技术涉及的方面非常广泛，要想真正理解运用它，必须深入到程序的内部，汲取更多更全面的计算机知识，潜入到破解的第一线，利用静态和动态分析工具跟踪程序，分析注册表，经常进行破解实战，才能通过破解练习更好地保护软件不被他人破解。同时，用户要注意注册表是Windows操作系统中的一个核心数据库，在对注册表修改之前，切记要做好对注册表的备份工作。

技巧与问答

❖ PE文件结构中的输入表详解？

要想了解输入表，首先还得从DLL文件入手。日常生活中我们会看见一些大型软件有很多DLL格式的文件，它们是“动态链接库文件”，这些文件中有很多导入函数，这些函数不会直接被执行，当一个程序（EXE）运行时，导入函数是被程序调用执行的，其执行的代码是不在主程序（EXE）中的一小部分函数，其真正的代码却在DLL文件中。这时我们就会想，EXE主程序是如何找到这些需要导入的函数呢，这就要归结于“输入表”了。输入表就相当于EXE文件与DLL文件沟通的钥匙，可以形象地比喻成两个城市之间交流的高速公路，所有的导入函数信息都会写入输入表中，在PE文件映射到内存后，Windows将相应的DLL文件装入，EXE文件通过“输入表”找到相应的DLL中的导入函数，从而完成程序的正常运行，这一动态连接的过程都是由“输入表”参与的。

❖ 程序静态分析的方式有哪几种？

（1）基于规则的代码静态分析

通过自动化逐行扫描程序代码，从中查找是否存在与事先构建好的规则模式相匹配的代码，如果发现相匹配的代码，则报告相应错误。基于规则的静态分析方式可以预防非法代码的出现，既可满足业界标准（如MISRA、JSF、ELLEMTEL），也可满足企业内部自定义规范，同时还可以增强团队编码规范的一致性。通过基于规则的代码静态分析，可以及时发现并修复违规代码，为后续工作节省大量测试及调试时间，大大降低引入违规代码所导致的成本，可以预防内存泄露、资源泄露、安全性漏洞等问题的发生。据统计，通过基于规则的静态分析可以预防大约60%的代码缺陷发生。

（2）基于数据流的代码静态分析

通过静态模拟应用程序的执行路径，帮助用户找到运行时才能暴露的一些严重错误，如资源泄露、空指针异常、SQL注入以及其他的安全性漏洞等潜在的运行时错误。这些严重的错误往往通过一般的静态规则扫描难以查找，此时需要数据流分析技术，也叫BugDetective。该技术对于嵌入式环境测试尤其有效，原因很简单，嵌入式环境往往需要整个项目代码初步实施完成，相应的硬件测试环境准备充分，方可测试一些运行时错误，而通过数据流分析技术，可以在项目开始的初期即引入测试，及早发现或修复一些严重错误。

❖ 如何修改注册表内容可以加快开机及关机速度？

执行[开始]→[运行]→键入[Regedit]→[HKEY_CURRENT_USER]→[Control Panel]→[Desktop]，将字符串值[HungAppTimeout]的数值数据更改为[200]。

将字符串值[WaitToKillAppTimeout]的数值数据更改为1000。另外在[HKEY_LOCAL_MAC HINE]→[System]→[CurrentControlSet]→[Control]中，将字符串值[HungAppTimeout]的数值数据更改为[200]，将字符串值[WaitToKillServiceTimeout]的数值数据更改为1000。

第6章
静态反汇编工具

在软件的调试跟踪和分析中，解密者经常使用某些工具软件来对程序进行静态分析。在对程序实施静态分析的过程中，经常用到的工具有：IDA Pro、W32Dasm、C32asm等。当然，在实际的应用过程中，还必须借助于其他工具来增强反编译和分析的效果，比如FileInfo、GTW、FS、Hiew等。

6.1 认识常用的反汇编程序

常用的反汇编工具有IDA Pro、W32Dasm、C32asm等。用W32Dasm打开一个没有加壳的应用程序，等到反汇编结束就会看到汇编出的源代码了。由于代码太多，所以这里仅节选一小部分讲解。C32asm的反汇编结果和W32Dasm的相同，只是有些信息需要通过菜单调用，而且外观和表达方式不同。

6.1.1 程序的基本信息

用W32Dasm反汇编出的源代码的最开始部分并不是程序的真正源代码，而是反汇编工具能够分析出的关于程序的一些基本信息，显示出了关于程序使用的各个寄存器段、区段、程序包的对象及引用的函数等信息分析的结果。

1．寄存器段和区段的基本信息

在反汇编代码的最开始可以看到如图所示的信息。

反汇编的程序名。此程序文件名为：ss.exe。

段的偏移地址及大小。比如代码（Data）的偏移地址（Offset）是24000H（这里的数值都是用十六进制表示）；段的大小是4000H。

区段数目、偏移地址、大小和标志，以及程序的基地址。

反汇编开始的代码

2．程序菜单和对话框的基本信息

在Windows系统下开发的应用程序一般都会包含菜单、对话框等空间对象，这部分显示出了程序的菜单信息和对话框信息，包括各自的ID号。如果要修改菜单或对话框，通过ID号就可以快速转到所在的地址处。具体的源代码如图所示。

程序菜单和对话框的源代码

3．程序的引入表函数的基本信息

程序的引入表函数就是程序中调用了哪些外部函数，这部分显示出了程序所调用的外部dll文件名及dll中的函数，如图所示。

程序的引入表函数的源代码

然后，就会看到程序真正反汇编的源代码部分了，如图所示。以这部分的代码作为分析对象，虽然比较多，不过不需要对全部代码进行分析，只需分析其中比较关键的几条或几十条语句即可，所占比例比较小。

程序的反汇编源代码

从上面的源代码可以看出401000H处就是程序代码的起始处，表明这就是程序源代码最开始的地址了。

另外，还可以看到“Program Entry Point=004017B0”这行代码，意思就是程序的入口点地址是004017B0。程序的入口点也就是程序开始执行的地方，比如运行一个程序，双击后它会从入口点地址处开始执行。它和程序代码的起始处并不是一个概念，请一定要分清。如果想让程序在运行处中断下来，就可以在入口点地址处下断点。

6.1.2 程序的反汇编源代码

在W32Dasm主窗口中把光标定在其中的一行，如图所示。例如，光标定位在“00401046 53 push ebx”这行代码上，虽然只定位了其中一行，却能告诉用户许多信息。

· “00401046”：这个是源代码的虚拟地址。无论在硬盘上还是映射到内存中，这个虚拟地址都是不变的。将来进行动态调试时，如果想对这行代码下断点，所下的断点地址也就是这行代码的虚拟地址。

· “53”：这是源代码所对应的机器码。我们可以把机器码理解为源代码对应的机器语言，是用十六进制来表示的。用十六进制编辑器修改程序也就是对机器码进行修改。

· “push ebx”：这就是程序反汇编后其中一行的源代码了。

· “@Offset 00001046h”：在任务栏的右侧，可以看到这个信息。意思是源代码的偏移地址是1046H。在十六进制编辑器中显示的源代码地址就是这个偏移地址，所以，如果我们想用编辑器对源代码进行修改，则要输入源代码的偏移地址，而不是虚拟地址。

W32Dasm的主窗口

其他还有一些语句需要理解掌握，对于将来分析源代码是非常重要的，如图所示。

其用线框起来的这段代码的意思是：401040处开始的源代码是由401014(U)处跳转过来的。（C）就是有条件地进行跳转；（U）就是无条件地进行跳转。

需要理解掌握的语句代码

看下面一段代码。

        ＊Referenced by a CALL at Addresses:
        |:004010A4 .
        |
        :0040100A  E981020000  jmp  00401290
        : 0040100F  E94C030000  jmp  00401360

这段代码的意思是：40100A处开始的代码是由上面的Call调用的。比如源程序执行到4010A4时，由于是个Call（调用子程序）语句，所以程序会接着来到40100A处开始的子程序开始执行，遇到jmp到401290下面一条语句接着执行。

下列代码的意思是：401067处的Call语句执行的是USER32.dll中的LoadStringA这个API函数，如图所示。

程序代码

其他还有许多语句需要用户在实践中去亲自体会，如果用户掌握了一些API编程，就会发现对于分析程序有很大的帮助。

6.2 静态反汇编工具概述

IDA Pro虽然是目前功能较强大的反汇编工具，但对于入门者来说或处理一般的软件时却基本用不到。此外，并不是所有的软件反汇编都能够得到有用的信息，比如反汇编使用VFP、PB等语言编写的程序，基本上反汇编不出什么。因此，对于这类程序还需使用相应的反编译工具，才能得到相关的信息。

6.2.1 静态反汇编工具W32Dasm

W32Dasm是一款URSoft开发的反汇编和调试工具，远程攻击者可以利用这个漏洞来构建恶意的可执行文件，诱使用户处理，即能以用户进程权限在系统上执行任意指令。W32Dasm静态分析软件操作简单，使用方便，通常被程序员使用。使用静态分析软件W32Dasm与Crack相关的步骤如下。

1．对选择的文件进行反汇编

选择一个要进行反汇编的文件，对其进行反汇编的具体操作步骤如下。

1 运行W32Dasm之后（以程序文件test.exe为例），选择【反汇编】→【打开文件】菜单项或单击工具栏按钮，弹出【Select File to Disassemble】对话框，选择test.exe文件，单击【打开】按钮，如图所示。

【Select File to Disassemble】对话框

2 选择【反汇编】→【设置反汇编选项】菜单项，即可弹出【W32Dasm选项】对话框，其中3个复选框都选择，然后单击【确定】按钮，如下图所示。

【W32Dasm选项】对话框

3 可以通过选择【反汇编】→【字体】→【选择字体】菜单项，在弹出的【字体】对话框中进行适当调整（默认字体为8号的Courier New字体），单击【确定】按钮，如下图所示。

【字体】对话框

2．保存反汇编文本文件

如果想实现保存反汇编文本文件和创建方案文件，则可选择【反汇编】→【保存反汇编文件和创建工程文件】菜单项，弹出【另存为】对话框，这里选择驱动器为E盘，再单击【确定】按钮，对反汇编文件进行保存就可以了，如图所示。

【另存为】对话框

3．反汇编源代码的基本操作

对反汇编源代码进行相关操作的具体步骤如下。

1 在工具栏上直接单击按钮或选择【转移】→【转到代码头Ctrl S】菜单项，即可将光标转到代码的开始处，此时即可通过双击或用方向键改变光标的位置，如图所示。（代码的开始处是指反汇编代码列表清单汇编指令的开始，而不是指代码运行的起点，程序运行的起点称为程序入口点。）

来到代码的开始处

2 此时只要在工具栏上单击按钮或选择【转移】→【转到程序入口处F10】菜单项（或按<F10>键），即可将光标转到程序的入口点处，这里就是程序执行的起始点，一般动态调试时也就停在此处，如图所示。

光标定位到程序入口点

3 在工具栏上单击按钮或选择【转移】→【转到页F11】菜单项（或按<F11>键），即可弹出一个【转到页】对话框，在【页】文本框中输入页数，单击【确定】按钮，就可以跳转到相关页面中了，如下图所示。

【转到页】对话框

4 在工具栏上单击按钮或选择【转移】→【转到代码位置Shift F12】菜单项（或按<F12>键），即可弹出一个【转移代码位置（32位）】对话框，在【代码偏移】文本框中输入“00401000”，单击【确定】按钮，即可跳转到代码偏移00401000后的位置，如下图所示。

【转移代码位置(32位)】对话框

5 实现执行文本的跳转功能在【执行文本】菜单选项中，【执行跳跃】功能的激活条件是光标在代码的跳转指令行上（此时光条呈高亮度的绿色），并且此时的工具条按钮也将被激活，如图所示。

光标在代码的跳转指令行上

6 此时如果单击按钮或选择【执行跳跃】菜单项（或按右方向键），光条将被切换到跳转指令所指向的位置（在本例中，光条将切换到：00401040 55 push ebp这一行代码处），如图所示。

来到：00401040 55 push ebp行的代码处

7 如果此时想返回到上一次跳跃处，则可以选择【执行文本】→【返回上一跳跃】菜单项，不过该选项只有在【执行文本跳转】功能完成后才能够被激活。当执行文本跳转的条件成立时，单击按钮即可激活此项功能。单击按钮或选择【执行文本】→【返回上一跳跃】菜单项或（按左方向键），光条将返回到上一次跳跃位置处，如图所示。

【返回上一跳跃】菜单项

8 选择【执行文本】→【执行呼叫】菜单项，即可激活【执行呼叫】功能，此功能激活的条件是光条在CALL指令一行。此时该行光条呈绿色高亮显示，工具按钮将被激活，执行时光条将会来到CALL所指的地址处（本例中光条在：00401129 E8C2050000 call 004016F0一行上），如图所示。

光条来到CALL所指的地址004016F0行

9 如果此时想重新返回到刚才起点的位置：00401129 E8C2050000 call 004016F0这一行，则只需选择【执行文本】→【返回上一呼叫】菜单项或按左方向键就可以了，该项指令仅仅是在【执行呼叫】功能完成之后，才可以被激活，如图所示。

【返回上一呼叫】菜单项

10 导入功能主要用于查看W32Dasm的输入函数清单，单击工具按钮或选择【函数】→【输入】菜单项，即可打开【W32Dasm输入函数清单】窗口，在其中将列出当前文件的W32Dasm输入函数清单，只需双击其中的函数项目，窗口中的光条即可转到调用这些函数的代码处，如图所示。

【W32Dasm输入函数清单】窗口

11 选择【函数】→【输出】菜单项，即可调用【输出】功能查看导出的W32Dasm函数，在打开的窗口中将列出当前文件所导出的W32Dasm函数。

【输出】功能主要用于查看PE文件的导出函数（Exports），导出函数一般在DLL文件中，其作用是提供内部函数接口供外部程序调用（如USER32.DLL链接库提供各种API函数接口给应用程序调用）。EXE文件一般只有导入表，没有导出表，而DLL文件则两者都有。

12 在【参考】菜单中还包含有【菜单参考】、【对话框参考】和【串式参考】三个子菜单功能选项，分别对应工具按钮：、和。其中的【串式参考】功能列出了程序中相关的字符串、对话等信息。

在静态分析中，利用这些信息可快速定位到相关代码上。在【串式参考】窗口中，可以通过双击这些字串跳到相关代码上。若存在多处调用，双击这个字符串时，光条将在调用它的几个位置代码处循环。

其中【串式参考】选项在功能破解时很常用，例如选择它，即可打开【W32Dasm串式参考内容清单】窗口，如图所示。

【W32Dasm串式参考内容清单】窗口

而【W32Dasm对话框参考内容清单】窗口中的内容清单形式则是Dialog:DialogID_0067，其中数据“0067”是该窗口的ID号（十六进制表示），该ID号的十进制为103，用资源工具查看，可知其是test.exe程序的【test】对话框，如下图所示。

【W32Dasm对话框参考内容清单】窗口

如果双击“Dialog:DialogID_0067”光条，即可跳转到相关代码上。如多处调用，双击这个文本时，光条将在调用它的几个位置代码处循环。

提示

在软件解密过程中，将资源查看工具与W32Dasm对话框参考配合，能快速定位到调用对话框的代码处。

由于W32Dasm的使用并不怎么复杂，因此对于那些没有破解经验的计算机爱好者，只要有一点汇编语言知识，再看一些用W32Dasm破解的文章，用不了多久，就可以掌握一些简单的破解技术了。

4．复制汇编代码文本

由于W32Dasm允许用户进行打印或复制指定行的汇编代码，也就是说用户可以对反汇编代码文本进行复制，要实现这一过程其实很简单，具体的操作步骤如下。

1 将光标移到W32Dasm的最左边单击，将会出现一个小红点，如图所示。

W32Dasm反汇编窗口1

2 再按住<Shift>键，向下移到我们需要的那一行，然后再单击一下鼠标左键，这时候就可以发现将选中的一段全部标上了小红点，如图所示。

W32Dasm反汇编窗口2

3 按下<Ctrl +C>组合键对其进行复制或选择【反汇编】→【复制选定行】菜单项（或单击按钮）之后，就可以把数据复制到剪贴板里了，如图所示。

【复制选定行】菜单项

5．装载32位的汇编代码动态调试

W32Dasm不仅是一款出色的静态分析工具，同时也具有动态分析功能，但在关于动态分析技术的一章中将有更优秀的工具，所以此处不再介绍W32Dasm的动态调试功能。

如果想要装载32位的汇编代码动态调试，则可以通过执行如下的操作步骤来实现。

1 确定需要反汇编程序test.exe。选择【反汇编】→【打开文件】菜单项，弹出【Select File to Disassemble】对话框，选取要打开的文件，单击【打开】按钮，如下图所示。

【Select File to Disassemble】对话框

2 选择【调试】→【加载处理】菜单项或按下<Ctrl+L>组合键，即可打开一个【载入反汇编对话框】对话框，在【任选命令行】文本框输入命令行，单击【载入】按钮，如下图所示。

【载入反汇编对话框】对话框

3 单击【载入】按钮之后，程序test.exe开始被W32Dasm进行动态调试，此时将出现左右两个调试窗口，分别如下图和下图所示。在初始化该程序后，指令将停留在入口点处。调试窗口1列出各种状态器，如CPU寄存器数据、CPU标志位、断点、用户指定的内存数据、活动的DLL、段寄存器等；调试窗口2则显示当前执行的汇编指令和运行、暂停、单步运行以及终止等各种调试操作。

调试窗口1

调试窗口2

下面依次来讲解各部分的含义。

· 区块信息

代码清单第一部分列出的是PE文件区块信息。

以上清单显示被反汇编程序的．text,.rdata,.data三个区块的地址、大小和属性。

· 菜单与对话框信息

由于反汇编的程序没有菜单及对话框资源，因此这部分无内容。

· 导入函数信息

此部分列出反汇编程序的输入函数信息，内容与选择【函数】→【输入】菜单项打开的对话框一样。

· 导出函数信息

由于该EXE文件无导出函数，因此这部分无内容。

· 汇编代码列表

该部分是被反汇编的程序在被反汇编后的代码数据列表，这段汇编代码和该程序的源程序是对应的，但程序执行入口点并不在这里，操作系统实际上并不调用用户所编写的进入点函数。

如果想要对自己反汇编的程序进行运行、暂停或终止服务，则可以通过如下的操作步骤来实现。

1 在调试窗口2中，单击【运行】按钮或按<F9>键，程序test.exe将运行起来，如下图所示。

单击【运行】按钮

2 如果在调试窗口2中单击【暂停】按钮或空格键，则反汇编程序将暂停运行，这个操作在对反汇编程序进行单步跟踪的时候经常用到，如下图所示。

单击【暂停】按钮

3 如果在调试窗口2中单击【终止】按钮，弹出【Debugger Message】对话框，单击【是】按钮则反汇编程序将停止运行，并退出该反汇编程序的动态调试环境，如图所示。

单击【终止】按钮

6．对程序实行单步跟踪

如果想要对自己反汇编的程序实行单步跟踪，则可以执行如下的操作步骤。

1 重新加载程序test.exe。W32Dasm程序主界面如图所示。

W32Dasm程序主界面

2 在程序加载后，停留在入口点，单击【调试】→【加载处理】菜单项，弹出【载入反汇编对话框】对话框，单击【载入】按钮，如下图所示打开调试窗口1，如下图所示在这里可以通过按<F7>键或<F8>键进行单步调试程序。这两个键所不同的是按<F7>键是跟进CALL里，而按<F8>键则只是路过。

【载入反汇编对话框】对话框

调试窗口1

3 直接按<F5>键进入自动调试或按<F6>键结束自动调试。

7．设置和激活断点

在W32Dasm的主窗口中上下移动光条选择要设置断点的指令，或选择【转移】→【转到代码位置】菜单项，或使用【参考】操作来选择指定的代码位置，然后按<F2>键或选择【调试】→【打开/关闭断点】菜单项，这样就可以在该代码中加上断点了。

如果想要设置该程序的激活断点，则可以执行如下的操作步骤。

1 重新加载程序test.exe。

2 在W32Dasm主窗口中选择【转移】→【转到代码位置】菜单项，在打开的【转移代码位置（32位）】对话框中输入“00401005”之后，单击【确定】按钮，即可跳转到00401005地址一行（此时W32Dasm的主窗口可能最小化了，把其还原即可）。光条在这一行显亮绿色，按<F2>键或单击最左边（同时按住<Ctrl>键）来设置断点，如图所示。

【转移代码位置（32位）】对话框

3 此时如果断点设置成功，则在光条最左边将会出现一小段黄条，显示此行为断点，如图所示。如果断点不在这里，则整行光条的显示将是黄色的。

设置断点

4 当断点设置好之后，在调试窗口中的断点小窗口中，将会显示出断点的具体情况，右边有一个“*”号。这时候如果按下<F2>键或使用鼠标左键+<Ctrl>键，则断点将被取消；如果按下<F9>键，则运行test.exe程序并在程序运行到相关断点时停止，当前选择菜单的复制（或按<Ctrl+C>组合键）时，就会激发该断点，程序就会暂停在004017B0的位置，如图所示。

断点小窗口

8．偏移地址和虚拟地址转换

由于机器指令在文件里是按偏移地址存放的，但W32Dasm显示的是虚拟内存地址。不过不必担心，W32Dasm已经帮用户自动作了转换，在W32Dasm主窗口最下面的状态栏里显示当前指令的偏移地址，如图所示。

虚拟地址与文件偏移地址的转换

在该窗口底部有一行字指示其偏移地址，如虚拟地址为：Code Data@00401969，而偏移地址为：@Offset 00001969h，这就是偏移地址。

希望大家在阅读汇编代码时注意如下几点。

· 函数入口参数按从右到左的顺序入栈，API函数自动平衡堆栈。push指令使“esp-4”, pop指令使“esp+4”，此处的4表示4个字节（32位）。

· 为了注释的方便，这里将机器码去除了。每条语句后面的注释“esp”是表示这条语句执行完后的堆栈值，所有数据皆是十六进制。

· 对于WM_DESTROY、WM_CLOSE、IDYES、IDNO等标识符的具体数值，可在Microsoft的Visual C++头文件中找到对应值（大多数在WINUSER.H中），如：

        #define IDYES 6
        #define IDNO 7
        #define WM_CLOSE 0x0010
        #define WM_DESTROY 0x0002

有关W32Dasm更加具体的操作请参阅其帮助文档。除W32Dasm之外，还有其他的工具如Source7、IDA Pro和Hiew等都是比较出色的反汇编工具。此外，为了编辑修改代码，还需要经常用到UltraEdit（文本编辑器）和Hiew（十六进制编辑器）等辅助编辑工具。

6.2.2 静态反汇编工具C32asm

C32asm是款非常好用的反汇编程序，它的功能已强于Winhex 16、W32Dasm，具有反汇编模式和十六进制编辑模式，能跟踪EXE文件的断点，也可直接修改软件内部代码。使用静态分析软件C32asm相关的步骤如下。

1．对选择的文件进行反汇编

选择一个要进行反汇编的文件，对其进行反汇编的具体操作步骤如下。

1 打开C32asm程序，待程序完全启动后，C32asm程序的主界面如图所示。

C32asm程序的主界面

2 单击【文件】→【打开文件进行反汇编】菜单项或单击工具栏按钮，弹出【打开】对话框，选择要反汇编的文件，单击【打开】按钮即可，如图所示。

【打开】对话框

3 单击【高级】→【反汇编配置】菜单项，弹出【反汇编设置】对话框，用户可在【显示】区域设置字体和背景颜色，如图所示。

【反汇编设置】对话框

2．保存反汇编文本文件

用户要保存反汇编文本文件和创建方案文件，则可选择【文件】→【保存】菜单项，弹出【C32Asm】对话框，单击【是】按钮，对反汇编文件进行保存就可以了，如图所示。

【C32Asm】对话框

3．反汇编源代码的基本操作

对反汇编源代码进行相关操作的具体步骤如下。

1 选择【编辑】→【转到入口点】菜单项或在工具栏上直接单击按钮，程序即可转到程序的入口点，如图所示。

【转到入口点】菜单项

2 此时只要在工具栏上单击按钮或选择【编辑】→【跳到】菜单项（或按<Ctrl+G>组合键），弹出【Eip跳转对话框】对话框，在【EIP】文本框中输入“00401004”，单击【确定】按钮即可将光标转到程序的“00401004”处，如图所示。

【Eip跳转对话框】对话框

3 实现执行文本的跳转功能在【编辑】→【跳转】菜单项中，【跳转】功能的激活条件是光标在代码的跳转指令行上（此时光条呈红色），并且此时的工具条按钮也将被激活，如图所示。

【跳转】菜单项

4 此时如果单击按钮或选择【跳跃】菜单项，光条将被切换到跳转指令所指向的位置（在本例中，光条将切换到：:00401290:: 55 PUSH EBP\:BYJMP JmpBy:0040100A这一行代码处），如图所示。

切换到：:00401290:: 55 PUSH EBP行的代码处

5 如果此时想返回到上一次跳跃处，则可以选择【编辑】→【返回跳转】菜单项，不过该选项只有在【跳转】功能完成后才能够被激活。当执行文本跳转的条件成立时，单击按钮即可激活此项功能。单击按钮或选择【编辑】→【返回跳转】菜单项，光条将返回到上一次跳跃位置处。

【返回跳转】菜单项

6 选择【编辑】→【调用】菜单项，即可激活【调用】功能，此功能激活的条件是光条在CALL指令一行。此时该行光条呈紫红色高亮显示，工具按钮将被激活，执行时光条将会来到CALL所指的地址处（本例中光条在：:004011FA:: E8 F1040000 CALL 004016F0 \:JMPDOWN一行上），如图所示。

【调用】菜单项

7 如果此时单击工具按钮或再选择【编辑】→【调用】菜单项，则光条将移动到CALL所指的地址004016F0这一行，如图所示。

来到地址004016F0这一行

8 如果此时想重新返回到刚才起点的位置：:004011FA:: E8 F1040000 CALL 004016F0\:JMPDOWN这一行，则只需选择【编辑】→【返回调用】菜单项就可以了。该项指令仅仅是在【调用】功能完成之后，才可以被激活，如图所示。

【返回调用】菜单项

9 当上述条件成立时，工具栏中的按钮也将被激活。此时单击按钮或选择【编辑】→【返回跳转】菜单项，即可将光条返回到上一次呼叫位置处，如图所示。

上一次呼叫位置

4．复制汇编代码文本

由于C32asm允许用户进行打印或复制指定行的汇编代码，也就是说用户可以对反汇编代码文本进行复制，要实现这一过程只要执行如下的操作就可以了。

具体的操作步骤如下。

1 将光标移到C32asm的最左边按住左键单击，向下移到我们需要的那一行，然后再松开鼠标左键，即可选定要复制的代码，如图所示。

选定要复制的代码

2 按下<Ctrl +C>组合键对其进行复制或选择【编辑】→【拷贝】菜单项（或单击按钮），就可以把数据复制到剪贴板里了。

【拷贝】菜单项

有关C32asm更加具体的操作请参阅其帮助文档。用户可以使用C32asm进行反汇编，也可以借助于其他工具进行反汇编。

6.2.3 静态分析软件IDA Pro

IDA Pro 6.6（简称IDA）可以说是目前功能较为强大、丰富，支持文件种类、CPU指令集较多，交互性较强的一款交互式静态反汇编的工具了，它的交互性可以让用户很方便地得到符合自己要求的反汇编代码。

由于现在的软件越来越多地使用了代码运行中解密、Import Table破坏等防Crack的技术，使用IDA Pro就可以很方便地在静态的条件下，还原出代码和数据的本来面目。

下面就来看一下它的各项功能。

1.IDA Pro的主窗口和菜单配置

IDA Pro的主程序是IDAG.EXE，其分析完程序后界面比W32Dasm更专业和复杂，如图所示。该软件可以尽量为用户分析该程序各模块的功能，并给出相应提示，如为API函数的参数自动加上注释，相当直观易懂。

IDA Pro运行主窗口

用户在主窗口还可以通过选择【视图】→【打开子视图】→【反汇编】菜单项来打开反汇编子窗口，这样就可以用多个子窗口来分析同一段程序，不必来回翻页查看代码了，如图所示。

【反汇编】菜单项

如果选择【视图】→【工具栏】→【导航】菜单项或在主窗口下方提示窗口中右击，就可以打开导航器了，如图所示。

【导航】菜单项

IDA最下面的提示窗口主要用来向用户反馈各种信息，如果在机器上该窗口没有出现，则只要打开注册表，查找“DataRescue”并删除该主键，重新启动机器就可以看到它了。

下面通过菜单中的【设置】选项讲解一下IDA Pro的配置。值得一提的是，这种配置仅对当前的项目有效。

（1）【反汇编】选项卡

选择【设置】→【常规】菜单项，即可打开【IDA常规选项】对话框，如图所示。

【IDA常规选项】对话框

（2）【分析选项】选项卡

单击其中的【分析选项】选项卡后打开分析配置选项。IDA会自动识别程序类别与处理器类型，在大多数情况下，分析选项的默认值在准确性与方便性之间提供一个折中参数。当IDA分析出有问题的代码时，将【内核选项1】按钮点开后出现的【内核分析设置1】对话框中的【确认最终的分析】复选项关闭，单击【确定】按钮，如图所示。

【分析选项】选项卡

有些情况下，一些代码因为不在预计的位置而得不到确认，这里建议大家不妨尝试单击【内核选项2】按钮，弹出【内核分析设置2】对话框，勾选【在最终确认后锁定数据区段】复选项，或许会带给自己一些帮助，如图所示。

【内核选项2】按钮

（3）【交叉参考】选项卡

单击【交叉参考】选项卡之后，即可打开交叉参考配置选项，如图所示。只要在这里改变【显示交叉参考数】文本框中的值，就可以使交叉参考的显示个数发生改变了。

【显示交叉参考数】文本框

（4）【字符串】选项卡

单击【字符串】选项卡之后，即可打开字符串配置选项，如图所示。在IDA Pro中我们需要在ASCII字符串前加个前缀“a”，因此搜索字符串时，要加个前缀“a”才能找到。

【字符串】选项卡

（5）其他选项配置

大家可能已经看到了，在后面还有【浏览器】、【图表】和【杂项】等选项卡，不过由于这些选项卡一般设置不设置都没什么根本性的变化，因此只选默认设置即可。

2．打开/装载文件

仍然以small test.exe程序为例来介绍如何在IDA Pro中装载一个新的文件，具体的操作步骤如下。

1 运行IDA图形界面程序IDAG.EXE之后，选择【文件】→【打开】菜单项，如下图所示，弹出【选择一个文件以进行反汇编】对话框，选择small test.exe程序，单击【打开】按钮，如下图所示。

【打开】菜单项

【选择一个文件以进行反汇编】对话框

2 如果程序有资源，则在单击【打开】按钮后，弹出【保存数据库】对话框，可以勾选【打包数据库（存储）】单选按钮，单击【确定】按钮，如下图所示，然后再重新选择文件打开。

【保存数据库】对话框

由于Windows 7以上系统的可执行文件都是PE格式文件，因此这里的文件类型可选择PE格式。IDA装载PE文件时，是按区块（区段）来装载的，比如，.text=代码块，.data=数据块，.rsrc=资源块，.idata=输入表，.edata=输出表等。

一般情况下，如果想查找API函数，就到．idata块；如果想知道DLL输出函数，就到．edata块。设置好后，直接单击【确定】按钮IDA Pro就开始反汇编了，反汇编的时间与程序大小及复杂程度有关。一般的小软件只要等一会儿就可以了，大软件则可能时间会长一些。

这个过程通常可分作两个阶段。

① 将程序的代码和数据分开，然后为各函数作标记并分析其参数调用，分析跳转、调用等指令关系并给标签赋值等。

② 如IDA识别出文件的编译类型，则装载对应的编译器特征文件，然后给各函数赋名。

3．注释、交叉参考与查找字符串

使用IDA Pro可以很方便地在代码后面输入注释。在主窗口右边空白处右击，即可看到如图所示的快捷菜单，只要单击【输入注释】选项，弹出【Please enter text】对话框，即可在【Enter comment 】文本框中输入说明性文字，单击【确定】按钮，如图所示。

【Please enter text】对话框

而通过单击【视图】→【打开子视图】→【交叉参考】菜单项，则可以知道指令代码相互调用的关系，其表现如图所示，这句中“j”表示跳转（jump）。此时只要双击“jnz short loc_4031E3”或按<Enter>键，即可跳到调用该处的地方。

“jnz short loc_4031E3”代码

虽然IDA Pro里的字符串无法在串式参考列表上明确显示，但却可以在Names窗口看到，只要选择【视图】→【打开子视图】→【名称】菜单项，即可打开【Names window】窗口，如图所示。

【Names window】窗口

从这个窗口中可以看出，如果想要查找某个字符串，则只要在这个字符串上双击鼠标将跳到该行字符串上。然后双击该字符串，就可以转到定义该字符串的代码处了。

4．参考重命名

“参考重命名”是IDA Pro的一个极好功能，它增加了代码的可读性，用户可以随便在窗口函数中找一段代码来设置重命名参考点，利用参考重命名功能便可以把所有参考点一次改动。

只要在选中的代码上右击，再在弹出的右键菜单中选择【重命名】选项，或把光标移到地址0x40342B上并按<N>键，【重命名地址】对话框便出现了，如下图所示。

【重命名地址】对话框

在此处给它赋予“test”这个名字，在单击【确定】按钮之后，马上可以看到所有“off_446A20”标签都改变为新名称test了，如图所示。

改名后的参考点

5．标签与进制的转换

只要选择【跳转】→【标记位置】菜单项，即可打开标记当前位置对话框，如图所示。在【Enter mark description】文本框中输入标记名称，如“small test”标签，便是需要返回的位置。

标记当前位置对话框

当在离开这个标记而返回时，只要选择【跳转】→【跳转到标记的位置】菜单项或按下<Ctrl+M>组合键执行【跳转到标记的位置】功能，即可打开跳转到标记位置窗口，如图所示。在选择返回的标签之后进行双击，就可以转到指定代码处了。

跳转到标记位置窗口

下面再来看一下在IDA Pro中进制的转换功能。由于IDA Pro可以提供多种进制代码显示，如在本程序中的十六进制“0FFh”上单击鼠标右键，便会弹出选择菜单，如图所示。用户可以从中选择十进制（255）、八进制（377o）或二进制（11111111b）等。

进制转换

6．数组和结构体

在IDA Pro中进行数据处理时，还可以按数组的形式显示，具体的操作步骤如下。

1 将光标移到需要处理的数据处之后，选择【编辑】→【数组】菜单项或单击按钮，即可打开【Convert to array】对话框，如图所示。

【Convert to array】对话框1

2 在这里看一下此处数据如果按3×12格式排列的情况，如图所示。其中，如果在【Convert to array】对话框中的【Items on a line】文本框中填“0”，则每行项数根据页面自动调整；如果想让每行显示更多数据，可以在反汇编选项中调整右边距（Right margin）。

【Convert to array】对话框2

利用IDA Pro还可以查看数据的结构（Structures），并用有含义的名字代替无意义的数字，具体的操作步骤如下。

1 选择【视图】→【打开子视图】→【结构体】菜单项或在工具栏上单击按钮，就可以打开【结构体】窗口了，如图所示。

【结构体】窗口

2 在查看数据的结构体之后，单击【编辑】菜单，在弹出的下拉菜单中单击【添加结构体类型】菜单项，即可增加一个新的结构体类型，此时将会弹出【Create structure/union】对话框，在【Structure name】文本框中输入“struc_1”，如下图所示。

【Create structure/union】对话框

3 此时如果按<D>键，则可以加入数据（如id,age）；而按<A>键，则可以加入ASCII字符（如name）为结构成员。

4 当增加新结构成员时，IDA Pro会自动加以命名，如rield_0等；还可以通过按<N>键，弹出【Rename Structure】对话框，在【Structure name】文本框中输入名称来修改新结构成员的名字，如下图所示。

【Rename Structure】对话框

5 还可以在操作数类型中重新定义现有的数据，只要在选中需要重新定义的数据之后，选择【编辑】→【字段类型】→【偏移】→【偏移量（结构体）】菜单项的结构偏移功能，就可以看到经过处理的结构代码了，如图所示。

【偏移量（结构体）】菜单项

7．枚举类型与堆栈变量

在利用IDA Pro反汇编时，还可以利用其动态地定义与操作枚举类型。如在用IDA Pro反汇编后得到的是一些没有意义的数字，那么，这时候就可以用枚举类型来表示这些数字。

具体的操作步骤如下。

1 选择【视图】→【打开子视图】→【枚举】菜单项，即可打开枚举窗口，如图所示。

【枚举】窗口

2 在空白处单击右键，在下拉菜单中单击【添加枚举】菜单项，弹出【Add enum type】对话框，即可插入一个新的枚举类型，如在这里将其取名为test，如下图所示。在新建的test枚举类型中按<N>键添加枚举成员，0对应MONDAY,1对应TUESDAY，依此类推，如下图所示。

【Add enum type】对话框

添加的枚举成员

3 将光标移到需要重新定义的数据上之后，单击右键，在下拉菜单中单击【编辑枚举成员】菜单项，如下图所示，弹出【Edit enum member】对话框，在【Name】文本框中输入“MONDAY”就可以转换成指定的枚举成员了，如下图所示。

【编辑枚举成员】菜单项

【Edit enum member】对话框

4 经过上述处理之后，就可以看到IDA Pro已经用MONDAY、TUESDAY等代替了代码中原先无意义的数字0、1等，这样就大大地增加了代码的易读性。

在IDA Pro里，堆栈变量也可以给出有意义的名称。只要在选中的任何函数堆栈上（如Msg上）双击鼠标左键或按下<Ctrl+K>组合键，即可打开堆栈变量窗口，然后在指定光标处按<N>键即可打开命名窗口，接着在其中输入新的名称就可以了。

8.IDC脚本控制器

几乎所有比较典型的反汇编过程都会面临许多重复的任务。这时候，某些编译器给输入表所在的区段生成的默认名字是．idata。实际上，输入表可以放在任意一个区段中，.idata不是输入表的标志，定位输入表的正确方法是根据PE文件的结构特征。

在IDA中查看输入表可执行如下的操作步骤。

1 单击【视图】→【打开子视图】→【名称】菜单项，弹出【Names window】窗口，双击其中一个API函数就能来到．idata段，如图所示。

【Names window】窗口

2 选择【视图】→【打开子视图】→【区段】菜单项或按下<Shift+F7>组合键，就可以打开段窗口了，双击其中的．idata选项即可，如图所示。

出现的段窗口

3 选择【视图】→【打开子视图】→【交叉参考】菜单项，交叉参考可跳转到相应的调用代码处，如图所示。

跳转到相应的调用代码处

4 选择【文件】→【打开脚本文件】菜单项或直接按下<Alt+F7>组合键，即可打开【Run script】对话框，在选中文件之后，单击【打开】按钮即可，如图所示。

【Run script】对话框

5 如果在这里同时打开了多个脚本，则这些脚本将会依次排列，然后只要在相应按钮上右击，即可删除相应脚本，脚本输出的结果将会在IDA Pro提示窗口中显示。

下面再来看一下如何在IDA Pro中查看输出函数，具体的操作步骤如下。

1 用IDA Pro来打开LangDLL.dll文件，选择【设置】→【名称表示】菜单项，即可打开【名称】对话框来设置符号名表示，此时就可以看到其中的“常规”“公共”“自动生成”三项是默认勾选的，在此只选【公共】一项，如图所示。

【名称表示】菜单项

2 另外，可以通过选择【跳转】→【跳转到入口点】菜单项来打开入口点选择窗口，里面列出的这些就是输出函数了，如图所示。

【跳转到入口点】菜单项

为了方便IDA Pro的功能扩展，IDA Pro提供了丰富的插件接口，这些插件程序大家可根据自己的需要进行选择，在此不作介绍。

9．输出反汇编代码

下面再来看一下IDA Pro中反汇编代码的输出功能，选择【文件】→【输出文件】菜单项，即可按MAP、ASM、LST、EXE、DIF等格式输出文件，如图所示。

【输出文件】菜单项

下面来看一下这些创建的不同格式文件的区别。

① 创建MAP文件。

创建出的MAP格式文件是一个文本文件，在里面记录了各个函数等的符号信息。

② 创建ASM文件。

创建出的ASM文件仅仅是其汇编代码部分，并且在每行代码前没有地址。

③ 创建LST文件。

创建出的LST文件则是将代码窗口中的所有文本输出，包括地址、汇编代码、注释等。如果我们想要复制其中的某段代码，只要在按住<Shift>键的同时再移动光标键选择该段代码，然后按<Ctrl+C>组合键复制到剪贴板就可以了。

④ 创建EXE文件。

这一种输出格式仅对DOS程序有效，利用该功能可以生成DOS系统下的EXE或COM文件。

⑤ 创建DIF文件。

创建出的DIF文件记录了该程序代码修改前后的变化，类似于DOS系统下的“fc”命令。

⑥ 转储数据库为IDC文件。

如果选择了【转储数据库为IDC文件】选项，则相当于将当前IDA的数据变化记录到了IDC文件中，供日后恢复当前数据使用。值得一提的是，该命令充分体现了向下兼容的特点，可将低版本中的工作记录转换到高版本中来。

⑦ 转储类型信息为IDC文件。

【转储类型信息为IDC文件】命令主要用来保存一些用户自定义数据类型到IDC文件中，如关于枚举、结构等的用户自定义类型。用户可以轻而易举地利用该命令将一个程序的数据类型导入到另一个程序中去。

6.3 静态分析解密详解

本节通过静态分析破解ss test.exe，来简单讲述一下静态分析解密程序的一般过程。由于不涉及动态调试，所以静态分析解密只能简单地修改程序的内容。

6.3.1 静态分析解密的过程概述

静态分析的第一步就是分析程序的类型，了解程序编写使用的语言，编译程序使用的编译器，程序有没有加壳保护，然后才能进行下一步工作。

静态分析工具主要由反汇编工具、程序类型分析工具和资源编辑器工具3部分构成，下面就来对其进行一些简单的介绍。

由于目前的大多数软件在设计时，都采用了人机对话方式，因此也就给解密带来了方便。这主要是因为，人机对话的实质就是软件在运行过程中需要由用户选择的地方（如注册窗口、信息提示对话框等）都会显示相应的提示信息，并等待用户对其进行选择。而在执行完某一段程序之后，便显示一串提示信息，以反映该段程序运行后的状态，是否正常运行或提示用户如何进行下一步工作。

对软件采用静态分析破解一般有如下几个步骤。

① 运行软件，查看软件究竟有哪些限制或出错信息。

② 查壳。如果有壳则需脱壳，否则无法进行静态反汇编或者反汇编的结果不正确。

③ 静态分析。利用反汇编工具进行反汇编，然后根据软件的限制或者出错信息找到相关的代码处，找到关键call和关键跳转。

④ 修改程序。通过十六进制编辑器或者汇编编辑来修改机器码或者汇编代码。

⑤ 制作补丁程序。

软件制造者一般都要根据输入的用户名或者机器码来构造注册算法，注册算法反映在汇编源代码中，一般都类似于子函数，被call语句调用，这些调用了注册算法的call就叫作关键call（不过只有通过动态调试跟踪，才可以判断哪些是关键call及注册算法是什么）。

在调用之后，就要和输入的注册码进行校验判断（或输入的注册码也要参与到计算机当中），如果正确则跳转到正确处执行，如果错误则跳转到错误处执行，这些跳转都是通过跳转指令来实现的（可能有一处或者多处判断，对应的跳转也会是一个或多个），由于该跳转决定了注册成功与否，所以被解密者称为“关键跳转”。

通过修改关键跳转来修改程序运行的流程（如让程序只能跳到正确的流程处），就可以达到解密的目的了。

6.3.2 两种注册判断的修改方法

对真假注册码判断一般都有两种方法，从中可以看出ss test.exe是属于第1种流程的，如下表所示。

常用的两种注册码判断流程

对于第1种流程，因为跳转指令紧接着是成功信息，所以一般可以把跳转指令改为空指令（nop）。

对于第2种流程，因为跳转指令紧接着是错误信息，所以必须跳过，这里一般可以把条件跳转指令改为无条件跳转指令（jmp），让流程强制性地跳过错误信息。

另外，也可以将跳转指令改为与其功能相反的跳转指令，如可以将jne指令改为je指令。也就是输入假注册码反而可以注册成功，输入真注册码反而会注册失败，不过输入真注册码的概率太小了。这样做的好处就是可以用最小的改动来实现对软件的解密。

6.3.3 常见指令的机器码值

一般修改的代码多是跳转指令和空指令，上面的方法就是利用汇编编辑来直接修改代码达到破解的目的。用户还可以通过修改代码对应的机器码来达到破解的目的，如表中给出了一些最常见的跳转指令和空指令的机器码值，仅供大家参考。

跳转指令非常多，用到时可以查阅汇编书籍或上网进行搜索。修改时推荐初学者使用汇编编辑功能，这样既不用去记忆而且也不容易出错。常见的汇编指令及对应的机器码如下表所示。

常见的汇编指令及对应的机器码

另外，也有一些小工具提高了将汇编代码转为机器码的功能，如选择C32asm主窗口中的【工具】→【汇编代码查询】菜单项就提供了这个功能，如图所示。

C32asm的【汇编转换】对话框

6.4 可执行文件编辑修改工具

在对可执行文件进行编辑修改时，可以使用某些专门的工具软件对文件进行分析，前面讲的W32Dasm、C32asm和IDA Pro都是分析文件的工具，而十六进制编辑的工具有WinHex、Hiew、HexWorkshop和UltraEdit，本节对其进行具体介绍。

6.4.1 WinHex使用简介

WinHex是一个专门用来对付各种日常紧急情况的小工具，可以用来检查和修复各种文件、恢复删除文件、硬盘损坏造成的数据丢失等，同时它还可以让你看到其他程序隐藏起来的文件和数据；可进行Hex与ASCII码编辑修改、多文件替换功能、一般运算及逻辑运算、磁盘磁区编辑（支持FAT16、FAT32和NTFS）自动搜寻编辑、文件比对和分析等功能。通过本节的学习，用户可以学会使用WinHex对十六进制代码进行编辑。WinHex的具体使用步骤如下。

1 打开WinHex的程序，等待程序完全启动。

2 单击【文件】→【打开】菜单项，弹出【打开文件】对话框，选取要进行打开的文件，单击【打开】按钮，如图所示。

打开文件

3 在此简单地介绍一下其内存编辑功能。选择【工具】→【打开RAM】菜单项或直接按<Alt+F9>组合键，即可打开【编辑主内存】对话框，如图所示。

内存编辑功能

RAM工具将列出当前内存的所有进程，可以对每个进程以十六进制形式进行编辑查看。在未注册版本中，只能浏览而不能编辑RAM区域。

6.4.2 Hiew使用简介

Hiew是一款优秀的十六进制编辑器，特别的Hiew可以对应用程序进行反汇编，Hiew同时支持对可执行文件的十六进制代码的及汇编语言代码的修改，使用起来非常方便。Hiew是一个十六进制工具，除了普通十六进制的功能外，还能反汇编文件，并可以用汇编指令修改程序，且方便快捷。

由于Hiew使用传统的DOS界面，因此其操作多用功能组合键来完成。下面主要介绍如何使用它来编辑可执行应用程序。

只要将下载后的Hiew直接解压到一个目录下即可使用，不需要安装。主执行程序为hiew32.exe，直接运行这个文件即进入Hiew编辑环境，如图所示。

Hiew编辑主界面

此时在屏幕底部的命令行有相关提示，对应的是功能键F（n），如按<F1>键出现帮助功能，如图所示。

按<F1>键出现的各功能键的帮助信息

其中：<F1>表示帮助；<F3>表示按文件名字排序；<F4>表示按文件扩展名排序；<F5>表示按文件时间排序；<F6>表示按文件大小排序；<F7>表示无序排列；<F8>表示逆序排列；<F9>表示文件记录；<F10>表示设置文件掩码；按<Alt+F1>组合键可选择驱动器，按<Alt+F2>组合键可选择驱动器，如图所示。

选择驱动器

按<Alt+F3>组合键可打开设备，如图所示。

按<Alt+F4>组合键可重新读入词典；按<Ctrl+F1>组合键选择存储文件路径1；按<Ctrl+F2>组合键可重新选择存储文件路径1；按<Ctrl+F3>组合键选择当前文件存储路径2；按<Ctrl+F4>组合键重新选择当前文件存储路径2。

按<Alt+F3>组合键打开设备

这时候如果按上下键选中想要编辑的程序，按<Enter>键后即可进入编辑区，默认进入文本浏览模式，如图所示。

文本编辑区

可以按<F4>键或连续按<Enter>键在【文本模式（Text）】【十六进制代码（Hex）】【汇编代码模式（Decode）】之间转换，其中在文本模式下不能进行编辑修改，只可以进行查看，3种模式的界面如图所示。

3种模式

在各模式下都可设置书签，具体方法如下。

· “+”：在当前位置设置书签，共可设置8个。

· Alt+“N”：回到第 n 个书签（ n 等于1～8）。

· Alt +“-”：取消当前位置的书签（不能是小键盘上的“-”）。

· Alt +“0”：取消所有的书签。

通常使用汇编代码模式比较方便，首先按<F7>键搜寻需要修改的代码。

例如，下面的一段程序。

        54    push  esp

如果要将push esp（十六进制代码为54）改为pop esp（十六进制代码为5C），在Hiew中按<F7>键后输入两句原始程序的十六进制代码26 38 55 FF 74 08，如图所示。

输入要搜索的代码

Hiew找到这段代码后将这段程序显示在屏幕中间，如果按<F3>键即可进入该程序文件的编辑状态，进入后有两种方法可以修改程序，如图所示。

十六进制（Hex）代码模式

① 可以直接修改十六进制代码，如可以将54改为5C，如图所示。

十六进制（Hex）代码模式1

② 直接按<Tab>键将修改模式切换到汇编代码模式，在其中也可以直接将54修改为5C，如图所示。

十六进制（Hex）代码模式2

在Hiew中可以按<Esc>键退出当前的命令状态，修改完指令代码后直接按<F9>键更新程序（即存盘），然后退出Hiew，即可完成对应用程序的修改。

6.4.3 Hex Workshop使用简介

Hex Workshop是一款功能强大、非常专业的十六进制编辑器，可以方便地进行十六进制编辑、插入、填充、删除、剪切、复制和粘贴工作，配合查找、替换、比较以及计算校验和等命令，会使工作更加快捷。除此之外，Hex Workshop速度快，算法精确，并附带有计算器和转换器工具，功能十分强大。通过本节的学习，用户可以使用Hex Workshop对十六进制进行编辑。

Hex Workshop的具体操作步骤如下。

1 运行Hex Workshop并选择【File】→【Open】菜单项来打开目标文件。弹出【打开】对话框，选择文件，单击【打开】按钮即可，如图所示。

【Open】菜单项

2 打开文件后Hex Workshop主窗口中左边是文件偏移地址区（默认是十六进制），中间是十六进制数据代码区，右边是文本字符代码区，如图所示。

Hex Workshop主窗口

3 可以在主窗口中修改十六进制代码或ASCII代码。假设要修改虚拟地址0000004C处的代码，将其转换成文件偏移地址0000005F之后，选择【Edit】→【Goto】菜单项或按<Ctrl+G>组合键，即可打开【Goto】对话框，如图所示。

【Goto】菜单项

4 在【Goto】对话框中直接输入0000005F（注意选择【Beginning of File】单选按钮和【Hex】单选项），单击【Go】按钮即可。当找到指定偏移量时，即可用十六进制或ASCII码形式来修改指定的数据，如图所示。

【Goto】对话框

5 如果建立一个简单的十六进制新文件，则可通过选择【File】→【New】菜单项来建立，如图所示。

【New】菜单项

6 在选择【Edit】→【Insert】菜单项之后，即可打开【Insert Bytes】对话框，其中的第一行是需要增加文件的字节数，第二行是默认填充的数字，如图所示。

【Insert】菜单项

7 假设在这里需要加入0x42个字节的数据（填充值默认为0），在【Number of bytes】文本框中输入42，单击【Hex】单选项，单击【OK】按钮，就可以有一个0x42个字节的文件出现了，此时即可直接对此文件进行编辑。如果还想要删除其中的一些东西，则只需在将其选中之后，直接按<Delete>键或<Backspace>键即可，如图所示。

【Insert Bytes】对话框

8 有时候还需要按一定格式复制Hex Workshop所显示的十六进制数据，Hex Workshop支持C语言、Java语言、HTML、文本、RTF等格式。此时只要选中一块十六进制数据，选择【Edit】→【Copy As】→【C Source】菜单项，即可将所选数据转换成C语言格式，如图所示。

【C Source】菜单项

转换后的代码如下。

      // Generated by BreakPoint Software's Hex Workshop v6.8.0.5419
      //    http://www.hexworkshop.com
      //    http://www.bpsoft.com
      //
      //  Source File: D:\VC6++\ss\Debug\ss test.exe
      //           Time: 2016/1/31 16:39
      // Orig. Offset: 115 / 0x00000073
      //         Length: 90 / 0x0000005A (bytes)
      unsigned char rawData[90] =
      {
          0x65, 0x2E, 0x0D, 0x0D, 0x0A, 0x24, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x60, 0xA5, 0x4C,
          0x1A, 0x24, 0xC4, 0x22, 0x49, 0x24, 0xC4, 0x22, 0x49, 0x24,
    0xC4, 0x22, 0x49, 0x12, 0xE2, 0x29,
          0x49, 0x25, 0xC4, 0x22, 0x49, 0xA7, 0xD8, 0x2C, 0x49, 0x2A,
    0xC4, 0x22, 0x49, 0x12, 0xE2, 0x28,
          0x49, 0x1C, 0xC4, 0x22, 0x49, 0x24, 0xC4, 0x23, 0x49, 0x6C,
    0xC4, 0x22, 0x49, 0x46, 0xDB, 0x31,
          0x49, 0x21, 0xC4, 0x22, 0x49, 0xE3, 0xC2, 0x24, 0x49, 0x25,
    0xC4, 0x22, 0x49, 0xCC, 0xDB, 0x29,
          0x49, 0x26, 0xC4, 0x22, 0x49, 0x52, 0x69, 0x63, 0x68, 0x24,
      } ;

9 如果需要对两个相似的文件进行比较，以判断文件是否被修改或修改了何处，则可以在【Tools】→【Compare】→【Compare Files】菜单项中打开比较工具，弹出【Compare】对话框，在【Source】组合框和【Target】组合框中打开想要比较的两个文件之后，单击【OK】按钮即可开始比较，如图所示。

【Compare】对话框

10 在比较结束之后，将会出现如图所示比较结果，此时单击【Replaced】（替换）选项，即可在主窗口显示被替换的具体字节。

比较结果

6.4.4 UltraEdit使用简介

UltraEdit是一套功能非常强大的文本编辑器，使用这款软件可以编辑文本、十六进制、ASCII码等。UltraEdit编辑器库内建有英文单字检查、C++以及VB指令凸显，用户可以在同一时间编辑多个文件，即使开启很大的文件，也能够迅速地响应。下面主要介绍一下IDM Computer Solutions公司出品的著名文本编辑器UltraEdit-32 21.30汉化版。

1．菜单及工具栏

1 UltraEdit的启动很简单，可以选择要编辑的文件之后右击，在弹出的快捷菜单中选择【UltraEdit】选项，使用起来简单、方便，如图所示。

【UltraEdit】选项

2 在UltraEdit-32 21.30的主窗口中，上面是标题栏、菜单和工具栏，下部右侧为文本编辑区，打开的文件就显示在这里，下部左侧为驱动器文件列表，如图所示。

UltraEdit-32 21.30的主窗口

看一下UltraEdit工具条上的这些按钮，里面包含了UltraEdit-32 21.30的常用命令，如图所示。

工具条上的按钮

2.UltraEdit-32 21.30编辑器的主要功能介绍

（1）十六进制编辑模式

可以打开一个文件，然后选择【编辑】→【十六进制编辑】菜单项，文件就变为十六进制了，如下图所示。做了相应的修改后，仍然可用同样方法来退出十六进制编辑状态。

【十六进制编辑】菜单项

（2）多文件的查找和替换

由于UltraEdit-32支持多文件的查找替换，所以用起来十分方便。如果想要把打开的几个文件中的“mode”全部替换为“/mode”，在菜单中选择【搜索】→【替换】选项，弹出【替换】对话框，然后分别输入要查找的词和要替换的词，单击【全部替换】按钮，如图所示。完成替换操作后会告诉我们一共替换了多少处。

【替换】选项

（3）同时编辑多个文件

如果要同时修改很多文件，则可以将其全部选中之后右击，在弹出的快捷菜单中选择【UltraEdit-32】选项，就可以一次性地全都打开了。打开了多个文件后，在工具条下面会出现文件切换条，既可以单击相应条目来切换文件，也可通过按下<Ctrl+F6>组合键或<Ctrl+Shift+F6>组合键来按顺序对其进行切换，如图所示。

切换多个文件

（4）保存为项目文件

如果经常要修改一些文件，可以将这些文件全部打开之后，保存为一个项目文件，以后只要打开这个项目文件，就自动打开此项目下的所有文件。如已经打开了好几个文件，选择【项目】→【新建项目/工作区】菜单项，打开【指定项目文件】对话框，将这些文件保存为一个项目文件之后，单击【保存】按钮就可以直接打开保存的项目文件进行编辑了，如图所示。

【项目设置】对话框

（5）拼写检查

如果选择【编辑】→【拼写检查】菜单项或直接按<Ctrl+K>组合键，就可以对所打开的文件进行拼写检查了，使用上有点像操作Word，如下图所示。

使用拼写检查

利用UltraEdit-32自带的单词库可检查文章中的每个单词，如果单词库中不包含所书写的单词，会弹出对话框让用户做出相应处理：改正错误或添加新单词。在第一次使用拼写检查时，如果UltraEdit报告如下错误，说明系统没找到配套的字典文件＊.txt。

一般从网上下载的UltraEdit都内置了三个字典文件：Html. tlx（超文本语法单词）、 Autocorr.tlx（通用语法单词）和Condcorr.tlx（易拼写错误的单词）。UltraEdit支持英文、法文、德文、西班牙文等八种文字的拼写检查，当然前提是下载了相应的字典文件。

（6）支持多种文件格式

UltraEdit支持的文件包括：＊.txt、＊.doc、＊.Bat、＊.ini、（C语言源程序）＊.c和＊.cpp、头文件＊.h和＊.hpp、HTML语言＊.html和＊.htm、Java语言＊.java和＊.jav，这些文件类型基本覆盖了所有的常见文件类型。

还可以让UltraEdit打开指定类型的文件，具体的添加方法如下。

选择【高级】→【配置】菜单项，在【文件类型】选项卡中就可以添加新的文件类型了，单击【确定】按钮，如图所示。

【文件类型】对话框

如果选择【文件】→【转换】菜单项，则可展开UltraEdit的文本格式转换菜单。这里提供了UNIX/MAC与DOS、EBCDIC与ASCII、OEM与ANSI之间文本的相互转换，如图所示。

菜单中的转换选项

（7）宏功能

在【宏】菜单下提供了丰富的宏功能处理：宏记录、宏编辑、宏调用等，可以简化文本编辑中经常重复的操作，以提高使用效率，如图所示。

【宏】菜单

（8）支持多种字体

UltraEdit-32 21.30可以支持我们在Windows系统里安装的所有字体，其中包括中文Windows和其他外挂字体，如RichWin、中文之星等软件中所提供的字体。

如果要选择显示屏幕字体，可以选择【视图】→【设置字体】菜单项，弹出【字体】对话框，如图所示；如果要设置打印字体，可以选择【文件】→【打印设置/配置】→【设置打印机字体】菜单项。

【设置字体】菜单项

所有编辑器均能设定不同的字体、大小及风格，并且按照用户设定的字体进行打印，唯一不同的是，UltraEdit的显示字体及打印字体是分开设置的。这就意味着可以不同于显示的字体来进行打印，毕竟显示器和打印机还是有着一些差别的，显示出来字体好看，打印出来未必就好看。下图为【设置打印机字体】菜单项。

【设置打印机字体】菜单项

（9）命令调用

使用【高级】菜单可以在UltraEdit环境下直接调用DOS和Windows命令，具体的操作步骤如下。

选择【高级】→【DOS命令】菜单项或直接按<F9>快捷键，则会弹出【Dos命令】对话框，用户可以在里面运行DOS命令，例如，DIR、MEM等，如下图所示。这些结果便会直接输出到UltraEdit的编辑区中，利用这项功能可以截取DOS下的文本信息。

【Dos命令】对话框

如果选择【高级】→【运行Windows程序】菜单项或直接按<F10>键，就会弹出【运行Windows程序】对话框，调用Windows应用程序，程序的结果就会输出到UltraEdit的编辑区中，如下图所示。

【运行Windows程序】对话框

“日期/时间”可以在文章中自动加入日期；“只读”则可以将正在阅读的文件设成只读，防止对重要文件进行误操作等。当然最主要的还是它的十六进制编辑功能，希望大家能够好好利用它。

本节就讲这些UltraEdit的主要功能了，它的恢复/重复操作、拷贝/粘贴操作，多项选择、多项删除等编辑功能请大家自己参考软件自带的说明书。

6.4.5 eXeScope使用简介

eXeScope能在没有资源文件的情况下分析，显示不同的信息，重写可执行文件的资源，包括（EXE、DLL、OCX）等。eXeScope是方便强大的汉化工具，可以直接修改用VC++ 及Delphi编制的程序的资源，包括菜单、对话框、字串表等。

1 打开eXeScope程序，等待程序完全启动后，eXeScope程序运行的主窗口如图所示。

eXeScope程序运行的主窗口

2 当想要对一个文件的资源进行修改时，用户可以先用eXeScope打开文件，选择【文件】→【打开】菜单项，弹出【打开】对话框，以程序ss test.exe为例，单击【打开】按钮，如图所示。

【打开】菜单项

3 展开各级菜单，就可以对要修改的资源进行编辑修改了。通常情况下，要修改的地方一般都在【资源】菜单下。对于菜单项，可以直接在右侧的窗口原文字上进行修改；对于对话框等资源，可以在选定相应的资源后，再在上面【标题】文本框中对其进行修改；如果认为修改后的字体大小不合适，还可以对字体进行重新定义，如图所示。

重新定义资源文件

4 在【样式】选项卡中有【可见】和【禁用】两个复选项，当软件开发者为了让某个选项（按钮）不显示或者禁止使用的时候，通常就要对上面的两项进行设置了。在对付某些有功能限制的简单程序的时候，就可以通过使用这个方法来让按钮或菜单重新产生作用，如图所示。

【样式】选项卡

5 如果想在修改后，不退出eXeScope就预览修改的结果并移动某个按钮或图片的位置，还可以通过对正在编辑的【对话框】资源进行单击，弹出【确认】对话框，单击【确定】按钮，这样就能立即更新可执行文件了，如图所示。

立即更新可执行文件

6 如果要改变软件中的某个图片资源，可以先选择目标图片资源之后，再通过选择【文件】→【导出】菜单项，弹出【另存为】对话框，单击【保存】按钮，把文件从软件中提取出来，如图所示。

【导出】菜单项

7 用户就可以对文件进行编辑或找相应尺寸的图片文件来替换原有图片了，此时再选择【文件】→【导入】菜单项，弹出【打开】对话框，选择图片文件，单击【打开】按钮即可，如图所示。

【导入】菜单项

另外，如果想要先把软件资源提取出来，再将其保存成修改资源．rc，则可以使用其他软件来对其进行修改和编辑，再用其他软件导入到软件中，比如test等。

现在可执行文件修改的工具很多，初学者要尽可能地到网络上搜索一些相应的程序并加以研究和学习。

6.5 小结

本章主要是对三种静态反汇编工具W32Dasm、C32asm和IDA Pro的讲解。尤其对IDA Pro进行了详细的讲解，由于IDA Pro的功能十分强大，初学者可以多学习IDA Pro的使用，运用该工具可以加深用户对反汇编技术的了解。静态反汇编通俗点讲就是对软件重新进行编辑，找到与破解有关的关键代码，即关键call指令和关键跳转，一般使用的反汇编工具有W32Dasm、IDA Pro、C32asm等。但对于使用VFP、PB等高级语言编写的程序，则需要使用与之相应的反汇编工具，才可以得到相关信息，如使用VB4工具、UnFoxAll工具等。熟练使用各类反汇编工具，在以后的解密工作中可以更加得心应手。同时，初学者要明白反汇编是项相当烦琐的工作，需要投入大量的精力，千万不能急于求成，要多思考、多实践，等熟练掌握了学习的内容，往往就能达到事半功倍的效果了。

技巧与问答

❖ 什么是静态反汇编？

静态反汇编是从反汇编出来的程序清单上分析，从提示信息入手进行分析。目前，大多数软件在设计时，都采用了人机对话方式。所谓人机对话，即在软件运行过程中，需要由用户选择的地方，软件即显示相应的提示信息，并等待用户按键选择。而在执行完某一段程序之后，便显示一串提示信息，以反映该段程序运行后的状态，是正常运行，还是出现错误，或者提示用户进行下一步工作的帮助信息。为此，如果我们对静态反汇编出来的程序清单进行阅读，可了解软件的编程思路，以便顺利破解。常用的静态分析工具是W32Dasm、IDA和Hiew等。

❖ 在运行C32Asm反汇编时为什么经常会出现死机情况，这样如何解决呢？

在反汇编某些脱壳后的文件会出现非法操作，大多数是处理import时出现了指针非法而造成的死机。原因是对文件脱壳后没有重建import文件，里面的import非常乱，而反汇编则是按照正常的处理流程肯定非法，所以要重写import处理函数，当发出报告的时候反汇编某些软件就会退出。经过深入分析发现有些软件的资源、菜单和对话框都不是标准的，如果处理则容易引起非法，因此需要打开分析报告功能，可以从分析报告中看出到底哪里非法，然后在反汇编行为的选项里屏蔽相应的处理部分。

❖ 为什么使用UltraEdit打开大文件需要很长的时间？

UltraEdit可以处理超过4GB的文件。UltraEdit是基于磁盘进行编辑的，这意味着每次只在内存中载入文件的一小部分，因此它不使用所有的内存，并停止其他应用程序的运行。但是，它会生成一个文件的临时副本来达到这个目的，因此打开大文件需要一些时间。

在高级菜单和配置区域中有两个选项可以考虑。

· 禁用行号。选中这个选项来禁用大文件的行号显示功能，尽管它会使文件查询更快一点。

· 不使用临时文件打开文件。这个选项用来告诉UltraEdit不创建文件的临时副本。这么做会使它直接编辑原始文件，并且所做的更改是永久性的。一些用户对于大文件可以使用这个选项。你可以设置这个选项的极限值，使它只在文件大小超过你规定的数值后使用这个选项。

另外，你可以禁用配置中自动从UNIX转换到DOS功能 (如果已经启用)。

❖ 在安装UltraEdit新版本的时候是否需要将旧版本卸除？

我们建议覆盖现有的UltraEdit安装程序而不是将其卸载，这能维护您现有的INI设置。系统将会提示您是否需要覆盖旧的文字文档，如果您的文字文档已被覆盖而您又希望再使用时，旧文档将会被重新命名为wordfile.old。

❖ 在UltraEdit中应该怎样进行多行替换？

你可以用“^p”作为CR/LF来执行这个操作，UltraEdit会将“^p”解释成硬回车。你也可以在搜索和替换字符中分别用“^c”和“^s”来表示剪贴板内容或选定的文本。你可以用这个方法复制/选择30000 字节以下的字符串。

第7章
动态跟踪分析工具

软件的各项功能和数据在运行时会被加载到内存当中，此时可以对软件进行具体的跟踪和分析。在跟踪软件的过程中，可以观察到软件功能的执行、内存中软件内容的调用，在运行时要注意这些信息。

在进行手动脱壳、跟踪算法和爆破之前，都需要先使用动态调试工具进行分析。常用的动态调试工具有：SoftICE、TRW2000和OllyDbg。支持Windows7系统的只有OllyDbg。

7.1 OllyDbg动态跟踪分析工具

OllyDbg是由德国人编写的一款ring 3免费汇编调试器，具有强大的功能和可视化的友好界面，支持对反汇编译后的代码加上自己的注释，而且可以定义复杂的断点条件，使众多解密者都用它来进行程序的调试。

OllyDbg将静态分析和动态调试功能完美地结合在了一起，还可以调试很多线程的应用程序，从一个线程切换到另一个线程、挂起、恢复和终止或改变其优先级。附加正在运行的应用程序，它还支持DLL动态链接库的调试。

此外，OllyDbg还支持插件的功能，使得它的功能更具全面性，比如目前常用的插件有：中文字符插件、脚本命令插件、命令行插件、文件转储插件等。

OllyDbg使用可视化窗口操作，非常简单，用户不必记忆烦琐的命令行，直接显示FPU、MMX、SSE、3DNOW等特殊寄存器，只在一些特定的情况和不太复杂的跟踪时使用。

为有效地调试一些不带源码的程序，必须先理解它是如何工作的。先来看一下OllyDbg的基本特性。

OllyDbg包含一个内置的代码分析器。分析器遍布整个代码，分出指令和数据，识别出不同的数据类型和过程，分析出标准API函数的参数并且尝试猜出未知函数的参数数目（也可加入自己的函数说明）。它标记出程序入口点和跳转目的地，识别出跳转表和指向字符串的指针，加入一些注释，甚至标示出跳转的方向等。

在分析结果的基础上，调用树型显示哪些函数被指定过程调用（直接或间接）并识别出递归调用、系统调用和子过程。如果需要，还可以设置解码提示来帮助分析器解析那些不明确的代码或数据。

OllyDbg还包含Object扫描器。如果有库文件或目标文件，扫描器会在被调试的程序中定位这些库函数。在全部函数调用中，对标准函数的调用占很重要的一部分。如果知道正要被调用的函数的功能，就不必把注意力集中在这个函数上，可以简单地单步跳过这个CALL。分析器知道400多个标准C函数，如FOPEN和MEMCPY。

Object扫描器也能够识别输入库。如果某个DLL是按序号输出的，则不会看到函数名。这种DLL的开发者通常会提供一个输入库来实现函数符号名与序号间的对应。让OllyDbg使用这个输入库，则可恢复原始的函数符号名。

面向对象的语言（如C++）使用了一种叫作名称修饰的技术，把函数类型和参数都加入函数名中。OllyDbg可以解码这种函数名，使程序更容易理解。OllyDbg完全支持Unicode，几乎所有对ASCII字符串的操作都可以同样应用于Unicode。

汇编指令的相似性，常会使用户搞不清自己是不是已经跟踪过某一段代码。这时就可以在OllyDbg中加入自己的标签和注释，从而极大地方便了调试。一旦注释了某个DLL之后，以后每次加载这个DLL时，注释和标签都有效。

OllyDbg可以跟踪标准的栈帧。现代编译器有禁止产生标准栈框架的选项，在这种情况下分配栈是不可能的。当程序运行到已知的函数时，栈窗口解析它的参数，调用栈窗口显示到达当前位置所调用函数的序列。多种不同的搜索选项可以找到二进制代码或数据、命令或命令序列、常量或字符串、符号名或在Run跟踪中的一条记录。

对于任何地址或常量，OllyDbg都可以找出参考到该地址或常量的全部命令的列表。举例来说，某个函数可能被直接调用，或经过编译器优化后把地址放入寄存器间接调用，或把地址压入堆栈作为一个参数，OllyDbg总会找出所有这样的地方，甚至能找到并列出所有和某个指定的位置有关的跳转。

OllyDbg支持所有标准类型的断点，可以是非条件和条件断点、内存断点（写入或访问）、硬件断点或在整个内存块上下断点。条件表达式可以非常复杂，可以设定一条或多条指令，当程序暂停时由OllyDbg传递给插件。除了暂停，还可以记录某个表达式的值或记录OllyDbg已知函数的参数。

OllyDbg最重要的特性是跟踪。OllyDbg支持两种方式的跟踪：Hit和run。在Hit情况下，它对指定范围内的每条指令上设置断点（比如在全部可执行代码中）。当到达设断的指令后，OllyDbg清除断点并且把该指令标记为Hit，这种方法可以用来检测某段代码是否被执行。Hit跟踪速度惊人得快，在一个很短时间的启动后程序几乎达到了全速，当代码没有被分析时Hit跟踪是不可以使用的。

Run跟踪是一步一步地执行程序，同时记录精确的运行历史和所有寄存器的内容、已知的参数和可选的指令。当然，这需要大量的内存。可以只在选定的一段代码甚至是一条指令中进行Run跟踪，或跳过无关紧要的代码。

对于每个地址，OllyDbg能够计算这个地址在Run跟踪日志中出现的次数，虽然会导致执行缓慢，但是可以得到代码执行的统计。比如，某命令让在每个已识别的过程入口处进行Run跟踪，则统计就会给每个过程被调用的次数。在到达某条指令、某个地址范围或指令计数器达到某一数值时Run跟踪可以自动地暂停。

7.1.1 认识动态跟踪分析工具OllyDbg主窗口

OllyDbg主窗口各部分及特点具体介绍如下。

1．反汇编窗口（Disassembler）

当载入程序之后，窗口内显示了程序反汇编后的源代码，与W32Dasm等反汇编工具反汇编出来的源代码相同，这里不包括基本信息、菜单对话框信息和引入表信息，而且还会自动定位到程序的入口处。这就是静态反汇编与动态反汇编调试相结合的一个窗口。

OllyDbg主窗口

OllyDbg的窗口分为4列，分别如下。

· 地址（虚拟地址）栏：显示相对地址；再次双击其地址返回到标准地址模式。

· 十六进制：显示地址的十六进制。

· 反汇编（汇编代码）：调用汇编器。

· 注释（可以自定义注释内容）：注释内容栏目还会显示一些API函数等信息，可以方便在进行调试时的参考。

2．信息窗口（Information）

在动态调试时，窗口内会显示出当前代码行的各个寄存器的信息，或API函数的调用、跳转等信息，可以用来辅助了解当前代码行的寄存器的运行情况。

3．数据窗口（Dump）

默认以十六进制方式显示文件在内存中的数据，还可以用鼠标右键设置用文本或反汇编等的方式来显示数据。窗口分为3列，分别为：地址（内存当中的虚拟地址）、HEX数据和ASCII（十六进制对应的字符）等，还可以设置为Unicode等模式。

4．寄存器窗口（Registers）

动态显示CPU各个寄存器的内容，包括数据寄存器（EAX、EBX、ECX和EDX）、指针及变址寄存器（ESP、EBP、ESI和EDI）、段寄存器（CS、DS、SS和ES）、控制寄存器中的程序状态字寄存器（CF、PF、AF、ZF和SF）等。

此外，还可能通过鼠标右键设置查看MMX等寄存器状态。这些寄存器当中应用最广泛的就是数据寄存器和指针及变址寄存器，像输入的用户名、机器码及注册码等信息一般都会放在这些寄存器当中，因此在动态调试时要多注意查看这些寄存器的内容。

5．堆栈窗口

显示堆栈的内容，也就是EBP和ESP所指向的内容。堆栈是很重要的，API函数或子程序都会利用它来传递参数、变量等信息。而且如果是字符串信息，则会在注释里面显示出来。有的用明码比较注册信息的程序，可能会在这里面显示出正确的注册码。

6．命令行（Command）

在原来的OllyDbg中是没有命令行的，这是一个外置的插件，方便在动态调试时输入命令，一般主要用于输入下断点或清除断点的命令（FLY修改版主程序文件夹下的“命令行命令．txt”文件中有详细的命令及功能，可以进行查看）。前5个窗口统称为CPU窗口，是启动OllyDbg时默认打开的窗口，另外还有很多其他的功能窗口，可通过【查看】菜单中的命令打开，或通过工具栏上的按钮打开。

7．突出特点

· 界面友好直观，四个窗口一目了然，分别是代码窗口、寄存器窗口、内存窗口、堆栈窗口。

· 强大的字符串和函数名的搜索功能，有利于快速准确判断出程序片断的具体用法，可以快速找到停下的程序片断。

· 可以在代码的旁边随时添加注释，只要按下分号键，即可一边执行程序，一边添加注释，并在OllyDbg退出后自动保存这些信息，同时也保存设下的断点，以方便下次使用。

· 复制到可执行文件功能可以很轻松地修改程序，因此，如果要修改多处代码，可以在每次修改后的代码处右击，在弹出的快捷菜单中选择【复制到可执行文件】选项，最后一起使用“保存文件”功能将其保存到文件。

· 具有书签功能。每次跟踪代码时可以定义10个书签，方便用户快速地在程序中大范围跳读代码。

7.1.2 配置动态跟踪分析工具OllyDbg

OllyDbg的配置选项包括界面选项、调试设置、即时调试设置和系统外壳扩展等4项，用户可以在【选项】菜单中进行选择，本节说明一下UDD文件和插件的设置。

（1）界面设置

选择【选项】→【界面选项】菜单项，即可弹出【界面选项】对话框，选择“目录”选项卡，设置好UDD文件和插件的路径后，单击【确定】按钮，如图所示。

【界面选项】对话框

UDD文件和插件的路径必须根据存放OllyDbg的实际位置进行改变，其中UDD文件是反汇编程序后自动由OllyDbg生成的，其中包含了反汇编信息、断点设置信息、设置的标签、注释等信息内容（在调试完程序之后可以删掉相应的UDD文件）。

在插件路径中设置一个插件存放目录之后，只有将插件放在所设定的目录下才会被OllyDbg所识别。如果插件目录有插件，则OllyDbg的菜单中将会自动显示【插件】菜单。

（2）调试设置

单击【选项】→【调试设置】菜单项，即可弹出【调试选项】对话框，一般按默认设置即可。其中【异常】选项卡可以设置让OllyDbg忽略或不忽略那些异常，现在建议全部选上，单击【确定】按钮，如图所示。

【异常】选项卡

（3）即时调试设置

选择【选项】→【即时调试设置】菜单项，即可弹出【即时调试设置】对话框，单击【设置OllyDbg为即时调试器】按钮，单击【关闭】按钮即可，如图所示。

【即时调试设置】对话框

（4）系统外壳扩展

选择【选项】→【系统外壳扩展】菜单项，即可弹出【安装系统外壳扩展】对话框，在对话框中单击【添加OllyDbg到系统资源管理器菜单】按钮，再单击【关闭】按钮即可。

【安装系统外壳扩展】对话框

7.1.3 动态跟踪分析工具OllyDbg的常用功能与操作

OllyDbg的操作主要有反汇编操作、断点操作、动态调试操作以及其他的一些附加功能。下面是对这些常用功能与操作的简单介绍，以帮助用户加深对它的了解。

1．反汇编操作

· <Ctrl+N>组合键：在弹出的对话框中会显示程序调用的外部模块和函数，即程序的引入表。也可以在反汇编窗口中右击，选择【搜索】→【当前模块中的名称（标签）】菜单项，即可打开【名称在ntdll】对话框，如下图所示。

【名称在ntdll】对话框

· <Ctrl+G>组合键：在弹出的【请输入要跟随的表达式】对话框中输入虚拟地址，选择【VA/API】单选按钮，单击【确定】按钮，就可以跳到该地址处，如下图所示。也可以在反汇编窗口单击鼠标右键，选择【前往】→【表达式】菜单项打开窗口。

【请输入要跟随的表达式】对话框

· <Ctrl+F>组合键：在反汇编窗口中可以搜索汇编代码。弹出【查找命令】对话框，在文本框中输入代码，单击【查找】按钮即可进行搜索，如下图所示。也可以在反汇编窗口单击鼠标右键，选择【搜索】→【命令】菜单项打开窗口。

【查找命令】对话框

· <:>键：在反汇编窗口输入冒号（英文字符），弹出【加标签】对话框，可以对当前代码行添加标签，方便下次快速定位到此代码处。也可以在反汇编窗口单击鼠标右键，选择【标签】菜单项打开窗口，在文本框中输入名称，单击【确定】按钮，如下图所示。

【加标签】对话框

· <;>键：在反汇编窗口输入分号（英文字符），弹出【加注释】对话框，可以对当前代码行添加注释，比如可以输入此行代码的功能方便记忆。也可以在反汇编窗口单击鼠标右键，选择【注释】菜单项打开窗口，在文本框中输入名称，单击【确定】按钮，如下图所示。

【加注释】对话框

· <Ctrl+E>组合键：可以用十六进制方式修改汇编代码，弹出【编辑代码】对话框，也可以在反汇编窗口单击鼠标右键，选择【二进制】→【编辑】打开窗口，在文本框中输入十六进制数，单击【确定】按钮，如下图所示。

【编辑代码】对话框

· <Space>键：在反汇编窗口输入空格键，弹出【汇编】对话框，可对当前代码进行汇编编辑，单击【汇编】按钮，方便修改指令，如下图所示也可以在反汇编窗口单击鼠标右键，选择【汇编】菜单项。而且修改后可以右击，选择【复制】→【到文件中】菜单项，弹出【保存选择内容到文本文件】对话框，单击【保存】按钮即可将修改内容保存到文件中，如下图所示。

【汇编】对话框

【保存选择内容到文本文件】对话框

· 在寄存器窗口双击寄存器，即可修改寄存器的值；双击程序状态字寄存器（标志位），即可修改程序状态字寄存器（标志位）的值。

2．动态调试操作

· <F9>键：载入程序之后，按<F9>键就可以运行程序。

· <F7>键：单步跟踪，也就是一条代码一条代码地执行程序，遇到Call语句会跟入执行Call语句调用的地址代码或调用函数代码，遇到RET语句返回。

· <F8>键：单步跟踪，同<F7>键一样，只是遇到Call语句不会跟入，相当于把Call语句调用的地址处代码或子函数代码执行完。

· <F4>键：执行到所选代码。比如执行到一条语句时，下面有几条语句觉得没有用处，这时就可以单击这几条语句下面的某条语句，再按<F4>键，就会执行到单击的那条语句处（经常用在跳出无用的循环情况下）。

· <Ctrl+F9>组合键：执行到返回语句，当进入一个函数调用时，比如进入Call语句调用的一个子函数时，如果想快速执行到结束函数调用的RET语句，则可以用此快捷键（如果函数没有什么用处，此功能可以略过）。

· <Alt+F9>组合键：执行到程序领空，如果进入引用的DLL模块领空，则可以用此快捷键快速返回到程序领空。

3．断点操作

进行动态操作调试时要让程序中断在关键的代码处，再根据显示的动态信息进行代码分析。这里不需要对程序下断点，断点有一般断点、内存断点和硬件断点等类型，其中一般断点是最常用的断点方式（关于断点的详细说明和操作可以查看OllyDbg的帮助文件）。

（1）一般断点（Ordinary Breakpoint）

一般断点就是将输入的断点地址处第一个字节用INT3指令来代替。当程序运行到断点地址时，就会执行INT3指令，OllyDbg就会捕捉到这个指令而中断下来，由于INT3指令的设置数量是没有限制的，所以可以同时下多个断点。

下一般断点的方式有如下两种。

· <F2>键：在反汇编窗口中的代码行上直接按<F2>键就可以下断点。下断点后的虚拟地址处呈红色状态。如果想取消断点，再按一下<F2>键就可以了。

· 命令行方式：可以在命令行中使用bp命令来下断点。bp命令后面可以跟代码的虚拟地址或API函数。

（2）内存断点

内存断点分为内存访问断点和内存写入断点两种。OllyDbg一次只允许有一个内存断点。

① 内存访问断点。

程序运行时调用被选择的内存数据，就会被OllyDbg中断，根据这个特点在破解跟踪时，只要在关键数据内存中下断点，就可以知道程序在什么地方和什么时候用到了跟踪的数据，对于一些复杂算法的跟踪有很大的帮助。

从解密上讲，一个注册码的生成一定是有一些关键数据的，那么内存访问断点就是比较好的中断方法。

② 内存写入断点。

程序运行时向被选择的内存地址写入数据，就会被OllyDbg中断。根据这个特点，在解密时可以跟踪一个关键数据是什么时候生成的，生成的代码段在哪个地方。因此，一个关键的数据如果不知道其由来，就可以用内存写入断点来查找计算的核心。

如果想下内存断点，则可以在数据窗口中的HEX数据栏内，用鼠标选择一部分内存数据之后右击，在弹出的右键快捷菜单中选择【断点】选项，从打开的【断点】窗口中选择内存访问方式或内存写入方式就可以了。

内存中断的下断点还有另一种方法：程序运行时，如果知道关键的数据，比如输入的试验码、程序生成的序列号等。这时在内存中一定存在这些数据。用<Alt+M>组合键打开内存窗口之后，在其中搜索知道的关键数据。用光标选择这些数据，同样下内存中断，这种方法更容易找到关键的数据。

（3）硬件断点

硬件断点是OllyDbg所特有的断点，它不会因为重新运行而被销毁，只要不被删除，跟踪这个程序时就是有效的。下中断的方法和内存断点的方法相同，有3种方式，分别是HR（硬件访问）、HW（硬件写入）、HE（硬件执行），此外还有HD（清除所下断点）。OllyDbg允许设置4个硬件断点，一般用硬件访问和硬件写入这两种方式进行断点设置。

如果想下硬件断点，可以在数据窗口中的HEX数据栏内用光标选择一部分内存数据之后右击，在弹出的右键快捷菜单中选择【断点】选项，从打开的【断点】窗口中选择设置硬件访问、硬件写入和硬件执行断点等，或在命令行内直接输入语句就可以了。

4．附加功能

除正常载入程序反汇编后进行调试的方法之外，OllyDbg还可以用附加程序的功能，来对正在运行的程序进行调试。例如，有些程序有反调试的校验功能或程序加壳使得OllyDbg无法加载，这时就可以先运行程序，再尝试使用附加功能载入程序并进行调试。

5．线程

OllyDbg以简单而有效的线程管理为特色。如果是单步调试、跟踪、执行到返回或执行到所选，则线程管理器将停止除当前线程以外的所有线程，即使当前线程被挂起，它也会将其恢复。在这种情况下，如果手动挂起或者恢复线程，动作将被延期。如果运行被调试的应用程序，OllyDbg将恢复最初的线程状态。

依据这种方案，线程窗口可能会有如下5种线程状态。

· 激活（Active）：线程运行中或被调试信息暂停。

· 挂起（Suspended）：线程被挂起。

· 跟踪（Traced）：线程被挂起，但OllyDbg正在单步跟踪此线程。

· 暂停（Paused）：线程是活动的，但OllyDbg临时将其挂起，并在跟踪其他的线程。

· 结束（Finished）：线程结束。

线程窗口同时也显示了最后的线程错误，并计算该线程以用户模式和系统模式运行的时间，线程窗口还会高亮度显示主线程的标识符。

7.1.4 动态跟踪分析工具OllyDbg的常用插件

虽然OllyDbg本身的功能已经很强了，但在实际操作中仍然是远远不能满足解密需要的。不过OllyDbg支持插件功能，对于解密者而言，就能更好地调试程序了。如果想使用OllyDbg的插件功能，可以到【插件】菜单中进行选择。

下面就对OllyDbg中最常用的一些插件进行一下介绍。

· CommandBar：这个插件就是显示在左下角的命令行，它是作为一个默认为显示状态的插件。

· GoDUP Plugin：这个插件可以调用其他反汇编工具或者反编译工具导出的sig或map文件。OllyDbg本身的反汇编功能虽然也比较好，但相对于专业的反汇编工具仍然有一定差距，所以可以用该插件来载入其他专业反汇编工具的反汇编结果，再配合OllyDbg的调试功能，可以将动静结合得更加合理。

· IsDebugPresent：这个插件可以用来隐藏OllyDbg，让OllyDbg躲过一些反调试检测，这类插件相当多。

· OllyDump：脱壳辅助工具可以进行文件转储。此外，还有OllyDbg PE Dumper等功能相同的插件。

· Ollyscript：是OllyDbg的脚本插件，可以通过Ollyscript之类的语法规则写脚本来完成批处理指令。目前Ollyscript主要用在脱壳方面，可以用来脱去某个壳或某个壳的版本，非常方便，脱壳效果也非常好。

· Puntos Magicos：用来下万能断点的插件，而且还能找到用VB程序编写的万能断点。

· LoadMap：用来载入Map。

· 断点管理器：管理导入断点和导出断点。

· OllyUni：用于设置界面、递归长度、冗长、字符串。

OllyDbg的功能还有许多，希望读者在以后的操作中可以自己学习并且加深对OllyDbg操作的理解。

7.2 动态跟踪分析工具OllyDbg动态调试解密

大多数程序在调试时不是一次可以完成的，需要用户付出大量的时间和精力，同时还要知道程序是否加了壳、加了什么类型的壳，先要对程序进行脱壳处理，才能开始动态调试。本节用两个实例讲解OllyDbg动态调试解密的步骤。

7.2.1 动态调试解密过程

借助于动态跟踪分析工具，可以提高破解的成功率。

下面讲解动态调试解密的流程。

① 用OllyDbg加载脱壳后的程序，如果脱不掉壳，可以尝试使用附加功能来附加正在运行的程序。

② 利用反汇编得到的信息下断点地址，如果得不到有用的信息或无法脱壳，可以下API函数断点。

③ 按<F9>键让程序运行，这时程序会中断在所下的断点处。

④ 单步调试程序，找到需要更改信息出现的位置。

⑤对该位置的ASCII码进行更改即可。

7.2.2 实例：动态调试解密

这里以VC 6++编写的程序Hello World为例，讲解如何使用OllyDbg动态调试解密程序。

由于Hello World程序没有加壳，所以不用进行脱壳，首先运行OllyDbg程序，打开Hello World.exe程序，具体反编译代码如下。

        地址        十六进制转储          命令                      注释
        00401002         CC               INT3
        00401003         CC               INT3
        00401004         CC               INT3
        00401005    /$   E906000000      JMP main
        0040100A    |    CC               INT3
        0040100B    |    CC               INT3
        0040100C    |    CC               INT3
        0040100D    |    CC               INT3
        0040100E    |    CC               INT3
        0040100F    |    CC               INT3
        00401010    |>   55               PUSH EBP     ; Hello_World.main(void)
        00401011    |.   8BEC             MOV EBP, ESP
        00401013    |.   83EC 40          SUB ESP,40
        00401016    |.   53               PUSH EBX
        00401017    |.   56               PUSH ESI
        00401018    |.   57               PUSH EDI
        00401019    |.   8D7D C0          LEA EDI, [LOCAL.16]
        0040101C    |.   B910000000      MOV ECX,10
        00401021    |.   B8 CCCCCCCC      MOV EAX, CCCCCCCC
        00401026    |.   F3:AB            REP STOS DWORD PTR ES:[EDI]
        00401028    |.   68 2C204200      PUSH OFFSET 0042202C
                                                    ; ASCII "Hello World! "
    0040102D  |.   E8 3E000000      CALL printf    ; [printf
    00401032  |.   83C4 04          ADD ESP,4
    00401035  |.   68 1C204200      PUSH OFFSET 0042201C
                                                   ; ASCII "I love China"
    0040103A  |.   E831000000      CALL printf    ; [printf
    0040103F  |.   83C4 04          ADD ESP,4
    00401042  |.   33C0             XOR EAX, EAX
    00401044  |.   5F               POP EDI
    00401045  |.   5E               POP ESI
    00401046  |.   5B               POP EBX
    00401047  |.   83C4 40          ADD ESP,40
    0040104A  |.   3BEC             CMP EBP, ESP
    0040104C  |.   E8 9F000000      CALL __chkesp
    00401051  |.   8BE5             MOV ESP, EBP
    00401053  |.   5D               POP EBP
    00401054  \.   C3               RETN
    00401055       CC               INT3
    00401056       CC               INT3
    00401057       CC               INT3
    00401058       CC               INT3
    00401059       CC               INT3
    0040105A       CC               INT3
    0040105B       CC               INT3
    0040105C       CC               INT3
    0040105D       CC               INT3
    0040105E       CC               INT3
    0040105F       CC               INT3
    00401060       CC               INT3
    00401061       CC               INT3
    00401062       CC               INT3
    00401063       CC               INT3
    00401064       CC               INT3
    00401065       CC               INT3
    00401066       CC               INT3
    00401067       CC               INT3
    00401068       CC               INT3
    00401069       CC               INT3
    0040106A       CC               INT3
    0040106B       CC               INT3
    0040106C       CC               INT3
    0040106D       CC               INT3
    0040106E       CC               INT3
    0040106F       CC               INT3
    00401070  /$   55              PUSH EBP    ; Hello_World.printf(void)
    00401071  |.   8BEC             MOV EBP, ESP
    00401073  |.   83EC 0C          SUB ESP,0C
    00401076  |.   53               PUSH EBX
    00401077  |.   56               PUSH ESI
        00401078    |.   57               PUSH EDI
        00401079    |.   8D45 0C          LEA EAX, [ARG.2]
        0040107C    |.   8945 F4          MOV DWORD PTR SS:[LOCAL.3], EAX
        0040107F    |>   837D 08 00       /CMP DWORD PTR SS:[ARG.1],0
        00401083    |.   75 1E            |JNE SHORT 004010A3
        00401085    |.   6848204200      |PUSH OFFSET 00422048
                                            ; ASCII "format ! = NULL"
        0040108A    |.   6A 00            |PUSH 0
        0040108C    |.   6A 36            |PUSH 36
        0040108E    |.   68 3C204200      |PUSH OFFSET 0042203C
                                            ; ASCII "printf.c"
        00401093    |.   6A 02            |PUSH 2
        00401095    |.   E8 D6150000      |CALL _CrtDbgReport
                                            ; [_CrtDbgReport
        0040109A    |.   83C4 14          |ADD ESP,14
        0040109D    |.   83F8 01          |CMP EAX,1
        004010A0    |.   75 01            |JNE SHORT 004010A3
        004010A2    |.   CC               |INT3
        004010A3    |>   33C9             |XOR ECX, ECX
        004010A5    |.   85C9             |TEST ECX, ECX
        004010A7    |.^ 75 D6             \JNZ SHORT 0040107F
        004010A9    |.   68604A4200      PUSH OFFSET 00424A60
        004010AE    |.   E8 0D020000      CALL _stbuf     ; [_stbuf
        004010B3    |.   83C4 04          ADD ESP,4
        004010B6    |.   8945 FC          MOV DWORD PTR SS:[LOCAL.1], EAX
        004010B9    |.   8B55 F4          MOV EDX, DWORD PTR SS:[LOCAL.3]
        004010BC    |.   52               PUSH EDX
        004010BD    |.   8B45 08          MOV EAX, DWORD PTR SS:[ARG.1]
        004010C0    |.   50               PUSH EAX
        004010C1    |.   68604A4200      PUSH OFFSET 00424A60
        004010C6    |.   E815040000      CALL _output    ; [_output
        004010CB    |.   83C4 0C          ADD ESP,0C
        004010CE    |.   8945 F8          MOV DWORD PTR SS:[LOCAL.2], EAX
        004010D1    |.   68604A4200      PUSH OFFSET 00424A60
        004010D6    |.   8B4D FC          MOV ECX, DWORD PTR SS:[LOCAL.1]
        004010D9    |.   51               PUSH ECX
        004010DA    |.   E841030000      CALL _ftbuf          ; [_ftbuf
        004010DF    |.   83C4 08          ADD ESP,8
        004010E2    |.   8B45 F8          MOV EAX, DWORD PTR SS:[LOCAL.2]
        004010E5    |.   5F               POP EDI
        004010E6    |.   5E               POP ESI
        004010E7    |.   5B               POP EBX
        004010E8    |.   8BE5             MOV ESP, EBP
        004010EA    |.   5D               POP EBP
        004010EB    \.   C3               RETN
        004010EC         CC               INT3

程序原来的执行效果如图所示。

程序原来的执行效果图

反编译后的代码如下。

    地址          十六进制转储          命令                    注释
    00401000           CC               INT3
    00401001           CC               INT3
    00401002           CC               INT3
    00401003           CC               INT3
    00401004           CC               INT3
    00401005      /$   E906000000      JMP main
    0040100A      |    CC               INT3
    0040100B      |    CC               INT3
    0040100C      |    CC               INT3
    0040100D      |    CC               INT3
    0040100E      |    CC               INT3
    0040100F      |    CC               INT3
    00401010  |>       55              PUSH EBP      ; Hello_World.main(void)
    00401011      |.   8BEC             MOV EBP, ESP
    00401013      |.   83EC 40          SUB ESP,40
    00401016      |.   53               PUSH EBX
    00401017      |.   56               PUSH ESI
    00401018      |.   57               PUSH EDI
    00401019      |.   8D7D C0          LEA EDI, [LOCAL.16]
    0040101C      |.   B910000000      MOV ECX,10
    00401021      |.   B8 CCCCCCCC      MOV EAX, CCCCCCCC
    00401026      |.   F3:AB            REP STOS DWORD PTR ES:[EDI]
    00401028      |.   68 2C204200      PUSH OFFSET 0042202C
                                                     ; ASCII "I have changed"
    0040102D      |.   E8 3E000000      CALL printf  ; [printf
    00401032      |.   83C4 04          ADD ESP,4
    00401035  |.   68 1C204200      PUSH OFFSET 0042201C
                                              ; ASCII "                 "
    0040103A  |.   E831000000      CALL printf       ; [printf
    0040103F  |.   83C4 04          ADD ESP,4
    00401042  |.   33C0             XOR EAX, EAX
    00401044  |.   5F               POP EDI
    00401045  |.   5E               POP ESI
    00401046  |.   5B               POP EBX
    00401047  |.   83C4 40          ADD ESP,40
    0040104A  |.   3BEC             CMP EBP, ESP
    0040104C  |.   E8 9F000000      CALL __chkesp
    00401051  |.   8BE5             MOV ESP, EBP
    00401053  |.   5D               POP EBP
    00401054  \.   C3               RETN
    00401055       CC               INT3
    00401056       CC               INT3
    00401057       CC               INT3
    00401058       CC               INT3
    00401059       CC               INT3
    0040105A       CC               INT3
    0040105B       CC               INT3
    0040105C       CC               INT3
    0040105D       CC               INT3
    0040105E       CC               INT3
    0040105F       CC               INT3
    00401060       CC               INT3
    00401061       CC               INT3
    00401062       CC               INT3
    00401063       CC               INT3
    00401064       CC               INT3
    00401065       CC               INT3
    00401066       CC               INT3
    00401067       CC               INT3
    00401068       CC               INT3
    00401069       CC               INT3
    0040106A       CC               INT3
    0040106B       CC               INT3
    0040106C       CC               INT3
    0040106D       CC               INT3
    0040106E       CC               INT3
    0040106F       CC               INT3
    00401070  /$  55              PUSH EBP    ; Hello_World.printf(void)
    00401071  |.   8BEC             MOV EBP, ESP
    00401073  |.   83EC 0C          SUB ESP,0C
    00401076  |.   53               PUSH EBX
    00401077  |.   56               PUSH ESI
    00401078  |.   57               PUSH EDI
    00401079  |.   8D45 0C          LEA EAX, [ARG.2]
    0040107C      |.   8945 F4          MOV DWORD PTR SS:[LOCAL.3], EAX
    0040107F      |>   837D 08 00       /CMP DWORD PTR SS:[ARG.1],0
    00401083      |.   75 1E            |JNE SHORT 004010A3
    00401085      |.   6848204200      |PUSH OFFSET 00422048
                                                     ; ASCII "format ! = NULL"
    0040108A      |.   6A 00            |PUSH 0
    0040108C      |.   6A 36            |PUSH 36
    0040108E      |.   68 3C204200      |PUSH OFFSET 0042203C
                                                            ; ASCII "printf.c"
    00401093      |.   6A 02            |PUSH 2
    00401095      |.   E8 D6150000      |CALL _CrtDbgReport
                                                            ; [_CrtDbgReport
    0040109A      |.   83C4 14          |ADD ESP,14
    0040109D      |.   83F8 01          |CMP EAX,1
    004010A0      |.   75 01            |JNE SHORT 004010A3
    004010A2      |.   CC               |INT3
    004010A3      |>   33C9             |XOR ECX, ECX
    004010A5      |.   85C9             |TEST ECX, ECX
    004010A7      |.^ 75 D6             \JNZ SHORT 0040107F
    004010A9      |.   68604A4200      PUSH OFFSET 00424A60
    004010AE      |.   E8 0D020000      CALL _stbuf         ; [_stbuf
    004010B3      |.   83C4 04          ADD ESP,4
    004010B6      |.   8945 FC          MOV DWORD PTR SS:[LOCAL.1], EAX
    004010B9      |.   8B55 F4          MOV EDX, DWORD PTR SS:[LOCAL.3]
    004010BC      |.   52               PUSH EDX
    004010BD      |.   8B45 08          MOV EAX, DWORD PTR SS:[ARG.1]
    004010C0      |.   50               PUSH EAX
    004010C1      |.   68604A4200      PUSH OFFSET 00424A60
    004010C6      |.   E815040000      CALL _output        ; [_output
    004010CB      |.   83C4 0C          ADD ESP,0C
    004010CE      |.   8945 F8          MOV DWORD PTR SS:[LOCAL.2], EAX
    004010D1      |.   68604A4200      PUSH OFFSET 00424A60
    004010D6      |.   8B4D FC          MOV ECX, DWORD PTR SS:[LOCAL.1]
    004010D9      |.   51               PUSH ECX
    004010DA      |.   E841030000      CALL _ftbuf             ; [_ftbuf
    004010DF      |.   83C4 08          ADD ESP,8
    004010E2      |.   8B45 F8          MOV EAX, DWORD PTR SS:[LOCAL.2]
    004010E5      |.   5F               POP EDI
    004010E6      |.   5E               POP ESI
    004010E7      |.   5B               POP EBX
    004010E8      |.   8BE5             MOV ESP, EBP
    004010EA      |.   5D               POP EBP
    004010EB      \.   C3               RETN
    004010EC           CC               INT3

改变后的程序效果如图所示。

改变后的程序效果图

对上述实例代码使用OllyDbg进行解密的操作步骤如下。

① 脱壳及静态分析。先对它查壳，由于事先已知这个实例没有加壳，所以不必对它进行脱壳。

② 开始进行动态调试。运行OllyDbg并加载Hello World.exe的程序，按下<F8>快捷键，程序步过被调试程序的call，到程序运行到00401214。

③ 下断点。按下<F2>功能键，将00401214设置为断点地址之后，当程序运行到这里时，会被OllyDbg中断下来。

④ 按下<Ctrl+F2>组合键重新载入Hello World.exe程序，按下<F9>快捷键，程序来到断点处，再按<F8>快捷键，程序来到00401028，在命令栏下找到存储内容的地址为0040202C和0040201C。

⑤ 在数据窗口处找到0040202C的地址，对该地址的ASCII码进行二进制编辑，输入“I have changed ”即可，找到0040201C地址，对该地址的ASCII码进行二进制编辑，输入“ ”即可。

⑥ 多次按下<F8>快捷键，直到程序执行完成。

7.3 小结

本章主要介绍了动态调试工具OllyDbg的使用方法，具体讲解了OllyDbg的安装与配置，常用快捷键和具体操作步骤，并以具体实例讲解了OllyDbg调试程序的过程，读者可以进行实际操作熟悉OllyDbg的使用。

读者在对软件进行加密解密时，要弄懂软件的功能到底如何执行，程序在内存中如何调用，熟悉用动态调试工具进行调试。在调试过程中要学会使用OllyDbg的快捷键，这样可以提高使用工具的速度。

技巧与问答

❖ 使用OllyDbg可以并行进行程序调试吗？

使用OllyDbg可以并行调试程序。无须暂停执行程序，就可以浏览代码和数据、设置断点、停止或恢复线程，甚至直接用于修改内存等操作（可以视为一种软件调试的模式，与之相对应的硬件模式则是当进程在运行时调试器被阻滞）。假使程序的操作比较复杂，OllyDbg会让进程终止一小段时间，但这种暂停对于用户而言是看不到的。

❖ OllyDbg里的patch窗口怎么用？

patch窗口用来保存你在调试的程序中修改过的代码。比如你前面调试了一个程序，在OD中把某处的JE改成了JMP,OD会在patch中记录这个修改，你下次再载入程序时，就能在OD的patch窗口中看到你原来改动的代码。按空格键就可以激活patch，就是在OD中还把原来位置的JE改成JMP。这个只是在OD中做更改，并没有实际保存到文件，主要是方便在OD中修改程序时测试。

❖ OllyDbg中如何调用pdb文件？

pdb文件是VC++调试编译生成的文件，由编译器直接生成。pdb文件要配合源文件使用，不同的源文件pdb文件不同。用OD装入可执行文件后，点击CPU窗口中的注释段可出现源码。不过不是所有的源码都可以显示，VC++ 6.0以下都可以显示。还有一种不显示的原因是缺少路径。点击OD主菜单的【查看】→【源文件】，如果【源码】段出现（缺少）字样的话，说明此路径的源码是看不了的，这时设置正确的路径就可以了。

❖ 如果已经知道某一CALL的具体作用，能否把后面所有相同的CALL都改成函数名形式？

比如CALL 110000此句中已经知道110000是一个核心计算。则如下操作，让光标停在CALL 110000这个语句上，按<Enter>键会跳到110000的地址上去显示，之后让光标停在110000上，按<Shift>键和<;>（分号）其实就是完成一个：（冒号）的动作，输入名称，这回所有的调用110000处，都会显示CALL你刚才输入的名称了。

❖ OllyDbg中的代码乱码如何解决？

        004365E0 >db 68 ; CHAR 'h'
        004365E1 >db A4
        004365E2 >db 7A ; CHAR 'z'
        004365E3 >db E5
        004365E4 >db B8
        004365E5 >db E8
        004365E6 >db BB

在OllyDbg中单击鼠标右键，单击【分析/从模板中删除分析】菜单项，如不行，按<Ctrl+A>组合键重新分析。

第8章
补丁技术

各种程序都会有漏洞，为了修复这些漏洞，程序发布者会制作补丁，给程序打上补丁。有的软件存在某些功能的不足，软件发布者会对这些软件进行升级（即给软件打补丁）。例如，Windows系统软件是我们比较熟悉的软件，当这些系统推出以后不久，微软公司就会不定期地向社会发布一些与该系统相关的补丁程序来更改新系统的错误或弥补其某些功能上的不足。当我们在玩游戏时，由于游戏的难度太高，往往会借助于一些修改器对它进行修改。这些修改器也是一些内存补丁，因此学习制作补丁是一个有趣的过程，可以凸显自己的破解能力。

8.1 程序补丁概述

补丁是改进原有程序功能的程序，因为一个程序开发完成之后，本身难免会存在一些问题、需要为程序增加一些这样或那样的新功能。为了应对程序升级的需要，为程序打上各种“补丁”，补丁其实也就是一些小的程序，这些小程序运行到程序中，使程序的功能更加完善，对程序自身也起着一定的保护作用。

8.1.1 补丁的分类

没有哪个软件能做到尽善尽美，任何程序在发布出来之后，都要通过用户的实际应用，找到程序中存在的错误和需要改进的地方，再根据用户的需要，给其打上这样或那样的补丁来完善它们的功能。

补丁技术总体上分为初级和高级两种。初级的补丁技术很容易掌握，就是暴力破解注册流程，可以直接在软件内部修改很小一部分的代码，来把软件修改得符合用户的需要。高级的补丁技术主要是指通过把功能代码写在自己的DLL上，而在软件内部作极小的修改，使其调用自己的DLL文件来达到给软件增加各种功能的目的。常见增加功能的补丁有存盘补丁和打印补丁两种。

对于高级补丁的制作，主要可通过以下3个方面来进行理解。

· 需要对PE文件的格式做一个深入理解，这样才能引入真正需要的DLL文件和DLL文件中的函数项。

· 需要对补丁的软件运作机制有详细的分析和理解。因为软件代码中一个很普通的堆栈调整或参数传递错误，都会让软件甚至操作系统彻底崩溃。如果对目标软件了解不够深入，千万不要轻易去为其打补丁。

· 自己要有深厚的编程功底，因为Windows系统内部的运行机制，决定了DLL文件的编写和一般EXE文件的编写是不同的，作为自己给软件增加的DLL文件，要传递什么参数给DLL文件是个最大的问题。

有时候，当DLL编写快要完成时，会突然想起来还有某个参数没有传递过来，那就意味着要全部重做了。再就是需要用的参数还很难找，在调试器里能看到的只是一大段一大段的汇编代码，而且对于软件内部要使用什么样的数据格式也不是很清楚，这些都需要动脑子对其进行全面分析。

一个高手往往只需要非常少的补丁代码就能实现自己需要的功能，这就要求其对软件运行的机制有一个全面而且透彻的理解，要达到这种境界就需要更加努力了。

编写高级补丁的具体步骤如下。

① 列出想要增加或者修改的功能。

② 分析程序，找到增加功能所需要的各种参数的保存位置。

③ 编写DLL，推荐使用Masm32编写。

④ 修改软件的引入表和代码段，加入自己的DLL和DLL中的功能函数。

⑤ 进行最后的调试，排除软件中的Bug。

8.1.2 如何构成补丁

补丁文件主要分为文件补丁和内存补丁两大类。文件补丁就是通过修改文件本身某个数据来达到修改效果；内存补丁主要是在内存中打补丁，即对正在运行程序的数据进行修改，以达到某种修正的效果。

下面对文件补丁和内存补丁进行一下简单的比较，如下表所示。

文件补丁与内存补丁

1．文件补丁

由于文件补丁主要针对没有被加密过，没有被加过壳，没有被CRC（循环冗余码）校验的目标程序，因此实现起来比较容易。

在对修改前和修改后的目标程序代码进行比较之后，把不同之处记录下来，然后编写一个程序实现补丁功能即可。

2．内存补丁

Windows是一个抢占式的操作系统，每个进程都拥有自己的运行空间。每当操作系统切换到另一个进程时，虚拟内存映射也跟着切换到当前的进程。

内存补丁的总体思路就是在某个时刻（在解压、校验或某种情况发生之后），在目标程序的地址空间中修改数据，因此也称为loader。一般情况下，用户不能进行修改操作，也不能对程序本身进行修改，实现方法是从另一个进程中获得对目标程序地址空间的访问权。

微软提供一个接口，可以对其他进程的空间进行读写，也就是WriteProcessMemory函数，用户要做的就是通过某种方式得到目标程序进程ID号，利用WriteProcessMemory函数对其进行修改。常用到的内存补丁相关的API函数如下表所示。

常用补丁相关的API函数

为程序打补丁是常见的一种软件升级和更新手段。这里讲的打补丁的形式是在修改源程序二进制代码方式下进行的。

8.2 DLL劫持内存补丁工具

如果大家不喜欢编程没有关系，也可以使用补丁制作工具来制作同样效果的补丁文件。补丁制作工具的种类较多，这一点大家只需要到网上浏览一下就可以知道，在本节中将以DLL劫持内存补丁工具为例来进行介绍。

DLL劫持内存补丁制作工具是一款专业的补丁制作工具，用户可以利用它制作注册补丁，使软件变成注册版。其运行的主窗口如图所示。

DLL劫持内存补丁制作工具运行的主窗口

本软件可以同时支持同目录下多目标破解，如图所示。

多目标破解

用户可以在【补丁数据】操作区的【原始数据】文本区中输入原始数据，在【补丁数据】文本区中输入更改的信息，在【备注信息】文本区中，输入补丁的备注信息，如图所示。

【补丁数据】操作区

用户输入的补丁信息将在补丁信息区显示，可以显示补丁的序号、补丁地址、原始数据、补丁数据、备注信息、文件名等信息，如图所示。

补丁信息区

DLL劫持内存补丁制作软件中各个按钮的含义如下。

：选择需要破解的程序。

：将补丁添加到补丁信息区。

：加载已有的补丁。

：保存补丁。

：生成补丁文件。

：本软件的制作信息。

：显示帮助信息。

：退出软件。

下面以本软件自带的测试程序为例讲一下制作补丁的过程，具体的操作步骤如下。

1 选择【目标1】选项，单击选择需要破解的程序按钮，弹出【加载补丁数据】对话框，选择需要制作补丁的文件，单击【打开】按钮，如图所示。

选择需要破解的程序按钮

2 用户需要打开x64_dbg动态调试工具，其操作方法与OllyDbg相似，单击【文件】→【打开】菜单项或单击工具栏按钮，弹出【打开】对话框，选择x64Test.exe文件，单击【打开】按钮，如图所示。

【打开】菜单项

3 用户打开文件后，在x64_dbg动态调试工具上单击按钮，程序开始执行，如图所示。

单击按钮

4 当程序运行到“sub rsp, 28”时，在该行代码处右键单击，弹出下拉菜单，选择【搜索】→【字符串引用】菜单项，程序进入【引用】选项，如图所示。

【字符串引用】菜单项

【引用】选项

5 双击【引用】选项中的“PYG2015”字符串，代码跳转到“test al,al”处，可以看出程序是从这里开始执行的，下一步代码是跳转到注册界面，所以我们将下一步的代码“nop”掉，使该代码可以跳过注册界面，如图所示。

代码跳转到“test al,al”处

6 在代码“74,19”处右键单击，弹出下拉菜单，选择【复制】→【地址】菜单项，该行代码的地址被复制，如图所示。

【地址】菜单项

7 在DLL劫持内存补丁制作软件的【补丁地址】文本框中粘贴复制的地址，这里地址为“000000013F6D14B7”，如图所示。

【补丁地址】文本框

8 在x64_dbg界面的代码“74,19”处右击，弹出下拉菜单，选择【二进】→【复制】菜单项，该行代码的二进制数据被复制，如图所示。

【复制】菜单项

9 在DLL劫持内存补丁制作软件的【原始数据】文本区中粘贴复制的二进制数据，这里二进制数据为“74 19”，如图所示。

【原始数据】文本区

10 在DLL劫持内存补丁制作软件的【补丁数据】文本区中输入nop的二进制数据，这里为“90,90”，在【备注信息】文本区中输入“跳过注册”，如图所示。

【补丁数据】文本区和【备注信息】文本区

11 在DLL劫持内存补丁制作软件主界面单击【增加】按钮，将补丁添加到补丁信息区，然后单击【补丁成功弹出信息】复选框，如图所示。

DLL劫持内存补丁制作软件主界面

12 在DLL劫持内存补丁制作软件主界面中单击【生成类型】下拉菜单，选择“version.dll”，单击【生成】按钮，弹出【Pyg_dll_patcher_x64】对话框，提示成功生成“version.dll”文件，单击【OK】按钮，如图所示。

DLL劫持内存补丁制作软件主界面

13 将在【x64Test】文件夹下的version.dll文件复制到【简单测试用例】文件夹中，如图所示。

【简单测试用例】文件夹

14 双击打开“x64Test.exe”文件，弹出【劫持测试程序——注册码：PYG2015】对话框，单击【确定】按钮，弹出【ok】对话框，单击【确定】按钮，注册成功，如图所示。

“x64Test.exe”文件

8.3 用Keymake制作补丁程序

软件被解密之后，一般都会被解密者做一个破解补丁进行发布，常用的补丁制作工具当属Keymake。Keymake（注册机编写器）是一款用Win32汇编语言编写的用来生成注册机的程序，功能非常强大，Keymake主要是通过汇编语言来编写算法注册机。对于初学者来说，经常用到它的是制作内存注册机、文件补丁、内存补丁这3项功能。

8.3.1 文件补丁程序的制作

Keymake的文件补丁制作功能是根据对比解密前后文件内容的不同来制作补丁程序的。现在制作一个x64Test.exe的文件补丁程序。

具体的操作步骤如下。

1 双击Keymake图标，等待一段时间，即可进入【注册机编写器】主窗口。

2 选择【其他】→【制作文件补丁】菜单项，如下图所示，即可打开【制作文件补丁】对话框，如下图所示。

【制作文件补丁】菜单项

【制作文件补丁】对话框

3 在【制作文件补丁】对话框的【窗口标题】文本框、【你的主页】文本框、【你的邮件】文本框及【补丁说明】文本区中输入自己定制的信息之后，通过右上角的计算机图标，还可以选择自己指定的程序图标，接着在原始文件和已破解文件处选择对应的文件就可以了，Keymake会自动对比两个文件，单击【制作】按钮，生成相应的补丁，如下图所示。

设置【制作文件补丁】对话框

4 单击【制作】按钮，即可弹出【选择补丁文件界面】对话框，就可以对这两种方案进行选择了，单击【确定】按钮，如下图所示。

【选择补丁文件界面】对话框

5 在选择完毕之后单击【确定】按钮，弹出【另存为】对话框，此时将会提示用户需要输入文件名，输入完毕之后，单击【保存】按钮就可以在CrackMe程序所在文件夹下生成文件补丁程序了。

【另存为】对话框

8.3.2 内存补丁程序的制作

文件补丁是通过修改文件得到的，而内存补丁则仅仅在程序运行时对放在内存当中的程序代码进行更改。这样做是为了对实际的文件不做修改，不过比较麻烦的是，每次都得运行补丁才能得到解密后的效果。

下面以x64Test.exe为例来制作一个内存补丁程序，具体的操作步骤如下。

1 在Keymake的主窗口中选择【其他】→【制作内存补丁】菜单项，如下图所示，即可打开【制作内存补丁】对话框，如下图所示。

【制作内存补丁】菜单项

【制作内存补丁】对话框

2 在【程序名称】处单击按钮，弹出【打开】对话框，选择要打内存补丁的程序，这里为x64Test.exe源程序，单击【打开】按钮即可。单击右上角的图标，弹出【选择图标文件】对话框，即可选择自己定制的图标，单击【打开】按钮，如下图所示。在弹出的【制作内存补丁】对话框中【消息标题】文本框和【启动提示】文本区中填入自己定制的信息，如下图所示。

【选择图标文件】对话框

【制作内存补丁】对话框

3 此时单击【制作内存补丁】对话框的【添加】按钮，如下图所示，即可打开【添加数据】对话框，如下图所示。

【制作内存补丁】对话框

【添加数据】对话框

· 修改地址：这里填写要修改的虚拟地址，此虚拟地址为451302。

· 搜索范围：地址的搜索范围。

· 修改长度：这里要填写机器码的字节数目，机器码7519，对应的字节数就是机器码长度除以2，也就是4/2=2。

· 原始命令：这里要填写指令对应的机器码，所以为7519。

· 修改指令：当修改为nop指令，nop指令的机器码是90，由于长度要和原始指令相同，所以这里填写为2个nop（也就是9090）。

4 在填写完毕之后，单击【添加】按钮，由于只有这一处需要修改，所以单击【关闭】按钮关闭窗口，不再添加。

5 单击【生成】按钮之后，弹出【另存为】对话框，如下图所示，在【文件名】文本框输入文件名，单击【保存】按钮，就可以生成内存补丁了，如下图所示。

【生成】按钮

【另存为】对话框

8.4 小结

本章主要介绍了补丁的定义以及补丁的作用，从而使得读者能够使用补丁制作工具为软件制作需要的补丁。相信广大读者只要亲自动手去实际运用所学的内容，并多加练习补丁工具的运用，一定可以熟练掌握为软件打补丁的技术。使用补丁可以使未注册版软件变为注册版软件，进而使用软件的其他功能。

技巧与问答

❖ 为什么要通过程序补丁对源代码进行修订？

在项目的开发过程中，多名开发人员通过CVS等工具对源代码进行版本控制，软件发布以后，开发人员会根据程序运行过程中出现的BUG，或者新的需求对源代码进行一定的修改，然后以补丁（patch）的方式发布程序补丁，其余开发人员或者使用人员对原先的源代码进行打补丁，从而完成BUG或者新的功能的修订。

❖ 如何为所有的软件打上补丁？

有不少人为了不同的目的关闭了操作系统的自动更新功能，如关闭了Windows和其他程序的更新特性。其基本想法是自己的系统不可能崩溃，用不着修复。那么，不妨考虑一下这个问题，你的程序（包括操作系统）可能已经“老掉牙”了，而你却浑然无知。要知道，现代的网络犯罪分子更喜欢秘密地以一种你无法察觉的方式来控制计算机，而不打补丁的软件恰恰给他们提供了这样一种机会。

现在，攻击者们最喜欢的方式是劫持一个网站或网页，然后就可以借此发动一系列的探测活动，查找用户的计算机上没有打补丁的漏洞并加以利用。

用户的对策应当是，通过保持所有的程序最新，而不仅仅是操作系统的最新或浏览器的最新，就可以阻止基于漏洞的多数攻击。攻击性站点可以很快地找到用户系统上所安装的看似无关紧要的一些应用软件的漏洞。因此，请打开所有软件的自动更新功能，这也是用户最快捷的得到补丁和更新的方法。

❖ 如何打补丁？

（1）做好备份

由于补丁程序大多会修改一些系统中已有的文件，这就有可能造成部分文件损坏的情况，因此建议大家要事先进行备份操作。这里所说的备份包括两方面：一是原先程序的安装目录，通常对于汉化补丁、游戏补丁只需要备份这个目录即可；二是需要备份系统中相关的DLL动态链接库文件，以防止安装补丁程序时覆盖了部分DLL文件而导致系统出现问题。

（2）使用补丁过程

使用补丁程序的时候，一定要退出对应的程序，否则将会出现部分文件正在使用而无法安装补丁的情况。对于一些开机时自动运行的程序最好将其从启动项目中去除，以防止调用的某些系统文件无法完全从内存中退出而造成补丁无效。还有就是在Windows 2000中，对于Imail、Ser-U、Winoppet之类启动之后作为服务运行的程序，还要将其从服务列表中关闭，这样才能真正有效地使用补丁程序。

（3）安装完毕

有些补丁程序使用之后会提示用户重启计算机，如果不重启则可能会造成补丁程序不起作用。因此建议大家根据系统的提示在安装完毕补丁程序之后马上重启机器测试一下，看看系统是否稳定、程序是否可以正常运行。

第9章
解密壳技术

壳对于一些加密解密的初学者来说是一个陌生的概念，但是它又是一个初学者必须掌握的概念，加壳是所有常见软件保护的基本措施。通过本章的学习，初学者在对软件解密时，可以学会分析程序的类型、程序是怎样编译出来的、程序有没有加壳保护、哪些软件可以压缩壳，这对以后的解密工作有着十分重要的意义。

9.1 快速理解壳

9.1.1 认识壳

在一些计算机软件里有一段专门负责保护软件不被非法修改或反编译的程序。它们一般都是先于程序运行，拿到控制权，然后完成保护软件的任务。就像动植物的壳一般都是在身体外面一样（但后来也出现了所谓的“壳中带籽”的壳）。由于这段程序和自然界的壳在功能上有很多相同的地方，基于命名的规则，大家就把这样的程序称为“壳”了。就像计算机病毒和自然界的病毒一样，其实都是命名上的方法罢了。从功能的角度出发，软件的壳和自然界中的壳相差无几，无非是保护、隐蔽壳内的东西。而从技术的角度出发，壳是一段执行于原始程序前的代码。原始程序的代码在加壳的过程中可能被压缩、加密，当加壳后的文件执行时，壳这段代码先于原始程序运行，它把压缩、加密后的代码还原成原始程序代码，然后再把执行权交还给原始代码。软件的壳分为加密壳、压缩壳、伪装壳、多层壳等类，目的都是隐藏程序真正的OEP（入口点，防止被破解）。

9.1.2 壳的作用和分类

壳的作用主要就是，保护软件不会被黑客破解和重新编译，保护软件作者的合法权益。它主要体现在两个方面。

（1）保护程序

这是给程序加壳的主要目的，就是通过给程序加上一段如同保护层的代码，使原程序文件代码失去本来的面目，保护程序不被非法修改和反编译。如果用反汇编工具对加过壳的程序进行反汇编，则根本看不到真实的可执行文件代码，而无法对程序进行修改。要想修改程序必须把这层壳脱掉，还原程序的本来面目。

（2）压缩程序

这项功能是随着软件功能的不断增强和程序体积的不断扩大而逐渐增强的，在DOS时代，软件程序的规模一般较小，一个3.5英寸的软盘可以存放几个普通软件。而现在一个程序动不动就有几十或几百兆，这就给程序的传播和存储带来很大的不便。于是对程序进行压缩的各种专用压缩工具就应运而生了。

例如一个1.82 MB的EXE文件，使用ASPack压缩后仅有292KB，这就大大减小了程序的体积，便于程序的传播和存储。

当然，这种压缩不同于WinZIP、WinRAR等文件压缩工具的压缩。WinZIP、WinRAR等文件压缩工具的压缩是对任何格式的文件压缩打包，一般是不可执行的。而这里所说的压缩是指用专用压缩工具对PE格式的EXE或DLL程序文件进行压缩，加过壳的EXE程序同正常的EXE程序一样，是可执行的。

由壳的作用不难推出壳的分类，主要有以下两类。

（1）保护壳

这些壳生来就是为了防破解，所以会使文件增大。相对而言现在比较流行的变态加密壳，运用各种加密算法和先进的加密技术，使得破解变得十分困难或者根本无法解密。但其最大缺点就是加壳后程序会变得相当庞大，运行速度极慢。

（2）压缩壳

压缩壳是大家最常见的，比如UPX等，它的原理就是通过算法对程序里面的一些相同字节集并相应的算法并压缩，相对而言这些压缩壳一般都支持反编译，也就是说压缩出来的程序可以被反编译成原来的未压缩的程序。

程序加壳之后，并不会影响原程序的正常运行，因为加壳只是修改了原程序中执行文件的组织结构，提前获得了原程序代码的控制权。

下面再来讲一下一般壳的装载过程。

① 获取壳自己所需要使用的API地址。

如果用PE编辑工具查看加壳后的文件，将会发现未加壳的文件和加壳后文件的输入表是不一样的，加壳后的输入表一般所引入的DLL和API函数很少，甚至只有Kernel32.dll以及GetProcAddress这个APl函数。

壳实际上还需要其他的API函数来完成工作，为了隐藏这些API，它一般只在壳的代码中用显式链接方式动态加载这些API函数。

② 解密原程序各个区块（Section）的数据。

壳出于保护原程序代码和数据的目的，一般都会加密原程序文件的各个区块。在程序执行时外壳将会对这些区块数据进行解密，以让程序正常运行。壳一般是按区块加密的，那么在解密时也按区块解密，并且把解密的区块数据按照区块的定义放在合适的内存位置。如果加壳时用到了压缩技术，那么在解密之前还有一道工序，就是解压缩。这也是一些壳的特色之一，比如说原来的程序文件未加壳时为1～2MB，加壳后反而只有几百千字节。

③ 重定位。

文件执行时将被映像到指定内存地址中，这个初始内存地址称为基地址（ImageBase）。当然这只是程序文件中声明的，程序运行时能够保证系统一定满足其要求吗？对于EXE的程序文件来说，Windows系统会尽量满足。例如，某EXE文件的基地址为0x400000，而运行时Windows系统提供给程序的基地址也同样是0x400000。在这种情况下，就不需要进行地址“重定位”了。由于不需要对EXE文件进行“重定位”，所以加壳软件把原程序文件中用于保存重定位信息的区块也删除了，这样就可以使得加壳后的文件更加小巧。有些工具提供“WipeReloc”的功能，其实就是这个作用。不过对于DLL的动态链接库文件来说，Windows系统没有办法保证每一次DLL运行时提供相同的基地址。这样“重定位”就很重要了，此时壳中也需要提供进行“重定位”的代码，否则原程序中的代码是无法正常运行起来的。从这点来说，加壳的DLL比加壳的EXE更难修正。

④ HOOK-API。

程序文件中输入表的作用是让Windows系统在程序运行时提供API的实际地址给程序使用。在程序的第一行代码执行之前，Windows系统就完成了这个工作，壳一般都修改了原程序文件的输入表，然后自己模仿Windows系统的工作来填充输入表中相关的数据。在填充过程中，外壳就可填充HOOK-API的代码地址，这样就可间接地获得程序的控制权。

⑤ 跳转到程序原入口点（OEP）。

从这个时候起壳就把控制权交还给原程序了，一般的壳在这里都会有一个明显的“分界线”。

9.2 常见查壳软件的使用方法

现在软件大多采用加壳对自身进行保护，所以在解密之前首先要利用工具对软件进行查壳，从而可以知道软件有没有加壳、加的是什么壳，这样才能找到破解的关键，对以后的破解起到事半功倍的作用。常用的查壳软件有PEiDentifier及Exeinfo PE等。

9.2.1 PEiDentifi er的使用

PEiD是一款著名的查壳工具，其功能强大，现在有很多软件都加了壳，给破解汉化带来非常大的不便。PEiD几乎可以侦测出所有的壳，其数量已超过470种PE文档的加壳类型和签名，另外还可识别出EXE文件是用什么语言编写的，如VC++、Delphi、VB等，其具体操作步骤如下。

1 打开PEiD v0.95的主程序。

2 单击浏览文件按钮，如下图所示，弹出【Choose the file to open】对话框，选取要检查的文件，单击【打开】按钮，如下图所示。

单击文件按钮

【Choose the file to open】对话框

3 打开需要检查的文件后，【File】文本框显示的是文件的保存位置，程序运行界面如下图所示。

程序运行界面

4 在【EP Section】文本框中，一般就能看出程序是否加壳以及壳的类型，这里显示的是“.aspack”，说明加壳类型为aspack，如下图所示。

【EP Section】文本框

5 在程序运行界面单击【Task Viewer】按钮查看当前正在运行的程序，检测加壳情况。弹出【PEiD Task Viewer v0.03】对话框，如图所示。

【Task Viewer】按钮

6 在程序运行界面单击【Options】按钮，设置PEiD选项可用于检测一些新壳，弹出【PEiD Options】对话框，选择“Minimize to System Tray”复选框，单击【Save】按钮，如图所示。

【Options】按钮

9.2.2 Exeinfo PE的使用

Exeinfo PE是一款免费的Win32可执行程序检查器，它可以检查程序的打包方式、exe保护等，可以帮助开发人员对程序进行破解。Exeinfo PE绿色版是一款查看PE文件信息的工具，可以查看EXE/DLL文件的编译器信息、是否加壳、入口点地址、输出表/输入表等PE信息，帮助开发人员对程序进行分析和逆向。Exeinfo PE还可以提取PE文件中的资源，可以提取图片、EXE、压缩包、MSI、SWF等资源。通过本节的学习，用户可学会使用Exeinfo PE软件对EXE文件进行查看，具体使用步骤如下。

1 双击打开Exeinfo PE程序，等待一段时间程序完全启动后，程序运行主界面，如下图所示。

Exeinfo PE程序运行主界面

2 在Exeinfo PE程序运行主界面单击浏览文件按钮，弹出【Open EXE, DLL fi le】对话框，选取要检查的EXE文件，单击【打开】按钮，如下图所示。

【Open EXE,DLL file】对话框

3 打开需要检查的文件后，【File】文本框显示的是文件的保存位置，程序运行界面如下图所示。

程序运行界面

4 可以在程序运行界面的【Entry Point】文本框中查看入口点的首地址，这里显示的入口地址为：“0013B001”，如下图所示。

【Entry Point】文本框

5 在【EP Section】文本框中，一般就能看出程序是否加壳以及壳的类型，这里显示的是“.aspack”，说明加壳类型为aspack，如下图所示。

【EP Section】文本框

6 在程序运行界面单击【Options】按钮，弹出【Disassembler:Thunder.exe】对话框，设置选项可用于检测一些新壳，单击【OK】按钮，如下图所示。

【Disassembler:Thunder.exe】对话框

9.3 几种常见的加壳软件

9.3.1 穿山甲加壳工具Armadillo的使用

Armadillo是一套强大的软件保护系统，可以为您的程序添加装甲般的外壳。通过艺术性的加密、数据压缩和其他安全特性保护程序不被盗版和破解。Armadillo可以在5分钟内为程序设计并添加一套完整的软件保护和注册系统，并且不需要修改任何程序代码。Armadillo支持所有语言编写的32位Windows EXE文件。有个C++线性代数库也叫Armadillo。通过本节的学习，用户可以学会使用Armadillo 8.0对软件进行加壳，Armadillo 8.0的具体使用方法如下。

1 运行Armadillo 8.0主程序之后，待程序完全启动即可弹出Armadillo 8.0的程序主界面，如图所示。

Armadillo 8.0程序主界面

2 在Armadillo 8.0程序主界面选择【文件】→【新建工程】菜单项，即可弹出【工程设置】对话框，如图所示。

【工程设置】对话框

3 选择【设置要保护的文件】选项之后，在【工程设置】对话框右侧可以选择要保护的文件，单击浏览文件按钮，弹出【打开】对话框，选择要保护的文件，单击【打开】按钮，可以对多个文件进行保护，如图所示。

【设置要保护的文件】选项

4 选择【程序启动画面1】选项之后，在【工程设置】对话框右侧可以设置启动方案，如图所示。

【程序启动画面1】选项

5 选择【保护选项】选项之后，在【工程设置】对话框右侧可以选择保护的类型，类型不同保护强度也不同，这里选择“仅标准保护”，如图所示。

【保护选项】选项

6 选择【压缩选项】选项之后，在【工程设置】对话框右侧可以选择压缩比（建议选择“最小/最快压缩”设置），如图所示。

【压缩选项】选项

7 选择【SoftICE检测】选项之后，在【工程设置】对话框右侧有三种方案供选择，这里选择“标准SoftICE检测”，如图所示。

【SoftICE检测】选项

8 选择【证书】选项之后，在【工程设置】对话框右侧可以设置软件注册情况，此时单击【新建】按钮，即可打开【证书设置】对话框，在其中可以填写“证书名称”“加密模板”“签名等级”等，如图所示。

【工程设置】和【证书设置】对话框

9.3.2 压缩壳UPX的使用

UPX Shell是一款应用程序专用压缩、解压缩软件，支持EXE、COM、DLL、SYS、OCX等多种文件格式的压缩。通过本节的学习，用户将学会使用UPX Shell对文件进行加壳，UPX Shell的具体使用步骤如下。

1 打开UPX Shell的程序，待程序完全启动后，UPX Shell程序的主界面如下图所示。

UPX Shell程序的主界面

2 刚开始运行UPX时，UPX窗口使用英文显示。此时，用户只需在【Options】选项卡中【Select your language】下拉菜单中选择【简体中文】选项，就可以将UPX窗口转换为中文显示的UPX窗口了，如下图所示。

UPX窗口

3 在UPX窗口中单击【打开文件】选项卡，单击【打开】按钮，在弹出的【打开】对话框中，选择需要进行压缩的文件，单击【打开】按钮，如图所示。

【打开文件】选项卡

4 选择好要压缩的文件后，所选中的文件即可自动添加到UPX窗口的【压缩】选项卡中，如图所示。

【压缩】选项卡

5 单击【压缩】选项卡中的【执行】按钮之后，就可以对要压缩的文件进行压缩操作，如图所示。

文件压缩成功

9.3.3 EncryptPE的使用

EncryptPE支持加密狗，加密壳与加密狗的完美结合。

① EncryptPE能加密保护常规PE文件（EXE、DLL、OCX等一般程序或NT服务程序），防静态分析修改，反动态跟踪调试，有效地保护软件，防止盗版。除常规的对抗调试器（SoftICE、TRW、OllyDbg等）、监视器、DUMP工具方法外，EncryptPE采用的加密保护的手段还有：随机加密算法、CRC校验、变形、代码替换、进程注入、APIHOOK、多线程、调试运行、全程监控等。

② EncryptPE能将普通软件变成共享软件，增加定时注册提醒、限制试用日期、限制试用次数、限制试用天数、限制每次试用时间等功能。

③ EncryptPE能根据最终用户的机器信息、注册用户及加密时的保护密码计算注册码，从诸多加密算法中随机选择一种用于注册码的计算。

④ EncryptPE支持多语言，并为待加密软件提供多语言接口。

⑤ EncryptPE向待加密软件提供丰富的方便的编程接口，便于设计个性注册方式，同时使被加密程序与加密壳融为一个整体，增加破解难度。通过本节的学习，用户可以学会使用EncryptPE对软件进行加壳，EncryptPE的具体使用步骤如下。

1 打开EncryptPE程序，待程序完全启动后，程序运行的主界面如图所示。

程序运行的主界面

2 在EncryptPE运行主界面左侧的【选项】窗口可以设置5类试用限制，分别是：软件运行后每隔多少分钟要求注册；限制软件的试用日期；限制软件的试用次数；限制软件的试用天数；限制软件每次试用多少分钟；用户可以根据需要进行设置，还可以填写用户的网址、E-mail、个人提示信息等，如图所示。

【选项】窗口

3 在右侧窗口的上半部分可以选择要加密的文件，单击打开文件按钮，弹出【打开】对话框，选择要加密的文件，单击【打开】按钮，如图所示。

打开文件按钮

4 设置好【保护密码】，再单击【加密】按钮，程序开始对文件进行加密，等待一段时间后，弹出【加密锁】提示框，文件加密成功，单击【确定】按钮，退出程序，如图所示。

【加密】按钮

9.3.4 ASPack的使用

ASPack可压缩Windows 32bit EXE文件与DLL文件，能将大多数32bit EXE文件及DLL文件平均压缩30%～40%，以节省硬盘空间。ASPack可选择压缩时产生备份文件（＊.BAK）及还原原来的文件类型，或者删除备份文件。通过本节的学习，读者可以学会使用ASPack软件对EXE文件加壳压缩，软件操作的步骤如下。

1 在打开ASPack主窗口之后，单击【Open】按钮，即可弹出【Select fi le to compress】对话框，在其中选择要压缩的EXE文件，单击【打开】按钮，如图所示。

打开要压缩的文件

2 单击【打开】按钮之后，ASPack就开始压缩了，同时显示压缩的过程。在压缩完成之后，ASPack程序主界面将会显示压缩比例，可以查看压缩文件的大小和压缩率，如图所示。

ASPack压缩界面

3 在压缩完成之后，可以单击【Test it!】按钮，来测试压缩后的程序执行是否正确。如果正确则会出现【Restore】按钮和【Erase.bak】按钮，用以恢复原有文件和删除原有文件，如图所示。

测试程序执行是否正确

9.4 几种常见的脱壳软件

用户要想解密软件，首先利用查壳软件对其进行分析，了解软件是否加壳、加的是哪种类型的壳，然后就是对其进行脱壳，解除它里面的无关信息和限制，恢复软件本来面貌。脱壳之后，要保证软件的功能没有改变，否则脱壳就是失败的。一般情况下，脱壳后的文件长度往往会大于原文件长度，即使同一个文件，使用了不同的脱壳软件进行脱壳之后，由于不同脱壳软件的机理不同，脱出来的文件大小也将不会完全相同。

9.4.1 探究脱壳工具（WSUnpacker v0.20）

脱壳工具WSUnpacker v0.20采用了优秀的脱壳引擎，能够支持新的vmprotect、SafeEngine、VProtect、ZProtect等加壳软件，支持Delphi、BCB、Visual C++、Masm、MingW32等常用的编译器，帮助用户对各种加壳的软件进行成功脱壳，是目前网络上优秀的脱壳工具。通过本节的学习，用户可以学会使用脱壳工具WSUnpacker v0.20脱upx壳、aspack壳等。本节以脱upx壳为例，讲解脱壳工具WSUnpacker v0.20的具体使用步骤。

1 打开脱壳工具WSUnpacker v0.20软件，待程序完全启动后，程序主界面如下图所示。

脱壳工具WSUnpacker v0.20的程序主界面

2 单击【浏览】按钮，弹出【打开】对话框，用户选取需要脱壳的软件，单击【打开】按钮，如下图所示。

【打开】对话框

3 打开文件后，在文本区会显示软件加壳的类型，然后单击【脱壳】按钮，程序会自动对所选文件进行脱壳，如下图所示。

【脱壳】按钮

4 程序对文件进行脱壳，等待一段时间后，文本区会显示脱壳结果，如果文本区显示“Finished”，说明脱壳成功，如下图所示。

文本区

9.4.2 万能脱壳工具详解

万能脱壳工具拥有强大的查壳及脱壳功能，支持PE文件编辑、导入表抓取、进程内存查看/DUMP等多种实用功能。通过本节的学习，用户可以学会使用万能脱壳工具对软件进行脱壳，本节以脱aspack壳为例讲解万能脱壳工具的具体的使用步骤。

1 打开万能脱壳工具程序，待程序运行一段时间，万能脱壳工具程序运行的主界面如下图所示。

万能脱壳工具程序主界面

2 在万能脱壳工具程序主界面中单击浏览文件按钮，弹出【打开】对话框，选取要脱壳的文件，单击【打开】按钮，如下图所示。

【打开】对话框

3 在【EP区段】的文本框中显示为“.aspack”，该文件加的壳为aspack壳，如下图所示。

【EP区段】文本框

4 在选取脱壳的文件后，需要在万能脱壳工具程序主界面中单击【脱壳】按钮，如下图所示。

【脱壳】按钮

5 程序开始对文件执行脱壳，等待一段时间后，在万能脱壳工具程序主界面的文本区中显示信息：“Unpack successfully!”，表明文件成功脱壳，如下图所示。

文本区

9.4.3 详解RL!dePacker软件

RL!dePacker（超级脱壳机）1.3版支持92种壳，使用RL!dePacker对付一些简单的壳，如ASPack系列、UPX系列等非常有效。通过本节的学习，用户将学会使用RL!dePacker对EXE文件进行脱壳，我们以脱aspack壳为例讲解RL!dePacker的具体使用步骤。

1 打开RL!dePacker程序，等待一段时间后程序完全启动，主界面如下图所示。

RL!dePacker程序主界面

2 在RL!dePacker程序主界面上单击【浏览】按钮，弹出【RL!dePacker-coded by ap0x】对话框，选取要脱壳的文件，单击【打开】按钮，如下图所示。

【RL!dePacker-coded by ap0x】对话框

3 打开文件后，单击【查找OEP!】按钮，等待一段时间，会在文本框中显示程序的OEP，文本框中显示“4012E8”，如图所示。

【查找OEP!】按钮

4 找到OEP后，需要在RL!dePacker程序主界面上单击【脱壳】按钮，程序开始进行脱壳，等待一段时间后，文本区显示“Unpack ended”，程序脱壳成功，如图所示。

【脱壳】按钮

9.4.4 随机注册码保护实例

现在网上脱ASPack壳的软件主要有UnAspack、ASPack-u和AspackDie 3种，用户可以从网上下载软件使用，本节将对它们进行介绍。

（1）UnAspack软件

UnAspack软件在运行之后，可以对脱壳软件直接进行脱壳。该软件采用图形窗口，使用方便，现在能脱掉ASPack 2.1以后的版本。UnAspack操作的主要步骤如下（以Unpacker为例）。

1 打开Unpacker软件，待程序完全启动后，Unpacker程序运行的主界面如下图所示。

Unpacker程序主界面

2 单击【文件】按钮，弹出【Open ASPACK packed file】对话框，选取要脱壳的文件，单击【打开】按钮，如下图所示。

【Open ASPACK packed file】对话框

3 选取要脱壳的文件后，单击【脱壳】按钮，程序将对选取的文件自动进行脱壳，如下图所示。

【脱壳】按钮

4 单击【脱壳】按钮后，弹出【Save as】对话框，在对话框中输入文件名称，单击【保存】按钮，如下图所示。

【Save as】对话框

5 等待一段时间后，文件脱壳成功，脱壳信息在【处理过程】文本区显示，单击【退出】按钮，退出程序。

【处理过程】文本区

（2）ASPack-u软件

ASPack-u脱壳软件和UnAspack脱壳软件的不同之处在于，它可以脱ASPack任何版本的壳，脱壳能力极强。只要查出是ASPack壳，就能使用ASPack-u进行脱壳。ASPack-u脱壳软件的程序主界面如图所示。

ASPack-u脱壳软件的程序主界面

（3）AspackDie软件

AspackDie软件不支持ASPack旧版本的壳，只支持ASPack 2000到ASPack 2.12b版的壳，其图形界面的窗口使用非常方便。AspackDie软件的具体使用步骤如下。

1 打开AspackDie软件，待程序完全启动后，AspackDie软件的主界面如图所示。

AspackDie软件的主界面

2 在AspackDie软件的主界面上选取需要脱壳的文件，单击【打开】按钮，即可打开需要脱壳的文件。

【打开】按钮

3 等待一段时间后，程序自动弹出【AspackDie-信息】提示框，程序脱壳成功，单击【确定】按钮，退出程序，如图所示。

【AspackDie-信息】提示框

9.5 小结

本章主要介绍了有关壳的技术，讲解了壳的概念、壳的种类及如何使用各类加壳软件、查壳软件、脱壳软件。希望通过这章的学习，读者能够加深对壳技术的理解，并熟练使用有关软件，掌握有关加壳、查壳、脱壳技术。壳技术在加密解密中占有重要的地位，不管是对什么样的软件进行解密，都要先对它脱壳，才能保证解密工作的继续进行。其实对文件加壳、查壳、脱壳并不难，因为有大量的相关软件来辅助用户完成这项工作，用户所要做的就是掌握软件的使用方法。这样，在对文件进行相关操作时才能得心应手。用户在使用脱壳程序时，首先要用查壳软件查出软件所加的壳，再从网上下载相应的脱壳软件进行脱壳。目前有很多加壳工具，脱壳即去掉软件所加的壳，软件脱壳有手动脱壳和自动脱壳之分，用户可以首先学会使用软件自动脱壳，然后再利用工具进行手动脱壳，以加深对脱壳的理解。

技巧与问答

❖ 由于通常的工具文件使用了一些压缩加壳软件加密过，这就需要对文件进行解压脱壳处理后，才能汉化。这种压缩与平时接触的压缩工具如WinZIP,WinRAR等压缩不同，WinZIP压缩后的文件不能直接执行，而这种EXE压缩软件、EXE文件压缩后，仍可以运行。那么，应该如何来脱掉EXE、DLL文件的壳呢？

这种压缩工具把文件压缩后，会在文件开头一部分加上一段解压代码。执行该文件时，该代码先执行解压还原文件，不过这些都是在内存中完成的，由于微机速度快，因此基本感觉不出有什么不同。这样的程序很多，如The bat、Acdsee、Winxfile等。

这种压缩加壳属于软件加密，现在越来越多的软件经过压缩处理，给汉化带来许多不便，因此软件汉化爱好者也不得不学习掌握这种技能。现在脱壳一般分手动和自动两种，手动就是用OllyDbg等调试工具对付，对脱壳者有一定的水平要求，涉及很多汇编语言和软件调试方面的知识。而自动就是用专门的脱壳工具来脱，几乎所有的压缩软件都有他人写的反压缩工具对应，有些压缩工具自身能解压，如UPX；有些不提供该功能，如ASPack就需要UNASPACK来对付。好处是十分简单，缺点是版本更新后就没用了。

❖ 一个加过壳的软件在经过脱壳之后，是否还会和原文件保持一样呢？

一般情况下，脱壳后的文件长度都要大于原文件的长度，即使是同一个文件，也会由于使用的脱壳软件不同而有所不同。由于不同脱壳软件使用的脱壳机理是不同的，因此要想实现脱出完美的壳，最好使用与加壳软件相同版本的脱壳软件去脱壳。但这在现实中几乎是不可能的，脱壳总是很难和加壳者使用的软件完全一致。因此，绝大多数软件在被进行脱壳之后，都会与原文件在体积上产生一些出入。

❖ 用查壳软件查不到壳，事实上是有壳的，这种情况怎么解决？

单步跟踪法。

① 用OD载入，点“不分析代码！”。

② 单步向下跟踪按<F8>键，实现向下的跳。也就是说向上的跳不让其实现。（通过按<F4>键）

③ 遇到程序往回跳的（包括循环），我们在下一句代码处按<F4>键（或者右键单击代码，选择断点→运行到所选）。

④ 绿色线条表示跳转没实现，不用理会，红色线条表示跳转已经实现。

⑤ 如果刚载入程序，在附近就有一个CALL的，我们就按<F7>键跟进去，不然程序很容易跑飞，这样很快就能到程序的OEP。

⑥ 在跟踪的时候，如果运行到某个CALL程序就运行的，就在这个CALL中按<F7>键进入。

⑦ 一般有很大的跳转（大跨段），如jmp XXXXXX或者JE XXXXXX或者有RETN的一般很快就会到程序的OEP。

补充：在有些壳无法向下跟踪的时候，我们可以在附近找到没有实现的大跳转，按【→】右键“跟随”，然后按<F2>键下断，按<Shift+F9>组合键运行停在“跟随”的位置，再取消断点，继续按<F8>键单步跟踪。一般情况下可以轻松到达OEP！

第10章
加密与解密的编程技术

初学者会认为加密解密是一项枯燥乏味的工作，事实上只要掌握了足够的破解知识，拥有一定的破解经验，并且积累一定的编程基础，就可以熟练地进行加密解密。本章介绍一些编程技术基础，将有助于初学者更加透彻地了解加密解密。Win 32可以对被调试的程序进行任意的修改，包括进程的内容、线程的运行环境等。用户还可以使用这些API写一个进程调试器，就像现在流行的调试器Visual C++调试器、WinDBG、OllyDbg等一样。当然，除了能写调试器外，利用调试API还能做很多其他工作。

10.1 编程资料基础

学习加密解密，关键的任务是学习编程技术，熟悉编程语言的格式、专业术语，掌握它们的具体用法，然后就可以使用它们编写一些程序来解决问题。下面介绍一些有关编程方面的相关技术资料，这对于破解软件是相当有用的。

10.1.1 VXD、KMD、WDM基本概念

在使用计算机的过程中，用户可以在系统文件夹下找到扩展名为．KMD的文件，一般都不知道它的作用，下面就先对这类文件进行介绍。

（1）KMD（Kernel Mode Driver）

KMD是在Win NT下提出的一种管理、维护硬件运作的驱动程序模式，它在Win NT系统中的Kernel模式下运行。

（2）VXD（Virtual X Driver，虚拟设备驱动程序）

VXD是系统用于对各种硬件资源进行识别、管理和维护运作。与虚拟机管理一起维持系统的正常运行，VXD运行在Intel系列CPU保护模式下的Ring0，控制着整个硬件系统。

（3）WDM（Win32 Driver Model）

WDM很少用到，它必须注册到相应的类里，有相应的例程时输出，Win 7中也使用了WDM。

（4）Win 7中的VXD文件

.VXD是为Windows 7系统提供NT类服务的核心驱动程序，它输出了几个VXD服务，是一个只有头文件没有文档的文件。

其VXD文件如下。

· BIOS.VXD：即插即用BIOS接口。

· CPQNDIS3.VXD:Compaq以太控制器NDIS驱动程序。

· DECLAN.VXD:DECLAN网卡驱动程序。

· EE16.VXD：虚拟设备驱动程序。

· EISA.VXD：即插即用EISA总线计数器。

10.1.2 程序自删除的实现

程序在没有注册成功的条件下，可以在程序运行时就把本身删除，这就是实现程序本身的自删除，具体的操作如下。

下面用一段代码来进行分析，具体的实现代码如下。

        Int main（int argc, char＊argv[]）
        {
          HMODULE module=GetModuleHandle（0）;
          CHAR buf[MAX-PATH];
          GetModuleFileName（module, buf, sizeof buf）;
          CloseHandle（HANDLE（4））;
          -asm {
            Lea  eax, buf
            Push  0
            Push  0
            Push  eax
            Push  ExitProcess
            Push  module
            Push  DeleteFile
            Push  UnmapViewOfFile
            Ret
          }
          Return 0;
        }

再来看一下堆栈里面：

        偏移     内容
        24       0
        20       0
        16        offese buf
        12       address of ExitProcess
        8         module
        4         address of DeleteFile
        0         address of UnmapView Of File

调用RET返回到Unmap View Of File，就是偏移0所指的位置，当进行到UnmapView Of File的流程时，栈中将显示返回地址DeleteFile和HMODUL module。当返回到DeleteFile时，即可看到ExitProcess的地址，也即返回地址和参数EAX，而EAX就是buffer（缓冲存储器）。

buffer里存放的EXE的文件名，可由GetModuleFileName返回得到，在执行过DeleteFile之后，就返回到了ExitProcess的函数入口，并且参数为0，而返回地址也是0。0是一个非法地址，如果返回地址0就说明出错，而调用ExitProcess就不会返回。

上述代码段的重点如下。

· 如果文件的HANDLE被打开，就不能删除文件了。因此，一定要设置Close Handle。HANDLE（4）是操作系统的硬编码，对应于EXE的IMAGE。在默认情况下，操作系统如果没有调用，就会关闭IMAGE SECTION的HANDLE。但这里HANDLE设置为关闭，删除文件就解除了文件对应的一个标识符。

· UnmapView Of File解除了另外一个对应IMAGE的HANDLE，还解除了IMAGE在内存的映射。所以，后面的任何代码都不可能引用IMAGE映射地址内的代码，否则操作系统就会出错。这里使用的代码在Unmap View Of File后面，正好没有引用IMAGE中的代码。

· 在退出进程之前，EXE文件就被删除了。

这里再介绍两种实现程序自删除的方法。

方法一：

运行环境：Windows 7

开发环境：VS 2010

调用接口函数库类型：Windows API，标准C库函数

程序原理：Windows加载程序后，进程无法删除进程映像，利用进程优先级，父进程结束，子进程删除父进程映像后结束，实现自删除。

程序代码：

        #include "stdafx.h"
        #include <stdio.h>
        #include <string.h>
        #include<windows.h>
        #include<ShlObj.h>
        VOID DelItself()
        {
            SHELLEXECUTEINFO stShellDel;
            TCHAR szBat[MAX_PATH];
            //获取文件路径名
            TCHAR szFileName[MAX_PATH], szComspec[MAX_PATH];
            if((GetModuleFileName(0, szFileName, MAX_PATH)! =0) &&
                (GetShortPathName(szFileName, szFileName, MAX_PATH)! =0) &&
                (GetEnvironmentVariable(L"COMSPEC", szComspec, MAX_
      PATH)! =0))
            {
                lstrcpy(szBat, L"/c del ");
                lstrcat(szBat, szFileName);
                lstrcat(szBat, L" > nul");
                stShellDel.cbSize = sizeof(stShellDel);
                //命令窗口进程句柄，ShellExecuteEx函数执行时设置
                stShellDel.hwnd = 0;
                stShellDel.lpVerb = L"Open";
                stShellDel.lpFile = szComspec;
                stShellDel.lpParameters = szBat;
                stShellDel.lpDirectory = NULL;
                stShellDel.nShow = SW_HIDE;
                //设置为SellExecuteEx函数结束后进程退出
                stShellDel.fMask = SEE_MASK_NOCLOSEPROCESS;
                //创建执行命令窗口进程
                if(ShellExecuteEx(&stShellDel))
                {
                    //设置命令行进程的执行级别为空闲执行，这使本程序有足够的时间
      从内存中退出
                    SetPriorityClass(stShellDel.hProcess, IDLE_PRIORITY_
      CLASS);
                    //设置本程序进程的执行级别为实时执行，这保证本程序能立即获取
      CPU执行权，快速退出
                    SetPriorityClass(GetCurrentProcess(), REALTIME_
      PRIORITY_CLASS);
                    SetThreadPriority(GetCurrentThread(), THREAD_
      PRIORITY_TIME_CRITICAL);
                    //通知Windows资源管理器，本程序文件已经被删除
                    SHChangeNotify(SHCNE_DELETE, SHCNF_PATH, szFileName,0);
                    ExitProcess(0);
                }
            }
        }
        int _tmain(int argc, _TCHAR＊ argv[])
        {
            char szInputStr[MAXBYTE];
            //输入delete，触发程序自删除
            scanf("%s", szInputStr);
            if(0 == (strncmp(szInputStr, "delete", MAXBYTE)))
        {
                  DelItself();
            }
            return 0;
        }

方法二：

调用接口函数库类型：Windows API，标准C库函数。

程序原理：Windows加载程序后，进程无法删除进程映像，利用批处理延时加载，进程退出后，利用批处理删除映像文件。

程序代码：

        #include "stdafx.h"
        #include <stdio.h>
        #include <string.h>
      #include <windows.h>
      VOID DelItself()
      {
          //获取文件路径名
              TCHAR szFileName[MAX_PATH] = {0};
              char filename[MAX_PATH] = {0};
          if((GetModuleFileName(0, szFileName, MAX_PATH)! =0))
              {
              wcstombs(filename, szFileName, MAX_PATH);
              std::ofstream ofs;
              ofs.open("del_itself.bat", std::ios_base::out);
              if (ofs.is_open())
              {
                    ofs<<"@echo del file yourself\r\n";
                    ofs<<":start\r\n";
                    ofs<<"\tif not exist \""<<filename<<"\"";
                    ofs<<" goto done\r\n”;
                    ofs<<"\tdel /f /q \""<< filename << "\"\r\n";
                    ofs<<"\tgoto start\r\n";
                    ofs<<":done\r\n";
                    ofs<<"\tdel /f /q del_itself.bat"<<std::endl;
              }
              ofs.close();
              ShellExecute(NULL, L"open", L"del_itself.bat", L"",
    L"", SW_HIDE);
              }
      }
      int _tmain(int argc, _TCHAR＊ argv[])
      {
            char szInputStr[MAXBYTE];
            //输入delete，触发程序自删除
            scanf("%s", szInputStr);
            if(0 == strncmp(szInputStr, "delete", MAXBYTE))
            {
                printf("程序正在自删除．..........\n");
                DelItself();
            }
            return 0;
      }

10.1.3 共享软件安全注册的实现

互联网上提供了大量的应用软件，可以使用户的生活更加方便。一般这些软件都会在一段时间内是免费的，超过软件允许的使用期限后就需要注册才能接着使用；但是一些破解高手会用破解手段制作注册机生成注册码，以便可以继续使用软件。面对这些问题，软件作者需要增加安全防范意识。

下面就介绍一种结合物理硬件信息的安全注册方式的实现过程。

（1）采集注册源的方法

注册码一定要是唯一的，所以在注册源的采集上就应该选择唯一的、不会轻易被人复制的硬件、软件的信息。其中硬件由于不可复制和物理唯一性是最好的选择，几乎所有的计算机配件都有一个唯一的序列号。比如，硬盘的序列号、网卡的序列号、BIOS中主板的序列号，从硬件通用性和实现起来的难易程度方面来考虑，以硬盘的序列号最为安全和常用。

在Windows API函数中，获取硬盘序列号的函数为：

        GetVolumeInformation ("C:\\", NULL, NULL, &dwIDESerial, NULL,
      NULL, NULL, NULL);

第一个参数“C:\\”表示要读取C盘驱动器的序列号，因为几乎每个用户都有C盘这个分区。在调用过该函数之后，DWORD类型的变量dwIDESerial中，就存储了获取到的32位长的磁盘序列号。

采集过注册信息之后，可以同用户输入的注册名定位操作加密后存放到一个文本文件中，上传给软件的作者进行软件注册。

（2）设计注册机的方法

软件作者在接收到从用户端发送的由硬盘序列号构成原始注册信息之后，就往往需要利用注册机生成注册码返回给用户。通常情况下，注册机要利用已定位操作和不可逆算法，生成用户容易操作使用的字符串注册码。在用户的共享软件中，要有一个具有相同算法的注册机，来计算使用当前注册名进行注册的正确注册码，查询与用户输入的注册码是否匹配，如果不匹配，则不对其进行注册。

如果是软件作者返回的正确注册码，则需要通过标记配置文件或向注册表中写入特定内容的键值，来表示软件是通过注册的。共享软件在每次运行时都要对用户注册码进行检验，以实现注册限制功能。当应用程序被复制到别的计算机上时，将会因硬件序列号不同而不能通过注册。要想使用这个软件，就必须重新进行注册。

下面是一段程序开发者部分的简单注册机实现代码。

        //strSerial是由用户返回的，同注册名进行过位运算的注册源信息
        ::strcpy（buf1, strSerial）;
        ::strcpy（buf1, strName）;
        //再次进行位运算，取得硬盘序列号
        For（int I=0; I<9; I++）
        Input[I]=buf1[I]^ buf2[I];
        //实现错序与位操作，产生与注册名相关的注册码
        Int  Index[9]={8,7,5,3,1,6,4,2,0};
        Char  Mask[9]={'Z', 'O', 'N', 'G', 'L', 'I', 'A', 'B', '0', };
        For（I=0; I<9; I++）
        {
          Result[I]=input[Index[I]] ^ buf2[I];
          Result[I]=result[I]^Mask[I];
        }
        ……

下面是一段用户部分的简单注册机实现代码。

        //根据输入的字符串计算出硬盘的序列号
        Int  Index[9]={8,7,5,3,1,6,4,2,0};
        Char  Mask[9]={'Z', 'O', 'N', 'G', 'L', 'I', 'A', 'B', '0', };
        For（I=0; I<9; I++）
        {
          Result[I]=input[Index[I]] ^ buf2[I];
          Result[I]=result[I]^Mask[I];
        }
        DwSerialID=::atoll（output）; //将序列恢复成数值
        //动态提取硬盘序列号
        GetVolumeInformation（"C:\\", NULL, NULL, &dwIDESerial, NULL, NULL,
      NULL, NULL）;
        If（dwSerialID==dwIDESerial）
        {
        ……//注册成功
        }
        Else
        {
        ……//注册失败
        }

上述代码只是简单说明了注册机的实现过程，在实际运用中，还需要考虑共享软件的价值、实现的复杂程度等具体情况，来决定采用哪种加密方法。一般的注册机要进行两层以上的函数逻辑运算，对于共享软件而言，最好是采用在线注册的方式进行注册。

10.2 Win32编程技术概述

尽管Windows应用程序千变万化，令人眼花缭乱，但是消息机制和窗口过程却始终是它们的基础，掌握了这两项技术，也就相当于把握住了问题的关键。Win32 API函数是微软用来操作Windows的一组接口函数，是用C语言编写的。其中包括文件操作、窗体类操作、消息处理、系统底层支持和注册表操作等，基本上所有开发工具都要用到，但也有一些工具的编译器不支持编译C语言编写的API函数。

10.2.1 Win32编程基本概念

1．消息驱动

在介绍Windows消息驱动概念之前，我们首先来回顾面向过程的程序结构。main()程序有明显的开始、中间过程和结束点，程序是围绕这个过程编写好相关的子过程，再把这些子过程串联在一起。程序编好以后，该过程也就确定了。程序必须按照规定好的顺序执行：是否需要用户的输入、输入什么、程序取得用户输入以后做什么处理，处理完毕将结果显示给用户。该过程一旦确定，程序的执行过程也是固定的，用户不能干预。

而Windows编程所采用的设计思想是消息驱动，又叫作事件驱动。在这种程序结构中，程序没有明显的开始、结束，程序流程的控制由各种随机发生、不确定、没有预先设定顺序的事件的发生来触发，是一个不断产生消息和处理消息的过程。

也就是说程序一运行开始处于等待消息状态，获取消息以后就对该消息做出相应的处理，完成处理以后又进入等待消息的状态。这种程序结构与Windows操作系统结合非常紧密，最明显的一点就是消息的管理是由操作系统完成的。应用程序从操作系统获得消息有两种方式：一种是应用程序调用Windows提供的消息获取函数；另一种就是回调函数，由操作系统自己调用。

这种消息驱动机制有点像银行的柜台业务：早上八点，银行开门（Windows应用程序开始运行），每个营业员（Windows线程）回到自己的柜台开始办公。如果有顾客来办理相关业务（相当于Windows消息），那么对应的业务员就进行处理。顾客来办理业务的时间以及业务类型都是随机的，如果某一时刻没有顾客办理业务并且没有到下班时间（Windows应用程序退出）的话，那么相关的业务员进入等待状态。所有的业务员不断重复该过程，直到下班（Windows应用程序退出）。

2．应用程序、操作系统、IO设备之间的相互关系

应用程序、操作系统、IO设备之间的相互关系

3．窗口元素

① 窗口：是Windows操作系统最重要最基本的一个概念。它是一个正在运行的应用程序相对应的矩形区域，通过它用户可以和应用程序进行交互。

② 客户区：是窗口中最大的一块空白的矩形区域，是用户和系统进行交互的主要区域，一般用于显示应用程序的输出。

③ 标题栏：位于窗口顶部，用于显示应用程序的名称。

④ 菜单栏：位于标题栏下方，菜单栏列出了应用程序支持的大部分功能。

⑤ 图标：适用于提醒用户的一个小图像，代表一个应用程序。

⑥ 光标：Windows光标显示在屏幕上的一个小位图。

⑦ 工具栏：一般位于菜单栏下方，上面有一些位图按钮，代表一些常用功能。

⑧ 状态栏：位于这个窗口底端，用于输出菜单提示信息和一些其他详细信息。

⑨ 对话框：一种特殊的窗口，用于接受用户的输入输出。

⑩ 控件：对话框上的许多小窗口都是控件，如按钮、编辑框等都是控件。

4．编程术语

① 窗口：是应用程序操作的基本单元，是用户可以通过它和应用程序进行交互的接口环境，也是系统管理应用程序的基本单位。从程序运行的内存组织结构看，窗口对应一个数据结构WNDCLASS。

② 实例：实际上就是一个可执行程序在内存中的复制。一个可执行程序运行多次，在内存中就有多个内存复制。系统是通过实例句柄来识别一个可执行程序的复制。

③ 句柄：系统用来识别不同对象或者同类对象的不同实例的“编号”，是一个无符号整数。几乎所有对对象的引用都是通过句柄来进行的，如使用HINSTANCE、HWND、HCURSOR、HICON、HDC、HBRUSH、HPEN等。

④ 资源：构成应用程序的元素称为资源，如菜单、工具条、位图、字符串等。

⑤ 窗口函数：用户通过窗口和应用程序交互时产生的消息，送给一个函数进行处理。该函数体结构大致由一个Switch结构组成，是消息驱动机制的发动机。

⑥ 图形设备接口：GDI（Graphic Device Interface）是Windows系统的重要组成部分。负责系统和用户或者绘图程序之间的信息交换，并控制输出设备上图形和文字的输出。最大的优点就是设备无关性，它将程序员和设备相隔离，程序员不必关心物理设备的细节，直接调用相关的API函数就可以在输出设备上显示图形或者文字。

⑦ 回调函数：写好了等系统进行调用的函数。只能由系统自动调用。前面所说的窗口函数就是一个典型的回调函数。

10.2.2 第一个Win32程序

在以前的C语言编程中，一个最简单的程序可以只有两行。

        void main(void)
        { printf "Hello World! "; }

而要实现同样功能的Windows程序却最少也要写几十行，这并不是说明Windows应用程序效率低下，难于掌握，只是说明程序在Windows环境下有更丰富的内涵。Windows程序的效率其实不低，在所有的Windows应用程序中，都有一个程序初始化的过程，且得用上几十条语句。这段初始化的代码对于任何Windows应用程序而言，都是大同小异的。下面以一个实现最简单功能的程序EasyWin为例，说明Windows程序的基本框架。

打开Visual C++ 6.0选择【File】→【New】菜单项，在出现的对话框中，选择Projects栏目（新建工程），并点取其下的Win32 Application项，表示使用Win32环境创建应用程序。先在Location（路径）中填入“c:\”，然后在Project Name（项目名称）中填入“EasyWin”，其他按照默认设置）。单击【OK】按钮。

再次选择【File】→【New】菜单项，在出现的对话框中选择Files栏目（新建文件），并点取其下的C++ Source File项，表示新建一个C++源文件。在右边的File栏中输入“EasyWin”，最后确定将Add to project检查框打上勾。单击【OK】按钮。

在EasyWin.cpp文件中输入以下源程序代码。

        //＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊
        // 工程：easywin
        // 文件：easywin.cpp
        //内容：一个基本的Win32程序
        //＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊
        #include <windows.h>
        #include <windowsx.h>
        //函数声明
        BOOL InitWindow( HINSTANCE hInstance, int nCmdShow );
        LESULT CALLBACK WinProc( HWND hWnd, UINT message, WPARAM
      wParam, LPARAM lParam );
        //＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊
        //函数：WinMain()
        //
        功能：Win32应用程序入口函数。创建主窗口，处理消息循环
        //＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊
        int PASCAL WinMain( HINSTANCE hInstance, //当前实例句柄
        HINSTANCE hPrevInstance, //前一个实例句柄
        LPSTR lpCmdLine, //命令行字符
        int nCmdShow) //窗口显示方式
        {
          MSG msg;
      //创建主窗口
      if ( ! InitWindow( hInstance, nCmdShow ) )
      return FALSE;
      //进入消息循环：
      //从该应用程序的消息队列中检取消息，送到消息处理过程，
      //当检取到WM_QUIT消息时，退出消息循环
      while (GetMessage(&msg, NULL, 0, 0))
      {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
      }
      //程序结束
      return msg.wParam;
      }
    //＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊
    //函数：InitWindow()
    //功能：创建窗口
    //＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊
    static BOOL InitWindow( HINSTANCE hInstance, int nCmdShow )
    {
    HWND hwnd; //窗口句柄
    WNDCLASS wc; //窗口类结构
    //填充窗口类结构
    wc.style = CS_VREDRAW | CS_HREDRAW;
    wc.lpfnWndProc = (WNDPROC)WinProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = hInstance;
    wc.hIcon = LoadIcon( hInstance, IDI_APPLICATION );
    wc.hCursor = LoadCursor( NULL, IDC_ARROW );
    wc.hbrBackground = GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName = NULL;
    wc.lpszClassName = "EasyWin";
    //注册窗口类
    RegisterClass( &wc );
    //创建主窗口
    hwnd = CreateWindow(
    "EasyWin", //窗口类名称
    "一个基本的Win32程序", //窗口标题
    WS_OVERLAPPEDWINDOW, //窗口风格，定义为普通型
    100, //窗口位置的x坐标
    100, //窗口位置的y坐标
    400, //窗口的宽度
    300, //窗口的高度
    NULL, //父窗口句柄
    NULL, //菜单句柄
    hInstance, //应用程序实例句柄
    NULL ); //窗口创建数据指针
    if( ! hwnd ) return FALSE; //显示并更新窗口
        ShowWindow( hwnd, nCmdShow );
        UpdateWindow( hwnd );
        return TRUE;
        }
        //＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊
        //函数：WinProc()
        //功能：处理主窗口消息
        //＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊＊
        LESULT CALLBACK WinProc( HWND hWnd, UINT message, WPARAM
      wParam, LPARAM lParam )
        {
        switch( message )
        {
        case WM_KEYDOWN://击键消息
        switch( wParam )
        {
        case VK_ESCAPE:
        MessageBox(hWnd, "ESC键按下了！", "Keyboard", MB_OK);
        break;
        }
        break;
        case WM_RBUTTONDOWN://鼠标消息
        {
        MessageBox(hWnd, "
        鼠标右键按下了！", "Mouse", MB_OK);
        break;
        }
        case WM_PAINT://
        窗口重画消息
        {
        char hello[]="你好，我是EasyWin ! ";
        HDC hdc;
        PAINTSTRUCT ps;
        hdc=BeginPaint( hWnd, &ps ); //取得设备环境句柄
        SetTextColor(hdc, RGB(0,0,255)); //设置文字颜色
        TextOut( hdc, 20, 10, hello, strlen(hello) ); //输出文字
        EndPaint( hWnd, &ps ); //释放资源
        break;
        }
        case WM_DESTROY://退出消息
        PostQuitMessage( 0 ); //调用退出函数
        break;
        } //调用缺省消息处理过程
        return DefWindowProc(hWnd, message, wParam, lParam);
        }

程序输入完毕，即可编译执行。在窗口中单击鼠标键或按<Esc>键时，会弹出一个对话框以表示你的操作。其实，这个程序可以看成是所有Win32应用程序的框架，在以后所有的程序中，你会发现它们都是在这个程序的基础之上再添加代码。

（1）WinMain()函数

WinMain()函数是应用程序开始执行时的入口点，通常也是应用程序结束任务退出时的出口点。它与DOS程序的main()函数起同样的作用，有一点不同的是，WinMain()函数必须带有四个参数，它们是系统传递给它的。WinMain()函数的原型如下。

          int PASCAL WinMain( HINSTANCE hInstance, //当前实例句柄
          HINSTANCE hPrevInstance, //前一个实例句柄
          LPSTR lpCmdLine, //命令行字符
          int nCmdShow) //窗口显示方式

第一个参数hInstance，是标识该应用程序当前实例的句柄。它是HINSTANCE类型，HINSTANCE是Handle of Instance的缩写，表示实例的句柄。hInstance是一个很关键的数据，它唯一地代表该应用程序，在后面初始化程序主窗口的过程中需要用到这个参数。

这里有两个概念，一个是实例，一个是句柄。实例代表的是应用程序执行的整个过程和方法，一个应用程序如果没有被执行，只是存在于磁盘上，那么就说它是没有被实例化的；只要一执行，则说该程序的一个实例在运行。句柄，顾名思义，指的是一个对象的把柄。在Windows中有各种各样的句柄，它们都是32位的指针变量，用来指向该对象所占据的内存区。句柄的使用，可以极大地方便Windows管理其内存中的各种对象。

第二个参数是hPrevInstance，它是用来标识该应用程序的前一个实例句柄。对于基于Windows 32的应用程序来说，这个参数总是NULL。这是因为在Windows 95操作系统中，应用程序的每个实例都有各自独立的地址空间，即使同一个应用程序被执行了两次，在内存中也会为它们的每一个实例分配新的内存空间，所以一个应用程序被执行后，不会有前一个实例存在的可能。也就是说，hPrevInstance这个参数是完全没有必要的，只是为了提供与16位Windows的应用程序形式上的兼容性，才保留了这个参数。在以前的16位Windows环境下（如Windows 3.2）,hPrevInstance用来标识与hInstance相关的应用程序的前一个句柄。

第三个参数是lpCmdLine，是指向应用程序命令行参数字符串的指针。如在Windows 95的“开始”菜单中单击“运行”，输入“easywin hello”，则此参数指向的字符串为“hello”。

最后一个参数是nCmdShow，是一个用来指定窗口显示方式的整数。这个整数值可以是SW_SHOW、SW_HIDE、SW_SHOWMAXIMIZED、SW_SHOWMINIMIZED等，关于这些值的含义，将在下一节说明。

（2）注册窗口类

一个应用程序可以有许多窗口，但只有一个是主窗口，它是与该应用程序的实例句柄唯一关联的。上面的例程中，创建主窗口的函数是InitWindow()。

通常要填充一个窗口类结构WNDCLASS，然后调用RegisterClass()对该窗口类进行注册。每个窗口都有一些基本的属性，如窗口边框、窗口标题栏文字、窗口大小和位置、鼠标、背景色、处理窗口消息函数的名称等。注册的过程也就是将这些属性告诉系统，然后再调用CreateWindow()函数创建出窗口。这也就像你去裁缝店定做一件衣服，先要告诉店老板你的身材尺寸、布料颜色以及你想要的款式，然后他才能为你做出一件让你满意的衣服。

在VC的帮助中，可以看到WNDCLASS结构是这样定义的。

          typedef struct _WNDCLASS {
          UINT style; //窗口的风格＊
          WNDPROC lpfnWndProc; //指定窗口的消息处理函数的远指针＊
          int cbClsExtra; //指定分配给窗口类结构之后的额外字节数＊
          int cbWndExtra; //指定分配给窗口实例之后的额外字节数
          HANDLE hInstance; //指定窗口过程所对应的实例句柄＊
          HICON hIcon; //指定窗口的图标
          HCURSOR hCursor; //指定窗口的鼠标
          HBRUSH hbrBackground; //指定窗口的背景画刷
          LPCTSTR lpszMenuName; //窗口的菜单资源名称
          LPCTSTR lpszClassName; //该窗口类的名称＊
        } WNDCLASS;

在Win95和WinNT的具有新界面特性的系统中，为了支持新的窗口界面特性，还有一种扩展的窗口类型WNDCLASSEX，它的定义如下。

        typedef struct _WNDCLASSEX {
        UINT cbSize; //指定WNDCLASSEX结构的大小
        UINT style;
        WNDPROC lpfnWndProc;
        int cbClsExtra;
        int cbWndExtra;
        HANDLE hInstance;
        HICON hIcon;
        HCURSOR hCursor;
        HBRUSH hbrBackground;
        LPCTSTR lpszMenuName;
        LPCTSTR lpszClassName;
        HICON hIconSm; //窗口的小图标
        } WNDCLASSEX;

WNDCLASS和WNDCLASSEX这两个结构基本上是一致的，只是WNDCLASSEX结构中多了cbSize和hIconSm这两个成员。WNDCLASS结构的各成员中，其注释后打了星号的表示该项应特别注意。WNDCLASS结构的第一个成员style表示窗口类的风格，它往往是由一些基本的风格通过位的“或”操作（操作符位“|”）组合而成。下表列出了一些常用的基本窗口风格。

在EasyWin应用程序中，是按如下方式对WNDCLASS结构进行填充和注册的。

        wc.style = CS_VREDRAW | CS_HREDRAW;
        wc.lpfnWndProc = (WNDPROC)WinProc;
        wc.cbClsExtra = 0;
        wc.cbWndExtra = 0;
        wc.hInstance = hInstance;
        wc.hIcon = LoadIcon( hInstance, IDI_APPLICATION );
        wc.hCursor = LoadCursor( NULL, IDC_ARROW );
        wc.hbrBackground = GetStockObject(WHITE_BRUSH);
        wc.lpszMenuName = NULL;
        wc.lpszClassName = "EasyWin";

可以看到，wc.style被设为CS_VREDRAW | CS_HREDRAW，表示只要窗口的高度或宽度发生变化，都会重画整个窗口。

第二个成员lpfnWndProc的值为(WNDPROC)WinProc。表明该窗口类的消息处理函数是WinProc()函数。这里要指定窗口的消息处理函数的远指针，输入消息处理函数的函数名称即可，必要时应该进行强制类型转换，将其转换成WNDPROC型。

接下来的cbClsExtra和wc.cbWndExtra在大多数情况下都会设为0。

然后的hInstance成员，给它的值是由WinMain()传来的应用程序的实例句柄，表明该窗口与该实例是相关联的。事实上，只要是注册窗口类，该成员的值始终是该程序的实例句柄，应该把它记住。

下面的hIcon是让你给这个窗口指定一个图标，调用LoadIcon( hInstance, IDI_APPLICATION )，可以调用系统内部预先定义好的标志符为IDC_APPLICATION的图标作为该窗口的图标。

同样，调用LoadCursor( NULL, IDC_ARROW )为该窗口调用系统内部预先定义好的箭头型鼠标。

hbrBackground成员用来定义窗口的背景画刷颜色，也就是该窗口的背景色。调用GetStockObject(WHITE_BRUSH)可以获得系统内部预先定义好的白色画刷作为窗口的背景色。

上面的LoadIcon()、LoadCursor()、GetStockObject()都是Windows的API函数，它们的用法可以参看VC的帮助，这里就不多介绍了。

lpszMenuName成员的值我们给它NULL，表示该窗口将没有菜单。如果你想让你的窗口拥有菜单，就把lpszMenuName成员赋值为标志菜单资源的字符串。

WNDCLASS结构的最后一个成员lpszClassName是让你给这个窗口类起一个唯一的名称，因为Windows操作系统中有许许多多的窗口类，必须用一个独一无二的名称来代表它们。通常你可以用你的程序名来命名这个窗口类的名称，这个名称将在创建窗口的CreateWindow()函数中用到。

填充完毕后，对于WNDCLASS结构，调用RegisterClass()函数进行注册；对于WNDCLASSEX结构，调用RegisterClassEx()函数进行注册，它们的原型分别如下。

        ATOM RegisterClass( CONST WNDCLASS ＊lpWndClass );
        ATOM RegisterClassEx( CONST WNDCLASSEX ＊lpwcx );

该函数如调用成功，则返回一个非0值，表明系统中已经注册了一个名为EasyWin的窗口类；如果失败，则返回0。

（3）创建窗口

当窗口类注册完毕之后，并不会有窗口显示出来，因为注册的过程仅仅是为创建窗口所做的准备工作。实际创建一个窗口的是通过调用CreateWindow()函数完成的。窗口类中已经预先定义了窗口的一般属性，而CreateWindow()中的参数可以进一步指定一个窗口更具体的属性。在EasyWin程序中，是如下调用CreateWindow()函数来创建窗口的。

        hwnd = CreateWindow(
        "EasyWin", //创建窗口所用的窗口类的名称＊
        "一个基本的Win32程序", //窗口标题
        WS_OVERLAPPEDWINDOW, //窗口风格，定义为普通型＊
        100, //窗口位置的x坐标
        100, //窗口位置的y坐标
        400, //窗口的宽度
        300, //窗口的高度
        NULL, //父窗口句柄
        NULL, //菜单句柄
        hInstance, //应用程序实例句柄＊
        NULL ); //一般都为NULL

CreateWindow()函数的参数的含义在上面的注释中已有介绍，注释后打了星号标记的参数应该着重注意，其他的参数都很简单，不多做介绍，可参看VC的帮助。

第1个参数是创建该窗口所使用的窗口类的名称，注意这个名称应与前面所注册的窗口类的名称一致。

第3个参数为创建的窗口的风格，下表列出了常用的窗口风格。

（4）风格含义

WS_OVERLAPPEDWINDOW创建一个层叠式窗口，有边框、标题栏、系统菜单、最大最小化按钮，是以下几种风格的集合：WS_OVERLAPPED,WS_CAPTION,WS_SYSMENU, WS_THICKFRAME,WS_MINIMIZEBOX,WS_MAXIMIZEBOX。

WS_POPUPWINDOW创建一个弹出式窗口，是以下几种风格的集合：WS_BORDER, WS_POPUP,WS_SYSMENU。

WS_CAPTION与WS_POPUPWINDOW风格必须一起使用才能使窗口菜单可见，WS_OVERLAPPED创建一个层叠式窗口，它有标题栏和边框，与WS_TILED风格一样。

常用的窗口风格及其含义

程序中使用了WS_OVERLAPPEDWINDOW标志，它是创建一个普通窗口常用的标志。而在DirectX编程中，我们常用的是WS_POPUP，用这个标志创建的窗口没有标题栏和系统菜单，如果设定窗口为最大化，客户区可以占满整个屏幕，以满足DirectX编程的需要。CreateWindow()函数后面的参数中，仍用到了该应用程序的实例句柄hInstance。

如果窗口创建成功，返回值是新窗口的句柄，否则返回NULL。

（5）显示和更新窗口

窗口创建后，并不会在屏幕上显示出来，要真正把窗口显示在屏幕上，还得使用ShowWindow()函数，其原型如下。

        BOOL ShowWindow( HWND hWnd, int nCmdShow );

参数hWnd指定要显示的窗口的句柄，nCmdShow表示窗口的显示方式，这里指定为从WinMain()函数的nCmdShow所传递而来的值。

由于ShowWindow()函数的执行优先级不高，所以当系统正忙着执行其他的任务时，窗口不会立即显示出来，此时调用UpdateWindow()函数可以立即显示窗口。其函数原型如下。

        BOOL UpdateWindow( HWND hWnd )

（6）消息循环

在Win32编程中，消息循环是相当重要的一个概念，看似很难，但是使用起来非常简单。在WinMain()函数中，调用InitWindow()函数成功地创建了应用程序主窗口之后，就要启动消息循环，其代码如下。

        while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg); DispatchMessage(&msg); }

Windows应用程序可以接收以各种形式输入的信息，包括键盘、鼠标动作、计时器产生的消息，也可以是其他应用程序发来的消息等。Windows系统自动监控所有的输入设备，并将其消息放入该应用程序的消息队列中。

GetMessage()函数则是用来从应用程序的消息队列中按照先进先出的原则将这些消息一个个地取出来，放进一个MSG结构中去。GetMessage()函数原型如下。

        BOOL GetMessage (LPMSG lpMsg, //指向一个MSG结构的指针，用来保存消息HWND hWnd,
        //指定哪个窗口的消息将被获取
        UINT wMsgFilterMin, //指定获取的主消息值的最小值UINT wMsgFilterMax
        //指定获取的主消息值的最大值 )；

GetMessage()将获取的消息复制到一个MSG结构中。如果队列中没有任何消息，GetMessage()函数将一直空闲直到队列中又有消息时再返回。如果队列中已有消息，它将取出一个后返回。MSG结构包含了一条Windows消息的完整信息，其定义如下。

        typedef struct tagMSG { HWND hwnd; //接收消息的窗口句柄UINT message；
        //主消息值
        WPARAM wParam; //副消息值，其具体含义依赖于主消息值LPARAM lParam；
        //副消息值，其具体含义依赖于主消息值DWORD time；
        //消息被投递的时间POINT pt; //鼠标的位置 } MSG；

该结构中的主消息表明了消息的类型，例如，是键盘消息还是鼠标消息等，副消息的含义则依赖于主消息值，例如，如果主消息是键盘消息，那么副消息中则存储了是键盘的哪个具体键的信息。

GetMessage()函数还可以过滤消息，它的第二个参数是用来指定从哪个窗口的消息队列中获取消息，其他窗口的消息将被过滤掉。如果该参数为NULL，则GetMessage()从该应用程序线程的所有窗口的消息队列中获取消息。

第三个和第四个参数是用来过滤MSG结构中主消息值的，主消息值在wMsgFilterMin和wMsgFilterMax之外的消息将被过滤掉。如果这两个参数为0，则表示接收所有消息。

当且仅当GetMessage()函数在获取到WM_QUIT消息后，将返回0值，于是程序退出消息循环。

TranslateMessage()函数的作用是把虚拟键消息转换到字符消息，以满足键盘输入的需要。DispatchMessage()函数所完成的工作是把当前的消息发送到对应的窗口过程中去。

开启消息循环其实是很简单的一个步骤，几乎所有的程序都是按照EasyWin的这个方法。你完全不必去深究这些函数的作用，只是简单地照抄就可以了。

（7）消息处理函数

消息处理函数又叫窗口过程，在这个函数中，不同的消息将用switch语句分配到不同的处理程序中去。Windows的消息处理函数都有一个确定的样式，即这种函数的参数个数和类型及其返回值的类型都有明确的规定。在VC的说明书中，消息处理函数的原型是这样定义的：

        LRESULT CALLBACK WindowProc（HWND hwnd, //接收消息窗口的句柄UINT uMsg,
        //主消息值WPARAM wParam, //副消息值LPARAM lParam //副消息值）；

如果你的程序中还有其他的消息处理函数，也都必须按照上面的这个样式来定义，但函数名称可以随便取。EasyWin中的WinProc()函数就是这样一个典型的消息处理函数。

消息处理函数的四个参数是由GetMessage()函数从消息队列中获得MSG结构，然后分解后得到的。第二个参数uMsg和MSG结构中的message值是一致的，代表了主消息值。程序中用switch语句来将不同类型的消息分配到不同的处理程序中去。

WinProc()函数明确地处理了4个消息，分别是WM_KEYDOWN（击键消息）、WM_RBUTTONDOWN（鼠标右键按下消息）、WM_PAINT（窗口重画消息）、WM_DESTROY（销毁窗口消息）。

值得注意的是，应用程序发送到窗口的消息远远不止以上这几条，像WM_SIZE、WM_MINIMIZE、WM_CREATE、WM_MOVE等这样频频使用的消息就有几十条。为了减轻编程的负担，Windows的API提供了DefWindowProc()函数来处理这些最常用的消息，调用了这个函数后，这些消息将按照系统默认的方式得到处理。因此，在switch_case语句中，只需明确地处理那些有必要进行特别响应的消息，把其余的消息交给DefWindowProc()函数来处理，是一种明智的选择，也是你必须做的一件事。

（8）结束消息循环

当用户按<Alt+F4>组合键或单击窗口右上角的退出按钮时，系统就向应用程序发送一条WM_DESTROY的消息。在处理此消息时，调用了PostQuitMessage()函数，该函数会给窗口的消息队列中发送一条WM_QUIT的消息。在消息循环中，GetMessage()函数一旦检索到这条消息，就会返回FALSE，从而结束消息循环，随后程序也结束。

10.2.3 应用程序编程接口

Windows操作系统通过两种接口对外提供服务：普通用户操作接口服务和程序员接口服务。其中用户操作接口服务是通过最终用户的操作来完成的，而程序员接口服务是提供一系列底层的实现操作系统最基本的函数（接口）供程序员调用来完成相关功能的。例如，一个文件的拷贝，普通用户可以通过鼠标菜单选择操作或者命令来完成，程序员可以通过程序代码调用文件操作的相关函数（API）来完成相同的功能。

所有这些系统底层、实现最基本功能、供程序员调用的函数集中起来形成一个集合，这就是API（Application Programming Interface）。Windows API编程是开发Windows应用程序最古老、最原始，同时又是检验一个程序员对Windows操作系统及相关数据结构掌握程度的最好方法。

Windows提供了一组供程序员使用的API，使用这些API能够建立或捆绑到已运行的程序上来对其进行调试，可获得程序的底层信息和调试信息。

如果愿意，甚至可以对被调试程序进行任意的修改，其具体定义如下。

1.ContinueDebugEvent

说明：此函数允许调试器恢复先前由于调试事件而挂起的线程。

语法：BOOL ContinueDebugEvent（DWORD dwProcessId,DWORD dwThreadId,DWORD dwContinueStaus）

参数：

        dwProcessId         DWORD被调试进程的进程标识符
        dwThreadId          DWORD欲恢复线程的线程标识符
        dwContinueStaus    DWORD
                          DBG_CONTINUE和DBG_EXCEPTION_NOT_HANDLED
        返回值              BOOL如果函数成功，则返回非0值；如果失败，则返回0

2.DebugActiveProcess

说明：此函数允许将调试器捆绑到一个正在运行的进程上。

语法：BOOL DebugActiveProcess(DWORD dwProcessId )

参数：

        dwProcessId         DWORD欲捆绑进程的进程标识符
        返回值              BOOL如果函数成功，则返回非0值；如果失败，则返回0

3.DebugActiveProcessStop

说明：此函数允许将调试器从一个正在运行的进程上卸载（Windows 9x内核不支持此函数）。

语法：BOOL DebugActiveProcessStop(DWORD dwProcessId )

参数：

        dwProcessId         DWORD欲卸载的进程标识符
        返回值              BOOL如果函数成功，则返回非0值；如果失败，则返回0

4.DebugBreak

说明：在当前进程中产生一个断点异常，如果当前进程不是处在被调试状态，那么这个异常将被系统例程接管，多数情况下会导致当前进程被终止。

语法：VOID DebugBreak(VOID)

参数：无

其他：其实这个函数的用处与在程序中直接插入INT 3的效果一样，如果反编译Windows 9X的KERNEL32.dll，读者可以发现这个函数只包含两句，一句是INT 3，一句是RET。

5.DebugBreakProcess

说明：在指定进程中产生一个断点异常。

语法：VOID DebugBreakProcess (HANDLE hProcess)

参数：

        hProcess            HANDLE进程的句柄
        返回值              无

6.FatalExit

说明：此函数将使调用进程强制退出，将控制权转移至调试器。与ExitProcess不同的是，在退出前会先调用一个INT 3断点。

语法：VOID FatalExit(int ExitCode)

参数：

        ExitCode            int退出码
        返回值              无

7.FlushInstructionCache

说明：刷新指令高速缓存。

语法：BOOL FlushInstructionCache(HANDLE hProcess, LPCVOID lpBassAddress, SIZE_T dwSize)

参数：

        hProcess         HANDLE进程的句柄
        lpBassAddress     LPCVOID欲刷新区域的基地址
        dwSize           SIZE_T欲刷新区域的长度
        返回值           BOOL如果函数成功，则返回非0值；如果失败，则返回0

8.GetThreadContext

说明：获取指定线程的执行环境。

语法：BOOL GetThreadContext(HANDLE hThread, LPCONTEXT lpContext )

参数：

        hThread          HANDLE欲获取执行环境的线程的句柄
        lpContext         LPCONTEXT指向CONTEXT结构的指针
        返回值           BOOL如果函数成功，则返回非0值；如果失败，则返回0

9.GetThreadSelectorEntry

说明：此函数返回指定选择器和线程的描述符表的入口地址。

语法：BOOL GetThreadSelectorEntry/(HANDLE hThread,DWORD dwSelector,LPLDT_ ENTRY lpSelectorEntry)

参数：

        hThread          HANDLE包含指定选择器的线程的句柄
        dwSelector        DWORD选择器数目
        lpSelectorEntry    LPLDT_ENTRY指向用来接收描述符表的结构的指针
        返回值           如果函数成功，则返回非0值，此外lpSelectorEntry指向的结构中将
                        被填入接收到的描述符表；如果失败，则返回0

10.IsDebuggerPresent

说明：此函数用来判断调用进程是否处于被调试环境中。

语法：BOOL IsDebuggerPresent(VOID)

参数：

        返回值           BOOL  如果进程处在被调试状态，则返回非0值；不处在被调试状
                        态，则返回0

11.OutputDebugString

说明：将一个字符串传递给调试器显示。

语法：VOID OutputDebugString(LPCYSTR lpOutputString)

参数：

        lpOutputString      LPCYSTR：指向要显示的以“00”结尾的字符串的指针
        返回值             无

12.ReadProcessMemory

说明：读取指定进程的某区域内的数据。

语法：BOOL ReadProcessMemory(HANDLE hProcess, LPCVOID lpBassAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T ＊ lpNumberOfBytesRead)

参数：

        hProcess               HANDLE进程的句柄
        lpBassAddress          LPCVOID欲读取区域的基地址
        lpBuffer                 LPVOID保存读取数据的缓冲的指针
        nSize                  SIZE_T欲读取的字节数
        lpNumberOfBytesRead   SIZE_T存储已读取字节数的地址指针
        返回值                 BOOL如果函数成功，则返回非0值；如果失败，则返回0

13.SetThreadContext

说明：设置指定线程的执行环境。

语法：BOOL SetThreadContext(HANDLE hThread, LPCONTEXT lpContext )

参数：

        hThread            HANDLE欲设置执行环境的线程的句柄
        lpContext          LPCONTEXT指向CONTEXT结构的指针
        返回值             BOOL如果函数成功，则返回非0值；如果失败，则返回0

14.WaitForDebugEvent

说明：此函数用来等待被调试进程发生调试事件。

语法：BOOL WaitForDebugEvent(LPDEBUG_ENENT lpDebugEvent, DWORD dwMilli seconds)

参数：

        lpDebugEvent       LPDEBUG_ENENT  指向接收调试事件信息的DEBUG_ENENT结构
                        的指针
        dwMilliseconds     DWORD  该函数用来等待调试事件发生的毫秒数，如果这段时间
                        内没有调试事件发生，函数将返回调用者；如果将该参数指定为
                        INFINITE，函数将一直等待，直到调试事件发生
        返回值             BOOL如果函数成功，则返回非0值；如果失败，则返回0

15.WriteProcessMemory

说明：在指定进程的某区域内写入数据。

语法：BOOL WriteProcessMemory(HANDLE hProcess, LPCVOID lpBassAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T ＊ lpNumberOfBytesRead)

参数：

        hProcess               HANDLE进程的句柄
        lpBassAddress          LPCVOID欲写入区域的基地址
        lpBuffer                 LPVOID保存欲写入数据的缓冲的指针
        nSize                  SIZE_T欲写入的字节数
        lpNumberOfBytesRead   SIZE_T存储已写入字节数的地址的指针
        返回值                 BOOL如果函数成功，则返回非0值；如果失败，则返回0

10.2.4 调试事件

调试器的工作原理就是：监视目标进程的执行、对目标进程发生的每一个调试事件做出应有的反应。当目标进程发生一个调试事件后，系统将会通知调试器来处理这个事件。

调试器将利用WaitForDebugEvent函数来获取目标进程的相关环境信息，可能存在的调试事件类型如下表所示。

调试事件类型

当WaitForDebugEvent函数接收到一个调试事件时，它将会把调试事件的信息填入DEBUG_EVENT结构中并返回，这个结构定义如下。

        typedef struct _DEBUG_EVENT {
              DWORD dwDebugEventCode；
              DWORD dwProcessId；
              DWORD dwThreadId；
              union {
                    EXCEPTION_DEBUG_INFO Exception；
                    CREATE_THREAD_DEBUG_INFO CreateThread；
                    CREATE_PROCESS_DEBUG_INFO CreateProcessInfo；
                    EXIT_THREAD_DEBUG_INFO ExitThread；
                    EXIT_PROCESS_DEBUG_INFO ExitProcess；
                    LOAD_DLL_DEBUG_INFO LoadDll；
                    UNLOAD_DLL_DEBUG_INFO UnloadDll；
                    OUTPUT_DEBUG_STRING_INFO DebugString；
                    RIP_INFO RipInfo；
                    } u；
        } DEBUG_EVENT；

在dwDebugEventCode中的值标记了所发生的调试事件的类型；dwProcessid的值是调试事件所发生的进程的标识符；dwThreadid的值是调试事件所发生的线程的标识符。

上述结构包含了关于调试事件的更多信息，根据上面dwDebugEventCode的不同，它可以是如下表所示的结构。

事件信息与结构中的u成员

但怎么才能访问这些数据呢？假设程序调用了WaitForDebugEvent函数并返回，则首先将要检查dwDebugEventCode字段中的值，根据它来判断debugger进程中发生了哪种类型的调试事件。例如，如果dwDebugEventCode字段的值为CREATE_PROCESS_DEBUG_EVENT，则可认为结构中u的成员为CreateProcessInfo，并可通过u.CreateProcessInfo来实现访问。

下面就是常用的CREATE_PROCESS_DEBUG_INFO结构的简要说明。

        CREATE_PROCESS_DEBUG_INFO结构定义：
        typedef struct _CREATE_PROCESS_DEBUG_INFO {
              HANDLE hFile;     // 进程文件的句柄，利用它可对文件进行操作
              HANDLE hProcess; // 进程的句柄，在进程空间中进行读写操作时要用到它
              HANDLE hThread;   // 主线程的句柄，在读取、设置线程环境时都要用到它
              LPVOID lpBaseOfImage;    // 进程执行的映像基地址
              DWORD dwDebugInfoFileOffset；
              DWORD nDebugInfoSize；
              LPVOID lpThreadLocalBase；
              LPTHREAD_START_ROUTINE lpStartAddress；
              LPVOID lpImageName；
              WORD fUnicode；
        } CREATE_PROCESS_DEBUG_INFO, ＊LPCREATE_PROCESS_DEBUG_INFO；

10.2.5 在调试时创建并跟踪一个进程

如果以前就使用过调试器，那么应该对“跟踪”有一些基本的了解。当“跟踪”一个程序时，程序在每执行一条指令后将会停止，这使用户有机会去检查寄存器/内存中的值。这种单步运行的官方定义为跟踪（tracing）。这是使用Win32调试API的第一步，可以通过以下方式创建进程。

1．创建一个新进程以供调试

通过CreateProcess创建新进程时，如果在dwCreationFlags函数的标志字段中设置了DEBUG_PROCESS或DEBUG_ONLY_THIS_PROCESS标志，将创建一个用以调试的新进程。

如果是以DEBUG_PROCESS标志创建新进程，调试器将会接收到目标进程及由目标进程创建的所有子进程发生的所有调试事件，一般情况下这是没有必要的。其实可以指定DEBUG_ONLY_THIS_PROCESS和DEBUG_PROCESS的组合标志来禁止它。

如果设置了DEBUG_ONLY_THIS_PROCESS标志，调试器将只会收到目标进程的调试事件，而对其子进程的调试事件就不予理睬了。当进程创建成功之后，可以通过查看PROCESS_INFORMATION结构来获取被创建进程及其主线程的进程标识符和线程标识符。

由于操作系统将调试对象标记为在特殊模式下运行，因此可以使用IsDebuggerPresent函数查看进程是否在调试器下运行。

2．如何将调试器捆绑到一个正在运行的进程上

利用DebugActiveProcess函数可以将调试器捆绑到一个正在运行的进程上，如果执行成功，则效果类似于利用DEBUG_ONLY_THIS_PROCESS标志创建的新进程。

其实将调试器捆绑到一个进程上一般是比较好的一种做法，但除了利用CreateProcess函数来载入进程外没有其他的办法。

到底该选择哪种方法呢？这就要结合用户将要进行什么样的工作。例如，要做一个简单的游戏修改器时，用临时捆绑调试器的方法可能比较好；如果要做一些不同寻常的工作，利用载入的方法会更好，因为它可以获得目标进程及其线程的所有控制权。

10.2.6 调试循环体

用调试API建立一个调试程序是非常简单的，所需要做的工作也只是创建一个用来调试的新进程，然后执行相关代码来监视所有的调试事件。只要把监视所有的调试事件的这部分代码称为“调试循环体”即可，因为它的实现非常简单，看上去就像一个“while”循环，所需要做的只是使用WaitForDebugEvent和ContinueDebugEvent函数。

WaitForDebugEvent在一段时间内等待目标进程中调试事件的发生，如果在这段时间没有调试事件发生，那么函数将返回FALSE。

如果在指定时间内调试事件发生了，那么函数将返回TRUE，并且它会把所发生的调试事件及其相关信息填写入一个DEBUG_EVENT结构。然后调试器会检查这些信息，并据此做出相应的反应。在对这些事件做出相应的操作后，就可以使用ContinueDebugEvent函数来恢复线程的执行，并等待下一个调试事件的发生（WaitForDebugEvent只能使用在创建的或是捆绑上的进程中的某个线程上）。

WaitForDebugEvent-ContinueDebugEvent循环的C语言示例：

        PROCESS_INFORMATION        pi；
        STARTUP_INFO                 si；
        DEBUG_EVENT              devent；
        If (CreateProcess(0, "target.exe",0,0 , FALSE , DEBUG_ONLY_THIS_
      PROCESS,0,0, &si, &pi))
        {
          while(TRUE)
            {
              if (WaitForDebugEvent( &devent,150))
                  //在150毫秒内等待调试事件
                    {
                    switch (devent.dwDebugEventCode)
        {
        case CREATE_PROCESS_DEBUG_EVENT：
        //在此填入处理程序
        break;
        case EXIT_PROCESS_DEBUG_EVENT：
        //在此填入处理程序
        break;
        case EXCEPTION_DEBUG_EVENT：
        //在此填入处理程序
        Break；
        }
            ContinueDebugEvent(devent.dwProcessId , devent.dwThreadId ,
      DBG_CONTINUE)；
                                      }
                            else
                                      {
                                      // 其他一些操作
                                      }
                            }
                  }  // while循环结束
        else
                  {
                  MessageBox(0, "Unexpected load error", "Fatal Error", MB_OK);
                  }

10.2.7 调试事件的处理

在知道调试器是如何捕获调试事件之后，即可利用C/C++中定义的case/switch语法做出相应的动作。当每一个调试事件发生时，根据事件的不同类型，都会有一段不同的处理程序来进行处理。

关于调试事件的更多信息可以根据DEBUG_EVENT中U结构的相应成员来取得。作为示例，再来看一下EXCEPTION_DEBUG_EVENT结构，选择它是因为遇到一个异常断点，并对其进行追踪都是经常要处理的事件。

EXCEPTION_DEBUG_EVENT结构：

        typedef struct _EXCEPTION_DEBUG_INFO
                  {
                            EXCEPTION_RECORD ExceptionRecord;
                            DWORD dwFirstChance;
                  } EXCEPTION_DEBUG_INFO;

其中EXCEPTION_RECORD结构包含很多异常的信息，内容如下。

        typedef struct _EXCEPTION_RECORD
                            {
                            DWORD ExceptionCode;
                            DWORD ExceptionFlags;
                            struct _EXCEPTION_RECORD ＊ExceptionRecord;
                            PVOID ExceptionAddress;
                            DWORD NumberParameters;
                            DWORD ExceptionInformation[EXCEPTION_MAXIMUM_
      PARAMETERS];
                            } EXCEPTION_RECORD;

结构中字段的定义如下。

ExceptionCode DWORD：用来描述异常类型的代码。

ExceptionFlags DWORD:0表示可继续异常。反之，则值为EXCEPTION_NONCONTINUABLE。

ExceptionRecord：指向_EXCEPTION_RECORD结构的指针。

ExceptionAddress PVOID：异常发生的地址。

NumberParameters DWORD:ExceptionInformation队列中定义的32位参数的数目。

ExceptionInformation：额外的32位消息队列，主要在嵌套异常时使用，对多数异常情况，它没有定义。

从这些结构中可查询需要的一切，比如可以判断出发生异常的类型、是否可以继续执行、异常发生的地址等信息。

其实最常见的异常情况是EXCEPTION_BREAKPOINT和EXCEPTION_SINGLE_ STEP。当执行时遇到一个INT 3断点时会产生EXCEPTION_BREAKPOINT异常，如果设置了单步执行标志，那么执行完一条指令后会发生一个EXCEPTION_SINGLE_ STEP异常。

关于异常还有一点要知道，当以调试的方式创建一个进程时，在进入进程之前，系统会先执行一次DebugBreak函数，这样会产生一个EXCEPTION_BREAKPOINT异常。如果一切正常，那么这应该是第一个遇到的也是必定会遇到的异常。可以调用ContinueDebugEvent函数来继续线程的运行，ContinueDebugEvent函数的dwContinueStatus参数有两个取值，分别是DBG_EXCEPTION_NOT_HANDLED和DBG_CONTINUE。

对于大多数调试事件，这两个值没有什么区别，都是恢复线程。唯一出现的例外情况是EXCEPTION_DEBUG_EVENT，如果线程报告发生了一个异常调试事件，就意味着在被调试的线程中发生了一个异常。

如果指定了DBG_CONTINUE，线程将忽略它自己的异常处理部分并继续执行。在这种情况下，程序必须在以DBG_CONTINUE恢复线程之前检查并处理异常，否则异常将不断地发生，直至程序被系统终止。

如果指定了DBG_EXCEPTION_NOT_HANDLED值，就是告诉Windows程序并不处理异常。Windows将使用被调试线程的默认异常处理函数来处理异常。

这类情况一般发生在进程被载入后发生的第一个EXCEPTION_DEBUG_EVENT，必须以DBG_CONTINUE为标志继续，在程序中如果调用了DebugBreak函数，或者插入INT3设置断点成功，并将内存恢复后，都应该使用DBG_CONTINUE为标志继续。

如果在程序中发生了不确定的异常，特别是调试带壳程序的时候，多半是由于外壳的SEH引起的，此时应该以DBG_EXCEPTION_NOT_HANDLED为标志继续，以便让被调试程序本身的异常处理机制来处理。

在发生其他调试事件时，可以利用类似的结构取得线程、进程所调用的DLL或其他一些事件的信息集合。

10.2.8 在另一个进程中注入代码

将一段代码注入某个进程的地址空间中并不是非常复杂的操作，可是在真正开始操作之前就得先解决一个小小的麻烦：需要一小段地址空间来存放补丁代码。

有的用户会提出疑问，利用VirtualAllocEx函数不就可以了吗？遗憾的是，VirtualAllocEx函数只在Windows NT内核下被支持，在Windows 9X内核下不被支持，那现在该怎么办呢？如果注入的代码很短小，那么可以利用原进程的各个区块之间的间隙，甚至可以把代码注入原进程文件头中的DOS stub部分来解决，当然这样的话执行之前先得更改目标进程文件头的读写属性。

如果要注入的部分比较大，则只能先将目标进程中的某个代码页保存之后，再注入新的代码，执行完后再将原始的代码写回。

具体的操作步骤如下。

① 利用CreateProcess函数创建一个供调试的进程。

② 建立WaitForDebugEvent函数和ContinueDebugEvent函数构成的调试循环体。

③ 利用SuspendThread函数挂起目标线程。

④ 利用VirtualProtectEx函数修改目标页的读写权限。

⑤ 利用ReadProcessMemory函数读取目标页。

⑥ 利用GetThreadContext函数保存线程环境。

⑦ 利用WriteProcessMemory函数写入新代码页。

⑧ 确认在新指令中的最后一个代码是INT 3，用户需要利用它在指令执行完成后获得系统控制权。INT 3产生的异常将会被用户的程序捕获（必须确认这是一个breakpoint异常，并且是在用户放置INT 3的位置）。

⑨ 保存一份CONTEXT结构的临时复制。

⑩ 在这份临时复制中设置新的EIP值。

⑪ 恢复原线程的执行，它将执行用户注入的代码，直到INT 3被执行，当它被执行时会被用户的程序捕获，目标线程再次被挂起。

⑫ 利用WriteProcessMemory函数恢复原始代码页。

⑬ 恢复原始代码页的读写属性。

⑭ 利用SetThreadContext函数恢复线程原始的环境。

⑮ 恢复原线程执行。

如果需要让注入的代码和进程原始的代码同时存在于进程空间中，而且准备注入的代码比较大，那么就需要为目标进程分配一些地址空间了。调用VirtualAlloc函数的代码是非常短小的，可以先在目标进程中注入调用VirtualAlloc函数的代码，利用它可以获取一些额外的地址空间。一般来说，几个KB就足够了，不要试图去申请很大的空间，那样很容易导致执行失败。当然还有另一个办法，可以在自己的进程中调用VirtualAllocEx函数，这也可以为目标进程分配一定的地址空间，可惜的是它只能运行在Windows NT内核下。

如果为了某些工作，需要将某个区块的相对地址转换为线性虚拟地址，就可以使用函数GetThreadSelectorEntry了。

使用Windows API实现一些简单的系统功能

建立一个控制台空工程，加入文件API.cpp，文件内容如下。

        // API.cpp : 定义控制台应用程序的入口点
        #include <iostream>
        #include <Windows.h>
        #include <stdio.h>
        #include <stdlib.h>
        using namespace std;
        //获取计算机名称，
        CPU数量，CPU类型，已用内存百分比，物理内存总数，空闲物理内存
        void GetSysInfo()
        {  //1. 定义数据结构存放计算机名称信息
      DWORD MaxComputerlenth = MAX_COMPUTERNAME_LENGTH;
      CHAR ComputerName[MAX_COMPUTERNAME_LENGTH];
      // 调用API函数GetComputerName获取计算机名称存放到ComputerName中
      GetComputerName(ComputerName, &MaxComputerlenth);
      cout << "本台计算机的名称为：" << ComputerName << endl;
      //2. 定义数据结构存放计算机系统信息
      SYSTEM_INFO siSysInfo;
      TCHAR tchBuffer[100]; // 调用API函数GetSystemInfo获取计算机名称存放
    到siSysInfo
      GetSystemInfo(&siSysInfo);
      // 输出相关系统信息
      sprintf(tchBuffer, "CPU的数量： %u\tCPU类型： %u",
      siSysInfo.dwNumberOfProcessors, siSysInfo.dwProcessorType);
      cout << tchBuffer << endl;
      //3. 定义数据结构存放内存状态信息
      MEMORYSTA
      TUS stat;
      GlobalMemoryStatus(&stat);  // 进行输出
      printf("%d%% 的内存在使用\n", stat.dwMemoryLoad);
      printf("总共有%8ldk的物理内存空间． \n", stat.dwTotalPhys/1024);
      printf("总共有%8ldk空闲的物理内存空间． \n", stat.dwAvailPhys/1024); }
      void CopyCppFile()
      {
      WIN32_FIND_DA
      TA FileData;
      HANDLE hSearch;
      DWORD dwAttrs;
      TCHAR szDirPath[] = TEXT("c:\\CppFiles\\");
      TCHAR szNewPath[MAX_PATH];
      BOOL fFinished = FALSE;
      // Create a new directory.
      if (! CreateDirectory(szDirPath, NULL))
      { printf("Could not create new directory.\n");
      //return;
      }
      // Start searching for text files in the current directory.
      hSearch = FindFirstFile(TEXT("＊.cpp"), &FileData);
      if (hSearch == INVALID_HANDLE_VALUE)
      {
      printf("No ＊.cpp files found.\n");
      return;
      }
      // Copy each .TXT file to the new directory
      // and change it to read only, if not already.
      while (! fFinished)
      {
      if (CopyFile(FileData.cFileName, szNewPath, FALSE))
      {
    dwAttrs = GetFileAttributes(FileData.cFileName);
    //if (dwAttrs==INV
    ALID_FILE_ATTRIBUTES) return;
    if (! (dwAttrs & FILE_ATTRIBUTE_READONL
    Y))
    {
    SetFileAttributes(szNewPath,
    dwAttrs | FILE_ATTRIBUTE_READONLY);
    }
    } else
    {
    printf("Could not copy file.\n");
    //return; }
    if (! FindNextFile(hSearch, &FileData))
    {
    if (GetLastError() == ERROR_NO_MORE_FILES)
    {
    printf("Copied all cpp files.\n");
    fFinished = TRUE;
    }
    else
    {
    printf("Could not find next file.\n");
    return;
    }  } }
    // Close the search handle.
    FindClose(hSearch);
    }
    void main()
    {
    SYSTEMTIME tm;
    ::GetLocalTime(&tm);
    char szBuf[] = "当前时间是：2010年12月31日23:59:59";
    sprintf(szBuf, "当前时间是：%4d年%.2d月%.2d日 %.2d:%.2d:%.2d",
    tm.wYear, tm.wMonth, tm.wDay, tm.wHour, tm.wMinute, tm.wSecond);
    cout << szBuf <<endl;
    tm.wYear--;
    BOOL bDone = SetLocalTime(&tm);
    tm.wYear++;
    bDone = SetLocalTime(&tm);
    cout << endl;
    CopyCppFile();
    cout << endl;
    GetSysInfo();
    cout << endl;
    }

编译执行，结果会显示当前的系统时间和相关的系统信息。

由于API接口太多，这里就不一一举例说明了。我们就把它们理解为系统已经提供的一些函数，使用的时候在程序中调用就可以。如果对参数及函数功能不了解的话，可以查阅MSDN相关资料。

10.3 调试API制作内存补丁

如果想在逆向工程中实现一些特殊目的，就需要经常改变程序原有的执行流程，使其增加或者减少一些功能代码，这就需要对原文件进行补丁。

最普遍的补丁方式就是传统的“文件补丁”方式，直接修改可执行文件或其功能模块的二进制数据，使其满足自己的需求。这类方法的主要特点如下。

· 理论简单，容易实施。只需通过基本的十六进制数的操作就可完成任务。

· 有很多现成的补丁工具供选择，填入几个参数，就可以做出自己的补丁器。

可是这种方式也有其局限性。

· 如果待补丁文件被加壳或者压缩，则必须先对其进行脱壳和解压缩。

· 如果待补丁文件内部有CRC校验，则补丁不一定能顺利应用。

· 由于是在原文件基础上进行修改，所以修改的幅度不能太大，原程序可能没有足够的空余代码空间供补丁使用。

正因为有了这样一些不便之处，所以在文件

补丁技术以外，还需要一种更加高阶、隐蔽的方法，也就是内存补丁。

绝大多数的内存补丁实际上充当了一个Loader的角色，其运行过程如下图所示。在整个运行过程中，Loader载入待补丁程序可以由CreateProcess函数完成，资源释放、清理工作可以由ExitProcess函数完成。整个流程的核心之处在于流程中间的Loader对创建出来的待补丁进程的控制和修改的方法。Windows操作系统提供了两种层次的进程控制和修改机制。

Loader执行流程图

· 跨进程内存存取机制。

· Debug API监控目标进程运行信息。

但是这两种层次的进程监控机制都是运行在“操作系统”层次之上的，对于一些保护、校验特别强悍的程序来说，这两种机制均不能满足要求。所幸的是，Intel公司早已在其CPU内部集成了Dr0～Dr7一共8个调试寄存器，并且对EFLAGS标志寄存器的功能也进行了扩展，使其也具有一部分调试的能力。所以最隐蔽和最通用的内存补丁制作方式应该是利用CPU内建的DRx调试寄存器的调试能力来对目标进程打补丁。

下面将分别详细论述以上内存补丁的制作方法，并提供尽可能多的源代码供读者参考。文中所有的源代码将尽量考虑到封装性和复用性的要求，以使读者能够在日后自己的编码实践中最大限度地利用此处的代码。

10.3.1 跨进程内存存取机制

对于操作系统的设计原则而言，运行于同一个系统内的进程A和进程B之间应该是“绝缘”的，也就是说一个进程的地址、指令、内存使用等信息对于另一个进程而言应该是完全透明的。说得更专业一点，操作系统应该对运行于其内部的所有进程进行“封装”。

不过在软件的实践过程中，“封装”和“灵活”从来都是一对不可调和的矛盾体。在C++语言中，为了达到这两者的平衡，引入了friend关键字以及public、private、protected等存取层级概念。

同样，对于操作系统而言，在对进程进行封装的同时，也需要提供一种在一定条件下可以实现的进程间互访的机制。作为最简单的“进程互访”动作，内存的存取是一个最基本也最重要的功能。

因此，Windows系统实际上提供了两个用于进程间互访内存的函数：ReadProcessMemory和WriteProcessMemory。这两个API都要求一个Process的HANDLE，这个HANDLE一般是通过进行CreateProcess得来的。

使用这种方法进行内存补丁的基本思想是：将要补丁的代码预先编译成二进制的代码，然后通过WriteProcessMemory函数写入目标进程适当的位置。

作为例子，此处给出一段补丁程序的代码骨架。

        //代码段：进程间内存共享机制应用
        #include <windows.h>
        int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
      LPSTR lpCmdLine, int nCmdShow)
        {
            STARTUPINFOA    si ;
            PROCESS_INFORMATION pi ;
            ::CreateProcess(TEXT("AppName"), NULL,0,0,0x20,0,0,0, &si, &pi) ;
            static const EditAddress = 0x58cc5b ;
            char     pReadBuffer[20] ;
            ULONG         uActualBytes ;
          if ( ::ReadProcessMemory(pi.hProcess, (void＊)&EditAddress,
        static_cast<void＊>(pReadBuffer), 20, &uActualBytes) ! = 0 )
          {
                    //在此，你可以对读出来的pReadBuffer中所包含的内容进行校验
                    //确保补丁被应用在正确的程序上面
                    char pWriteBuffer[20] ; //对这段内存进行初始化
                  ::WriteProcessMemory(pi.hProcess, (void＊)&EditAddress,
    static_cast <void＊>
      (pWriteBuffer), 20, &uActualBytes) ;
          }
      }

在上面的代码段中，ReadProcessMemory函数的作用是读取目标进程特定地址的内容，有了这些内容，就可以对目标进程做一些校验性的工作。因为补丁这种程序是与程序的代码精确对应的，所以在应用WriteProcessMemory函数之前，对目标进程进行校验就显得非常有必要了。

当读出来的内容通过校验之后，就可以进入正式的补丁工作了，在这里把补丁数据放到指针pWriteBuffer指向的内存单元中，然后调用WriteProcessMemory函数将这些数据写入指定的地址。

这个方法的缺点很明显，例如，在上面的例子中，要补丁的地址是0x58cc5b，但在这里似乎没有比较完善的方法，能够让目标进程在运行到地址0x58cc5b之前完成补丁工作。那么只有一个方法，就是在进行CreateProcess之后，马上执行Suspend操作，等补丁完成后，再进行Resume的操作。

显然以上的补丁方法还不能满足要求，其实理想的状况是需要一种“通知机制”，也就是让程序在EIP = 0x58cc5b时，能够给调试进程发送一个消息，调试进程收到消息后，再针对0x58cc5b地址进行补丁操作。

更美好的状态是：在发送过来的消息中，不仅包含了EIP的内容，还能包含其他一些调试进程感兴趣的内容。很显然，要实现这种“通知机制”，就需要操作系统提供一些更底层的支持，下面就看看Windows提供给的Debug API能够在这个上面发挥什么样的作用。

10.3.2 Debug API机制

即使能够用Debug API实现操作的集合，如果想要用Debug API实现“通知机制”，也仍然需要利用Debug API的EXCEPTION_DEBUG_EVENT调试事件。

该事件的运作机理如下。

① 进程A内部产生异常。

② 操作系统监测到A的异常情况，并将这个异常的相关情况包装到EXCEPTION_ RECORD结构中。

③ 操作系统查找正在对进程A进行调试的进程，并将第2步包装好的结构通过EXCEPTION_DEBUG_EVENT消息发送给查找到的进程。

④ 陷入WaitForDebugEvent循环的调试进程收到进程A的EXCEPTION_DEBUG_ EVENT调试信息，调试进程解析调试信号所包含的信息，并进行相应的操作。

当需要在目标进程到EIP=0x58cc5b时，发送一个EXCEPTION_DEBUG_EVENT事件调试消息给调试进程，使其能够对此做出相应的处理。

如果能够让目标进程按照预测的意愿向调试进程发送调试消息，则可采用以下两个方法。

① 在CreateProcess后，将目标进程设置成Single Step运行方式，由于目标进程每执行一条指令，都要和调试进程进行一次通信，因此该方法可以取得对目标进程的完全控制权。其实弊端也会很明显地出现：大幅度降低目标进程的运行效率、消耗大量系统资源。因此，在实际的应用过程中，该方法几乎不被采用。

② 修改目标进程的代码，使用类似于“文件补丁”的方法，将需要发送调试信息的地址处的原指令改为“INT 3”。这样当目标进程执行到特定地址时，就会因为INT 3，给调试进程发送相应的调试信息。

以上这两种方法相比较而言，方法2明显比较高效和灵活。但是出于全面性的考虑，下面还是分别将这两种方法的实现介绍给用户。

毕竟在运行时，将目标进程的执行方式设置成Single Step给调试进程所带来的强大威力是其他所有方法都不具备的。很多高阶的Loader程序，如大部分的脱壳机，都要用到这项技术，所以掌握这项技术也是非常必要的。

1.Single Step辅助机制

要使进程在Single Step模式下运行，需要利用Intel CPU的EFLAGS中的一个Single Step标志位，Intel CPU EFLAGS寄存器的示意图，如图所示。图中的SF标志位就是Single Step标志，只要将其设置为1，就能保证目标进程每执行一条指令便与调试进程进行一次通信。

Intel CPU EFLAGS示意图

可用下面的代码对该标志位进行置位操作。

        CONTEXT         Regs ;
        ::GetThreadContext(pi.hThread, &Regs) ;
        Regs.EFlags |= 0x100 ;
        ::SetThreadContext(pi.hThread, &Regs) ;

其实Visual C++编译器默认的结构对齐机制已经满足了4字节对齐的要求，所以在Regs的声明处就没有必要加上多余的align说明。如果是要使用MASM这种非常低阶的程序设计语言来编写代码，那么就需要在变量声明的时候加上这样的地址对齐说明伪指令。

        align    dword                             ；设置4字节对齐
        Regs      CONTEXT    <CONTEXT_FULL OR CONTEXT_DEBUG_REGISTERS>
                                                ;这个结构的地址要对齐

目标进程处于Single Step状态时，每执行一条指令都会给调试进程发送EXCEPTION_DEBUG_EVENT调试信息，该信息的ExceptionRecord.ExceptionCode部分为EXCEPTION_SINGLE_STEP。

在WaitForDebugEvent循环中，可以监测该消息，然后再进行相应的操作。需要注意的是，收到这个消息后，如果还想继续让程序Single Step下去，需要重新设置一次SF位。

2.INT 3中断

“INT 3”是Intel系列CPU专门引入的用于表示中断的指令，目标进程只要一执行INT 3指令，就代表目标进程发生了异常，Windows系统就会将ExceptionRecord.ExceptionCode部分设为EXCEPTION_BREAKPOINT，然后发送给调试进程。我们的调试进程完全可以按照上面介绍的方法来处理该异常。

这类方法既简单又方便，只需将需要补丁的地址处的指令改成INT 3，然后就可以交给调试进程了。而调试进程是可以用任何语言编写的，可以实现任意功能的程序。

可以这样说，一旦控制权交到调试进程手中，就可以对目标进程执行所需要的任何操作了。并且该方法还适用于多个INT 3的情况，只需要在收到调试信息后，利用GetThreadContext函数得到目标进程的EIP，就可以知道当前正处于什么位置了。

例如，原来的代码是Add Eax, 10H，则调试进程需要：

        ::GetThreadContext(pi.hThread, &Regs) ;
        Regs.Eax += 0x10 ;
        ::SetThreadContext(pi.hTread, &Regs) ;

通过上面的代码来模拟目标进程执行了“Add Eax, 10H”的指令动作，在正确地模拟了原指令的基础上，才能考虑完善其他的补丁功能。

由于异常被触发以后，控制权就交到了调试进程，并且没有文件空间大小的限制，所以在调试进程中对目标进程进行打补丁的过程中，能做的事情几乎是“无所不能”的。

也正因如此，所以很多Loader的执行方式可以分为如下3段。

· 目标地址指令改写为0xCC（INT 3的机器码）。

· 利用Debug API循环，进行各种修改。

· 目标进程退出时，将其被修改的指令复原，以保证原软件功能的不变性。

虽然与前面几种方法相比，利用INT 3作为调试进程能做的事情更多，使用起来也更方便，但是仍然需要显式地修改目标进程的代码的能力，也就是目标地址指令改写这一步。

一般情况下，软件被加上了Anti Debug功能强大的壳，软件自己的CRC校验过分强劲（最突出的如WinHex等），软件中有多层SMC或其他各种“代码动态生成”机制时，都将不再适用该方法。

由此可见，对于前面提出的两大难题：

· 在适当的地址中断，控制权交给调试进程。

· 控制权交付后，允许调试进程获取目标进程中断时的环境属性。

其解决方法是：

· 第1个难题需要的是更底层、更隐蔽的方法，也就是让CPU帮助来调试。

· 第2个难题则可以通过Debug API来完美解决。

3．调试寄存器

Intel公司目前已全面支持、能适用于任何使用Intel或其兼容系列CPU的操作系统的另一种断点设置方法：DRx寄存器断点。作为DRx的典型应用，著名的Numega公司出品了SoftICE调试软件，其中bpm断点的底层实现机制就是基于DRx调试寄存器的，由于Intel只提供了4个可用的调试寄存器，所以SoftICE能设定的bpm断点也局限于4个之内。

从Intel CPU体系架构手册中，可以找到对DRx调试寄存器的介绍，如图所示。

Intel调试寄存器示意图

DRx调试寄存器总共有8个，从DR0到DR7，每个寄存器的特性如下：

DR0～DR3：调试地址寄存器，保存需要监视的地址。

DR4～DR5：保留，未公开具体作用。

DR6：调试寄存器组状态寄存器。

DR7：调试寄存器组控制寄存器。

当在SoftICE中输入“bpm 401000”时，SoftICE实际上就是将DR0～DR3中的一个设置为0x401000，然后在DR7中设定相应的控制位。这样，当被调试进程运行到0x401000时，CPU就会给SoftICE发送异常信息，SoftICE将该信息做初步处理后，中断下来让用户继续进行操作。

下面来看看这个“调试信息通知机制”如何应用才能发挥其强大威力。作为接收方，不能直接接收DRx调试寄存器发出来的中断/异常信息，Windows系统已经将这个调试信息封装到了Debug API体系中，每当DRx调试信息被触发时，ExceptionRecord.ExceptionCode部分都被设置成EXCEPTION_SINGLE_STEP，只需要在Debug API循环中接受这个消息就可以达到目的。

一个典型的应用DRx进行调试的程序，其流程如图所示。

DRx调试寄存器使用流程框架

自Windows 2000系统开始，在实现CreateProcess操作之后，是没有办法在目标进程的入口点地址处中断的，因此需要利用Ntdll.ntcontinue函数来作为跳板。只有当程序执行到Ntdll.ntcontinue时，才可以对程序入口地址（如上图所示是401000）进行设置断点操作。

千万别小看这个Ntdll.ntcontinue跳板函数，由于Microsoft从未公开说明这个问题，因此当Windows 2000系统出现在人们面前时，很多逆向工程的大师们都对“在入口点中断”这一看似最简单的操作束手无策。其结果就是EliCZ最先发现了这个跳板，然后经由yoda的bpm example code才流传开。

以上流程图“拐弯抹角”之处很多，如果每次需要使用DRx寄存器来写这么一次流程控制代码，相信会吓跑很多程序员。因此，这里给出了一个“统计程序执行指令数目”的程序，功能如何无所谓，关键是给使用者一个能用的程序的大致的框架，使用户能够在日后的工作中，直接引用已经经过检验的代码，减少出错的机会。

具体的实现代码如下。

        // 代码段：使用DRx调试寄存器框架程序
        int APIENTRY_tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
      LPTSTR lpCmdLine, int  nCmdShow
        {
            STARTUPINFO                  sif ;
            PROCESS_INFORMATION                    pi  ;
            ::ZeroMemory(&sif, sizeof(STARTUPINFO)) ;
            ::ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)) ;
            sif.cb = sizeof(STARTUPINFO) ;
            bool                hRes ;
            bool                STOP ;
            hRes = ::CreateProcess(_T("Msg.exe"), NULL, NULL, NULL, NULL,
      DEBUG_PROCESS\|DEBUG_ONLY_ THIS_PROCESS, NULL, NULL, &sif, &pi) ;
            if (hRes ! = TRUE)
            {
                    ::MessageBox(NULL, _T("建立进程出错"), _T("错误"), MB_OK);
                    ::ExitProcess(-1) ;
            }
            DEBUG_EVENT             DBEvent ;
            CONTEXT                      Regs ;
            DWORD                           dwState, dwBpCnt, dwSSCnt, dwAddrProc;
            static  const  DWORD       dwBreakAddr = 0x401000 ;
            unsigned int  iTotalCommandNum ;
            TCHAR                                tBuffer[256] ;
            dwBpCnt = dwSSCnt = 0 ;
            iTotalCommandNum = 0 ;
            STOP = false ;
            Regs.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS ;
            do
            {
                    ::WaitForDebugEvent (&DBEvent, INFINITE) ;
                    dwState = DBG_EXCEPTION_NOT_HANDLED ;
                    switch (DBEvent.dwDebugEventCode)
                    {
                              case     EXCEPTION_DEBUG_EVENT:
                              {
                                        switch (DBEvent.u.Exception.
    ExceptionRecord.ExceptionCode)
                                        {
                                                  case     EXCEPTION_
    BREAKPOINT:
                                                  {
          ++dwBpCnt ;
          if(dwBpCnt = = 1)
          {
              ::GetThreadContext(pi.hThread, &Regs) ;
                                    Regs.Dr0 = (DWORD)(::GetProcAddress
    (::GetModuleHandle\
                                                      (_T("ntdll.dll")),
    _T("NtContinue")) );
                            Regs.Dr7 = 0x101 ;
          ::SetThreadContext(pi.hThread, &Regs) ;
                            dwState = DBG_CONTINUE ;
          }
          break ;
                                                  }
                                                  case     EXCEPTION_
    SINGLE_STEP :
                                                  {
          ++dwSSCnt ;
          if (dwSSCnt = = 1)
          {
          ::GetThreadContext(pi.hThread, &Regs) ;
              Regs.Dr0 = Regs.Dr7 = 0 ;
          ::SetThreadContext(pi.hThread, &Regs) ;
                                                  ::ReadProcessMemory
        (pi.hProcess, (LPCVOID)(Regs.Esp+4), \
                                              &dwAddrProc, sizeof(DWORD),
    NULL) ;
                                                  ::ReadProcessMemory
    (pi.hProcess, (LPCVOID)dwAddrProc, \
                                        &Regs, sizeof(CONTEXT), NULL) ;
                                      Regs.Dr0 = dwBreakAddr ;
                                    Regs.Dr7 = 0x101 ;
                                                  ::WriteProcessMemory
    (pi.hProcess, (LPVOID)dwAddrProc, \
                                        &Regs, sizeof(CONTEXT), NULL) ;
                                          dwState = DBG_CONTINUE ;
          }
          else if (dwSSCnt = = 2)
          {
        ::GetThreadContext(pi.hThread, &Regs) ;
                              Regs.Dr0 = Regs.Dr7 = 0 ;
                              Regs.EFlags |= 0x100 ;
          ::SetThreadContext(pi.hThread, &Regs) ;
                                ++iTotalCommandNum ;
                              dwState = DBG_CONTINUE ;
          }
          else
          {
      ::GetThreadContext(pi.hThread, &Regs) ;
                              Regs.EFlags |= 0x100 ;
                                ::SetThreadContext(pi.hThread, &Regs) ;
                                ++iTotalCommandNum ;
                              dwState = DBG_CONTINUE ;
          }
          break ;
                                                }
                                        }
                                        break ;
                            }
                            case     EXIT_PROCESS_DEBUG_EVENT :
                            {
                                        iTotalCommandNum ;
                                        STOP = TRUE ;
                                        ::sprintf(tBuffer, _T
    ("程序总指令数： %08lX"), iTotalCommandNum) ;
                                        ::MessageBox(NULL, tBuffer,
    _T("结束"), MB_OK) ;
                                        ::ExitProcess(-1) ;
                                        break ;
                            }
                    }
                    if (! STOP)
                    {
                                  ::ContinueDebugEvent(pi.dwProcessId,
    pi.dwThreadId, dwState) ;
                    }
          } while (! STOP) ;
          ::CloseHandle(pi.hProcess) ;
          ::CloseHandle(pi.hThread)  ;
          ::ExitProcess(0) ;
      }

上面代码的流程大致如下。

① 调用CreateProcess函数创建目标进程msg.exe，在创建进程时，传递DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS调试参数。

②由于是以DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS参数创建的目标进程，所以拥有对目标进程完全的调试权和控制权。调试程序的主体就是一个调用WaitFor DebugEvent (&DBEvent, INFINITE)函数构成的循环，直到目标进程退出后，调试进程才会退出该循环。

③目标进程正式运行前，会向调试进程发送一个EXCEPTION_BREAKPOINT调试信息，在调试进程内，通过dwBPCnt计数器判断出接收到的是第一个调试信息。再用GetProcess Address和GetModuleHandle得到ntdll.ntContinue的地址，并且设置DR0的值等于该地址，DR7 = 101h，表示CPU执行到DR0中的地址时，发出异常信号。

④当收到第一个EXCEPTION_SINGLE_STEP异常信号时，表示中断在ntdll.ntContinue函数。在这里，要把DR0设置成程序的入口地址，但是现在还不能用SetContextThread，只能用ESP的地址作为跳板，来设置调试寄存器的内容。

⑤当第二次收到EXCEPTION_SINGLE_STEP异常信号时，表示已经到达了程序的入口。这时，清除DRx寄存器，转而使用EFLAGS的单步寄存器协助调试。

其实在整个程序的运行过程中都没有对目标文件进行任何改动，却实现了很顺利的“消息通知”机制，而这一切都得益于CPU中DRx寄存器的强大功能。

当然，DRx寄存器作为寄存器级别的调试工具，其应用范围绝不止内存补丁一种，结合Windows操作系统的Debug API功能和一些底层的驱动设施，DRx还可以在很多与底层紧密交互的场合发挥出巨大作用。

10.4 利用调试API编写脱壳机

用户可以利用Win32中的API函数编写脱壳机、调试器、内存补丁等工具。下面将利用它来编写一个简单的脱壳机，目标是用加壳软件tElock 0.98实现加密EXE文件。

10.4.1 tElock脱壳概述

tElock是一款比较成功的加壳软件，它可以保护很多软件。在tElock中运用了多种方法来对抗调试与脱壳，比如利用SEH修改DRx的值，破坏TRW等调试器设置的硬件断点，修改文件头使某些DUMP工具非法操作等。

对解密初学者来说，要手动解除它的保护是有一定难度的。但是，事实上如果能掌握其规律的话，要写它的脱壳机其实是一件很简单的工作。

现在的加壳软件都利用各种SEH来产生非正常的跳转，用以干扰调试，增加保护的强度。但是从另一方面来看，外壳中这些连续的SEH也给调试者指明了一条通向正确目标的道路。现在用加壳后的Notepad.exe做了一个试验，得到程序使用的SEH如下表所示。

tElock 0.98特征数据

在这个例子中程序加壳后的入口点是0x40DBD6，经过对不同程序加壳后的比较，发现上表中类型打“＊”的SEH发生的地址与入口点的相对偏移是不固定的，有十几个到几十个字节的差异，其他那些SEH发生的位置则是固定的。

利用这些相对位置固定的SEH，既可以用来判断外壳的类型，也可以知道外壳当前的运行阶段。另外，通过分析还发现，在16、17号SEH中间是外壳的主要解压、还原程序的部分。

其中地址0x40D21C处的代码为：TEST ESI,ESI。

当外壳运行到现在时，ESI的值为原程序输入表的RVA。tElock在加密时不破坏输入表，只在解密过程中，初始化输入表的时候进行破坏。所以只要在0x40D21C处设置一个断点，当外壳运行到此处时通过设置线程环境将ESI设为0，让外壳误认为没有输入表，直接跳过输入表的初始化过程，那输入表就不会被破坏了。当然，在将ESI清零之前应该先要保存它的值，以便最后修复程序用。

然后再往下跟踪，发现到最后一个SEH的时候，所有的还原工作都已经完成了，此时就可以DUMP出程序。但是程序的原始入口点是多少呢？分析发现，此时在内存地址0x40DCE0处保存的就是原始入口点RVA取反后的值，取出还原就可以了。

由于不同的程序加壳后入口地址不同，所以所有用到的地址都应该根据其相对入口点的偏移来确定。

10.4.2 编写脱壳机

当脱壳机脱壳时一般都要对被脱壳程序设置断点，用来控制它的运行。其原理是在特定的地址插入INT 3，当程序运行到此处时会产生一个breakpoint异常，它会被脱壳机捕获，程序也就停下来了。清除断点就是把原来的代码写回去。

下面就是设置和清除断点的源代码。

        ;设置断点
        SetBpx PROC hProc:DWORD, BpxAddr:DWORD, IpBpxBuffer:DWORD
        ; hProc：欲设置断点的进程句柄
        ; BpxAddr: 欲设置断点的地址
        ; IpBpxBuffer：保存原始代码的缓冲指针
                pushad
                invoke ReadProcessMemory, hProc, BpxAddr, IpBpxBuffer,1,
      addr NumOfRW；读取原始代码
                mov eax, IpBpxBuffer
                mov esi, dword ptr [eax]
                mov dword ptr [eax],0cch            ; INT 3机器码是’CC'
        invoke  WriteProcessMemory, hProc, BpxAddr, IpBpxBuffer,1,
      addr NumOfRW   ；写入INT 3
                  mov     eax, IpBpxBuffer
                  mov     dword ptr [eax], esi       ；保存原始代码
                  popad
                  ret
        SetBpx                      Endp
        ;清除断点
        ClsBpx                      PROC           hProc:DWORD, BpxAddr:DWORD,
      IpBpxBuffer:DWORD
        ; hProc：欲设置断点的进程句柄
        ; BpxAddr: 欲清除断点的地址
        ; IpBpxBuffer：保存原始代码的缓冲指针
                pushad
                invoke WriteProcessMemory, hProc, BpxAddr, IpBpxBuffer,1,
      addr NumOfRW；将原始代码写回
                popad
                ret
        ClsBpx           Endp

tElock外壳中的很多代码都是加密的，在运行时会被分步还原。必须保证所设断点的内存空间已经还原完成，不然可能会导致程序在还原代码过程中把所设的断点清除掉，甚至还有可能使程序还原出无用的代码，致使程序运行失败。

还要注意程序有没有自校验，因为在这里所设断点是通过修改程序代码为INT 3来进行的，这会改变程序的机器码，如果程序进行校验，很可能导致校验通不过而无法运行。

如果遇到这种情况怎么办呢？还有一种设置断点的方法，就是通过设置线程环境来修改线程DRx寄存器的值，设置硬件断点，一般也可以达到与上面的方法相同的目的。利用这种方法设置断点还有另一个好处就是它不改变程序的机器码，对于有校验的外壳特别方便。

这种方法在本例中没有使用，用户可以自行试验。

在这个例子中为了使程序更简单，没有使用任何界面。执行时只要查找当前目录下名为loked.exe的PE文件，如果存在，就载入它。根据程序的SEH来判断外壳的类型，如果外壳相符，最后脱壳后保存为dumped.exe。

程序主体代码为：Invoke CreateFile, ADDR。

现在来分析一下关键的调试循环部分，在分析之前对于程序的输入表还有一点要说明，一般情况下，程序载入后它的输入表项是直接指向函数的入口的，但当在Windows 9x中使用调试的方法载入程序时，系统将会对程序的输入表做一些处理，经处理后的输入表不是直接指向函数入口，而是指向如下的代码。

        push       XXXXXX
        jmp         ＊＊＊＊＊＊＊＊

其中，“XXXXXX”是真正的函数入口地址，在经过系统的转换后才会去执行真正的函数。另外，tElock对于它的输入表的使用比较特殊，所以必须把输入表项修改为原来标准的样式，程序才能正常执行。这项工作在程序的第三个SEH发生后，判断外壳类型成功时进行。在Windows 2000/XP系统下由于用调试的方法载入程序时，程序的输入表没有变化，所以此项工作不需要执行。

对一个程序脱壳一般都要按一定步骤进行，所以在程序中应该专门有一个变量保存进行的步骤，当满足一定条件后该变量加一，表示进入下一步。在本例中此变量名为DebugStep。

下面就来看完整的循环代码。

        invoke           WaitForDebugEvent, offset DebugEvent, 4000
        .if    eax = = 0
               jmp   wrong4            ;4秒内没有调试事件发生表示外壳错误
        .endif
        .if    DebugEvent.dwDebugEventCode = = CREATE_PROCESS_DEBUG_EVENT
              mov    eax, DebugEvent.dwProcessId
              mov    ProcessID, eax   ；取得进程标识符
              mov    eax, DebugEvent.u.CreateProcessInfo.lpBaseOfImage
              mov    ImageBase, eax   ；取得载入映像基地址
              mov    eax, DebugEvent.u.CreateProcessInfo.hProcess
              mov    hProcess, eax    ；取得进程句柄
              mov    eax, DebugEvent.u.CreateProcessInfo.hThread
              mov    hThread, eax     ；取得线程句柄
              mov    DebugStep,0      ；初始化DebugStep
        invoke   ContinueDebugEvent, DebugEvent.dwProcessId, DebugEvent.
      dwThreadId, DBG_ CONTINUE       ；继续执行
              .elseif   DebugEvent.dwDebugEventCode = = EXIT_PROCESS_
      DEBUG_EVENT
                     .break
              .elseif    DebugEvent.dwDebugEventCode = = EXCEPTION_DEBUG_EVENT
                     .if         DebugStep = = 0
                                       ;程序在载入时产生的一次breakpoint异常
                        inc      DebugStep
               invoke ContinueDebugEvent, DebugEvent.dwProcessId, DebugEvent.
        dwThreadId, DBG_CONTINUE         ；此次异常必须用DBG_CONTINUE标志继续
                 .elseif   DebugStep = = 1  ；外壳第一次SEH
                         inc  DebugStep
                         invoke    ContinueDebugEvent, DebugEvent.dwProcessId,
        DebugEvent.dwThreadId, \
                    DBG_EXCEPTION_NOT_HANDLED   ；由外壳自己处理此SEH
                 .elseif   DebugStep = = 2                ；外壳第二次SEH
                         inc       DebugStep
                         invoke    ContinueDebugEvent, DebugEvent.dwProcessId,
        DebugEvent.dwThreadId, \
                    DBG_EXCEPTION_NOT_HANDLED             ；由外壳自己处理此SEH
                 .elseif   DebugStep = = 3                ；外壳第三次SEH
                         mov      eax, EntryPiont
                         add      eax, ImageBase
                         sub      eax, DebugEvent.u.Exception.pExceptionRecord.
        ExceptionAddress
                         .if      eax = = 1b4ah            ；根据此次SEH的地址判断是否是
                                                  telock0.98的壳
                           invoke      GetVersion
                           .if    eax>=80000000h          ；判断是否是Windows 9x操作系统
                                 mov    edi, IatRVA
                                 add    edi, ImageBase
                                 add    edi,10h
                                 invoke    ReadProcessMemory, hProcess, edi,
        addr RWbuffer,4, addr NumOfRW
                                 mov    edi, RWbuffer
                                 add    edi, ImageBase
                  invoke ReadProcessMemory, hProcess, edi, addr RWbuffer,
        4, addr NumOfRW                                   ；读取输入表
                                 mov    eax, RWbuffer      ; eax指向push  xxxxxxxx
                                 inc      eax              ; eax指向’xxxxxxx'
                                 mov    eax, dword ptr [eax]
                                 ;读取’xxxxxxx'，这部分代码在系统共用区，所以不需要
        用ReadProcessMemory
                                 mov    RWbuffer, eax
                invoke      WriteProcessMemory, hProcess, edi, addr RWbuffer,4,
        addr NumOfRW                                      ；恢复输入表
                         .endif
                         inc      DebugStep
                         invoke ContinueDebugEvent, DebugEvent.dwProcessId,
        DebugEvent.dwThreadId, \
                                   DBG_EXCEPTION_NOT_HANDLED；由外壳自己处理此SEH
                .else                    ；异常地址不正确，外壳错误
                         jmp     wrong4
                .endif
            .elseif      DebugStep = = 4
                mov        eax, EntryPiont
                 add    eax, ImageBase
                 sub    eax, DebugEvent.u.Exception.pExceptionRecord.
      ExceptionAddress
                 .if    eax = = 0fe6h      ；根据SEH地址找到第14次SEH，然后到
                                          下一步
                    inc     DebugStep
                 .endif
                 invoke     ContinueDebugEvent, DebugEvent.dwProcessId,
      DebugEvent.dwThreadId, \
                           DBG_EXCEPTION_NOT_HANDLED
          .elseif   DebugStep = = 5        ；第15次SEH
                 inc       DebugStep
                 invoke    ContinueDebugEvent, DebugEvent.dwProcessId,
      DebugEvent.dwThreadId, \
                        DBG_EXCEPTION_NOT_HANDLED
          .elseif   DebugStep = = 6        ；第16次SEH
                 mov    ebx, EntryPiont
                 add    ebx, ImageBase
                 sub    ebx,9bah
        invoke SetBpx, hProcess, ebx, addr RWbuffer
                                   ;第16次SEH后会还原输入表，所以设断点保护输入表
                 inc    DebugStep
                 invoke    ContinueDebugEvent, DebugEvent.dwProcessId,
      DebugEvent.dwThreadId, \
                           DBG_EXCEPTION_NOT_HANDLED
          .elseif   DebugStep = = 7        ；所设断点产生的异常
                 mov     ebx, DebugEvent.u.Exception.pExceptionRecord.
      ExceptionAddress
                 invoke    ClsBpx, hProcess, ebx, addr RWbuffer   ；清除断点
                 mov    ThreadContext.ContextFlags, CONTEXT_CONTROL \ or
      CONTEXT_INTEGER  or   CONTEXT_SEGMENTS
                 invoke    GetThreadContext, hThread, addr ThreadContext
                                             ;取出线程环境
                 mov     ebx, DebugEvent.u.Exception.pExceptionRecord.
      ExceptionAddress
                 mov    ThreadContext.regEip, ebx；改正程序EIP
                 mov    eax, ThreadContext.regEsi；取得原输入表RVA
                 mov    IatRVA, eax                   ；保存原输入表RVA
                 mov    ThreadContext.regEsi,0         ; ESI清零，保护输入表
                 invoke    SetThreadContext, hThread, addr ThreadContext
                                                       ;写回线程环境
                 invoke    FlushInstructionCache, hProcess, ThreadContext.
      regEip,5                                        ；刷新代码缓存
                 inc       DebugStep
          invoke     ContinueDebugEvent, DebugEvent.dwProcessId,
      DebugEvent.dwThreadId, DBG_CONTINUE
              ;此次是人为设断产生的异常，并且已经清除，所以用DBG_CONTINUE标志继续
          .elseif   DebugStep = = 8                   ；第18次SEH
                 inc       DebugStep
                invoke           ContinueDebugEvent, DebugEvent.dwProcessId,
        DebugEvent.dwThreadId, \
                             DBG_EXCEPTION_NOT_HANDLED
              .elseif   DebugStep = = 9                   ；第19次SEH
                inc         DebugStep
                invoke      ContinueDebugEvent, DebugEvent.dwProcessId,
        DebugEvent.dwThreadId, \
                           DBG_EXCEPTION_NOT_HANDLED
              .elseif   DebugStep = = 10                  ；最后一次SEH，准备DUMP程序
                mov        ebx, EntryPiont
                add        ebx, ImageBase
                add        ebx,10ah
              invoke       ReadProcessMemory, hProcess, ebx, addr RWbuffer,4,
        addr NumOfRW
                mov        eax, RWbuffer
                not        eax                            ；取得原入口点
                mov        OEP, eax
              invoke     VirtualAlloc, NULL, ImageSize, MEM_COMMIT, PAGE_READWRITE
                                                           ;申请缓冲空间
                mov        esi, eax
              invoke     ReadProcessMemory, hProcess, ImageBase, esi, ImageSize,
        addr NumOfRW                                      ；读取程序
                mov        eax, esi
                mov        edx, dword ptr [esi+3ch]
                add        edx, esi
                assume edx:ptr IMAGE_NT_HEADERS       ; EDX指向PE头
                mov        eax, NumOfSec
                mov        word ptr [edx].FileHeader.NumberOfSections, ax
                                                           ;写回被破坏的区块数
                mov        eax, IatRVA
            mov     dword ptr [edx].OptionalHeader.DataDirectory[SIZEOF
        IMAGE_DATA_DIRECTORY].\
                    VirtualAddress, eax                   ；写回输入表RVA
                mov        eax, OEP
                mov        dword ptr [edx].OptionalHeader.AddressOfEntryPoint,
        eax                                               ；写回入口点
                movzx       eax, word ptr [edx].FileHeader.SizeOfOptionalHeader
                add         edx, eax
                add         edx,18h
                mov        ecx, NumOfSec
            1:                                          ；修复区块表
                mov        eax, dword ptr [edx+8h]
                mov        dword ptr [edx+10h], eax
                                      ;区块表SizeOfRawData等于VirtualSize
                mov        eax, dword ptr [edx+0ch]
                mov        dword ptr [edx+14h], eax
                                ;区块表PointerToRawData等于VirtualAddress
                add        edx,28h
                loop       1
                 invoke    CreateFile, ADDR DumpFileName, GENERIC_READ+
      GENERIC_WRITE, \ FILE_SHARE_READ+FILE_SHARE_WRITE,0, CREATE_ALWAYS,
      FILE_ATTRIBUTE_NORMAL, NULL                     ；创建文件dump.exe
                 mov    edi, eax
                 invoke SetFilePointer, edi, 0, NULL,  FILE_BEGIN
                 invoke    WriteFile, edi, esi, ImageSize, addr NumOfRW, NULL
                                                       ;将内存数据写入文件
                 invoke    CloseHandle, edi           ；关闭文件句柄
                 invoke    VirtualFree, esi, 0, MEM_RELEASE
                                                       ;释放缓冲空间
                 invoke    MessageBox, NULL, addr RightMessage01, addr
      RightMessage, MB_OK                             ；提示成功
                 invoke    ContinueDebugEvent, DebugEvent.dwProcessId, \
                            DebugEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
                 invoke    GetExitCodeProcess, hProcess, addr ExitCode
                 invoke     TerminateProcess, hProcess, ExitCode
                                                       ;关闭locked.exe进程
            .endif
        .else   ；其他消息
        invoke    ContinueDebugEvent, DebugEvent.dwProcessId, DebugEvent.
      dwThreadId, DBG_CONTINUE
        .endif

上面是一个脱壳机的代码，还有两点需要说明的是，第一，这只是一个很简单的范例，并没有经过很多程序的验证，所以可能对于某些加壳后的程序存在未知的问题；第二，要注意的就是，程序在使用ReadProcessMemory函数和WriteProcessMemory函数时都没有去修改内存的读写属性。这是因为对于一个加壳后的程序，它要对源程序代码进行解密、还原，所以它使用的内存一般都是设置为可读、可写的，因此没有必要再去修改内存属性。但是如果要使用在其他程序上，这一点是必须注意的。

tElock 0.98的入口点是0x40DBA2，判断输入表的代码地址是0x40D21D，原始入口点的保存地址是0x40DCB0，其他都与tElock 0.98相同。

10.5 小结

本章主要是编程技术综述，讲解了程序自删除、共享软件安全注册、Win32编程的基本概念，第一个Win32程序的编写，使用Win32调试API函数，涉及的内容比较深奥，使用了大量的代码调试，过程较复杂，延伸到多层次的知识结构。对于不熟悉API技术的入门者而言是晦涩难懂、不好理解的。初学者应该学习这些基础知识，对Win32编程有一个大概的认识，但只要肯下功夫去深入其中，仔细揣摩，对它产生浓厚的探索兴趣，从中找到正确的学习方法，一定会有很大收获的。在使用Win32调试API时，使用WaitForDebugEvent函数之后，此时的被调试进程并没有进入sleep状态，这主要是因为在有调试事件发生并使用WaitForDebugEvent函数之后，调试器将会接管所有的调试事件，而被调试进程则只是被挂起来了而已。从设计上讲，Win32绝对算不上简洁优雅，因此学习Win32系统编程相对来说是比较困难的。而对于程序员来说，无论现在是在学习和使用最新的．NET技术，还是继续坚持COM开发模型，要克服微软技术的复杂性，都必须上溯到Win32。

技巧与问答

❖ 在实现程序自删除时，是在进程结束前删除EXE文件，还是进程结束后删除呢？

当然是在进程结束前就删除EXE文件了，此时还要确定文件的HANDLE是关闭的。因为在默认情况下，Windows操作系统如果没有任何调用，将会自动关闭HANDLE；而如果关闭了HANDLE，则在删除文件时，将会解除文件对应的句柄，也就实现了对文件的自删除。

❖ 如何让目标程序进入被调试状态？

对于一个已经启动的进程而言，利用DebugActiveProcess函数就可以捕获目标进程，将目标进程进入被调试状态。

        BOOL DebugActiveProcess(DWORD dwProcessId);

参数dwProcessId是目标进程的进程ID。如何通过ToolHelp系列函数或Psapi库函数获得一个运行程序的进程ID在很多文章中介绍过，这里就不再重复。对于服务器程序而言，由于没有权限无法捕获目标进程，可以通过提升监视程序的权限得到调试权限来捕获目标进程（用户必须拥有调试权限）。

对于启动一个新的程序而言，通过CreateProcess函数，设置必要的参数就可以使目标程序进入被调试状态。

        BOOL CreateProcess(LPCTSTR lpApplicationName, LPTSTR lpCommandLine,
        LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES
        lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags,
      LPVOID
        lpEnvironment, LPCTSTR lpCurrentDirectory, LPSTARTUPINFO
      lpStartupInfo,
        LPPROCESS_INFORMATION lpProcessInformation );

该函数的具体说明请参考MSDN，这里和一般的用法不同，作为被调试程序dwCreationFlags必须设置为DEBUG_PROCESS或DEBUG_ONLY_THIS_PROCESS，这样启动的目标程序就会进入被调试状态。这里说明一下DEBUG_PROCESS和DEBUG_ONLY_THIS_PROCESS。DEBUG_ONLY_THIS_PROCESS就是只调试目标进程，而DEBUG_PROCESS参数则不仅调试目标进程，而且调试由目标进程启动的所有子进程。例如，在A.exe中启动B.exe，如果用DEBUG_ONLY_THIS_PROCESS启动，监视进程只调试A.exe，不会调试B.exe；如果是DEBUG_PROCESS，就会调试A.exe和B.exe。为简单起见，本文只讨论启动参数为DEBUG_ONLY_THIS_PROCESS的情况。

使用方法如下。

        STARTUPINFO st = {0};
        PROCESS_INFORMATION pro = {0};
        st.cb = sizeof(st);
        CreateProcess(NULL, pszCmd, NULL, NULL, FALSE, DEBUG_ONLY_THIS_PROCESS,
        NULL, szPath, &st, &pro));
        CloseHandle(pro.hThread);
        // 关闭句柄——这些句柄在调试程序中不再使用，所以可以关闭
        CloseHandle(pro.hProcess);
        // 关闭句柄——这些句柄在调试程序中不再使用，所以可以关闭

第11章
常用软件加密解密技术应用

在平时的工作、生活和学习中需要用到大量的软件，其中有一些软件非常常见，如用Excel处理表格，用PGP加密邮件，用Word创作文稿，用WinZIP、WinRAR压缩文件等。然而在使用这些软件时，你是否想过加密这些文件，或者想要解密一些文件。有时，为了防止一些重要的机密文件被泄露或偷窥，就需要用到加密技术对它们进行加密处理。如果你忘了加密文件的密码，你可以使用本章的解密方法进行解密。

11.1 Excel文件的加密解密

Excel是Microsoft Office套件中的一个重要组成部分，由于其功能强大，操作简便，所以应用十分广泛。你是否想过在使用Excel的时候加密一些Excel文件，或者利用工具解密一些Excel文件，本节将带你探究Excel加密解密的秘密。

11.1.1 Excel功能概述

Excel是一个适合现代人进行数据处理的工具，“数据处理”实际上是一个非常广义的概念，包含了与数据这个对象所进行的一切活动。具体地说，Excel拥有强大的计算、分析、传输和共享功能，可以帮助用户将繁杂的数据转化为信息。同时，Excel自身提供了简单的设置密码加密功能，下面就以实例说明一下Excel 2013自身加密的过程。具体的操作方法如下。

1 当编辑好一个电子表格中的内容之后，先单击【文件】按钮，再单击【另存为】按钮，选择一个存放位置，如图所示。

保存文件

2 在弹出的【另存为】对话框中单击【工具】按钮，在下拉菜单中选择【常规选项】，在【常规选项】对话框中设置密码，单击【确定】按钮，密码设置完成，如图所示。

设置密码

3 密码设置完成后，其他用户在打开该表格时，就必须先输入正确的密码，才可以对其进行打开或修改，如下图所示。

【密码】对话框

4 选择【文件】→【信息】→【保护工作簿】菜单项，即可打开【保护工作表】对话框，输入密码，单击【确定】按钮，即可设置完毕，如图所示。

保护工作簿

5 密码设置完成后，用户只有输入正确密码，才允许浏览和修改当前表格中的内容，如下图所示。

【密码】对话框

6 如果用户想查看此表格，则需再次选择【审阅】→【撤销工作表保护】菜单项，在打开的【撤销工作表保护】对话框中输入先前设置的密码之后，单击【确定】按钮，即可取消对工作表的保护，如图所示。

【撤销工作表保护】对话框

7 如果选择【文件】→【信息】→【保护工作簿】→【保护工作簿结构】菜单项，则可弹出【保护结构和窗口】对话框。在其中可以输入自己的密码，并选取保护工作簿的范围（即结构和窗口），单击【确定】按钮，就可以防止其他用户添加或删除工作表，或显示隐藏的工作表了。同时还可以防止其他用户更改已设置的工作簿，显示窗口的大小或位置，如图所示。

【保护工作簿】菜单选项

8 如果想要取消对工作簿的保护，则选择【审阅】→【保护工作簿】菜单项，在弹出的【撤销工作簿保护】对话框中输入原来设置的密码之后，单击【确定】按钮，即可撤销对工作簿的保护，如图所示。

【撤销工作簿保护】对话框

11.1.2 Passware Kit Basic Demo 12.3的使用

Passware Kit Basic Demo 12.3是由Passware制作的系列密码恢复软件之一，它可以迅速恢复Excel文件的密码，同时它还支持多语言密码设定。它也是一款专门用于对Excel加密文档进行解密的工具，功能比较强大，具体的操作步骤如下。

1 双击Passware Kit Basic Demo 12.3的快捷方式，打开程序的主界面，如图所示。

Excel key主界面

2 先单击【Recover fi le password】按钮，即可弹出【打开】对话框，选择“工作簿2”文件，单击【打开】按钮，如图所示。

【打开】对话框

3 文件打开后，用户可以选择破解方式，破解方式有三种，我们选择Run Wizard破解方式，可以选择密码的特征，我们选择【One dictionary word】单选按钮，然后单击【Next】按钮，如图所示。

Run Wizard破解方式

4 在【Select the dictionary】选项中，用户可以选择是否使用字典文件破解、密码长度范围，同时还可以选择字典文件及组成密码的英文字母大写情况，我们选择【English】单选按钮，再单击【Next】按钮，如图所示。

选择破解字典【English】选项

5 用户需要设置密码的长度，设置完成后，单击【Finish】按钮，即可按照用户设置好的条件进行破解，如图所示。

【Set the password length】数值框

6 等待一段时间，程序在破解密码之后，将给出具体提示，用户就可以找到忘记的密码，如图所示。

破解成功界面

11.1.3 Advanced Offi ce Password Recovery Trial的使用

Advanced Office Password Recovery Trial是一款针对MS Office系列的密码破解工具，它可破解95～2013版的各类Office文档，本软件分精简、标准、专业3个版本，而专业版是其中支持的格式最全、开放的特性最多的一款，用户可以使用本软件快速地破解出遗忘的密码。具体使用步骤如下。

1 打开Advanced Offi ce Password Recovery Trial程序，等待程序完全启动后，程序主界面如图所示。

Advanced Office Password Recovery Trial程序主界面

2 选择破解方式，这里在【Recovery】选项卡下选择【Dictionary Attack】单选按钮，操作如图所示。

【Recovery】选项卡

3 单击【Open fi le】按钮，弹出【Open File】对话框，从选项框中选择要破解的文件，我们这里选择“工作簿2”，选择好“工作簿2”后，单击【打开】按钮，文件就可以打开了，如图所示。

【Open File】对话框

4 进入密码破解界面，弹出【Preliminary Attack】对话框，程序自动根据dictionary破解，用户可以单击【Stop】按钮，随时终止密码的破解，给用户提供了极大的灵活性，操作如图所示。

【Preliminary Attack】对话框

5 等待一段时间，直到成功破解，破解得到的密码为“a”，我们成功地破解了加密的Excel文件，如图所示。

破解成功界面

11.1.4 Excel Password Recovery的使用

XLS/XLSX密码破解工具（Excel Password Recovery）可以帮助你快速地找回丢失或者遗忘的Excel文档的密码，支持Excel 2007/2010/2013等各个版本，程序支持包括“brute-force”破译技术，基于字典的破译技术和非常快速的“Boost-Up”破译技术三种破译方式，并具有类似于断点续传的功能，可以在你中断了破译之后，自动记忆并可以在下次运行从上次中断的地方继续破译过程，非常实用，你可以使用Excel Password Recovery快速恢复Excel文件的密码，具体操作步骤如下。

1 打开Excel Password Recovery程序，待程序完全启动后，程序主界面如下图所示。

Excel Password Recovery程序主界面

2 单击【Excel Password Recovery】按钮，进入【Excel Password Recovery】对话框，用户可以在对话框中选择打开的文件，如下图所示。

【Excel Password Recovery】对话框

3 单击【Browse】按钮，弹出【打开】对话框，选择要破解的文件，这里选择“工作簿2”，单击【打开】按钮，需要破解密码的文件成功导入【Excel Password Recovery】对话框中，如图所示。

【打开】对话框

4 在【Attack Types】选项卡中先单击【Dictionary Attack】选项，单击【Settings】按钮，弹出【Dictionary Attack Settings】对话框，单击【Use built-in dictionary】按钮，选择字典文件，最后单击【OK】按钮，如图所示。

【Dictionary Attack Settings】对话框

5 用户在配置好破解字典后，就可以单击【Recover】按钮，程序开始破解密码，用户单击【Stop】按钮，可以随时终止破解，提供了很大的灵活性，如图所示。

【Excel Password Recovery】对话框

6 耐心等待一段时间后，程序破解密码成功，“工作簿2”的密码在【File Password】文本框中显示，破解的密码为“123”，我们成功地破解了加密文件的密码，如图所示。

破解成功界面

11.2 邮件加密软件PGP的使用

PGP加密软件（Symantec PGP Command Line）是一款加密工具，可加密大量的批处理信息、维护备份过程安全，用户能在自动运行脚本中插入PGP加密和数字签名功能，确保信息在传递、存储或备份过程中的安全。这是PGP的命令行工具，允许组织对秘密信息进行自动加密和签名。PGP是目前优秀、安全的加密方式。Symantec PGP Command Line可以使企业快速轻松地将加密功能集成到批处理、脚本和应用程序中，从而确保公司静态或传输中数据的安全。不管您的挑战是保护信用卡信息、金融交易、薪水信息、医疗记录还是其他机密信息，Command Line的脚本化加密都是企业数据保护工具包的重要组成部分。本节我们介绍PGP加密软件的使用，通过本节的学习，使读者熟悉使用PGP加密的操作过程，在以后加密邮件时，可以方便地进行操作，保护邮件内容，防止他人进行破解。

现在以Symantec Encryption Desktop Win 64-10.3.1为例，讲解PGP加密的步骤，具体操作如下。

1 打开Symantec Encryption Desktop Win 64-10.3.1程序，待程序完全启动后，程序主界面如图所示。

Symantec Encryption Desktop Win 64-10.3.1程序主界面

2 创建自己的钥匙，单击【File】工具栏，再单击【New PGP Key】选项，弹出【PGP Key Generation Assistant】对话框，单击【下一步】按钮，如图所示。

【PGP Key Generation Assistant】对话框1

3 在【PGP Key Generation Assistant】对话框中，用户需要在【Full Name】文本框中输入密钥的名称，如“zlb”，在【Primary Email】文本框中输入邮箱，最后单击【下一步】按钮，如图所示。

【PGP Key Generation Assistant】对话框2

4 用户需要在【PGP Key Generation Assistant】对话框中为创建的钥匙设置密码，在【Enter Passphrase】文本框中输入密码，如“沃尔斯特拉123”，在【Re-enter Passphrase】文本框重新输入设置的密码，如“沃尔斯特拉123”，单击【下一步】按钮，注意【Passphrase Quality】显示密码的复杂度，密码长度至少为8位，如图所示。

【PGP Key Generation Assistant】对话框3

5 等待一段时间后，【PGP Key Generation Assistant】对话框中显示的信息如图所示，表明我们成功创建了密钥，单击【下一步】按钮，密钥创建成功，单击【Done】按钮，退出创建密钥向导。

【PGP Key Generation Assistant】对话框4

6 用户创建密钥完成后，密钥在Symantec Encryption Desktop Win 64-10.3.1程序主界面上显示情况如图所示。

Symantec Encryption Desktop Win 64-10.3.1程序主界面

7 用户需要导出创建密钥的公钥，右键单击【zlb】选项，单击【Export】按钮，弹出【Export Key to File】对话框，选择公钥的存放位置，单击【保存】按钮，如图所示。

【Export Key to File】对话框

8 接收邮件的用户需要另外创建自己的钥匙，方法和上文创建钥匙一致，用户名为zlbfriends123，邮箱为zlbfriends123@sina.com，创建完成后，需要导出自己的公钥，在【Export Key to File】对话框中选择保存的位置，单击【保存】按钮，如图所示。

【Export Key to File】对话框

9 用户加密邮件前，需要先导入对方的公钥，右键单击对方公钥文件，单击【打开】按钮，弹出【Select key（s）】对话框，单击【Import】按钮，导入对方的公钥，如图所示。

【Select key(s)】对话框

10 用户对公钥签名，右键单击【zlb】选项，单击【Sign】按钮，弹出【PGP Sign Key】对话框，单击【OK】按钮，如图所示。

【PGP Sign Key】对话框

11 选择【PGP Zip】选项，弹出【PGP Zip Assistant】对话框，选择第2个按钮，添加邮件，单击【下一步】按钮，如图所示。

【PGP Zip Assistant】对话框

12 在【PGP Zip Assistant】对话框中选择【Recipient keys】选项，单击【下一步】按钮，对邮件进行加密，如图所示。

【PGP Zip Assistant】对话框

13 在【PGP Zip Assistant】对话框中单击【Add】按钮，选择加密的公钥，zlb<1071437653@qq.com>，单击【下一步】按钮，如图所示。

【PGP Zip Assistant】对话框

14 用户在【PGP Zip Assistant】对话框中加密完成，单击【完成】按钮，加密信息显示如图所示。

【PGP Zip Assistant】对话框

15 接收方在收到邮件后，右键单击对方发给你的扩展名为．PGP的邮件，单击【Symantec Encryption Desktop】选项，弹出邮件解密界面，邮件解密成功，如图所示。

邮件解密界面

11.3 Word文件的加密解密

Microsoft Office Word是微软公司开发的一个文字处理器应用程序，它最初是由Richard Brodie为了运行DOS的IBM计算机而在1983年编写的。随后的版本可运行于Apple Macintosh (1984年)、SCO UNIX和Microsoft Windows（1989年），并成为Microsoft Office的一部分。Word给用户提供了用于创建专业而优雅的文档工具，帮助用户节省时间，并得到优雅美观的效果，一直以来，Microsoft Office Word都是流行的文字处理程序。于是，对Word文件进行加密就势在必行了。对Word文件加密的方法有很多，这里以Microsoft Word 2013为例，对一些加密解密方法具体介绍一下。通过本节的学习，读者可以熟悉Word文件的解密和加密。

11.3.1 Word自身功能加密

Microsoft Word 2013自身就提供了简单的加密功能，用户可以通过下面两种方法轻松实现对Word文件的加密。

1．第一种方法

Word 2013文档加密的详细操作步骤如下。

1 用Word 2013打开需要加密的文档，然后单击【文件】按钮，在弹出的菜单中系统已经默认选择了【信息】选项，我们只需要在右侧的【信息】选项面板中单击【保护文档】组中的【用密码进行加密】选项即可，如图所示。

【用密码进行加密】选项

2 此时会弹出一个【加密文档】对话框，我们在【密码】文本框中输入密码，注意区分大小写，然后单击【确定】按钮，如下图所示。

【加密文档】对话框

3 现在会跳转到【确认密码】对话框中，用户再次输入密码进行确认，然后单击【确定】按钮，如下图所示。

【确认密码】对话框

4 返回到Word中，我们看到【保护文档】项已经变成了黄色，而且提醒我们“必须提供密码才能打开此文档”，这就说明我们加密成功了，如图所示。用户现在单击【关闭】按钮，然后一定要在弹出的Microsoft Word提示框中单击【保存】按钮，使密码生效。

【保护文档】选项

Word 2013文档解密的详细操作步骤如下。

1 输入密码，打开已经加密的文档，然后单击【文件】按钮，如下图所示。

【密码】对话框

2 此时系统已经自动在弹出的菜单中选择了【信息】选项，我们需要做的就是单击【保护文档】组中的【用密码进行加密】按钮，如图所示。

信息界面

3 此时会弹出一个【加密文档】的对话框，其【密码】文本框中本来是有密码的，我们选中它并用退格键删除，然后单击【确定】按钮，如下图所示，保存设置，文档解密成功。

【加密文档】对话框

注意

请切记自己加密时输入的密码，因为Word 2013不提供找回密码的功能，请大家一定要谨慎！

2．第二种方法

1 单击【文件】按钮，单击【另存为】按钮，弹出【另存为】对话框，选择保存位置，如图所示。

【文件】界面

2 用户单击【工具】按钮，单击【常规选项】按钮，弹出【常规选项】对话框，如图所示。

【另存为】对话框

3 用户需要在【常规选项】对话框输入相应密码，并且输入确认密码，单击【确定】按钮，完成加密，如图所示。

设置后的【常规选项】对话框

11.3.2 Word密码破解器2016的使用

Word密码破解器可帮你迅速找回Word密码。工作繁忙难免产生意外，比如对重要的Word文档加密之后却把密码忘得一干二净，这几乎会发生在每个人的身上。这时你可以通过Word密码破解器来暴力破解迅速找回Word密码，以后再也不必为了忘记Word密码而烦恼。为了测试程序的破译速度与效果，用户可以先随便设置一个4位数密码的Word文档进行破译。密码位数越长，破译速度越慢。“字典样本．txt”是一个密码字典文件的例子，你可以自己制作字典文件，但必须一行一个密码。

Word密码破解器2016的使用步骤如下。

1 用户应先打开Word密码破解器2016程序，待程序完全启动后，程序主界面如下图所示。

Word密码破解器2016程序主界面

2 单击【选择】按钮，弹出【请选择要被破译的Word文档】对话框，单击要选择的文件，单击【打开】按钮，如下图所示。

【请选择要被破译的Word文档】对话框

3 文件选定后，用户可以设置破解方式以及密码长度，我们这里选择【纯数字破译】选项，密码位数为2位，如图所示。

设置破解方式和密码长度

4 单击【开始破译】按钮，程序开始破译密码，用户可以单击【停止破译】按钮，随时终止破译，等待一段时间后，密码破解成功，密码为“12”，如图所示。

【开始破译】按钮

11.3.3 风语者文件加密器的使用

“风语者”文件加密器是一款不用安装就可以直接使用的绿色软件，它通过对文件进行加密，达到保护个人隐私的目的。用密码解密是恢复文件的唯一途径，加密后的文件必须凭借原加密密码才能恢复，所以要牢记密码，你可以通过简单设置，添加右键菜单和关联加密文件，更加方便地进行操作。本次推出了1.5版，与1.2版相比，增加了文件Md5Sum校验功能，增加了文件夹的隐藏功能。通过本节学习，可以学会使用“风语者”加密解密，具体使用步骤如下。

1 打开风语者V1.5.1程序，待程序成功启动后，程序主界面如下图所示。

风语者V1.5.1程序主界面

2 用户需要单击【文件】工具栏，单击文件加密按钮，弹出文件加密对话框，如下图所示。

文件加密对话框

3 用户可以在文件加密对话框中，单击【浏览】按钮，弹出【打开】对话框，选择需要加密的文件，单击【打开】按钮，如图所示。

【浏览】按钮

4 用户在【密码】文本框中输入自己的加密口令，单击【执行加密】按钮，即可将所选文件加密，加密成功后会弹出【加密】提示框，如图所示。

【执行加密】按钮

5 如果想要将已经加密过的文件进行解密，用户单击【文件】工具栏，单击【文件解密】按钮，弹出【文件解密】对话框，如图所示。

【文件解密】按钮

6 用户可以在【文件解密】对话框中，单击【浏览】按钮，弹出【打开】对话框，选择需要解密的文件，单击【打开】按钮，如图所示。

【浏览】按钮

7 用户选取需要解密的文件，再在【密码】文本框中输入自己的解密口令，单击【执行解密】按钮，即可将文件解密，弹出【解密】提示框，如图所示。

【执行解密】按钮

11.3.4 AOPR解密软件的使用

Advanced Office Password Recovery是一款针对MS Office系列的密码破解工具，它可破解95～2013 版的各类Office文档，它也可以破解Word文件的密码，其操作方法与Excel文件的破解过程类似，具体操作步骤如下。

1 运行Advanced Offi ce Password Recovery程序，即可弹出程序主窗口，如图所示。

Advanced Office Password Recovery程序主窗口

2 用户需要在【Recovery】选项卡中，选择【Dictionary Attack】单选按钮，设置好破解方式，如图所示。

【Dictionary Attack】选项

3 用户需要单击【Open File】按钮，弹出【Open File】对话框，选择解密的文件，单击【打开】按钮，如图所示。

【Open File】对话框

4 程序开始自动破解密码，等待一段时间后，成功破解出Word文件的密码，弹出【Word File Recovered】对话框，如密码为“a”，单击【OK】按钮退出程序，如图所示。

Word File Recovered对话框

11.4 WinRAR压缩文件的加密解密

WinRAR是一款功能强大的压缩包管理器，它是档案工具RAR在Windows环境下的图形界面。该软件可用于备份数据，缩减电子邮件附件的大小，解压缩从Internet上下载的RAR、ZIP及其他类型文件，并且可以新建RAR及ZIP格式等的压缩类文件。通过本节学习，读者可以学会使用WinRAR压缩文件的加密解密功能，本节以WinRAR 64位（v 5.30）为例，具体介绍其使用方法。

11.4.1 WinRAR加密文件

用户在使用WinRAR加密文件时，具体操作步骤如下。

1 用鼠标右击需要压缩并加密的文件，在快捷菜单中选择【添加到压缩文件】选项，如下图所示。

【添加到压缩文件】选项

2 在弹出的【压缩文件名和参数】对话框中单击【浏览】按钮，如下图所示。在弹出的【查找压缩文件】对话框中，选择文件存放位置，单击【保存】按钮，如下图所示。

【压缩文件名和参数】对话框

【查找压缩文件】对话框

3 单击【压缩文件格式】选项，选择“RAR”格式，再单击【设置密码】按钮，如下图所示，即可打开【输入密码】对话框，如下图所示。

【设置密码】按钮

【输入密码】对话框1

4 用户在【输入密码】对话框中输入自己的密码后，连续单击【确定】按钮，即可生成加密的RAR文件，如图所示。

【输入密码】对话框2

11.4.2 RAR密码破解（ARPR）1.53绿色版的使用

RAR密码破解ARPR1.53破解速度很快，可以帮你找回RAR文件的密码，注册后可以解开多达128位密码。它提供有预估算出密码所需要的时间；可中断计算与恢复继续前次的计算。该软件的具体使用步骤如下。

1 用户需要打开RAR密码破解（ARPR）1.53的程序，待程序成功启动后，程序主界面如图所示。

RAR密码破解1.53程序主界面

2 用户需要单击【打开】按钮，弹出【打开】对话框，选择需要解除密码的RAR文件，单击【打开】按钮，如图所示。

【打开】对话框

3 用户在【破解类型】选项选择暴力破解方式，在【范围】选项选择【所有数字（0-9）】选项，并在相应的选项卡中设置其选项，如图所示。

【破解类型】选项

4 用户需要单击【开始】按钮，程序进入破解界面，在破解过程中，用户可以单击【停止】按钮，终止程序的破解，如图所示。

程序破解界面

5 等待一段时间后，程序破解密码成功，破解的密码为“123”，单击【确定】按钮，可以退出【密码已成功恢复】对话框，如图所示。

【密码已成功恢复】对话框

11.4.3 RAR压缩包密码破解（RAR Password Recovery）V1.80特别版的使用

通过本节的学习，读者可以学会使用该软件快速地破解RAR密码，该软件的具体操作步骤如下。

1 打开RAR压缩包密码破解（RAR Password Recovery）V1.80特别版的程序，待程序完全启动后，程序主界面如图所示。

RAR Password Recovery V1.80特别版

2 单击浏览文件按钮，弹出【打开】对话框，用户需要选择要破解的文件，然后单击【打开】按钮，如图所示。

【打开】对话框

3 用户需要在【Recovery method】选项中选择【Brute-force attack】单选按钮，单击【Next】按钮，如图所示。

【Recovery method】选项

4 用户可以在【Password length】文本框中输入密码的最小长度和最大长度，如最小长度为3，最大长度为3，在【Character sets】选项中选择【Include digits】选项，然后单击【Next】按钮，用户也可以单击【Back】按钮，返回上一步操作，如图所示。

设置密码长度和特征

5 程序开始根据密码特征和长度的设定暴力破解密码，用户可以单击【Back】按钮，返回上一步操作，也可以单击【Stop Recovery】按钮停止程序的破解，如图所示。

密码破解界面

6 等待一段时间后，程序成功破解出RAR文件的密码，弹出【Congratulations】对话框，密码在文本框中显示，如该密码为123，单击【OK】按钮退出，如图所示。

【Congratulations】对话框

11.5 WinZip压缩文件的加密解密

一般我们的数据都是以标准的文件格式存放在存储介质上（如硬盘、光盘）的，因此它们很容易被复制，而这往往是我们所不想看到的。通过前面的学习，我们知道，利用WinZip就可以在压缩数据的同时帮助我们给数据加一个口令，即在加密文件的同时还压缩了我们的文件，本节以WinZip(R) 19.5-64位为例，介绍WinZip文件的加密解密。

11.5.1 WinZip加密文件

用户使用WinZip(R) 19.5-64位对文件进行加密的操作步骤如下。

1 在WinZip中，用鼠标右击需要压缩并加密的文件之后，选取快捷菜单中的【WinZip】→【添加到压缩文件】选项，即可打开【添加文件】对话框，如图所示。

【添加文件】对话框

2 用户在【添加文件】对话框中，勾选其中的【加密文件】复选框，弹出【加密】对话框，在【输入密码】文本框和【重新输入密码（确认）】文本框中输入自己的密码之后，单击【确定】按钮，即可生成压缩文件，如图所示。

【添加文件】对话框

3 用户查看压缩文件时，如果在压缩文件的后面有一个“ ”符号，表示该压缩文件已加密。用户以后要解压缩加密过的压缩文件时，会弹出【解密】对话框，需要输入正确的密码才能将文件解压缩，如图所示。

解密文件

11.5.2 解密Zip密码软件（ARCHPR）的使用

Advanced Archive Password Recovery（ARCHPR）是一个强大的压缩包密码破解工具，适用于ZIP和RAR档案的高度优化的口令恢复工具，通过本软件的使用，用户可以快速地破解Zip文件的密码，在本节的学习中，读者将学会如何使用ARCHPR破解Zip文件的密码。

Advanced Archive Password Recovery（ARCHPR）的使用步骤如下。

1 打开ARCHPR的程序，待程序完全启动后，程序主界面如图所示。

ARCHPR程序主界面

2 单击【打开】按钮，弹出【打开】对话框，选取要破解的文件，单击【打开】按钮，如图所示。

【打开】对话框

3 用户可以在打开文件后，在ARCHPR程序主界面选择攻击类型、范围、长度、开始等密码破解方式，我们这里选择攻击类型为“暴力”，范围为“所有数字（0-9）”，开始于“11111110”，最小长度为“8”，最大长度为“8”，如图所示。

ARCHPR程序主界面

4 单击【开始】按钮，程序开始破解，用户可以单击【停止】按钮，终止程序的破解，如图所示。

【开始】按钮

5 用户等待一段时间后，程序破解密码成功，弹出【口令已成功恢复】提示框，这个文件的口令为“11111111”，单击【确定】按钮，退出【口令已成功恢复】提示框，如图所示。

【口令已成功恢复】提示框

11.5.3 利用ZIP密码暴力破解工具探测口令

ZIP密码暴力破解工具支持由WinZIP、PKZIP以及其他兼容ZIP格式的软件（比如winRAR等）加密的ZIP压缩包，破解速度比较理想，软件的功能特性包括：WinZIP、PKZIP全系列支持，兼容ZIP部分支持；自设字符、开始密码及密码长度设定等，中规中矩；断点续破。通过本节的学习，读者可以使用ZIP密码暴力破解工具破解ZIP文件的密码。ZIP密码暴力破解工具的具体使用步骤如下。

1 打开ZIP密码暴力破解工具程序，待程序完全启动后，程序主界面如图所示。

ZIP密码暴力破解工具程序主界面

2 用户单击按钮，弹出【Select ZIP fi le】对话框，选取要破解的文件，单击【打开】按钮，如图所示。

【Select ZIP file】对话框

3 破解的文件选取后，用户需要在ZIP密码暴力破解工具主界面中设置密码的格式和破解方式，这里破解方式选择【强制模式】按钮，在【Custom Charset】选项中选择【数字】选项，在【Password Length】中设定最小长度和最大长度均为“8”，在【Start From】文本框输入“11111000”，如图所示。

ZIP密码暴力破解工具主界面1

4 用户在ZIP密码暴力破解工具主界面中单击【开始】按钮，程序开始破解密码，用户在破解过程中可以单击【停止】按钮，终止程序的破解，如下图所示。

ZIP密码暴力破解工具主界面2

5 等待一段时间后，程序自动破解密码，弹出【信息】提示框，密码为“11111111”，单击【确定】按钮，退出【信息】提示框，密码破解成功，如下图所示。

【信息】提示框

11.6 EXE文件的加密解密

目前对EXE文件的加密方法主要是两个：内嵌和加壳。内嵌就是在编写程序的时候就加入检测代码，防止盗用和修改，加壳就是在EXE文件已经产生之后，在EXE文件外面加了一层壳，从而隐藏了EXE文件的原始信息，起到保护的目的。

事实上，对EXE文件加口令也是对EXE文件增加了一层壳，由这层壳来完成口令的验证工作，如果验证通过，则壳就会加载原有的EXE文件来运行，否则就退出。

11.6.1 EXE文件添加运行密码

EXE文件添加运行密码是一款给EXE文件添加运行密码的小工具，用户可以通过本软件对EXE文件进行加密，具体使用步骤如下。

1 打开EXE文件添加运行密码程序，待程序完全启动后，程序的主界面如下图所示。

EXE文件添加运行密码程序主界面

2 单击【浏览】按钮，弹出【打开】对话框，选择要加密的EXE文件，单击【打开】按钮，如下图所示。

【打开】对话框

3 用户进入文件加密界面，在【运行授权密码】文本框中输入密码，在【确认运行授权密码】对话框中重新输入密码，最后单击【开始加密】按钮，如下图所示。

加密界面

4 等待一段时间，加密成功，程序弹出【恭喜】提示框，单击【确定】按钮即可退出【恭喜】提示框，如下图所示。

【恭喜】提示框

5 用户在打开加密EXE文件时需要口令，说明加密成功，用户输入密码，单击【确定】按钮，即可打开加密的EXE文件，如图所示。

输入密码界面

11.6.2 EXE加壳保护工具下载汉化绿色版的使用

VMProtect Professional是一款超强的EXE加壳保护工具，它可以有效保护并压缩开发的程序，将保护后的代码放到虚拟机中运行，这将使分析反编译后的代码和破解变得极为困难。通过本节的学习，读者将学会使用VMProtect Professional保护自己的EXE文件，EXE加壳保护工具下载汉化绿色版的具体操作步骤如下。

1 双击打开EXE加壳保护工具下载汉化绿色版的程序，待程序成功启动后，程序的主界面如下图所示。

VMProtect Professional程序的主界面

2 用户单击按钮，弹出【打开】对话框，选择要加壳的EXE文件，单击【打开】按钮，如下图所示。

【打开】对话框

3 用户打开要加壳的EXE文件后，弹出VMProtect Professional程序的主界面，单击【下一地址】按钮，如下图所示。

VMProtect Professional程序的主界面

4 用户进入【编译】对话框，程序开始对EXE文件进行加壳，编译完成后，单击【下一地址】按钮，如下图所示。

【编译】对话框

5 用户进入【离开】对话框，程序界面上显示【打开新文件】选项和【离开】选项，用户可以继续对新文件加壳，也可以退出程序，单击【下一地址】按钮，如下图所示。

【离开】对话框

11.7 小结

本章讲解的主要内容是常用软件加密解密技术，集中介绍了大量的加密解密软件，以助于读者全面了解常见应用软件的一些加密解密技术。通过对本章的学习，读者一般都可以达到独立解决常见软件的加密解密问题。

在本章中增加了PGP加密软件的使用，它是一款优秀的加密软件，既可以加密邮件，也可以加密文件，在邮件加密时，需要对方的公钥。

虽然在本章中向大家推荐了众多的加密解密软件，但也不能单纯依赖它们。这些加密解密软件其实也仅仅是一种对文档进行适当保护的工具而已，根本谈不上绝对的安全。

因此，为了自己的数据安全，在使用加密解密软件时一定要选用最新注册的版本，发挥软件全面的功能，并能熟练掌握加密解密软件的具体用法，对应用软件的加密解密也就运用自如了。

技巧与问答

❖ 在使用压缩工具WinRAR对文件进行压缩加密时，如果不想让别人看到自己设置的密码，应该如何设置呢？

以一个名为“新建备份文件夹”的文件夹为例，如果不想让别人看到自己设置的密码，可参考下列的方法进行设置。

选中该文件夹并右击之后，在弹出的快捷菜单中选择【添加到压缩文件】选项，即可打开【压缩文件名和参数】对话框。

在其中选择【高级】选项卡，并单击【设置密码】按钮之后，即可弹出【带密码压缩】对话框，此时不要勾选“显示密码”复选框，由于设置文档加密的密码是以不可见的“＊”形式显示的，因此，设置完成后单击【确定】按钮即可。

❖ 在使用WinRAR压缩工具对文件进行压缩加密时，能不能同时连文件名一起加密？

答案当然是肯定的，具体可参考如下的步骤来进行操作。

在WinRAR主窗口中，单击【添加】按钮，即可打开【压缩文件名和参数】对话框。在其中选择【文件】选项卡之后，在“要添加的文件”选项框中添加一个要压缩的文件。再选择【高级】选项卡之后，单击【设置密码】按钮，即可打开【带密码压缩】对话框，在其中输入压缩文件的密码并勾选【加密文件名】复选框，单击【确定】按钮，就可以连同文件名一起被加密了。

❖ 给WinZip加密解密的技巧有哪些？

（1）逃过ZIP密码破解器

由于ZIP压缩格式的盛行，已成为业界实际意义上的标准压缩格式。这就使得现在有许多针对该类格式压缩文件口令的破解程序。不过，我可以告诉大家一个小技巧来逃过破解器，在输入口令时加大口令的长度，并多加几个空格符，并且不用别人很容易就猜出来的密码，这样这些破解器就很难破解出来了。

（2）口令仅对当前文件有效

有些人在使用WinZip添加文件时第一次加了口令，以后添加和更新文件时却不再重新设置密码，其实这样做是很危险的！因为，据笔者试验，WinZip的加密功能仅对当前文件有效，如第一次加密了file1至file10十个文件，然后又添加了filenew1和filenew2，但没有重新设置密码，那么后来添加的两个文件filenew1和filenew2将不处于密码保护状态！后来添加的文件是没有加密符号“+”号的，这样别人可以不需要任何密码将后来添加的文件顺利解压缩。

（3）请即刻为文件输入密码

正确的做法应该在“添加”文件窗口中单击“密码”按钮，并输入密码，只有这样才能够保证你添加的文件已经被加密保护。

（4）双重加密法

如果我们的某一类文件实在太重要，可以先使用WinZip带密码压缩一遍，然后再使用WinZip重新带密码压缩一遍。这样，一旦第一次密码被识破，在压缩包中的文件还有密码，这样就可以达到双重保险了。这就是所谓的“加密过的加密文件”。

第12章
其他软件加密解密技术应用

我们一般采用加密软件对软件进行加密，就现在来说，这些加密工具还没有解密工具，所以，使用这些加密工具对文件加密是比较安全的。尽管这样说，但是对一些编程基础较好，深入掌握操作系统知识的解密高手来说，破解这些加密软件也不是什么难事。不能认为只要使用了加密软件进行加密就万无一失，没有加密软件能够做到绝对安全的。我们要提升自己的加密能力，学会使用各种加密软件保护自己的隐私安全。

12.1 破解MS SQL Server密码

自从和Sybase联合开发了SQL Server之后，微软将SQL Server注册作为了自己的数据库产品。和微软的其他产品一样，SQL Server具有操作方便、简单、易用的特点，而且速度和稳定性也不错，成为目前在Windows平台下数据库开发的首选。

如果丢失了数据库管理员sa的密码，就有可能无法登录至数据库，从而造成不必要的麻烦。但如果自己有运行数据库系统的服务器的系统管理员权限，则解决这个问题并不是很难。

12.1.1 SA口令清除工具的使用

SA口令清除工具是一款免费的SQL密码清除器，能够清除数据库SQL 2000,SQL 2005管理员SA的密码，并附带了一键修复数据库挂起和ping网络是否畅通功能。下面讲解一下SA口令清除工具的使用方法。

1 双击打开SA口令清除工具的程序，等待一段时间，SA口令清除工具的程序主界面如图所示。

SA口令清除工具的程序主界面

2 在【默认数据库类型】选项中单击下拉箭头，单击【SQL 2005】选项，如图所示。

【默认数据库类型】选项

3 在【数据库服务器名】文本框中输入服务器的IP地址，这里输入本地服务器的IP地址：127.0.0.1（本地计算机的环回地址），如图所示。

【数据库服务器名】文本框

4 在输入IP地址后，单击【ping查看网络是否畅通】按钮，如下图所示，弹出【SA口令清除工具v1.0】对话框，单击【OK】按钮，如下图所示。

【ping查看网络是否畅通】按钮

【SA口令清除工具v1.0】对话框

5 用户在ping通网络后，需要在【清除后SA口令】文本框中输入新密码，这里输入“123”，单击【开始修改SA口令】按钮，如下图所示，弹出【严重警告】对话框，单击【确定】按钮，等待一段时间，弹出【SA口令清除工具v1.0】提示框，提示修改密码成功，单击【OK】按钮即可完成密码修改，如下图所示。

【清除后SA口令】文本框

修改成功提示框

12.1.2 实现本地用户的账户登录

SQL Server提供了两种登录机制：本地账户登录和分配的数据库用户登录。前一种登录就是用像Administrator这样的系统账户来登录，后者则是在SQL Server中建立的登录账户来登录，最典型的例子就是用SA这个账户。

其中要实现本地用户的账户登录方法，可以用于拥有系统管理员权限的用户解决丢失SA口令的问题，而且很方便。

在服务器上拥有系统管理员权限的用户（以Administrator为例）登录之后，即可打开SQL Server的“企业管理器”（SQL Server 2005版），以Microsoft SQL Server Management Studio为例（SQL Server Management Studio是一个集成环境，用于访问、配置、管理和开发SQL Server的所有组件。SQL Server Management Studio组合了大量图形工具和丰富的脚本编辑器，使各种技术水平的开发人员和管理员都能访问SQL Server），下面讲解一下如何更改SA的登录密码。

1 打开Microsoft SQL Server Management Studio，等待一段时间，Microsoft SQL Server Management Studio的主界面如图所示。

Microsoft SQL Server Management Studio的主界面

2 打开【安全性】→【登录名】选项，在下拉列表中选择SA用户名，双击“sa”用户名，弹出【登录属性-sa】对话框，在【密码】文本框中输入密码，在【确认密码】文本框中再次输入密码确认，单击【确定】按钮即可完成更改，如图所示。

更改登录名

用户更改完成后，首先退出SQL Server Management Studio，再重新打开SQL Server Management Studio，此时，SA登录就可以使用新的密码了。

12.1.3 文件复制的使用

SQL Server Management Studio是一个集成环境，用于访问、配置、管理和开发SQL Server的所有组件。其查询分析器的关键诊断特性是它能够显示某个查询的执行计划。这个执行计划会向用户提供各种类型的有用信息，例如，在查询的执行期间如何以及何时使用或者不使用索引。它还会提供许多其他的细节，例如，排序、平行（parallelism）、嵌套循环，以及SQL服务器在执行指定查询时必须做的其他事情，SQL Server Management Studio运行界面如图所示。

SQL Server Management Studio程序主界面

如果没有系统管理员权限，只要能够得到相应数据库的文件（一般是两个文件：一个扩展名为．mdf，这是数据文件；另一个扩展名为．ldf，这是日志文件），也能够获得相关的数据。

假定一个数据库的数据文件和日志文件分别为mydata1.mdf和mydata1.ldf，并且已经获得了这两个文件，将其放在d:\下，数据库的名称是“newsystem”，下面介绍一下具体的操作步骤。

1 打开SQL Server Management Studio后，在工具栏中单击【新建查询】按钮，如图所示。

【新建查询】按钮

2 在打开的新建查询文件中，输入sql语句：sp_attach_db 'newsystem', 'd:\mydata1.mdf', 'd:\mydata_log.ldf'，如图所示。

新建查询文件

3 单击工具栏按钮，如果在【消息】文本区提示“命令已成功完成”，则表明此时已经获得了对这个数据库的控制权，如图所示。

单击工具栏按钮

如果在采用这种方法时不能复制数据库文件且系统提示“共享冲突”，则可能是因为相应的数据库文件正在被SQL Server 2005使用。在这个过程中，这个文件是被SQL Server 2005独占使用的，无法实现Copy，只有停止SQL Server 2005服务才行，虽然这个时候能够用backup语句将它们备份出来，但如果能够backup，就说明有访问权限了。

12.2 宏加密解密技术的应用

在Microsoft Office套件中内嵌了一个Visual Basic编辑器，它就是宏产生的源泉。使用宏同样可以将Word、Excel文档进行加密。宏是一把双刃剑。对Word文档而言，最大的敌人当然就是宏病毒了。所以对于来源不明的文档，坚决拒之门外，以防止宏病毒“浑水摸鱼”。当然，对于已经确信的宏则可以添加到“可靠来源”中，放心使用。

12.2.1 宏加密文件

在Word里设置防范措施十分简单，选择【开发工具】→【宏安全性】菜单项，即可打开【信任中心】对话框，选择【禁用无数字签署的所有宏】单选按钮即可。这样，以后每打开一个文档，系统都会检查它的数字签名，一旦发现是不明来源的宏，即可将它置之门外。

【宏安全性】菜单项

另外，为了阻止可恶的宏病毒在打开文件时自动运行并产生危害，可以在打开一个Office文件时，很容易地阻止一个用VBA写成的在打开文件时自动运行的宏。

选择【文件】→【打开】菜单项，单击【计算机】→【我的文档】选项，弹出【打开】对话框，在打开的【打开】对话框中选择所要打开的文件名字，这里选择“test”，在单击【打开】按钮时按住<Shift>键，Office将在不运行VBA过程的情况下，打开该文件。按住<Shift>键阻止宏运行的方法，同样适用于选择【文件】菜单底部的文件（最近打开的几个文件），如图所示。

【打开】菜单项

同样，在关闭一个Office文件时，也可以很容易地阻止一个用VBA写成，将会在关闭文件时自动运行的宏。从中选择【文件】→【关闭】菜单项，在单击【关闭】按钮时按住<Shift>键，Office将在不运行VBA过程的情况下关闭这个文件（按住<Shift>键同样适用于单击窗口右上角的“×”关闭文件时阻止宏的运行）。

【关闭】菜单项

其实，还可以利用宏来自动加密文档，选择【开发工具】→【宏】菜单项，即可打开【宏】对话框，如下图所示。

【宏】对话框

用户在【宏名】文本框中输入“AutoPassword”之后，在【宏的位置】下拉列表框中选择“所有的活动模板和文档”选项，如下图所示。

【宏名】文本框和【宏的位置】下拉列表框

单击【创建】按钮，即可显示【Microsoft Visual Basic】窗口，如图所示。

【Microsoft Visual Basic】窗口

在“End Sub”语句的上方插入如下代码。

        With Options
            .AllowFastSave = True
            .BackgroundSave = True
            .CreateBackup = False
            .SavePropertiesPrompt = False
            .SaveInterval = 10
            .SaveNormalPrompt = False
            End With
            With ActiveDocument
            .ReadOnlyRecommended = False
            .EmbedTrueTypeFonts = False
            .SaveFormsData = False
            .SaveSubsetFonts = False
            .Password =“2016”
            .WritePassword =“2016”
            End With
            Application.DefaultSaveFormat = ""

其中的．Password = "2016"表示设置打开权限密码，.WritePassword = "2016"表示设置修改权限密码。

在输入完上述代码之后，选择【文件】→【保存Normal】菜单项，再执行关闭并返回到Microsoft Word即可，如图所示。

【保存Normal】菜单项

为了更方便地使用该宏，需要为它指定一个快捷键，具体操作方法如下。

1 在Word工具栏上单击鼠标右键，在弹出的快捷菜单中选取【自定义功能区】选项，即可打开【Word选项】对话框，如图所示。

【自定义功能区】选项

2 用户在【Word选项】对话框中选取【键盘快捷方式】区域的【自定义】按钮，即可打开【自定义键盘】对话框，如图所示。

【自定义键盘】对话框

3 用户在【类别】列表中选择【宏】选项，在【宏】列表中选择【AutoPassword】选项，并同时按<Alt>、<Ctrl>、<F6>三个键，此时在【请按新快捷键】文本框将出现“Alt+Ctrl+F6”，如图所示。

设置快捷键

4 用户设置完成快捷键后，单击【指定】按钮，即可将<Alt+Ctrl+F6>组合键赋予“AutoPassword”的宏，如图所示。

【指定】按钮

5 单击【关闭】按钮关闭【自定义键盘】对话框之后，再单击【确定】按钮关闭【Word选项】对话框，如图所示。

【关闭】按钮

这样，当新建一个Word文档之后，用户只要按下<Alt+Ctrl+F6>组合键，就可以自动给当前文档设置密码了，如图所示。

【确定】按钮

12.2.2 破解宏密码

Word密码破解器可以破解迅速找回Word密码，它是一款专门用于Office文档通过宏加密后的解密工具，其操作界面如下图所示。也就是说，它需进行破解选项设置。其操作步骤如下。

Word密码破解器2016操作界面

1 用户单击【选择】按钮，弹出【请选择要被破译的Word文档】对话框，单击要选择的文件，单击【打开】按钮，如下图所示。

【请选择要被破译的Word文档】对话框

2 文件选定后，用户可以设置破解方式以及密码长度，我们这里选择【纯数字破译】选项，密码位数为4位，如图所示。

设置破解方式和密码长度

3 单击【开始破译】按钮，程序开始破译密码，如下图所示。用户可以单击【停止破译】按钮，随时终止破译，等待一段时间后，密码破解成功，密码为“2016”，如下图所示。

【开始破译】按钮

破解成功

还可以采取多种软件加密方法的综合，以及几种加密技术交叉使用（如有把软件狗加密技术和磁盘加密技术结合起来做的），足以让许多解密者知难而退。因为必须了解所有的加密技术原理才有可能解密，这就增加了难度。

12.3 多功能文件加密工具应用

在实际生活中，除了要对一些图片格式、音频格式和视频格式类文件进行必要的加密之外，有时还要对一些其他格式的文件进行加密处理。为了帮助大家解决这个问题，这里就介绍几个多功能文件加密工具。

12.3.1 “超级加密精灵”的使用

超级加密精灵是一款加密速度极快且功能强大的加密工具，它是一款共享软件，可用于加密任何格式的文件，几乎集成了当前所有加密工具的功能。

安全可靠是本软件的最大特点，首先采用了先进加密算法加密数据，真正达到了超高的加密强度；其次使用安全上，本软件需凭口令登录进入加密主窗口，卸载也需要登录后进行。本软件适于文件和文件夹的加密，是将文件通过加密变换将其内容变为乱码，真正保证数据的安全，适于安全性要求很高的应用场合，如商业数据和极其重要的个人数据。

超级加密精灵运行方式如下。

从Windows桌面的【开始】菜单中找到“超级加密精灵”程序组并启动它，或单击相应的快捷方式图标。

运行超级加密精灵应用程序，即可弹出加密精灵的主窗口，如图所示。

加密精灵主窗口

超级加密精灵工具栏上有5个主要的按钮，它们的作用分别如下。

：用来加密所选中的文件或文件夹。

：用来解密所选中的加密文件。

：用来对加密数据进行打包。

：对文件夹进行保护。

：用来彻底粉碎所选中的文件或文件夹。

实例1：对单个文件进行加密解密

利用超级加密精灵可以加密任何格式的文件，这里以对“D:\test1\ZipLib.dll”文件进行加密为例进行讲述。

加密的具体步骤如下。

1 在超级加密精灵主窗口中，单击【我的电脑】选项，在下拉列表中单击【本地磁盘（D:）】选项，单击“test1”文件夹，打开“ZipLib.dll”文件，如图所示。

超级加密精灵主窗口

2 用户单击工具栏上的【数据加密】按钮，即可弹出【加密密码设置】对话框，如图所示。

【数据加密】按钮

3 用户在【加密密码设置】对话框的【输入密码（3-16字符）】文本框中输入密码，密码的范围在3～16个字符，在【密码确认】对话框中重新输入密码，单击【确定】按钮，完成加密，如下图所示。

设置后的【加密密码设置】对话框

4 解密的过程与加密过程相似，需要在文件列表里选中要解密的文件“ZipLib.d l.spe”，单击工具栏上的【数据解密】按钮，即可打开【解密口令验证】对话框，如图所示。

【数据解密】按钮

5 用户在【解密口令验证】对话框的【请输入密码】文本框中输入密码，单击【确定】按钮即可解密，如下图所示。

设置后的【解密口令验证】对话框

实例2：对数据进行加密打包

在对数据加密时可能还需要对数据进行打包，超级加密精灵提供了此功能。

数据加密打包的具体步骤如下。

1 用户在超级加密精灵主窗口中，单击【我的电脑】选项，在下拉列表中单击【本地磁盘（D:）】选项，单击“test2”文件夹，如图所示。

超级加密精灵主窗口

2 单击工具栏上的【数据加密打包】按钮，即可弹出【加密密码设置】对话框，如图所示。

【数据加密打包】按钮

3 用户在【加密密码设置】对话框的【输入密码（3-16字符）】文本框中输入密码，密码的范围在3～16个字符，在【密码确认】对话框中重新输入密码，单击【确定】按钮，完成加密，如下图所示。

设置后的【加密密码设置】对话框

4 程序在上一步执行完后，弹出【浏览文件夹】对话框，用户选择文件夹位置，这里选择【本地磁盘（D:）】选项，单击【确定】按钮，如下图所示。

【浏览文件夹】对话框

5 完成数据打包加密后，数据打包成压缩包的形式，用户在解压时需要在【自解压解密】对话框的【输入密码】文本框中输入密码才能解压，如图所示。

【自解压解密】对话框

实例3：超级加密精灵实现文件夹保护

用户要想对文件夹进行加密，可以使用超级加密精灵的文件夹保护功能。

文件夹保护的具体步骤如下。

1 用户在超级加密精灵主窗口中，单击【我的电脑】选项，在下拉列表中单击【本地磁盘（D:）】选项，单击“test1”文件夹，如图所示。

超级加密精灵主窗口

2 用户单击工具栏上的【文件夹保护】按钮，即可弹出【文件夹加密精灵】对话框，如下图所示。

【文件夹加密精灵】对话框

3 用户单击【文件夹加密精灵】对话框的【加密】按钮，弹出【设置操作信息】对话框，如下图所示。

【加密】按钮

4 用户在【设置操作信息】对话框的【输入密码】文本框中输入密码，在【确认密码】文本框中重新输入密码，勾选【快速加密】复选框，单击【提交】按钮，如下图所示。

设置后的【设置操作信息】对话框

5 用户单击【文件夹加密精灵】对话框的【伪装】按钮，弹出【伪装文件夹类型选择】对话框，这里选择【打印机】单选按钮，单击【确定】按钮，如下图所示。

【伪装文件夹类型选择】对话框

6 对文件进行解密时，可以选择需要解密的文件，然后单击【文件夹加密精灵】对话框的【解密】按钮，即可解密，如图所示。

【解密】按钮

12.3.2 “易通文件夹锁”的使用

易通文件夹加密软件是一款安全易用的文件夹加密软件，集文件夹加密、文件加密、磁盘加密（隐藏磁盘和USB锁）、高级加密四大保护功能。加密后的资料防删除、复制等，还可以记录文件（夹）的新建、复制、移动、删除等所有相关操作。软件提供了锁定、隐藏等五种加密方式来满足不同加密需求，是文件资料的安全屏障。

运行易通文件夹锁应用程序，即可弹出易通文件夹锁的主窗口，它主要有三种加密方式，分别是文件夹加密、文件加密、磁盘加密，如图所示。

易通文件夹锁的主窗口

实例1：对文件夹进行批量加密

易通文件夹锁可以对文件夹进行批量加密，其操作的具体步骤如下。

1 在易通文件夹锁的主界面单击【文件夹加密】选项的【批量加密】按钮，弹出【批量加密】对话框，如图所示。

【批量加密】对话框

2 用户在【批量加密】对话框中单击【添加目录】按钮，弹出【浏览文件夹】对话框，选择需要加密的文件夹，单击【确定】按钮，如下图所示。

【浏览文件夹】对话框

3 用户使用上述方法再添加一个文件夹，然后在【批量加密】对话框的【输入密码】文本框和【确认密码】文本框中输入密码和确认密码（试用版不能设置密码），单击【加锁加密】单选按钮，单击【批量加密】按钮，完成加密，如下图所示。

【批量加密】对话框

解密过程为：用户双击加密的文件夹，弹出【文件夹解密】对话框，用户在【输入密码】文本框中输入密码（试用版初始密码为空），单击【解密】按钮，弹出【易通文件夹锁】对话框，单击【确定】按钮，完成解密，如图所示。

文件夹解密

实例2：对多个文件进行批量加密

易通文件夹锁可以实现对多个文件进行批量加密，其操作的具体步骤如下。

1 在【易通文件夹锁】主界面单击【文件加密】选项，单击【批量加密】按钮，弹出【批量加密】对话框，如图所示。

批量加密

2 用户在【批量加密】对话框中单击【添加文件】按钮，弹出【打开】对话框，选择需要加密的文件，单击【打开】按钮，如下图所示。

【打开】对话框

3 用户采用上述方法再添加一个文件，在【批量加密】对话框的【输入密码】文本框和【确认密码】文本框中输入密码和确认密码，单击【加锁加密】单选按钮，单击【批量加密】按钮，完成加密，如下图所示。

【批量加密】对话框

解密的步骤如下：用户双击加密的文件，弹出【文件解密】对话框，用户在【输入密码】文本框中输入密码（试用版初始密码为空），单击【解密】按钮，弹出【易通文件夹锁】对话框，单击【确定】按钮，完成解密，如图所示。

文件解密

实例3：对磁盘进行加密

易通文件夹锁可以对磁盘进行加密，其操作的具体步骤如下。

1 在【易通文件夹锁】主界面单击【磁盘加密】选项，单击【加密磁盘】按钮，弹出【选择需要加密的磁盘分区】对话框，如图所示。

【易通文件夹锁】主界面

2 用户在【选择需要加密的磁盘分区】对话框的下拉列表中选择需要加密的磁盘，这里选择“H:\光盘驱动器”，单击【确定】按钮，完成加密，如下图所示，加密后的磁盘会隐藏，如下图所示。

【选择需要加密的磁盘分区】对话框

选择H盘进行加密

解密的步骤如下：在【易通文件夹锁】主界面单击【解密磁盘】按钮，如下图所示，弹出【易通文件夹锁】对话框，单击【是】按钮，完成解密，磁盘恢复，如下图所示。

【易通文件夹锁】主界面

【易通文件夹锁】对话框

12.3.3 “文件密使”的使用

文件密使是一个功能齐全易用的数据加密和安全通讯工具，能确保数据的安全，并提供了密码管理器。其文件夹加密功能既可以隐藏密文，也可以生成可自解密的文件。多样的加密方式很大程度上改变了以往加密工具单调枯燥的样式。为了保护通信的安全，文件密使新添加了即时安全通信工具“密聊”和USB移动存储器通用版本。

使用文件密使可以对任意格式的文件进行加密，而且操作简单。文件密使用的加密算法公开，加密过程安全可靠，对于刚刚接触加密的新手来说，是一个不错的选择。

文件密使使用的密码由256字节，任意两个字节不相等的字符串组成，用户输入的密码将自动、正确地变换成为用户要求的密码。密码被存到一个数组，从数组中得到一个16字节的子密码，再用子密码对要加密的文件数据进行异或等变换操作。

文件密使下载安装完毕之后，即可自动运行

文件密使主窗口中间是源文件和输出文件的路径，下面是【加密】、【解密】、【生成EXE】三个按钮。右侧的2个按钮分别用来选择加密单个文件和整个文件夹。

实例1：对指定的文件进行加密解密

这里对一个．mdf格式的文件进行加密和解密，具体的加密步骤如下。

1 用户运行文件密使应用程序，等待一段时间，即可打开文件密使运行的主窗口，如下图所示。

文件密使运行的主窗口

2 用户单击【源文件】输入框右边的打开文件按钮，即可弹出一个【打开】对话框，选择要加密的文件，单击【打开】按钮即可，如下图所示。

【打开】对话框

3 在【输出文件】文本框右边单击输出文件按钮，弹出【另存为】对话框，选择相同文件夹下的同一个文件，单击【保存】按钮，这样加密的结果将会自动覆盖源文件，如下图所示。

【另存为】对话框

4 单击【加密】按钮，即可弹出【密码和注释】对话框，这里提供了两种密码设置的方式，一种是单一式，直接输入密码即可加密；一种是提问式，以提问题的方式输入密码，这种密码方式更加安全（这里采用单一式密码），在【密码】文本框和【确认】文本框输入密码，如下图所示。

【密码和注释】对话框

5 在【密码和注释】对话框中，单击【密文注释】选项卡还可以给密文加上注释，密文注释是可选的，只是作为提示加密文件的加密信息，可以通过查看注释的方式查看到加密文件的注释，如下图所示。

【密文注释】选项卡

6 用户在输入密码之后，单击右下角的【确定】按钮，就可以完成对文件的加密了，如下图所示。

【确定】按钮

解密的具体操作步骤如下。

1 解密的前3步的步骤与加密过程的前3步一样。

2 把要解密的源文件和输出文件的路径都设好之后，单击【解密】按钮，即可弹出【密码和注释】对话框，如下图所示。

【密码和注释】对话框

3 在【密码和注释】对话框中的【密码】文本框和【确认】文本框中输入设置的密码之后，单击【确定】按钮，就可以完成对文件的解密了，如下图所示。

【密码和注释】对话框

实例2：对文件夹进行加密解密

这里选择一个文件夹进行加密，观察文件夹中加密后的内容之后，再对其进行解密恢复。具体的操作步骤如下。

1 用户单击【文件夹】输入框右边的打开文件按钮，即可弹出一个【浏览文件夹】对话框，选择要加密的文件夹，单击【确定】按钮即可，如图所示。

选择要加密文件夹

2 用户单击【加密】按钮，即可弹出【密码和注释】对话框，这里采用提问方式的密码输入，对输入密码的问题和答案一定要牢记，否则加密后的文件就不能解密了，如图所示。

【密码和注释】对话框

3 用户在【密码和注释】对话框中输入提问式的密码之后，单击【确定】按钮，即可实现对文件夹进行加密。文件夹被加密之后，里面的内容就看不到了。

实例3：以【生成EXE】的加密方式对指定文件进行加密

前面介绍的两个实例都需要在文件密使环境下才能实现对加密的文件进行解密，现在采用“生成．EXE”的方式进行加密，这样可以在没有文件密使的条件下进行解密，恢复原文件。

具体加密的操作步骤如下。

1 前3步与实例1的前3步相同，不再赘述。

2 用户在【文件密使】主界面设置好源文件路径和输出文件的路径，单击右下角的【生成EXE】按钮，即可弹出【生成自解密文件】对话框，如图所示。

【生成自解密文件】对话框

3 用户在【生成自解密文件】对话框中勾选【完成后删除源文件】复选框之后，单击【确定】按钮，即可弹出【密码和注释】对话框，如图所示。

【密码和注释】对话框

4 在【密码和注释】对话框中输入密码之后，单击【确定】按钮，就可以将被加密的文件加密成可自解密的EXE文件了，文件名为“mydata$mdf.exe”，在“D:\test3”文件夹下可以看到该文件的图标，如图所示。

可自解密文件的图标

用“生成EXE”的方式加密后的文件，还可以在没有文件密使的情况下进行自解密，解密的具体步骤如下。

1 用户双击自解压加密文件的图标，即可弹出【文件密使】对话框，单击【解密】按钮，即可弹出【密码】对话框，如图所示。

【密码】对话框

2 用户在【密码】对话框的【密码】文本框和【确认】文本框中输入加密时设置的密码之后，单击【确定】按钮，就可以完成对文件的解密了，如图所示。

【密码】对话框

12.3.4 “隐身侠”文件夹加密软件的使用

隐身侠文件夹加密软件是一款永久免费的加密/隐藏文件的软件，可以加密计算机硬盘、U盘、移动硬盘设备中的文件夹、视频、文档、程序等各种文件。隐身侠文件夹加密软件的具体使用步骤如下。

运行隐身侠文件夹加密软件，等待一段时间，隐身侠文件夹加密软件的主界面如图所示。

隐身侠文件夹加密软件的主界面

实例1：使用保险箱保护文件

隐身侠支持保险箱创建及加密备份、恢复功能，可创建 n 个保险箱，保险箱的容量不限，其保险箱的创建步骤如下。

1 用户单击【保险箱】按钮，再单击【新建保险箱】按钮，弹出【创建保险箱】对话框，如图所示。

【新建保险箱】按钮

2 用户在【创建保险箱】对话框中的【保险箱创建在】下拉列表中选择“D:”，在【保险箱容量】选项区域调节保险箱的容量，这里选择“0.01GB”，在【保险箱名称】文本框中输入保险箱的名称，这里为“test保险箱”，单击【开始创建】按钮，如下图所示，完成创建保险箱，如下图所示。

设置后的【创建保险箱】对话框

【加密】按钮

使用保险箱加密文件的具体步骤如下。

1 用户单击【加密】按钮，弹出【文件加密】对话框，在对话框中单击【添加文件】按钮，如下图所示，弹出【打开】对话框，选择需要加密的文件，单击【选择】按钮，如下图所示。

【文件加密】对话框

【打开】对话框

2 用户选择加密的文件后，单击【下一步】按钮，如下图所示，弹出【文件加密】对话框，单击【完成】按钮，实现加密，如下图所示。

【下一步】按钮

【文件加密】对话框

3 用户如果想查看保险箱中的文件，可以单击【查看】按钮，即可打开保险箱查看文件，如图所示。

查看文件

实例2：使用加密云盘加密文件

隐身侠提供了加密云盘加密文件的功能，其操作的具体步骤如下。

1 用户单击【加密云盘】按钮，再单击【加密上传】按钮，弹出【打开】对话框，如图所示。

【打开】对话框1

2 用户在【打开】对话框中选择需要上传的文件，单击【选择】按钮，如下图所示，即可完成上传。

【打开】对话框2

实例3：粉碎文件

隐身侠提供了粉碎文件的功能，可以对单个文件进行粉碎，文件粉碎后不可恢复，其操作的步骤如下。

1 用户单击【粉碎文件】按钮，如下图所示，弹出【粉碎文件】对话框，如下图所示。

【粉碎文件】按钮

【粉碎文件】对话框

2 用户在【粉碎文件】对话框中单击【选择文件】按钮，弹出【打开】对话框，如下图所示。

【打开】对话框

3 用户在【打开】对话框中选择需要删除的文件，单击【打开】按钮，文件打开后，单击【开始粉碎】按钮，如下图所示。

单击【开始粉碎】按钮

4 用户单击【开始粉碎】按钮后，弹出【粉碎文件】对话框，单击【确定】按钮，等待一段时间，弹出【粉碎文件】对话框，单击【确定】按钮，文件成功删除，如下图所示。

粉碎结束

12.4 FTP密码的探测

FTP（File Transfer Protocol，文件传输协议）是指可以进行文件传输服务的服务器，是Internet传统的服务之一。FTP使用户能在两个联网的计算机之间传输文件，它是Internet传递文件最主要的方法。通过FTP，用户可以获取Internet丰富的资源。

在Internet上使用FTP客户端下载文件时，可能会遇到某些FTP服务器需要输入用户密码验证后才能登录的情况。在这些情况下，若无有效的用户密码就不能登录该FTP服务器。

FTP的密码破解也就是破解FTP服务器的账户信息，通常的破解方式和电子邮件几乎一样，主要分为如下两种。

· 针对存在弱密码的账户进行弱密码探测（如使用网络刺客Ⅱ、流光等探测密码）。

· 针对专门的账户进行穷举探测。

在利用流光探测后，有的FTP服务器会记录用户的IP地址，如果在一段时间内，用户的连接请求大于一定的数量，服务器会自动拒绝用户的连接请求。因此，遇到这种邮件服务器，此种探测方式就会失效。

12.4.1 使用网络刺客Ⅱ探测FTP口令

网络刺客Ⅱ是一款功能强大，并且完全免费的网络检测工具，主要包括的工具有：网络嗅探器，共享资源扫描，在线弱口令检测以及提供一些常用的网络工具，能够帮助用户检测互联网上的主机安全。它也是一个功能强大的破解工具，它能破解包括FTP和POP3等多种协议的网络密码。它支持多线程，可以方便地设置密码字典，并且使用起来也比较方便。

假设要破解一个FTP地址为ftp.music.net的FTP服务器，具体的破解步骤如下。

1 用户运行网络刺客Ⅱ，等待一段时间后，网络刺客Ⅱ运行的主界面如图所示。

网络刺客Ⅱ运行的主界面

2 用户在破解FTP时，首先选择【猜解机】→【FTP猜解机】菜单项，即可弹出FTP猜解机对话框，如图所示。

【FTP猜解机】菜单项

3 用户在FTP猜解机对话框的【目标地址】文本框中输入要破解的FTP地址（如ftp.music. net）之后，选择线程数目为30（可根据你的机器性能和网络速度调节），在【端口】文本框中输入端口号“21”，在网络协议里选择【FTP（文件服务）】单选按钮，如图所示。

设置后的FTP猜解机对话框

4 单击【字典设置】按钮，即可弹出【字典设置】对话框，在其中可设置密码字典的格式，例如这里选择【穷举法】复选框，在【字符集】选项中选择使用数字、小写字母和特殊字符组成的所有8位以内的字符串进行穷举，在【密码长度】文本框输入最小为1，最大为8，如下图所示。

【字典设置】对话框

5 选择【手工用户名字典】选项卡，在文本框中可以手工输入想破解的FTP用户名，比如输入administrator、pub、hide等，如下图所示。

【手工用户名字典】选项卡

6 另外，如果不知道该FTP的用户名，也可以单击【读取】按钮，弹出【打开字典文件】对话框，选中文件，单击【打开】按钮，从外部文件读入用户名，如下图所示。

【打开字典文件】对话框

7 在选择好密码字典的格式和用户名之后，在【字典设置】对话框中单击【确定】按钮。单击【综合设置】按钮，即可弹出【综合配置】对话框，如下图所示。

【综合配置】对话框

8 在【综合配置】对话框中，可以设置代理服务器的参数，当然也可以选择不使用代理服务器。在设置完毕之后，单击【开始猜解】按钮，即可开始破解。

9 现在系统启动30个线程同时穷举破解，直到破解到密码为止，用户可以单击【停止破解】按钮终止破解，单击【保存进度】按钮保存破解进度，如下图所示。

系统启动30个线程同时穷举破解

12.4.2 “流光”探测FTP口令

流光软件具有分析目标主机的信息的能力，可以扫描目标IP地址（主机）或IP段是否有典型开放的端口，然后根据这些所开放的端口进行渗透入侵等操作。它支持探测POP3、FTP、HTTP、PROXY、FROM、SQL、SMTP、IPC等各种漏洞，并针对各种漏洞设计不同的破解方案。下面就详细介绍一下如何使用流光探测FTP口令的方法。

具体的操作步骤如下。

1 把将要破解的FTP站点加入到FTP主机下面，加入方法和POP3主机加入方法一样，右键单击，选择【编辑】→【添加】菜单项，如下图所示，弹出【添加主机（FTP）】对话框，在文本框输入主机IP地址，单击【确定】按钮即可，如下图所示。

【添加】菜单项

【添加主机（FTP）】对话框

2 添加需要破解的用户名文件，由于是弱密码破解，用户名文件要包含很多用户名，只有这样才能增加破解的成功率。右键单击，选择【编辑】→【从列表添加】菜单项，如下图所示，弹出【打开】对话框，选择Normal.dic文件，单击【打开】按钮，如下图所示。

【从列表添加】菜单项

【打开】对话框

3 右键单击FTP主机，选择【编辑】→【从列表添加】菜单项，如下图所示，弹出【打开】对话框，密码文件还是选择流光自带的弱密码文件password.Dic，单击【打开】按钮，如果觉得里面的密码太少，可以按照字典文件的格式添加进去，如下图所示。

【从列表添加】菜单项

【打开】对话框

4 用户名文件和弱密码文件添加完毕，单击菜单中的【探测】→【标准模式探测】菜单项，探测就开始了，如图所示。

【标准模式探测】菜单项

12.4.3 实例：流光针对专门的账户进行穷举探测

采用小榕的流光5.0工具软件对专门的账户进行穷举探测，具体操作步骤如下。

1 加入将要破解的主机地址，方法和弱密码探测一样。

2 添加用户名文件，此时要破解的仅仅是几个用户，因此不用添加列表文件。选择【编辑】→【添加】菜单项，依次添加完所有用户即可，如图所示。

【添加】菜单项

3 密码文件可以采用方案形式，也可以采用独立的字典文件形式，方案形式节约空间，独立的字典文件形式节约时间，可以任意选择，但不能混合使用。

4 上面的工作做完之后，选择【探测】→【标准模式探测】菜单项，探测就开始了，如图所示。

【标准模式探测】菜单项

穷举法探测需要大量的时间，并且不一定能成功，不过从理论上来说，只要有足够的耐心，一般都是可以探测成功的。

利用流光探测FTP服务器密码通常速度较快，个别出现速度慢的原因一般是网速太低，网络流量带宽有限，建议减少探测所用的线程数量，方法是选择【选项】→【系统设置】菜单项，弹出【系统设置】对话框，然后调低线程数，如下图所示。

【系统设置】对话框

同时，选择【选项】→【连接选项】菜单项，弹出【连接方式】对话框，选择正确的连接方式即可，如下图所示。

【连接方式】对话框

12.5 多媒体文件加密工具的应用

随着计算机功能的增强，多媒体技术应运而生，越来越多的多媒体软件进入人们的日常生活，像各种图像文件、音频文件和视频文件等。如果想在私密场合使用或传输这些文件，就要涉及加密问题了。

大家可能会经常使用压缩工具来对多媒体文件进行简单加密，但这样显然达不到安全要求，而且每次使用都会有压缩时间和空间的限制，非常不方便。这里介绍几款非常方便好用的多媒体文件加密工具，这些加密工具使用方便、功能强大，非常适合多媒体文件的加密。

12.5.1 CryptaPix加密软件的使用

CryptaPix是一款免费软件，专门针对图片软件进行加密。它能加密多种图像格式，并且在具备较好的图片浏览功能的同时，还提供了图像处理和图像格式转换等一些功能。

在下载CryptaPix之后，直接运行就可以安装了，安装过程非常简单，按照提示一步一步进行安装即可。

CryptaPix运行后的主窗口大致由左右两部分组成，左边为控制窗口，右边为显示窗口，如图所示。

CryptaPix运行主窗口

下面介绍一下CryptaPix的工具栏上的主要按钮及作用，如图所示。

CryptaPix的工具栏

：添加文件到收藏夹。

：添加或移动当前选中文件到目标文件夹。

：加密当前选中的文件。

：设置多项功能加密。

：打印选中的文件。

：开始自动播放幻灯片。

：停止播放幻灯片。

：设置用户优先权。

：放大或缩小图片。

：从当前列表中选择文件。

：选择视图方式。

使用CryptaPix对图片进行加密的具体操作步骤如下。

1 打开CryptaPix应用程序主窗口，在左边的资源管理器中选择要进行加密的图片，如图所示。

资源管理器

2 用户单击工具栏上加密按钮，即可弹出加密设置对话框，如下图所示。

加密设置对话框1

3 用户在加密设置对话框的【Password】文本框中输入密码，在【Confi rm Password】文本框中输入确认密码之后，单击【OK】按钮，弹出【Information】对话框，单击【OK】按钮，则所选图片就被加密了，如下图所示。

加密设置对话框2

12.5.2 Private Pix加密软件的使用

Private Pix提供了简单易用的界面来对图片进行管理、加密和浏览，让用户在查看图片文件的同时还能对图片进行加密，并且具有两种类型的加密方式。它支持加密的多媒体文件格式有：JPEG、BMP、GIF、AVI、MOV、MPG、MP3和WAV，帮助用户管理自己的图片和媒体文件。

Private Pix是一款功能强大的多媒体加密工具。一开始的Private Pix版本只能对图片文件进行加密，而新版的Private Pix也支持对音频文件或视频文件进行加密，为用户提供了更全面的功能。

1.Private Pix的特性

Private Pix主要有以下几个优点。

① Private Pix支持多种多媒体文件，包括的图片格式有JPEG、BMP、GIF等，音频格式有MP3、WAV等，视频格式有MOC、MPEG、AVI等。

② 它具有图片浏览、幻灯片浏览、音频和视频播放功能，操作简单。

③它还支持高达60位的非线性加密，为文件提供较高的安全性。

但Private Pix也有不完美的地方。

① 视频播放功能很不稳定，不支持扩展名为．rm格式的视频文件。

② 软件为共享软件，没有注册的版本，限制也较多。

2.Private Pix的运行和功能简介

用户运行Private Pix，等待一段时间，Private Pix运行主窗口如图所示。

Private Pix加密工具运行主窗口

Private Pix工具栏

Private Pix加密工具主要由两个窗口组成，左边是显示窗口，右边是控制窗口。

下面对Private Pix的工具栏做一下介绍。

：新建文件夹。

：复制文件。

：发送到设备。

：移动所选的文件到指定的文件夹。

：文件重命名。

：删除所选的图片。

：加密所选的图片或媒体文件。

：解密所选的图片或媒体文件。

：是否运行或暂停全屏幻灯片浏览。

：上一个文件。

：下一个文件。

：显示或隐藏幻灯片。

：切换全屏视图与普通视图。

：刷新文件和显示窗口。

：打开图片。

：关闭图片。

：关于软件。

：访问软件网站。

右边的控制窗口除了“Home Page”页面，还有一个【Settings】选项卡，拖动滑块，可以看到有3组参数，分别是Viewer Settings、Thumbnail Settings、Encryption Settings、Hide In Picture and Music、Administration Settings，它进行的是程序运行参数和加密时的设置，分别支持Media stealth、Fast encryption、DES encryption三种加密类型，如图所示。

【Settings】选项卡

· Piewer Settings主要用于设置图片显示的格式，选择是否居中和是否放大等。

· Thumbnail Settings主要用于设置应用程序运行参数和登录口令（登录口令是运行应用程序时需要输入的口令，默认的是private，也可以进行修改）。

· Encryption Settings主要是设置加密参数。

3．使用Private Pix对文件进行加密的实例

下面用一个例子来介绍一下如何使用Private Pix加密工具对一幅图片进行加密。先设定一个密钥为test之后，再使用这个密钥把加密的图片解密。

具体的操作步骤如下。

1 打开Private Pix应用程序主窗口之后，在左边窗口的资源管理器中选择要加密的多媒体文件。如果不设置密钥，则使用默认密钥，如图所示。

资源管理器

2 因为这里要设置密钥，所以选择【Settings】选项卡，如图所示。

【Settings】选项卡

3 在“Encryption Settings”选项下面单击“Filename Encrypt Key”中的按钮，即可弹出【Enter Password】对话框，该口令为“123”，如图所示。

按钮

【Enter Password】对话框

4 在【Enter Password】对话框的文本框中输入当前密码，单击【OK】按钮，即可弹出【Change Filename Key】对话框，输入新的密钥test之后，单击【OK】按钮即可，如图所示。

【Enter Password】对话框

【Change Filename Key】对话框

5 单击工具栏的加密按钮或按快捷键<F8>之后，所选的文件就可以被加密了。

6 解密方法与加密方法类似。用户需要先选择要进行解密的文件，如果要解密的文件与当前密钥不一样，则需要先修改当前的密钥或单击工具栏上的解密按钮或按<F9>快捷键，这样，被加密的文件就可以被恢复到原状了。

12.5.3 WinXFiles加密软件的使用

WinXFiles加密软件是为图片文件加密的专业加密工具，除了具备对图片加密的基本功能外，还具有图片浏览、自动幻灯片播放和图片缩略图显示等多种功能。WinXFiles图片加密原理是根据设置的密码，使用Blowfish加密算法对图片内容进行加密和修改，加密后的图片文件会自动生成后缀名为“.XFB”的WinXFile格式文件。所以，在对XFB格式的图片进行查看之前，就必须输入正确的密码才行。

WinXFiles运行主窗口如图所示。左边的文件管理器主要有3个区，上面部分是文件过滤器（在这里可以选择文件类型），中间部分是树状文件夹列表（在这里选择文件所在路径），下面部分是具体的图片文件名称列表。

WinXFiles运行主窗口

勾选WinXFiles主窗口左下方的【预览要加密的图片】复选框，即可实现对图片进行预览，如图所示。

【预览要加密的图片】复选框

右边工作区中分别有【加密/解密】、【查看】、【多个查看】、【信息】等4个选项卡，其中【加密/解密】是主工作区，【查看】是加密图片预览区，【多个查看】是加密图片的多视图浏览，【信息】是软件的产品信息。

右边工作区

在WinXFiles主窗口中默认为【加密/解密】选项卡，【加密/解密】选项卡下有4种状态选择，分别是加密、解密、放映幻灯和擦除，如图所示。

【加密/解密】选项卡

在工作区的左侧有4个用来添加或删除选到工作区文件的按钮，具体作用分别如下。

：清除工作区的所有的文件。

：添加一个左边文件区选中的文件到工作区。

：添加左边选中的文件夹的所有文件到工作区。

：移除工作区的一个选中的文件。

下面以一个实例来具体讲解一下如何使用WinXFiles对图片进行加密解密。

对图片加密的具体操作步骤如下。

1 运行WinXFiles应用程序，即可打开WinXFiles主窗口。

2 在左侧的树状文件夹列表中选择“E:\ppt宝藏”文件夹之后，单击右侧的【加密/解密】选项卡，选中【加密】单选按钮。

树状文件夹列表

3 单击右侧工作区的【全部添加】按钮，即可弹出【PEPSOFT WinXFiles Blowfi sh v5.0】对话框，单击【YES】按钮，如下图所示，左侧文件夹的图片就被添加到右侧的工作区，如下图所示。

【PEPSOFT WinXFiles Blowfish v5.0】对话框

添加文件到工作区

4 单击工作区右下角的【确定】按钮，如下图所示，即可弹出一个【PEPSOFT WinXFiles Blowfi sh v5.0 *加密*】对话框，在其中【密码】文本框输入密码（要求密码长度在10～56之间）之后，再在【重复确认】文本框中输入确认密码，如下图所示。

【确定】按钮

【PEPSOFT WinXFiles Blowfish v5.0 ＊加密＊】对话框

5 单击【OK】 按钮，即可完成对所选文件夹中的所有图片进行加密。图片经过加密之后，就变成了扩展名为．XFB的WinXFiles文件。

图片解密的具体步骤如下。

1 在WinXFiles工作区右侧【加密/解密】选项卡中，选择【解密】单选按钮之后，单击【全部添加】按钮，如下图所示，即可弹出【PEPSOFT WinXFiles Blowfi sh v5.0】对话框，如下图所示。

WinXFiles工作区

【PEPSOFT WinXFiles Blowfish v5.0】对话框

2 单击【YES】按钮，即可把所有加密图片都添加到解密工作区，如图所示。

解密工作区

3 单击工作区右下角的【确定】按钮，即可弹出【PEPSOFT WinXFiles Blowfi sh v5.0 *解密*】对话框，如下图所示。

【PEPSOFT WinXFiles Blowfish v5.0 ＊解密＊】对话框

4 在【PEPSOFT WinXFiles Blowfi sh v5.0 *解密*】对话框的【密码（10到56个字符）】文本框中输入密码之后，单击【OK】按钮，就可以把文件夹“E:\ppt宝藏”中所有的加密图片进行解密了，如下图所示。

设置后的【PEPSOFT WinXFiles Blowfish v5.0 ＊解密＊】对话框

12.6 小结

通过对本章的学习，读者可对目前常用的多媒体加密工具进行一些了解，可以使用探测工具对FTP密码进行探测，并掌握这些工具的基本用法。在本章中还介绍了数据库密码的破解，宏加密解密的使用，多功能文件加密工具的使用，这几款加密工具功能都非常强大，操作方法也非常简单。一般情况下，用户在对需要的文件进行加密时，都可以选择合适的加密工具轻松完成。我们要经常更换使用的加密工具，增强保护隐私的意识。

技巧与问答

❖ 为什么多媒体加密非常重要？

数字媒体文件易于复制和分发，同时还不会降低质量。因此，现在数字媒体文件借助Internet这一平台通过授权和未授权的分发渠道得到广泛传播。当缺少安全措施来保护内容时，盗版问题就不可避免地出现。通过多媒体加密，内容提供商可以保护自己的内容并控制分发。内容提供商可通过为每个数字媒体文件创建许可证来管理有关的权限。许可证注册过程也为这些公司提供了许多重要的客户信息。此类信息有助于内容提供商更了解自己的客户。采用可靠的DRM系统之后，可确保向用户不断提供各种各样高质量的音频和视频内容。

❖ 在Win7使用文件密使个人版V1.1结果出现了问题，文件无法恢复，一点“确认还原该文件夹下所有文件”就出现“无法操作，请确保被加密文件夹名称未被更改”，看来这种解密方式对Win7无效，有什么好办法可以解决？

① WinRAR压缩软件可以作为一个文件管理器使用，而且隐藏文件会直接显示出来，还会显示详细的文件夹，在遇到较多的隐藏文件夹的时候，使用WinRAR直接进入文件夹来查看。

② 用“FolderSniffer V3.51 ”搜索文件，可以解决一般的伪加密手段。

③ DISKGEN也可以查看到被隐藏、伪装的文件。

④ 进入PE环境尝试。

❖ 我用超级加密精灵加密了一个文件夹，可是它到时间要求注册，我就把超级加密精灵给删掉了，但是我加密的文件还是处于加密状态打不开，应该怎样将文件打开？

先备份好你这个文件，重新安装这个软件。不行就重新下载安装软件，把密码打开。如果还不行，你用另一个计算机下载安装这个软件，把文件拷过去，尝试打开该文件。

❖ 我加了宏命令在Word中，但是忘记密码，所有的文件都打不开，怎样删掉命令，或有好的解码软件吗？

依次点击 【工具】→【宏】→【管理器】→【宏方案项】→在Normal中选中你的那个宏，删除。这样设置后你以后新生成的文件就不会自动加密了。另外，加了密的文档，你要想使用的话，到网上找下破解工具。

❖ 隐身侠保险箱不见了可能存在的几种情况？

① 请确认您是否格式化保险箱所在的磁盘或全盘格式化，由于保险箱的关联文件是存在于保险箱所在盘符，格式化后关联文件会被删除了，会导致保险箱丢失。

② 请确认您是否有多个隐身侠账号，用A账号加密的文件是不能被B账号打开的。

③ 请确认您的保险箱是否是在当前计算机上创建的，在A计算机上创建的保险箱，在B计算机上是无法打开的。（若保险箱是创建在U盘或移动硬盘上面，则不存在前面的问题，只要将U盘或移动硬盘连接上计算机就可以查看了。）

第13章
光盘的加密解密技术

光盘由于具有存储方便和存储量大的优点，在实际生活中被越来越多的用户所使用，用户一般用光盘刻录一些传统的资料，也可以备份一些重要的资料，但在备份重要资料时，要注意不要被他人窃取，这时光盘加密就逐渐受到重视，光盘加密技术也快速发展起来。

13.1 认识光盘的加密与解密技术

13.1.1 光盘加密技术

目前，加密刻录技术及相关产品大体可分成两大类：① 纯软件处理；② 软硬结合。其中，纯软件处理又可以分成目录/文件隐藏、目录欺骗和第三方软件三类。

1．目录/文件隐藏

所谓目录/文件隐藏，其原理早在MS-DOS时代就为人所熟知——通过修改目录/文件属性字节，使其隐藏。只不过由于光盘刻录的特殊过程和Windows 9X等系统的磁盘读写特征，使得这一操作的对象变成了欲刻数据的映像文件，即常见刻录工具使用的．cue、.cif等文件。具体操作时，通过UltraEdit、WinHex等十六进制编辑器分析并定位对应的属性字节，将它改成“04”，存盘后再刻录。

2．目录欺骗

所谓目录欺骗，是利用映像文件的特殊结构，修改其中欲保密目录的实际显示名称，使它与实际操作名称不一致，造成文件访问异常，给人以“该盘有问题”或“系统有问题”的假象，迷惑他人。并且，这种方式常常与目录隐藏结合运用，增强保密效果。

3．第三方软件

第三方软件加密的基本方式为：对原始数据在刻录之前或者刻录的同时进行重新编码或者加壳处理，将解密程序置于光盘Autorun中，只有密码验证通过后，才能看到原始数据。否则，即使能够浏览光盘，所见到的也只是经过处理的“加工品”。至于具体的实现手法，则是“八仙过海，各显神通”。目前市面上流行多款加密刻录软件，如CD-Protector、SecureBurn和CryptCd等。

至于“软硬结合”，又可分成三类。

一类是通过诸如判断主板BIOS、设置硬盘加密点等手段来决定光盘信息是否可用，但这样势必降低光盘的通用性，所刻光盘的实用价值会大打折扣。因此，这类技术对于普通用户来讲，没有太大意义，市面上这类产品也很少。

另一类就是利用“超刻录”。所谓“超刻录”，有两种理解，一种是指超过标称值的刻录，标称值通常指标准值：74MIN@650MB和80MIN@700MB。由于光盘的外圈直径大、弧度大，这样就给刻录坑槽留下许多可利用的物理空间，于是在外圈进行加密刻录，设置加密信息点，既能增加容量，又能起到光盘加密的作用。由于超刻部分完全在光盘的外圈进行，坑槽位间距、位深不低于橙皮书规定的最小限制，因此理论上不会影响光驱的正常读取；另一种理解是指符合橙皮书2（CD-R盘片规格标准协议书）规定的大容量光盘刻录，比如90MIN@800MB以及99MIN@870MB的超刻录功能。超刻需要刻录机和软件的支持，对CD-R盘片的品质有一定的要求。

第三类是采用特殊的光轨写入方式，在光盘数据的存放形式上做文章。这种技术常用于实现光盘“防拷贝”。许多正版软件光盘不能复制，即使复制也不能使用就是这个原因。典型代表有：Free Lock、Safedisk等。如Safedisk主要通过验证数字签名、数据防护编码等手段实现光盘“防拷”。

13.1.2 光盘加密流技术

下面简单介绍几种常见的光盘加密流技术。

1．外壳保护技术

通常所说的外壳保护，就是在外壳程序中加入对软件锁或钥匙盘的验证部分。在网络流传的许多程序都是专门为加壳软件而设计的，主要作用就是反跟踪、保护代码和数据、保护程序的完整性。

2．光盘加密狗技术

光盘加密狗技术不需要特别制作母盘，也不需要改进母盘机，这种技术能通过识别光盘上特征对原版盘和非法拷贝盘进行区分。这里的特征是在光盘制作时就有了，同一张母盘制作出来的光盘特征是相同的，不同的母盘制作的光盘虽然内容是相同的，但特征一定不会相同，意思就是非法拷贝盘是无法提取和复制这种特征的。

3.CSS保护技术

CSS（Content Scrambling System，数据干扰系统）技术是将全球的光盘分为6个区域，对每个不同区域采用不同的保护技术，只有具备该区域解码器的光驱才能读取光盘中的数据。

使用该技术进行保护时，先要用编码程序来对光盘中所有信息进行处理，但要读取这些经过编码的数据，就需要先对这些数据进行解码了。

4.CPPH保护技术

CPPH保护技术也就是预录媒介内容保护技术，它通过在盘片的导入区域存放密钥的方法，来对光盘进行加密保护，这种保护技术的鉴定方案与CSS方案一样，所以无须改动现有的设备，非常实用。

5.DCPS保护技术

DCPS保护技术也就是数字复制保护系统技术，它不允许数字复制，只是让各个部件之间进行数字连接而已。

使用该项技术，用数字方式连接的设备，像DVD播放机和数字电视机或录像机，可以交换鉴定密钥建立安全的通道。DVD播放机对已编码的音频和视频信号进行加密保护，然后发送给接收设备，再由接收设备解密，这样做的好处是可有效杜绝那些未鉴定的已连接设备窃听信号。不保护那些无须复制的内容，新光盘、更新的密钥和列表的新设备也能获得保护。

6.CPRH保护技术

CPRH保护技术也就是录制媒介内容保护技术，它可以把媒介与录制联系起来。它对光盘进行保护的原理是：在空白光盘上有一个64比特盘片ID用于存放BCA（光盘的突发切断区）。

由盘片得到的56位密码对刻录到光盘上的内容进行保护，当读取光盘时，则要先从BCA中读取盘片之后，再生成盘片内容解密需要的密钥。如果光盘内容被复制到其他媒介，那么就找不到光盘ID或提示出错，数据根本就无法解密了。

7.CGHS保护技术

CGHS保护技术也就是内容复制管理技术，它主要用于阻止盗版光盘，可通过生成管理系统的方法对数字复制进行控制，通常是通过存放在每一张光盘上的一些特定信息来实现。

CGHS技术属于一种“串行”复制，生成管理系统既可以阻止母版软件的复制，也可以阻止对其子版软件进行再复制。而在允许复制的情况下，制作复制光盘的设备也要遵守规定，数字复制信息可经编码后输入视频信号，这样做就方便了数学设备的识别。

8.APS保护技术

APS保护技术也就是类比信号保护系统，它主要防止光盘到光盘的复制。这种保护技术关键是采用一个叫作Macrovision 7的芯片，用特殊信号影响光盘的复制功能，使光盘的图像产生横纹、对比度不均匀等。

当然，在使用计算机对光盘数据进行读取时，如果想通过显卡输出到电视上，则显卡一定要支持类比保护功能。否则，就将显示出错信息，而无法在电视上正常显示。

可以发现，现在大多数的软件都采用了加密技术来保护自己的数据安全，以免被不法分子非法复制，造成损失。事实上，截至目前，还没有哪一种软件的保护技术是绝对的安全，不同强度加密技术的使用也不过是相应地增加了软件的破解难度而已。

13.2 光盘映像ISO文件编辑

不论用户使用什么方法进行加密，首先要把加密的文件制作成影像文件。影像文件的制作有多种方法，可以使用直接生成ISO文件的刻录软件，也可以使用一个刻录软件把要加密文件刻录在CDRW盘，然后把该光盘制作为影像文件。

UltraISO软碟通（UltraISO下载）是一款光盘映像ISO文件编辑制作工具，软碟通UltraISO可以图形化地从光盘、硬盘制作和编辑ISO文件。

下面介绍一下使用UltraISO软碟通把文件转换为ISO影像文件的具体操作步骤。

1 打开UltraISO软碟通的程序，等待一段时间后，程序运行的主界面如图所示。

UltraISO软碟通程序主界面

2 用户可以在本地目录查找需要制作成ISO的文件的相关文件，如图所示。

本地目录

3 用户把需要制作的文件直接拖动到上方即可，其他文件查找完成之后，再次选择拖动至上方就可以了，如图所示。

拖动文件

4 单击【文件】→【另存为】菜单项，弹出【ISO文件另存】对话框，如图所示。

【ISO文件另存】对话框

5 用户在【ISO文件另存】对话框的【文件名】文本框中可以直接输入需要保存的文件名，在【保存类型】下拉列表中选择【标准ISO文件】选项，然后单击【保存】按钮即可，如图所示。

【ISO文件另存】对话框

13.2.1 使用光盘加密大师加密光盘

光盘加密大师是一款加密光盘制作工具。你可以用它可视化地修改光盘镜像文件（.ISO），将光盘镜像文件中的目录和文件特别隐藏，将普通文件变为超大文件，将普通目录变为文件目录。它是修改多种格式ISO文件系统的光盘工具，用户可以用它轻松制作自己的个性化加密光盘。下面介绍一下光盘加密大师的使用方法。

1 打开光盘加密大师的主程序，等待一段时间后，程序运行的主界面如图所示。

光盘加密大师的主程序

2 用户单击【文件】→【打开文件】菜单项或单击工具栏按钮，如下图所示，打开镜像文件，弹出【打开光盘镜像文件】对话框，选择要加密的ISO文件，单击【打开】按钮即可，如下图所示。

【打开文件】菜单项

【打开光盘镜像文件】对话框

3 用户打开文件后，单击【文件】→【光盘密码】菜单项或单击工具栏按钮，如下图所示，弹出【光盘加密】对话框，点选【首选项】→【写入解密程序cdrun.exe，并自动运行】单选按钮，并在【密码形式及密码】→【密码】文本框中输入光盘加密密码，单击【下一步】按钮，如下图所示。

【光盘密码】菜单项

【光盘加密】对话框

4 用户在单击【下一步】按钮后，弹出【确定写入光盘密码】对话框，单击【完成】按钮，如下图所示，弹出【光盘加密大师】对话框，提示光盘加密成功，单击【确定】按钮返回，如下图所示。

【确定写入光盘密码】对话框

【光盘加密大师】对话框

5 为了预防别人破解，建议选择【文件】→【镜像密码】菜单项或单击工具栏按钮，弹出【写入镜像密码】对话框，在【密码】文本框中输入密码，在【确认密码】文本框中再次输入密码，为ISO镜像文件写入镜像保护密码。单击【写入】按钮，如下图所示，弹出【光盘加密大师】对话框，提示镜像密码写入成功，这样即便是别人破解了光盘登录密码，如果没有镜像密码，同样无法使用光盘里的文件，如下图所示。

【写入镜像密码】对话框

【光盘加密大师】对话框

13.2.2 光盘加密软件（GiliSoft Secure Disc Creator）的使用

GiliSoft Secure Disc Creator是一款多功能的光盘刻录和光盘加密软件，它支持大多数CD/DVD。用户可以用密码保护刻录光盘DVD，可以刻录ISO到CD或DVD图像文件，它支持的ISO映像文件。GiliSoft安全光盘制作易于安装、运行和使用，并且具有友好的用户界面程序。

使用GiliSoft Secure Disc Creator制作加密ISO映像文件

1 打开光盘加密软件GiliSoft Secure Disc Creator，等待一段时间后，程序运行的主界面如图所示。

光盘加密软件GiliSoft Secure Disc Creator主界面

2 用户单击【Recorder】→【Make ISO Image File】菜单项，进行加密ISO镜像文件的制作，如图所示。

【Make ISO Image File】菜单项

3 用户单击【Add Files】按钮，如下图所示，弹出【Add Files】对话框，选择要制作镜像文件的文件，将文件拖到下列文本区，如下图所示。

【Add Files】按钮

【Add Files】对话框

4 用户单击【Make Image】按钮，如下图所示，弹出【GiliSoft Secure Disc Creator】对话框，在【PassWord】文本框中输入至少6位数的密码，在【Confi rm Password】文本框中输入确认密码，单击【OK】按钮，如下图所示。

【Make Image】按钮

【GiliSoft Secure Disc Creator】对话框

5 用户在上一步单击【OK】按钮后，弹出【Save as a ISO fi le】对话框，在【文件名】对话框中输入将要保存的文件名，单击【保存】按钮，弹出【GiliSoft Secure Disc Creator 7.0.0】提示框，说明加密镜像成功，如图所示。

【Save as a ISO file】对话框

13.2.3 IsoBuster Pro中文破解版的使用

IsoBuster Pro是一款专门提取ISO文件的工具，一般拷贝加密的影音光盘通常采用反编译软件去除加密代码或用nero保存轨道等方法，但对于一般新手可能望而生畏，而且有些双重加密的光盘用上述方法就更吃力了。本节介绍一款复制或提取加密光盘的内容的软件IsoBuster Pro，其使用方法如下。

1 运行IsoBuster Pro软件，等待一段时间后，IsoBuster Pro程序运行的主界面如图所示。

IsoBuster Pro程序运行的主界面

2 用户在运行时需要更改软件的语言，单击【Options】→【Language】菜单项，弹出【Options】对话框，在【Language】选项卡下选择【中文（繁体）】选项，单击【OK】按钮，即可将语言转换成中文，如图所示。

【Options】对话框

3 用户在提取加密的镜像文件时，单击【文件】→【打开】菜单项，弹出【打开】对话框，选择要提取的ISO镜像文件，单击【打开】按钮，如图所示。

【打开】对话框

4 用户选择要提取镜像文件中的内容，右键单击选定的内容，在弹出的下拉菜单中选择【抽取（E）.NET Framework.URL】菜单项，“Framework.URL”为抽取的文件名，弹出【抽取文件】对话框，在【文件名】文本框中输入“test4”，单击【保存】按钮，如图所示。

【抽取文件】对话框

13.2.4 加密DVD光盘的破解

现在一些DVD光盘上的内容都有保护，不允许用户自己复制文件，在播放时不使用计算机上的播放器，而是利用光盘自带的播放器。要想将其复制到自己的硬盘上，就需要使用一些方法去破解了，具体的操作步骤如下。

1.DOS模式复制文件

1 用户单击按钮，在【搜索程序和文件】文本框中输入“CMD”，按【Enter】键，进入到DOS模式，如图所示。

【搜索程序和文件】文本框

2 用户在DOS模式下输入“H:”进入H盘，如下图所示。

进入H盘

3 用户在DOS模式下输入cd styles命令，即可进入H:\styles文件夹，如下图所示。

进入H:\styles文件夹

4 用户在进入styles文件夹后，在DOS模式下输入dir命令，即可显示styles文件夹下的文件，如下图所示。

styles文件夹下的文件

5 用户在DOS模式下输入“copy panel_dili.swf E:”（将panel_dili.swf复制到E盘），这样就可以复制加密光盘上的文件，如下图所示。

将panel_dili.swf复制到E盘

2.IsoBuster Pro提取媒体文件

1 用户打开IsoBuster Pro的程序，等待一段时间，程序运行的主界面如图所示。

IsoBuster Pro的程序主界面

2 用户单击【SW3GB】按钮，在右边的文本区内双击选择【styles】文件夹，进入文件夹内部，如图所示。

【styles】文件夹

3 用户在【styles】文件夹内部，选择panel_dili.swf文件，右键单击，弹出下拉菜单，在下拉菜单中单击【抽取（E）panel_dili.swf】菜单项，弹出【抽取文件】对话框，在文本框中输入文件名，单击【保存】按钮，成功抽取媒体文件，如图所示。

【抽取（E）panel_dili.swf】菜单项

3.UltraISO软碟通打开媒体文件

1 用户打开UltraISO软碟通的程序，等待一段时间，UltraISO软碟通程序运行的主界面如图所示。

UltraISO软碟通程序主界面

2 用户在UltraISO软碟通的程序主界面单击【SW3GB（H:）】目录，即可打开【SW3GB（H:）】磁盘的文件，如图所示。

【SW3GB(H:)】目录

3 用户将【SW3GB（H:）】磁盘的【styles】文件夹拖到UltraISO软碟通的文件操作区，如图所示。

UltraISO软碟通的文件操作区

4 用户在文件操作区中双击【styles】文件夹，显示文件夹的内容，右键单击【panel_dili. swf】文件，弹出下拉菜单，在下拉菜单中选择【提取到】菜单项，弹出【浏览文件夹】对话框，选择保存位置，单击【确定】按钮，完成复制文件，如图所示。

【提取到】菜单项

13.2.5 隐藏文件夹、超大文件的制作

通常情况下，隐藏文件夹是指使用DIR/A看不到，工具软件也找不到，但使用命令“CD目录名”能进去查看的文件夹。而超大文件则是指文件超出一张CD容量限制的文件。这些技巧在一些光盘制作者中很多见，它的制作方法也就是修改ISO文件而已。

1．用UltraISO软碟通制作隐藏文件夹

1 用UltraISO软碟通打开ISO镜像文件，UltraISO软碟通的程序主界面如图所示。

UltraISO软碟通的程序主界面

2 用户在文本操作区中选择【SoftSnoop】文件夹，单击【操作】→【隐藏】菜单项，即可隐藏文件夹，如图所示。

【隐藏】菜单项

3 用户将文件夹进行隐藏后，再用UltraISO软碟通打开该ISO镜像文件，【SoftSnoop】文件夹变成隐藏模式，如图所示。

【SoftSnoop】文件夹

2．使用光盘加密大师制作隐藏文件夹

1 用户打开光盘加密大师，等待一段时间后，打开ISO镜像文件，光盘加密大师程序运行的主界面如图所示。

光盘加密大师程序运行的主界面

2 用户单击【编辑】→【隐藏选定目录】菜单项或单击工具按钮，将【SoftSnoop】文件夹进行隐藏，如图所示。

【隐藏选定目录】菜单项

3 用户对选定的文件进行隐藏后，【SoftSnoop】文件夹在光盘加密大师中显示，如图所示。

【SoftSnoop】文件夹

3．使用光盘加密大师制作超大文件

1 用户打开光盘加密大师，等待一段时间后，打开ISO镜像文件，光盘加密大师程序运行的主界面如图所示。

光盘加密大师程序运行的主界面

2 用户选择要操作的文件，单击【编辑】→【变为超大文件】菜单项或单击工具栏按钮，如图所示。

【变为超大文件】菜单项

3 用户将文件变成超大文件后，【LordElf.exe】文件在光盘加密大师中显示的情况如图所示。

【LordElf.exe】文件

13.2.6 用光盘隐藏文件查看器对付隐藏文件夹的加密光盘

光盘隐藏文件查看器是光盘隐藏文件的终结者，当遇到CD或者教学视频复制不到计算机的时候，可使用此软件查看到光盘里隐藏的内容。下面介绍一下它的使用方法。

1 使用UltraISO软碟通打开ISO镜像文件，发现该镜像文件中缺少【SoftSnoop】文件夹，如图所示。

UltraISO软碟通程序主界面

2 用户使用光盘隐藏文件查看器查看该镜像文件，光盘隐藏文件查看器的程序主界面如图所示。

光盘隐藏文件查看器的程序主界面

3 用户单击工具栏按钮，弹出【打开】对话框，选择光盘映像文件test5，单击【打开】按钮，即可打开文件，如图所示。

【打开】对话框

4 用户可以在光盘隐藏文件查看器程序主界面中查看到隐藏的文件夹【SoftSnoop】，如图所示。

光盘隐藏文件查看器程序主界面

13.3 小结

本章主要介绍了有关光盘的加密解密技术，ISO镜像文件的制作，使用文件加密大师加密光盘，光盘加密软件的使用，是为了使读者更透彻地了解加密解密方面的知识。除此之外，还介绍了隐藏文件和超大文件的制作，以及加密DVD光盘的破解，用光盘隐藏文件查看器查看隐藏文件，便于读者对本章内容进行深入学习和丰富加密解密知识。

技巧与问答

❖ 如何复制DVD光盘的加密视频？

（1）隐藏目录

最简单的加密方法是把视频DAT文件的属性设置为“隐藏”,VCD一放入光驱，大部分播放软件即开始搜索此类文件，经过加密后播放软件就无法播放了，而且进入光盘目录也不会显示这些文件，所以从光驱直接把DAT文件复制到硬盘上也是不可行的。

破解方法：

进入光盘目录，点击上面菜单栏的【工具】→【文件夹选项】→【查看】，在【高级设置】里选择显示所有文件和文件夹，这样光盘目录下的DAT文件就无处可藏了。

（2）自运行程序隐藏光盘

这种新型的VCD为了在一定程度上防止盗版，特别制作了一个程序，若把这个VCD放入电脑光驱，此程序会自动运行对光碟进行屏蔽，不能被所有的主流播放器认识，故不能播放。

破解方法：

就是在第一次放入此类光盘时，必须先按键盘上的<Shift>键，禁止光盘自动运行，这个屏蔽程序就不能安装。记住，只有一次机会，若是第一次放入此类光盘时，让屏蔽程序无声息地安装了，就无法破解了，此程序在添加/删除程序中是看不见的。笔者建议可以关闭光驱属性中的自动插入提醒，这样就不必每次按<Shift>键手动操作了。

❖ 如何读取光盘上的隐藏文件？

（1）要用到虚拟光驱软件和十六进制编辑器（如UltraEdit、WinHex）

用虚拟光驱软件把加密光盘做成虚拟光碟文件，进度到1%的时候就可以按<Ctrl+Alt+Del>组合键强行终止虚拟光驱程序的运行。

用十六进制编辑器打开只做了1%的光碟文件（后缀名为vcd或fcd的文件），在编辑窗口中上下查找任意看得见的目录名或文件名（由于文件不大很容易找到的），在该位置的上下就可以看见隐含的目录名或文件名了（一般是目录名）。

在MS-DOS窗口下用CD命令进入看到的那个目录，再Dir一下就可以看见你想要的了，此时就可以选择运行还是复制文件了。

（2）用UltraEdit等十六进制编辑器直接找到序列号

运行UltraEdit，用它打开光盘根目录下的SETUP.EXE，然后点击菜单上的“搜索”→“查找”，在弹出的对话框“查找什么”栏中填入“请输入序列号”，注意要将多选框“查找ASCII字符”勾选上，按<Enter>键，在找到的“请输入序列号”后面，接下去的数字就是序列号了。

（3）用IsoBuster等光盘刻录软件直接浏览光盘上的隐藏文件

运行IsoBuster，选择加密盘所在的光驱，点击选择栏旁边的刷新按钮，此时它就会读取光驱中的文件，这时你就会发现在左边的文件浏览框中多出一个文件夹，那里面就是你真正想要的文件。这时你就可以运行或复制这些文件了。

第14章
网络验证技术应用

随着网络越来越普及，出现的网络安全问题也随之增多，网络验证技术进一步发展。所谓网络验证，是指把软件的注册验证算法放到网络服务器上的一种验证方法，软件作者端负责与服务器通信取得验证的结果。这样做的优势是解密者不能再对注册验证算法进行分析解密，同时软件作者也可以及时了解到自己所编写的软件被用户使用后的反馈情况，有利于对软件进行有效的控制，从而防止软件盗版，保护软件作者的合法权益。而如何保证以数字身份进行操作的操作者就是这个数字身份合法拥有者，也就是说保证操作者的物理身份与数字身份相对应，身份认证技术就是为了解决这个问题应运而生的。

14.1 本地服务器验证加密技术

本地服务器验证就是把软件作者自己使用的计算机作为一台服务器，采用C/S结构的方式去对软件注册认证。软件作者可以自己设置在客户端和服务器端进行注册码验证的协议，还可以对数据包进行加密，来保证数据的安全性。注册验证过程对数据准确性的要求极高，因此，在进行注册验证时就不能使用不稳定的UDP，最好采用安全的TCP/IP来设计网络通信。

本地服务器验证的实现方案如下。

① 要进行注册验证的用户先填写用户名和注册码，并把客户端连接到服务器上，如果网络不通，将会提示用户先要连通网络，才能继续。

② 在连通网络之后，把用户名、硬件系列号、注册码等，一起从客户端发送到服务器端。服务器端在收到客户端传过来的数据包之后，再对这些注册信息进行验证。如果注册信息是正确的，将返回给客户端一个验证成功的数据包；如果注册信息不正确，将会给客户端返回一个验证失败的数据包。

③ 客户端收到服务器端返回的数据包之后，查看返回的是验证成功数据包还是验证失败数据包。如果返回验证成功的数据包，则表明用户注册码是正确的；如果返回验证失败的数据包，则表明用户注册码是错误的。

14.1.1 服务器端加密实现

不但注册码验证算法模块需要在服务器端来实现，同时，服务器端还要对客户端发送过来的数据包进行分析。对于是注册验证的数据包，将会调用注册码验证算法模块来判断是否通过了注册验证，并把验证的结果返回给客户端。客户端还要做反跟踪保护和程序完整性校验的工作，在比较重要的地方用加入花指令的方法，以加大解密的难度。

服务器端完整的示例代码如下。

        Uses  Base64Unit;
        //分解字符串
        Function  SubStrings(RootStr, ChildStr:String):TStringList;
        Var
        iTemp:integer;
        Begin
        Result:=TStringList.Create;
        iTemp:=pos(ChildStr, RootStr);
        While  iTemp>0  do  begin
        If  iTemp>1  then  result.Append(copy(RootStr,1, iTemp-1));
        Delete(RootStr,1, iTemp+length(ChildStr)-1);
        iTemp:=pos(ChildStr, RootStr);
      End;
      If  RootStr<> " " then  result.Append(RootStr);
      End;
      Procedure  TForml.Button1Click(Sender:TObject);
      Begin
      //服务器端口
      ServerTCP.Port:=6666;
      //开始监听
      ServerTCP.Listen:=True;
      If  ServerTCP.Listen  then
      Labell.Caption:="服务器已开始监听！"
      Else
      Labell.Caption:="服务器已关闭监听！"
      End;
      Procedure TForml.ServerTCPClientRead(Sender:TObject; Client:
    TSimpleTCPClient; Stream:T Stream);
      Var
      TempStr, Flag, UnStr, SLStr, RGStr:String;
      Str:TStrings;
      Flag1, Flag2:String;
      Begin
      SetLength(TempStr, Size);
      Read(TempStr[1], Size);
      End;
      Showmessage(Base64Decryption(TempStr));
      Str:=TStringList.Create;
      //解密后分解字符串，获取用户名、系列号和注册码
      Str:=SubStrings(Base64Decryption(TempStr), "＊");
      Flag:=Str[0];
      UnStr:=Str[1];
      SLStr:=Str[2];
      RGStr:=Str[3];
      Str.Free;
      //如果不是请示验证，则不往下执行
      If  Flag<> "REG" then
      Exit;
      Flag1:= Base64Decryption("OK＊YES＊YES");
      Flag2:= Base64Decryption("OK＊YES＊NO");
      //验证是否为合法的注册码
      Try
      If  Base64Decryption(RGStr)=UnStr+SLStr  then
      ServerTCP.Send(Client, PChar(Flag1), Length(Flag1))
      Else
      ServerTCP.Send(Client, PChar(Flag2), Length(Flag2))
      Except
      ServerTCP.Send(Client, PChar(Flag2), Length(Flag2))
      Else
      End;
      End;

在上述示例代码中服务器端使用了固定IP地址，考虑到目前大多数用户使用的IP基本上都是动态分配的，只要计算机重新启动之后，服务端IP也就同时跟着改变了，这样就很容易造成客户端在进行校验时找不到服务器的情况。

要解决这个问题，可参考如下的办法来进行。

在服务器端软件启动时，找到服务器端的外网IP并以一个文件形式将其进行保存之后，在服务器端软件中使用TIdFTP控件（Delphi中自带控件）把这个保存的文件上传到自己的虚拟空间中。这样，以后每次在客户端进行校验之前，只要使用TIdFTP控件从指定的地址获得这个保存文件中服务器的IP地址，来作为连接服务器IP就可以了。

14.1.2 客户端加密实现

在客户端实现验证的方法是：在获取自己机器的硬件系列号之后，按照某种协议的规定，再把注册信息发送到服务器端去进行验证，并对服务器返回的数据包进行分析，就可以判断软件是否验证成功了。

客户端完整示例的代码如下。

        Uses  Base64Unit, DiskSerialUnit;
        //分解字符串
        Function  SubStrings(RootStr, ChildStr:String):TStringList;
        Var
        iTemp:integer;
        Begin
        Result:=TStringList.Create;
        iTemp:=pos(ChildStr, RootStr);
        While  iTemp>0  do  begin
        If  iTemp>1  then  result.Append(copy(RootStr,1, iTemp-1));
        Delete(RootStr,1, iTemp+length(ChildStr)-1);
        iTemp:=pos(ChildStr, RootStr);
        End;
        If  RootStr<> " " then  result.Append(RootStr);
        End;
        //获取硬盘系列号
        Procedure  TForml.FormCreate(Sender:TObject);
        Begin
        SLEdit2.Text:=GetHddSerial;
        End;
        //连接到服务器
        Procedure  TForml.Button1Click(Sender:TObject);
        Var
        TempStr:String;
        Begin
        If  UnEdit.Text= " " then
      Begin
        MessageBox(Handle, "用户名不能为空！", "提示", MB-OK);
      Exit;
      End;
      If  RGEdit.Texi= " " then
        MessageBox(Handle, "注册码不能为空！", "提示", MB-OK);
      Exit;
      End;
      //客户端IP
      ClientTCP.Host:= "196.211.1.8";
      //客户端端口
      ClientTCP.Port:=6666;
      //连接到服务器
      ClientTCP.Connected:=True;
      //发送验证请求
      TempStr:=Base64Encryption("REG＊"+ UnEdit.Text+"＊"+SLEdit.Text+
    "＊" RGEdit.Text);
      ClientTCP.Send(PChar(TempStr), Length(TempStr));
      End;
      Procedure TForml.ClientTCPRead(Sender:TObject; Stream:TStream); ;
      Var
      TempStr, Flag, Flag1, Flag2:String;
      Str:TStrings;
      Begin
      //读取信息
      With  Stream  as  TMemoryStream  do
      Begin
      SetLength(TempStr, Size);
      Read(TempStr[1], Size);
      End;
      Str:=TStringList.Create;
      //解密数据包并分解出里面的结果
      Str:=SubStrings(Base64Decryption(TempStr), "＊");
      Flag:=Str[0];
      Flag1:=Str[1];
      Flag2:=Str[2];
      Str.Free;
      //如果不"OK"，说明不是服务器返回验证结果，不执行下面的代码
      If  Flag<>"OK"  then
      Exit;
      //判断两个标志是否相同
        If  Flag1=Flag2  then
        MessageBox(Handle, "注册验证成功！", " ", MB-OK);
      Else
        MessageBox(Handle, "注册验证失败！", "提示", MB-OK);
      End;

14.2 Web服务器验证加密技术

Web服务器验证加密技术是指把用户要进行注册的信息，放在自己网站的虚拟地址空间上，客户端到指定的虚拟地址空间里得到用户注册信息，用这样的方法去实现注册码的网络验证功能。

这里介绍一种简单的网络验证方式，来说明一下Web服务器验证加密技术，具体实现的思路如下。

① 软件作者在自己的虚拟空间上新建一个用来保存用户注册信息的文件夹，在这个文件夹中，还要为每一个注册用户都新建一个同样用户名的文件。文件内容由加密的用户名、硬件系列号、软件可用状态等三个部分共同组成。

② 在开始进行客户端验证时，从文件夹在虚拟空间的路径和用户名形成的地址获取文件内容，如果无法找到这个文件，则说明注册验证失败。如果成功找到这个文件，则读取文件的内容并判断其是否与当前用户输入的信息相同。如果信息相同，则表明已注册了的用户验证成功。如果信息不同，则表明是非法用户在进行注册，验证失败。

③ 在一个软件启动时，要先从虚拟空间的文件中查看软件是否处于可用状态，如果是则软件不可用，将会提示用户不可用，自动退出应用程序。此时的服务器端将主要用来存放用户注册信息文件的虚拟空间，即只用来存放注册信息文件。

④ 在软件作者的计算机上还需要添加一个程序，就是当有用户需要注册软件时，把注册用户名、硬件系列号、软件可用状态加密后等写入文件之后，再上传到虚拟空间的文件夹中，成功完成对Web服务器的验证。

14.2.1 本地计算机控制实现

本地计算机控制程序的实现十分简单，就是当用户要注册软件时，把用户名、硬件系列号和软件可用状态等信息加密之后，将会产生一个以用户名作为文件名的注册文件，将该文件上传到指定的虚拟空间的文件夹中。

在经过上述操作之后，当需要改变软件的可用状态时，只需要让程序打开指定用户的注册文件，更改软件的可用状态之后再对其进行注册，并上传到虚拟空间中就可以了。

本地计算机控制程序的完整示例代码如下。

        Uses Base64Unit;
        Var
        LicaStr:String;
        //生成随机数作为垃圾数据使用
        Function  MakeRandom()String;
        Var
      I:integer;
      CountStr:string;
      Begin
      CountStr:= " ";
      Randomize;
      For i:=1 to 10 do
      Begin
      CountStr:= CountStr+IntToStr(Random(i)+1);
      End;
      Result:= CountStr;
      End;
      //分解字符串
      Function SubStrings(RootStr, ChildStr:String):TStringList;
      Var
      iTemp:integer;
      Begin
      Result:=TStringList.Create;
      iTemp:=pos(ChildStr, RootStr);
      While iTemp>0 do begin
      If iTemp>1 then result.Append(copy(RootStr,1, iTemp-1));
      Delete(RootStr,1, iTemp+length(ChildStr)-1);
      iTemp:=pos(ChildStr, RootStr);
      End;
      If  RootStr< > " " then  result.Append(RootStr);
      End;
      //生成用户注册文件
        Procedure TForml.Button1Click(Sender:TObject);
      Var
      FileName, TempStr:String;
      Str:TStrings;
      Flag:Integer;
      Begin
      If  RegUn.Text= " " then
      Begin
      MessageBox(Handle, "请输入注册用户名！", "提示", MB-OK)
      Exit;
      End;
      If RegSerial.Text= " " then
      Begin
        MessageBox(Handle, "请输入注册序列号！", "提示", MB-OK);
      Exit;
      End;
      //设置标志
      If  MakeCheck.Checked=True  then
      Flag:=1
      Else
      Flag:=-1
      FileName:=ExtractFilePath(Application.ExeName)+"UserLic\"+
    RegUn.Text+".lic";
      TempStr:= RegUn.Text+"＊"+MakeRandom()+"＊"+ReSerial.Text+"＊"+
    MakeRandom()+"＊" IntToStr(Flag)+"＊"+MakeRandom();
      Str.SaveToFile(FileName);
      Str.Add(Base64Encryption(TempStr));
      //生成用户注册文件
      Str.SaveToFile(FileName);
      Str.Free;
        MessageBox(Handle, "文件生成成功！", "提示", MB-OK);
      End;
      //连接FTP
      Procedure TForml.Button2Click(Sender:TObject);
      Begin
      PutFTP.Host:=FTPAddr.Text;
      PutFTP.Port:=StrToInt(FTPPort.Text);
      PutFTP. Username:=FTPUn, Text;
      PutFTP. Password:=FTPPw, Text;
      Try
      PutFTP.Connect;
      Except
        MessageBox(Handle, "文件生成成功！", "提示", MB-OK);
      End;
      End;
      //上传文件以虚拟空间
      Procedure  TForml.Button3Click(Sender:TObject);
      var
      FileName:String;
      Begin
      FileName:=ExtractFilePath(Application.ExeName)+"UserLic\"+
    RegUn.Text+".lic";
      Try
      PutFTP.Put(FileName, Folder.Text, False); //上传到FTP目录
        MessageBox(Handle, "用户注册文件上传成功！", "提示", MB-OK);
      Except
        MessageBox(Handle, "用户注册文件上传失败！", "提示", MB-OK);
      End;
      End;
      //断开FTP连接
      Procedure TForml.Button4Click(Sender:TObject);
      Begin
      PutFTP.Disconnect;
      End;
      //查询虚拟空间上的软件可用状态
      Procedure TForml.Button5Click(Sender:TObject);
      Var
      TempStr:String;
      Str:TStrings;
      Begin
      Try
      //这里以本机做测试，用时修改为自己的虚拟空间地址
      TempStr:=Base64Decryption(GetHttp.Get("http://196.211.1.8/Test/"+
    UserName+".lic"));
      Except
      TempStr:= " ";
        MessageBox(Handle, "获取软件可用状态出错！", "提示", MB-OK);
      End;
      If  TempStr:= " " then
      Exit;
      Str:=TStringList.Create;
      Str:=SubStrings(TempStr, "＊");
      If  StrToInt(Str[4])=1 then
      CheckBox1.Checked:=True;
      Str.Free;
      End;
      //查询保存在本地的软件可用状态
      Procedure TForml.Button6Click(Sender:TObject);
      var
      FileName:String;
      Str:TStrings;
      Begin
      LicStr:= " ";
      FileName:=ExtractFilePath(Application.ExeName)+ "UserLic\"+
    RegUn.Text+".lic";
      Str:=TStringList.Create;
      //读取文件内容
      Str.LoadFromFile(FileName);
      LicStr:=Base64Decryption(Str[0]);
      Str.Free;
      Str:=TStringList.Create;
      //分解出软件可用状态
      Str:=SubStrings(LicStr, "＊");
      If  StrToInt(Str[4])=1 then
      CheckBox2.Checked:=True;
      Str.Free;
      End;
      //保存修改后的状态
      Procedure TForml.Button7Click(Sender:TObject);
      Var
      FileName, TempStr:String;
      Str:TStrings;
      Flag:Integer;
      Begin
      If  LicStr = " " then
      Begin
      MessageBox(Handle, "先查询软件可用状态！", "提示", MB-OK)
      Exit;
      End;
      FileName:=ExtractFilePath(Application.ExeName)+ "UserLic\"+
    LocalEdit.Text+".lic";
        If  MakeCheck.Checked=True  then
        Flag:=1
        Else
        Flag:=-1
        Str:=TStringList.Create;
        Str:=SubStrings(TempStr, "＊")
        TempStr:=Str[0]+"＊"+ Str[1]+"＊"+ Str[2]+"＊"+ Str[3]+"＊"+
      IntToStr(Flag)+"＊"+ Str[5];
        Str.Free;
        //保存更改后的软件可用状态
        Str:=TStringList.Create;
        Str.Add(Base64Encryption(TempStr));
        Str:=SaveToFile(FileName);
        Str.Free;
        MessageBox(Handle, "软件可用状态更改成功！", "提示", MB-OK)
        End;

在上述代码中对如何进行用户注册验证和实现软件可用状态检测进行了深入而详细的讲解，并添加了大量随时提醒用户注意的提示信息，使得设计更加人性化。

在实际应用中对文件进行保存时，为了增强软件的保护力度，推荐先使用高强度的加密算法来对注册信息加密，加密密钥也要一起保存。这样，在对经过加密的文件进行解密操作时，就需要先从得到的数据中找出密钥再开始解密。此外，还可以使用为客户端程序加壳，加入反跟踪代码和完整性检验代码，以及在比较重要的地方加入花指令增加解密难度等方法，来实现对软件进行更加有力的保护。

14.2.2 客户端加密实现

每当软件启动时，将会先检测网络的运行状态。如果网络没有连通，就不会检测软件的可用状态了。在进行注册验证时，同样也要先检测网络是否连通，如果网络未连通，将提示用户需要在连接网络之后，才可进行注册验证。

在实现网络连接之后，即可对用户名进行验证，判断用户是否是注册的用户名，并对硬件系列号进行比较，检验是否是唯一存在的。

下面是一个客户端实现的完整示例代码。

        Uses Base64Unit, DiskSerialUnit, WinInet, Registry;
        //分解字符串
        Function SubStrings(RootStr, ChildStr:String):TStringList;
        Var
        iTemp:integer;
        Begin
        Result:=TStringList.Create;
        iTemp:=pos(ChildStr, RootStr);
      While  iTemp>0  do  begin
      If  iTemp>1  then  result.Append(copy(RootStr,1, iTemp-1));
      Delete(RootStr,1, iTemp+length(ChildStr)-1);
      iTemp:=pos(ChildStr, RootStr);
      End;
      If  RootStr<> " "  then  result.Append(RootStr);
      End;
      //判断网络状态
      Function GetOnlineStatus:Boolean;
      Var
      ConTypes:Integer;
      Begin
      ConTypes:=INTERNET-CONNECTION-MODEM+ INTERNET-CONNECTION-LAN+
    INTERNET-CONNECTION-PROXY;
      If(InternetGetConnectedState(@ConTypes,0)=False)then
      Result:= False
      Else
      Result:=True;
      End;
      //检测软件可用状态
      Procedure  TForml.Show(Sender:TObject);
      Var
      Reg:TRegistry;
      UserName, TempStr:String;
      Str:TStrings;
      Flag:Integer;
      Begin
      UserName:= " ";
      //获取硬盘序列号
      Edit2.Text:=GetHddSerial;
      If not GetOnlineStatus then
      Exit;
      //取出注册时保存的用户名
      Reg:=TRegistry.Create;
      Reg.RootKey:=HKEY-CURRENT-USER;
      If  Reg.OpenKey("\Software\Zhang", False)then
      UserName:=Reg.ReadString("WebSN");
      Reg.CloseKey;
      Reg.Destroy;
      IF UserName=" " then
      Exit;
      Try
      //这里以本地地址作为演示，用户可以更改为自己的地址
      TempStr:=Base64Decryption(GetHttp.Get("http://196.211.1.8/Test/
    "+UserName+".lic"));
      Exceot
      TempStr:=" ";
      End;
      If  TempStr:= " "then
      Exit;
      Str:=TStringList.Create;
      Str:=SubStrings(TempStr, "＊");
      //取出软件可用状态
      Flag:=StrToInt(Str[4]);
      Str.Free;
      //如果可用标志为-1，软件不能运行
      If  flag=-1  then
      Close;
      End;
      Procedure  TForml.Button1Click(Sender:TObject);
      Var
      Reg:TRegistry;
      TempStr:String;
      Str:TStrings;
      Begin
      If  Edit1.Text=" "then
      Begin
        MessageBox(Handle, "注册码不能为空！", "提示", MB-OK);
      Exit;
      End;
      //保存用户名
      Reg:=TRegistry.Create;
      Reg.RootKey:=HKEY-CURRENT-USER;
      If  Reg.OpenKey("\Software\Zhang", True)then
      Reg.WriteString("Ran", Base64Encryption(Edit1.Text));
      Reg.CloseKey;
      Reg.Destroy;
      If not GetOnlineStatus then
      Begin
        MessageBox(Handle, "网络未连通，请在网络连通后才能进行注册验证！", "提示",
    MB-OK);
      Exit;
      End;
      Try
      TempStr:=Base64Decryption(GetHttp.Get("ttp://196.211.1.8/
    Test/"+UserName+".lic"));
      Exceot
      TempStr:=" ";
      End;
      If  TempStr:=" "then
      Begin
        MessageBox(Handle, "注册验证失败！", "提示", MB-OK);
      Exit;
      End;
      Str:=TStringList.Create;
      Str:=SubStrings(TempStr, "＊");
      If  (Str[0]= Edit1.Text)and(Str[2]=GetHddSerial)then
        MessageBox(Handle, "注册验证成功！", "提示", MB-OK)
        Else
          MessageBox(Handle, "注册验证失败！", "提示", MB-OK);
        Str.Free;
        End;

在进行注册认证时，一定不要忽视对出现注册认证时的错误和异常情况进行处理，当异常发生时，需要判定是不是验证失败或做一些正确的处理，这才是完善的注册认证。如果不对错误和异常进行适当处理，则后面的注册认证过程就将完全失败了。错误和异常在其他的判断过程中也同样需要处理。

14.3 在线升级验证加密技术

所开发的软件在第一次发布之后，软件作者往往会在原来软件的基础上，为软件增加一些新的功能或对发现的BUG进行修正，这种方式称作软件升级。利用这种方法对软件进行升级之后，往往还会重新打包再发布一次，用户得到升级版的软件后还需要重新下载安装，这样对用户和软件作者都将非常麻烦。

此时，网络在线升级这种简单方法就可以解决软件升级问题。网络在线升级是指软件作者不用把升级文件整个安装包放到服务器上，而是在软件里提供一个在线升级功能，这样，只要用户的机器能上网，就可以通过在线升级方法把软件升级到刚发布出来的最新版本，这对软件作者和用户而言，都将非常方便。

14.3.1 在线升级验证实现

即使是同一个软件，升级前版本和升级后的版本在功能上也可能会有很大不同，升级后的版本不但功能增强了，错误减少了，使用更方便了，而且还有可能会造成同一个软件升级后的版本价格要比以前版本高。

为了避免非注册用户使用升级版本，软件公司往往采用在线升级的方法对软件用户进行校验。如果是注册用户，就可以通过进行在线升级而得到更新版本软件的使用权，如果没有注册，就不能使用升级版本软件的功能了。

一般情况下，对软件进行在线升级的主要思路如下。

① 测试计算机是否连接到网络上，如果网络不能连通，则无法实现在线升级。

② 采用Web服务器的网络验证方法，对软件用户的注册情况进行验证。如果是注册用户，就可能进行在线升级而使用新版本软件，如果是非法用户，则不能在线升级。

③ 比较在服务器端放置的XML（扩展标记语言，属于一种简单的数据存储语言）文件和本地计算机上存放的XML文件，查看是否有新的文件，如果查找到有新文件，则尽量下载使用最新的文件。

④ 下载完升级文件之后，把服务器端的在线升级配置文件下载到本地计算机上，覆盖原来的升级配置文件。

进行网络在线升级的配置方法：打开自己的虚拟空间之后，在其中新建一个专门用来放置要更新文件的文件夹，把XML类型的更新配置文件也存放到该新建文件夹中，当然，也可以是其他路径下面。

XML更新配置文件的主要内容如下。

        <update>
            <File1  date="2016-01-10">main.exe </File1>
            <File2  date="2016-02-08">setup.ini </File2>
            <File3  date="2016-02-08">lic.dat </File1>
        </update>

XML类型文件内容格式可以按照自己的要求来更新，如果想使用更多不同的功能，则可以根据XML的规则来设计、补充更全面的功能。

        <update>
            <File1  date="2016-01-10">main.exe</File1>
        </update>

14.3.2 实例：验证在线升级

这里举例讲述一下在线升级验证的方法，例子中的方法是直接把要进行升级的文件存放到软件所在文件夹中，并对原文件进行覆盖。还有一种更好的方法，即把升级文件存放到一个另外的文件夹中，做一个源文件的备份文件，再用下载更新的文件对原文件进行覆盖。这样，即使覆盖以后的文件运行时出错了也不用担心，只要拿出那个备份文件，就可以继续使用了。

完整的在线升级验证示例代码如下。

        Unit Main;
        Interface
        Uses
        Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls,
      Forms, Diaglogs, XMLIntf, StdCtrls, XMLDoc, IdBaseComponent, IdComponen,
      WinInet, IdTCPConnection, IdTCPClient, IdHTTP, ComCtrls, IdAntiFreezeBase,
      Registry, IdAntiFreeze, Base64Unit, DiskSerialUnit;
        Type
        TForm1=class(TForm)
        Button2:TButton;
      Button1:TButton;
      ProgressBar1:TProgressBar;
      IdHTTP1:T IdHTTP;
      IdAntiFreeze1:T IdAntiFreeze;
      Label1:T Label;
      StatusBar1:T StatusBar;
      Procedure  IdHTTP1WorkEnd(Sender:TObject; AWorkMode:TWorkMode);
      Procedure  IdHTTP1Work(Sender:TObject; AWorkMode:TWorkMode;
    Const AWorkCount: Integer);
      Procedure  IdHTTP1WorkBegin(Sender:TObject; AWorkMode:
    TWorkMode; Const AWork CountMax:Integer);
      Procedure  Button1Click(Sender:TObject);
      Procedure  FormCreate(Sender:TObject);
      Procedure  Button2Click(Sender:TObject);
      Private
      Function  IsRegistry():Boolean;
      Public
      {public  declarations}
      End;
      Var
      Form1:TForm1;
      URlList:TStrings; //需要更新的程序
      UpdateUrl, LocalUrl:String;
      Implementation
      {$R ＊.dmf}
      //检查网络是否连接
      Function GetOnlineStatus:Boolean;
      Var
      ConTypes:Integer;
      Begin
      ConTypes:=INTERNET-CONNECTION-MODEM+INTERNET-CONNECTION-LAN+
    INTERNET-CONNECTION-PROXY;
      If(InternetGetConnectedState(@ConTypes,0)=False)then
      Result:= False
      Else
      Result:=True;
      End;
      //分解字符串
      Function  SubStrings(RootStr, ChildStr:String):TStringList;
      Var
      iTemp:integer;
      Begin
      Result:=TStringList.Create;
      iTemp:=pos(ChildStr, RootStr);
      While  iTemp>0  do  begin
      If  iTemp>1  then  result.Append(copy(RootStr,1, iTemp-1));
      Delete(RootStr,1, iTemp+length(ChildStr)-1);
      iTemp:=pos(ChildStr, RootStr);
      End;
      Procedure TForm1.IdHTTP1WorkEnd(Sender:TObject; AWorkMode:
    TWorkMode);
      Begin
      ProgressBar1.Position:=ProgressBar1.Max;
      End;
      Procedure  TForm1.IdHTTP1Work(Sender:TObject; AWorkMode:TWorkMode;
    Const AWork Count:Integer);
      Begin
      ProgressBar1.Position:=AWorkCount;
      End;
      Procedure  TForm1.IdHTTP1WorkBegin(Sender:TObject; AWorkMode:
    TWorkMode; Const  AWorkCountMax:Integer);
      Begin
      ProgressBar1.Max:=AWorkCountMax;
      ProgressBar1.Min:=0
      ProgressBar1.Position:=0
      End;
      //判断网上的文件是否在本地存在
      Function GetVaule(RootList:TStrings; SubStr:String):Integer;
      Var
      I, Flag:Integer;
      Begin
      Flag:=0;
      For  i:=0 to RootList.Count-1  do
      Begin
      If  RootList[i]=SubStr  then
      Flag:=1;
      End;
      Resulat:=Flag;
      End;
      Procedure FileComp();
      Var
      UrlXML, LocalXML:IXMLDocument;
      UrlNode, LocalNode:IXMLNode;
      I, j:integer;
      TempList:TStringList;
      Begin
      TempList:=TStringList.Create;
      //网上的更新配置文件
      UrlXML:=TXMLDocument.Create(UpdateUrl);
        UrlXML.Active;
        UrlNode:=UrlXML.DocumentElement;
      //本地的更新配置文件
      LocalXML:= TXMLDocument.Create(LocalUrl);
      For  i:=0  to  UrlNode.ChileNodes.Count-1  do
      Begin
      UrlNode:=UrlXML.DocumentElement;
      UrlNode:= UrlNode.ChileNodes.Nodes[i];
      LocalXML.Active;
      LocalNode:=LocalXML.DocumentElement;
      For  j:=0  to  LocalNode.ChileNodes.Count-1  do
      Begin
      LocalNode:=LocalXML.DocumentElement;
      LocalNode:= LocalNode.ChileNodes.Nodes[j];
      TempList.Add(LocalNode.NodeValue);
      If  UrlNode.NodeValue= LocalNode.NodeValue  then
      Begin
      //如果网上的日期比较新，把文件放到列表
      If  StrToDate(UrlNode.Attributes["date"])> StrToDate(LocalNode.
    Attributes["date"])  then
      UrlList.Add(UrlNode.NodeValue);
      End;
      End;
      //如果GetVaule函数返回值为0，说明网上有新文件需要下载更新
      If  GetVaule(TempList, UrlNode.NodeValue)=0  then
      UrlList.Add(UrlNode.NodeValue);
      End;
      TempList.Free;
      LocalXML:=nil;
      UrlXML:=nil;
      End;
      //检测是否为合法用户
      Function  TForm1.IsRegistry():Boolean;
      Var
      TempStr, UserStr:String;
      Str:TStrings;
      Reg:TRegistry;
      Begin
      //读取注册时保存的用户名
      Reg:=TRegistry.Create;
      Reg.RootKey:=HKEY-CURRENT-USER;
      If  Reg.OpenKey("\Software\Zhang", False)  then
      UserStr:=Reg.ReadString("WebSN");
      Reg.CloseKey;
      Reg.Destroy;
      Try
      //解码用户注册信息
      TempStr:=Base64Decryption(GetHttp.Get("http://196.211.1.8/Test/
    "+UserStr+ ".lic"));
      Except
      Label1.Caption:= "获取升级信息出错，请检查网络。";
      TempStr:= " ";
      End;
      If  TempStr:= ""  then
      Begin
      Result:=False;
      Exit
      End;
      Str:=TStringList.Create;
      Str:=SubStrings(TempStr, "＊");
      //判断是否为合法用户
      If  (Str[0]=UserStr)and(Str[2]=GetHddSerial) then
      Result:=True
      Else
      Result:=False;
      Str.Free;
      End;
      Procedure  TForml.Button1Click(Sender:TObject);
      Var
      MyStream:TMemoryStream;
      UrlPath:String;
      SavePath:String;
      I:Integer;
      Count:Integer;
      Begin
      If  not  GetOnlineStatus  then
      Begin
      MessageBox(Handle, "计算机没有连接到网络不能进行升级！", "错误提示",
    MB-OK+MB- ICONERROR);
      Exit;
      End;
      If  not  IsRegistry  then
      Begin
      Label1.Caption:= "不是合法的用户不能进行在线升级。";
      Exit;
      End;
      UrlPath:= "http://196.211.1.8/Update/ "; //更新文件路径
      SavePath:=ExtractFilePath(Application.ExeName); //更新文件保存路径
      FileComp();
      If  UrlList.Count<1  then
      Begin
      Label1.Caption:= "程序已经是最新版本不需要升级。";
      Exit;
      End;
      Button1.Enabled:=False;
      Button2.Enabled:=True;
      IdAntiFreeze1.OnlyWhenIdle:=False; //设置使程序有反应
      MyStream:=TMemoryStream.Create;
      Count:=1;
      For  i=0  to UrlList.Count-1  do
      Begin
      Label1.Caption:="程序正在升级，请稍等。("+IntToStr(Count)+"/"+
    IntToStr(UrlList.Count)+")";
      Try
      IdHTTP1.Get(UrlPath+UrlList[i], MyStream);
      Except
      Label1.Caption:="软件在线升级错误，请检查网络是否连通。";
        MyStream..Free;
        Exit;
        End;
        MyStream.SaveToFile(SavePath+UrlList[i]);
        MyStream..Clear;
        Count:=Count+1;
        End;
        Try
        //更新完成之后，把新的配置文件下载到本地以覆盖原文件
        IdHttp1.Get(UpdateUrl, MyStream);
        MyStream.SaveToFile(LocalUr1);
        Except
        MyStream..Free;
        End;
        MyStream..Free;
        UrlList.Free;
        Label1.Caption:="软件已经升级完成。";
        ProgressBar1.Position:=0;
        Button1.Enabled:=True;
        Button2.Enabled:=False;
        End;
        Procedure  TForml.FormCreate(Sender:TObject);
        Begin
        UpdateUrl:="http://196.211.1.8/Update/ver.xm1";
        LocalUrl:=ExtractFilePath(Application.ExeName)+"ver.xml";
        URLList:=TStringList.Create;
        End;
        Procedure TForml.Button2Click(Sender:TObject);
        begin
        Label1.Caption:="单击【开始升级】按钮对软件进行在线升级。";
        IdHttp1.Disconnect;
        Button1.Enabled:=True;
        Button2.Enabled:=False;
        ProgressBar1.Position:=0;
        End;
        End.

14.4 网络身份认证技术的应用

所谓网络身份认证技术就是通过计算机网络来确定使用者的身份，重点是为了解决网络双方的身份信息是否真实的问题，使通信双方在进行各种信息交流时可以在一个安全的环境中。在信息安全里，身份认证技术在整个安全系统中是重点，也是信息安全系统首要“看门人”。因此，基本的安全服务就是身份认证，另外的安全服务也都需要建立在身份认证的基础上，使身份认证系统具有了十分重要的地位，但也最容易受到攻击。

14.4.1 身份认证的含义

身份认证技术简单意义上来讲就是对通信双方进行真实身份鉴别，也是对网络信息资源安全进行保护的第一道防火墙，目的就是验证辨识网络信息使用用户的身份是否具有真实性和合法性，然后给予授权才能访问系统资源，不能通过识别用户就会阻止其访问。由此可知，身份认证在安全管理中是个重点，同时也是最基础的安全服务。

1．身份认证技术的应用

信息安全中身份认证是重要的一门技术，也是在网络安全里的第一道防线，可以很好地识别出访问的用户是否具有访问的权限，允许通过识别的用户进行访问操作，并进行一定的监督，防止出现不正当的操作情况，同时也是保护计算机不受病毒和黑客入侵的一个重要方法。使用者在进入网络安全系统的时候，先需要让身份认证系统识别出自己的身份，通过了身份认证系统识别以后，再依据使用者的权限、身份级别来决定可以访问哪些系统资源和可以进行哪些系统操作权限。与此同时，进入安全系统时，检测系统需要进行登记，包括记录、报警等，对用户的行为和请求进行记录，并识别出是否入侵了安全系统。

2．基于网络的身份认证

身份认证系统在安全系统中非常重要，虽然它是最基础的安全服务，但是另外的安全服务都需要它才能完成，只要身份认证系统受到攻击入侵，就会导致系统里的安全措施都无法产生作用，而黑客入侵的首要目标一般都是先攻破身份认证系统。但是因为网络连接具有复杂性、流动性等特点，在复杂的网络环境中会导致身份认证也变得复杂，分析网络运行时的各类因素，可得出在网络环境中的身份认证具有以下特点。

① 虽然在网络中使用身份认证技术后，提高了网络的安全性，但是因为身份认证技术需要使用加密算法来实施复杂计算，就会导致实时性变差，降低了网络通信率。

② 身份认证系统在处于单机状态下无法很好地实现。

③ 入侵者往往会通过对用户的通信通道上窃听出身份认证信息，从而非法获得合法用户的身份信息，并使用这些截取的身份信息入网。

④ 因为使用者一般具有不稳定性和流动性，所以进行身份认证就必须要考虑到相对应的方式，就是确认用户身份进行认证时具有唯一性、完整性，防止出现用户身份冒充、伪造等情况，在此基础上也需要考虑到认证系统是否会对系统流畅运行带来影响。

14.4.2 用户身份认证的技术应用

身份认证技术在以后的发展里，首先需要提高其安全性、稳定性、效率性、实用性等特点，在认证终端需要向小型化发展。重点可以从下面几个方面来进行发展。

1．生物认证技术

生物特征指的是人体自带的生理特征和行为特征。因为每个人的生物特征都具有唯一性，由此来对用户进行验证。生物特征的身份认证方法有着可靠、稳定等特点，也是最安全的身份认证方法。但是在目前还没有哪种生物认证的方法可以保证100%的正确率。因此，怎么提高识别算法和硬件水平来保证高正确率是一个重点。

2．非生物认证技术

非生物认证一般是采用口令的认证方式，而现在传统的身份认证技术就是使用口令认证。口令认证方法具有简单性、操作性等特点。认证者首先需要拥有用户使用账号，还需要保证使用账号在用户数据库里是唯一的。

口令认证一般包含两种认证方式：一种是使用动态口令，用户在使用网络安全系统的时候，所需要输入的口令都是变化的，不会固定，每次都有变化，就算这次输入的口令被他人获得，但是下次却不可以再次使用。另一种是静态口令，使用用户经过系统设定和保存后，在指定的时间内不会发生变化，一个口令能够长期使用，这种口令相比于动态口令虽然操作简单，但是却没有动态口令安全。

3．多因素认证

结合利用各类因素认证技术，增强身份认证的安全性。现在手机短信认证和Web的口令认证早已经在网络安全中得到利用，并获得不错的口碑。

14.4.3 常见的用户身份认证方式

作为防护网络资产的第一道关口，身份认证有着举足轻重的作用。下面介绍一下几种常见的用户身份认证方式。

1．智能卡（IC卡）

一种内置集成电路的芯片，芯片中存有与用户身份相关的数据，智能卡由专门的厂商通过专门的设备生产，是不可复制的硬件。智能卡由合法用户随身携带，登录时必须将智能卡插入专用的读卡器读取其中的信息，以验证用户的身份。

智能卡认证是通过智能卡硬件不可复制来保证用户身份不会被仿冒。然而由于每次从智能卡中读取的数据是静态的，通过内存扫描或网络监听等技术还是很容易截取到用户的身份验证信息，因此还是存在安全隐患。

2．短信密码

短信密码以手机短信形式请求包含6位随机数的动态密码，身份认证系统以短信形式发送随机的6位密码到客户的手机上。客户在登录或者交易认证时输入此动态密码，从而确保系统身份认证的安全性。

短信密码具有以下优点。

（1）安全性

由于手机与客户绑定比较紧密，短信密码生成与使用场景是物理隔绝的，因此密码在通路上被截取概率降至最低。

（2）普及性

只要会接收短信即可使用，大大降低短信密码技术的使用门槛，学习成本几乎为0，所以在市场接受度上面不会存在阻力。

（3）易收费

由于移动互联网用户天然养成了付费的习惯，这和PC时代互联网截然不同的理念，而且收费通道非常的发达，如果是网银、第三方支付、电子商务，可将短信密码作为一项增值业务，每月通过SP收费不会有阻力，因此也可增加收益。

（4）易维护

由于短信网关技术非常成熟，从而大大降低短信密码系统上的复杂度和风险，短信密码业务后期客服成本低，稳定的系统在提升安全的同时也营造良好的口碑效应，这也是目前银行大量采纳这项技术很重要的原因。

3．静态密码

密码是由用户自己设定的。在网络登录时输入正确的密码，计算机就认为操作者就是合法用户。实际上，由于许多用户为了防止忘记密码，经常采用诸如生日、电话号码等容易被猜测的字符串作为密码，或者把密码抄在纸上放在一个自认为安全的地方，这样很容易造成密码泄露。如果密码是静态的数据，在验证过程中需要在计算机内存中传输，该过程可能会被木马程序截获。因此，静态密码机制无论是使用还是部署都非常简单，但从安全性上讲，用户名/密码方式是一种不安全的身份认证方式。

4．动态口令牌

目前较为安全的身份认证方式——动态口令牌也是一种动态密码。

动态口令牌是客户手持用来生成动态密码的终端，主流的是基于时间同步方式的，每60秒变换一次动态口令，口令一次有效，它产生6位动态数字进行一次一密的方式认证。

由于它使用起来非常便捷，85%以上的世界500强企业运用它保护登录安全，广泛应用在VPN、网上银行、电子政务、电子商务等领域。

5.USB Key

基于USB Key的身份认证方式是近几年发展起来的一种方便、安全的身份认证技术。它采用软硬件相结合、一次一密的强双因子认证模式，很好地解决了安全性与易用性之间的矛盾。USB Key是一种USB接口的硬件设备，它内置单片机或智能卡芯片，可以存储用户的密钥或数字证书，利用USB Key内置的密码算法实现对用户身份的认证。基于USB Key身份认证系统主要有两种应用模式：一种是基于冲击/响应的认证模式，另一种是基于PKI体系的认证模式，目前运用在电子政务、网上银行。

6．生物识别技术

运用who you are方法，通过可测量的身体或行为等生物特征进行身份认证的一种技术。生物特征是指唯一的可以测量或可自动识别和验证的生理特征或行为方式。生物特征分为身体特征和行为特征两类。身体特征包括：指纹、掌型、视网膜、虹膜、人体气味、脸型、手的血管和DNA等；行为特征包括：签名、语音、行走步态等。目前部分学者将视网膜识别、虹膜识别和指纹识别等归为高级生物识别技术；将掌型识别、脸型识别、语音识别和签名识别等归为次级生物识别技术；将血管纹理识别、人体气味识别、DNA识别等归为“深奥的”生物识别技术，指纹识别技术目前应用广泛的领域有门禁系统、微型支付等。

7．双因素身份认证

所谓双因素就是将两种认证方法结合起来，进一步加强认证的安全性，目前使用最为广泛的双因素有以下几种。

· 动态口令牌+静态密码。

· USB Key+静态密码。

· 二层静态密码等。

计算机网络世界中一切信息包括用户的身份信息都是用一组特定的数据来表示的，计算机只能识别用户的数字身份，所有对用户的授权也是针对用户数字身份的授权。身份认证技术解决了在计算机网络中确认操作者身份的产生的问题。

14.5 小结

本章主要介绍了本地服务器验证、Web服务器验证和在线升级验证等三类网络验证方式，以及网络身份认证技术。网络验证是网络安全的基础，因此，用户需要弄懂网络验证之间各种逻辑关系，并熟悉代码的适时运用。而网络身份认证技术是安全系统中的重点技术，用户要熟悉网络身份认证的几种方式。在当今这个网络时代，网络的作用已经得到了很多方面的延伸，而不再仅仅是满足于信息共享，随着网络用户对各种网络中所传输数据加密性的要求提高，甚至可能传输的是某些不能够为其他用户所了解的重要机密信息，因此，保护这些机密信息（或数据）的安全就变得非常重要了。

技巧与问答

❖ 在进行网络验证时，使用的验证码有什么作用？

验证码的作用主要是为了防止用户自动注册，影响网站的正常运行。验证码也就是将一串随机产生的数字或符号，生成一幅带有一些干扰因素的图片，用户用肉眼辨认出验证码信息，并将其提交给网站，只有在等待网络验证成功之后，才可以使用网站中提供的某些功能。虽然使用验证码为用户登录产生了一些麻烦，但就网络安全方面的因素考虑而言，使用验证码还是很有必要的。

❖ 在进行网络验证连接网络时，系统一直提示网络不通，这是怎么回事？

造成网络不通的原因有很多，一般情况下，主要是网络配置不正确而引起的，在同一个局域网中规定每台机器的IP地址必须是唯一的，子网掩码也必须一样，并且默认网关必须是本机所在局域网和其他子网能够进行信息交换的网卡IP地址。

因此，要解决进行网络验证连接网络时网络不通的问题，就需要先检查TCP/IP的配置数据，查看IP地址、子网掩码、默认网关配置是不是符合要求，如果不符合要求，则重新设置一下即可。

另外，还需要检查一下，看看是不是网卡以及其连接设备出现了接触不良，如果遇到这种情况，则更换一块新的网卡或重新连接一下与其相连的设备即可。

❖ 什么是挑战/响应认证机制？

挑战/响应方式的身份认证机制就是每次认证时认证服务器端都给客户端发送一个不同的“挑战”码，客户端程序收到这个“挑战”码，根据客户端和服务器之间共享的密钥信息，以及服务器端发送的“挑战”码做出相应的“应答”。服务器根据应答的结果确定是否接受客户端的身份声明。从本质上讲，这种机制实际上也是一次性口令的一种。

认证过程如下。

① 客户向认证服务器发出请求，要求进行身份认证。

② 认证服务器从用户数据库中查询用户是否是合法的用户，若不是，则不做进一步处理。

③ 认证服务器内部产生一个随机数，作为“挑战”码，发送给客户。

④ 客户将用户名字和随机数合并，使用单向Hash函数（例如MD5算法）生成一个字节串作为应答。

⑤ 认证服务器将应答串与自己的计算结果比较，若二者相同，则通过一次认证；否则，认证失败。

⑥ 认证服务器通知客户认证成功或失败。

❖ 什么是MAC接入认证技术？

MAC地址认证是一种基于端口和MAC地址对用户的网络访问权限进行控制的认证方法，是一种二层网络接入认证技术，与802.1X以及Portal不同，用户不需要安装任何客户端软件就可进行认证。设备在启动了MAC地址认证的端口上首次检测到用户的MAC地址以后，即启动对该用户的认证操作。认证过程中，不需要用户手动输入用户名或者密码。若该用户认证成功，则允许其通过端口访问网络资源，否则该用户的MAC地址就被添加为静默MAC。在静默时间内（可通过静默定时器配置），来自此MAC地址的用户报文到达时，设备直接做丢弃处理，以防止非法MAC短时间内的重复认证。

第15章
自制加密工具

现在网络上有许多的加密工具，用户使用这些加密工具可以加密自己的数据，但是，由于这些加密工具的算法已知，破解者很容易找到相应的破解软件。所以，用户可以制作自己的加密工具，提高数据的安全性。

15.1 U盘制作Windows开机加密狗

U盘等移动设备除了可以用来存储文件，安装系统以外，还可以用来制作加密狗。本节中所指的加密狗仅指针对Windows系统开机的加密狗。它的作用是为了避免他人随便开启自己的计算机，查看自己的私密信息，除了设置个人密码外，还可以通过简单的设置让计算机只有在插入自己的U盘后才能启动，否则启动后即自动关机。

15.1.1 U盘制作Windows开机加密狗的制作步骤

制作这样一个加密狗并不需要很复杂的过程，它的制作步骤如下。

1 插上自己的U盘或者其他移动存储设备。

2 在U盘创建一个任意类型的文件，文件名也任意。这里为了后面方便讲解，用了一个空白的文本文件，取名为“加密测试”，文件扩展名为“.txt”。文件就放在U盘根目录下，U盘的驱动器盘符为E，如图所示。

加密测试．txt

3 在计算机上任意位置新建一个文本文件，在这个文本文件中输入如下内容。

        if not exist E：加密测试．txt shutdown -s -t 30-c "你无法使用该计算机"

这句话的意思是如果U盘中不存在加密测试．txt这个文件，则在30秒后关闭该计算机，并显示“你无法使用该计算机”这一提示信息，如图所示。

新建一个文本文件

语句中的30可以自己修改为其他，如100，提示语“你无法使用该计算机”也可改成其他语句。只要不改动整个语句的格式就可以了。

4 输入完成后，选择【文件】→【另存为】菜单项，弹出【另存为】对话框，在【文件名】文本框中输入test.bat，在【保存类型】下拉菜单中选择【所有文件】菜单项，单击【保存】按钮即可，如图所示。

【另存为】对话框

5 在Windows 7中按<Windows+R>组合键打开【运行】对话框，在 【打开】文本框中输入gpedit.msc，单击【确定】按钮，打开组策略对话框，如图所示。

【运行】对话框

6 通过鼠标单击定位到如下路径：【本地计算机策略】→【计算机配置】 →【Windows设置】→【脚本（启动/关机）】，然后在右边的框中选择【启动】项，如图所示。

【本地组策略编辑器】窗口

7 用户在【启动属性】对话框中单击【添加】按钮，弹出【添加脚本】对话框，单击【浏览】按钮，如图所示。

【添加脚本】对话框

8 当用户单击【浏览】按钮后，弹出【浏览】对话框，选择“test.bat”文件，单击【打开】按钮，如图所示。

【浏览】对话框

9 用户选择文件后，弹出【添加脚本】对话框，单击【确定】按钮，弹出【启动属性】对话框，单击【确定】按钮即可完成设置，如图所示。

完成设置

10 用户在不插U盘启动计算机的情况下，等待一会自动弹出提示框，提示用户将被注销，如图所示。

提示框

15.1.2 U盘遗失情况下的解决办法

如果用户制作的U盘丢失了，应该如何解决。下面是一些解决方法。

1 如果时间足够多，可以在【运行】对话框中输入“shutdown -a”来解除关机指令，如图所示。

【运行】对话框

2 备份你U盘中的文件到其他地方，当U盘丢失时将你制作的“加密测试．txt”复制到新U盘，一般情况下都可以解决问题。如果不行请检查当前U盘分区是否和原U盘一样。

3 将计算机启动到安全模式下将启动脚本删除，因为在安全模式下是不会加载开机脚本的。

15.2 手机应用软件加密

现在手机越来越成为人们生活中必不可少的部分，人们大量的信息存储在手机上，因此，提高手机数据的安全性在一定程度上保护了使用者的隐私。本节讲述如何对手机应用软件进行加密。

15.2.1 手机设置应用软件加密

虽然手机有锁屏的功能，但有些应用还是不想让别人看到，可以给应用软件单独加密，比如邮件、云盘、云笔记、记账软件等，也可以起到双重保护作用。

下面以MX5为例介绍一下手机应用加密的设置方法。

1 用户打开手机，点击【设置】按钮，如下图所示。

【设置】按钮

2 用户在【设置】选项中选择【指纹和安全】选项，如下图所示。

【指纹和安全】选项

3 用户在【指纹和安全】选项中点击【应用加密】选项，如下图所示。

【应用加密】选项

4 用户在【应用加密】选项中点击【应用加密】按钮，如下图所示。

【应用加密】按钮

5 用户点击应用加密，设计4～6位密码，简单易记就好，如下图所示。

设计4～6位密码

6 用户输入密码后，这里输入4位密码，点击【继续】按钮，如下图所示。

【继续】按钮

7 用户在点击【继续】按钮后选择【信息】选项，对【信息】进行加密，如下图所示。

【信息】选项

8 用户打开【信息】应用时将会弹出【输入密码】提示框，用户只有输入密码才能打开【信息】应用，如下图所示。

【输入密码】提示框

15.2.2 使用腾讯手机管家对应用软件加密

是否还在为别人随意翻看手机里的应用程序而感到烦恼和尴尬？如何锁住手机里的小秘密？腾讯手机管家可以充分保护个人隐私。

下面介绍一下腾讯手机管家对应用软件加密的具体步骤。

1 用户需要在手机上安装腾讯手机管家for Android的最新版，点击【隐私保护】选项，如下图所示。

【隐私保护】选项

2 用户进入【隐私保护】选项，点击【立即开启】按钮，如下图所示。

【立即开启】按钮

3 用户在【开启隐私保护】选项的【设置密码开启隐私保护】界面中输入手势，如下图所示，在【再次绘制图案密码】界面中绘制相同的手势，如下图所示。

设置密码开启隐私保护

再次绘制图案密码

4 用户进入【隐私保护】后点击【软件锁】按钮，如下图所示。

【软件锁】按钮

5 用户进入【软件锁】后，点击【添加】按钮，选择【手机淘宝】选项，再单击【添加】按钮，如下图所示。

软件锁

6 用户在打开【手机淘宝】时，需要输入手势密码，如图所示。

手势密码

15.2.3 使用360卫士对手机软件进行加锁

由于手机功能越来越强大，许多重要的东西都放在了手机上，这也给个人的隐私留下了隐患，如果能给某些重要的应用程序加上锁，那么就多了一种保障，即便是把手机交给别人使用也不用担心。

下面介绍一下使用360卫士对手机软件进行加锁的步骤。

1 用户点击手机桌面上的360卫士图标，打开软件，如图所示。

360卫士图标

2 用户进入360卫士主界面后，向上滑动出现功能列表，点击【程序锁】按钮，如下图所示。

【程序锁】按钮

3 用户进入程序锁后选择【微信】程序，点击【开启保护】按钮，如下图所示。

【开启保护】按钮

4 用户点击【开启保护】按钮后，需要设置手势密码和安全问题，点击【完成】按钮即可。

5 用户点击【完成】按钮后，弹出【开启程序锁权限】提示框，点击【开启】按钮，如下图所示，开启时弹出提示框，点击【确定】按钮，完成加锁设置，如下图所示。

【开启程序锁权限】提示框

【确定】按钮

15.3 小结

本章主要介绍了用户如何自制U盘Windows开机锁，以及如何使用手机自带的加锁功能给应用程序加锁，同时介绍了使用辅助软件对手机应用程序加锁等。用户在自制加密工具时不要忘了密码。

在当今这个网络时代，网络的作用已经得到了很多方面的延伸，而不再仅仅是满足于信息共享。随着网络用户对各种网络中所传输数据加密性的要求提高，甚至可能传输的是某些不能够为其他用户所了解的重要机密信息，因此，保护这些机密信息（或数据）的安全就变得非常重要了，用户可以学习一些自制加密工具的制作方法保护自己的数据。

技巧与问答

❖ 如何通过批处理及组策略来实现U盘制作开机密码锁开机钥匙盘？

① 打开Windows附件内自带的记事本程序，在其中输入内容“if not exist G:/It168.com shutdown -l -t 1 ”，然后执行【文件】→【另存为】，在文件名中输入“It168.BAT”（含英文双引号，这样会保存成一个BAT批处理文件）。

其中“G:”为你的U盘插入后Windows能自动识别得到的盘符，“It168.com”为你自定义的文件名和文件扩展名，可以为任意文件名和扩展名，shutdown是Windows自带的关机小程序，加参数“-l”意思是注销当前用户，“-t 1”是指等待1秒，整句程序的意思就是如果移动U盘（盘符为G）上没有名为“it168.com”的文件，则1秒后自动注销当前用户。

② 将USB.BAT文件复制粘贴到“C:\WINDOWS\System32\GroupPolicy\User\Scripts\Logon”（Windows XP）或“C:\WINNT\System32\GroupPolicy\User\Scripts\Logon”（Windows 2000）。

③ 按下<Win+R>组合键打开“运行”对话框，输入“gpedit.msc”打开组策略窗口，依次打开【本地计算机策略】→【用户配置】→【Windows设置】→【脚本（登录/注销）】，双击右侧的【登录】项，在【登录属性】中点击【添加】按钮，在【浏览】中选中我们制作的“it168.BAT”文件，选择“打开”，然后一一单击确定按钮来关闭组策略。

提示：为了保险起见，你也可在组策略中在【本地计算机策略】→【计算机配置】→【Windows设置】→【脚本（启动/关机）】中的【启动】项中使用同样方法添加此BAT文件，这样会对所有使用本计算机的用户都有效，如果你的计算机就一个用户账户则可免去此步骤。

❖ 为什么腾讯手机管家软件锁设置后，打开程序仍然没有锁？

你在初次使用软件锁的时候是要登录的，登录的密码是你原来设置的隐私保护的密码，输入密码登录进去，在界面的最下面有一个“添加要锁定的软件”，在你想要的锁定的软件后面打钩，然后点击添加软件就可以了，当你使用锁定的软件时，只要输入密码就好了。

❖ 腾讯手机管家软件锁密码忘记了怎么办？

① 如果你关联了QQ号码，可以根据提示操作，输入关联的QQ号进行重置密码。

② 如果你未关联QQ号码，因涉及用户信息安全，建议可以通过刷机或恢复出厂设置解决问题，但在之前请将隐私文件解密并备份。

❖ 如何解除手机360卫士的应用程序锁？

① 关闭的程序锁的方法：进入到程序锁界面，找到你要解锁的应用，可以看到在应用的旁边有个绿色的加速图标，点击一下即可关闭。

② 加锁的图片变成白色的锁就是为应用解除了程序锁，如果你要继续加锁，点击你要的应用的锁即可加锁。