软件加密技术及实现

合集下载
相关主题
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

软件加密技术及实现 whinah(原作)

关键字 encrypt 软件加密 保护 散列 数字签名
软件加密技术及实现
雷 鹏
( 桂林电子工业学院 计算机系 )
摘 要 当今盗版软件的泛滥成灾几乎已经成为了我们中国民族软件的灾难,为了防止软件的非法复制、盗版,保护软件开发商的利益,就必须对软件进行加密保护。现在市面上有许多反盗版软件,但这类软件多是单机处理,并且只使用简单的加密手段,很容易被解密者破解。
本文描述了一个通过Internet,集加密和电子注册于一身的完善的软件保护方案。该方案基于多种密码学意义上可靠的算法,如对称加密算法,散列算法,数字签名,密钥交换等等。通过对Windows下PE可执行文件的结构及载入机制进行深刻的剖析, 巧妙的使用这些密码学算法及多种反破解方案对PE文件进行加密保护。
在该方案的实现中,使用CryptoAPI中的数字签名算法RSA,加密算法RC2和RC4,散列算法SHA,同时自己编写了使用了MD5算法用于快速计算大量数据的摘要;网络接口使用WinSocket;编程语言选用汇编语言和C++混合编程方式;反破解方案有检测文件完整性、检测代码完整性、反跟踪、反-反汇编、反Dump、代码变形等等。
由于使用了可靠的密码学算法,使软件加密的强度大大提高;由于使用了Internet在线注册方式,用户使用也非常方便。
关键词 加密 ;数字签名 ;散列;反跟踪 ;电子注册

Software Protection technique and its realization
LEI Peng
( GuiLin Institute of Electronic Technology . The Department of Computing )
Abstract The flooding of pirate software has been a calamity of our national software industry . In order to prevent software from pirate , and protect the profit of the software developer , they must encrypt their software to get a protection . There are several software protection tools in the market currently , but these tools were standalone nine tenths , and they only used simple encryption algorithms , so they could be cracked easily by the crackers .
This thesis describes a perfect software encryption and protection scheme which integrate the encryption and electronic register . This scheme is based on multiple reliable cryptographic algorithms such as symmetric encryption algorithm , digital signature , hashing and key exchange . The PE file format (Portable Executable File format) and its loading mechanism under Windows are dissected thoroughly in this thesis . Then these cryptographic algorithms and several anti-crack method are used gracefully to encrypt and protect the PE file .
Within the realization of this scheme , the RSA digital signature algorithm , RC2 and RC4 encryption algorithm , SHA hasing algorithm etc in MicroSoft CryptoAPI are used . In order to increase the performace of cacu

late the digest of large number of data, MD5 hashing algorithm was rewritten . WinSocket API is used as the network interface . The blend of C++ and assembly are used for easily contoling the bottom layer of the system and simplify the programming . The anti-crack method consits the integralization of the file checking , the integralization of the code checking , and anti-debug , anti-disassembly , anti-dump and code metamorphose etc .
The reliable cyrpto algorithms guarantee the crypto strength . As a result of online register , the retail users and the software developers get convenience .
Key words Encrypt ; Digital Signature ; Hashing ; Anti-Debug ; Electronic Register



目 录
1 概述 1
2 密码学简介
2.1 概念 3
2.2 对称密码算法 6
2.3 公开密码算法 6
2.4 单向散列函数 7
2.5 数字签名 8
3 Windows 环境下 PE 文件简介
3.1 WIN32 与 PE 基本概念 10
3.2 PE首部 12
3.3 PE文件的导入表 14
4 当前流行的一些软件保护技术
4.1 序列号保护 21
4.2 时间限制 22
4.3 Key File 保护 23
4.4 CD-check 23
4.5 反跟踪技术(Anti-Debug) 23
4.6 反-反汇编技术(Anti- Disassmbly) 24
4.7 软件狗 25
4.8 Vbox 保护技术 25
4.9 SalesAgent 保护技术 26
4.10 SecuROM 保护技术 26
4.11 软盘加密 26
4.12 将软件与机器硬件信息结合 26
4.13 加壳 27
5 该软件的设计思想
5.1 传统保护的不足 28
5.2 网络的流行 29
5.3 我的方案 29
5.4 该方案的可行性分析 29
6 该软件的整体构架、开发工具及方法
6.1 需求分析 32
6.2 整体框架 35
6.3 各取所长(汇编与 C/C++ 各取所长) 35
6.4 C/C++ 与汇编语言混合编程时的互调协议 36
6.5 该软件中各模块对语言特性的限制及解决方法 40
6.6 C/C++ 和汇编语言的预编译 45
7 该软件的实现及技术细节
7.1 CryptoAPI 简介 47
7.2 几个公共函数和宏 49
7.3 模块共用的结构体定义 54
7.4 Shield 模块 56
7.4.1 壳程序中API和库函数的处理 59
7.4.2 壳程序主体 62
7.4.3 加密壳程序 63
7.4.4 运行中修改自身代码 64
7.4.5 代码散列校验 64
7.4.6 跳转到客户程序入口 65
7.4.7 载入并销毁 Client 程序的 ImportTable 66
7.4.8 自毁壳程序代码 69
7.4.9 编译方法 70
7.5 Merge 模块 71
7.6 Register 模块 76
7.7 Server 模块 77
7.8 软件授权协议的实现 78
7.9 Client的代码(数据)的加密/解密流程图示 82
8 使用说明及演示
8.1 使用说明 83
8.2 演示及效果 83
9 限制、不足与展望
9.1 使用该软件的限制 86
9.2 该软件的不足 86
9.3 对该软件的展望 87
10 结束语
10.1 总结 91
10.2 致谢 91
参考文献 92

1 概述
我引用《应用密码学》作者Bruce Schneier的话:
世界上有两种密码:一种是防止你的小妹妹看你的文件;另一种是防止当局者阅读你的文件资料。
如果把一封信锁在保险柜中,把保险柜藏在纽约的某个

地方…,然后告诉你去看这封信。这并不是安全,而是隐藏。相反,如果把一封信锁在保险柜中,然后把保险柜及其设计规范和许多同样的保险柜给你,以便你和世界上最好的开保险柜的专家能够研究锁的装置。而你还是无法打开保险柜去读这封信,这样才是安全的。
意思是说,一个密码系统的安全性只在于密钥的保密性,而不在算法的保密性。
对纯数据的加密的确是这样。对于你不愿意让他看到这些数据(数据的明文)的人,用可靠的加密算法,只要破解者不知道被加密数据的密码,他就不可解读这些数据。
但是,软件的加密不同于数据的加密,它只能是"隐藏"。不管你愿意不愿意让他(合法用户,或 Cracker)看见这些数据(软件的明文),软件最终总要在机器上运行,对机器,它就必须是明文。既然机器可以"看见"这些明文,那么 Cracker,通过一些技术,也可以看到这些明文。
于是,从理论上,任何软件加密技术都可以破解。只是破解的难度不同而已。有的要让最高明的 Cracker 忙上几个月,有的可能不费吹灰之力,就被破解了。
所以,反盗版的任务(技术上的反盗版,而非行政上的反盗版)就是增加 Cracker 的破解难度。让他们花费在破解软件上的成本,比他破解这个软件的获利还要高。这样 Cracker 的破解变得毫无意义--谁会花比正版软件更多的钱去买盗版软件 ?
然而,要做到"难破解",何尝容易? Sony 曾宣称的超强反盗版(Key 2 Audio音乐 CD反盗版),使用了很尖端的技术,然而最近却被一枝记号笔破解了,成为人们的饭后笑料!
所以,很多看上去很好的技术,可能在 Cracker 面前的确不堪一击。就像马其诺防线一样,Cracker 不从你的防线入手,而是"绕道"。这样,让你的反盗版技术在你做梦也想不到的地方被 Crack 了。
为什么会这样呢 ?归根到底是因为软件在机器上运行,并且软件和机器是分离的--这一点是关键,如果软件和硬件完全绑定,不能分离,是可以做到象 IDEA 之类几乎不可破解的系统的。这将在后面谈传统软件保护技术时详细说明。
对我的这个解决方案,我不能保证Crack高手在几天之内不能破解它,我只能说:"在这个软件中,我尽量堵住了当前破解者普遍使用的方法以及"我想得到"的可能的缺口。"但是我相信,倾注了我三个月心血的反盗版软件,决不是一个"玩具式"的反盗版软件。
2 密码学简介
2.1 概念
(1) 发送者和接收者
假设发送者想发送消息给接收者,且想安全地发送信息:她想确信偷听者不能阅读发送的消息。
(2) 消息和加密
消息被称为明文。用某种方法伪装消息以隐藏它的内容的过程称为加密,加

了密的消息称为密文,而把密文转变为明文的过程称为解密。图2-1表明了这个过程。

图2-1 加密和解密
明文用M(消息)或P(明文)表示,它可能是比特流(文本文件、位图、数字化的语音流或数字化的视频图像)。至于涉及到计算机,P是简单的二进制数据。明文可被传送或存储,无论在哪种情况,M指待加密的消息。
密文用C表示,它也是二进制数据,有时和M一样大,有时稍大(通过压缩和加密的结合,C有可能比P小些。然而,单单加密通常达不到这一点)。加密函数E作用于M得到密文C,用数学表示为:
E(M)=C.
相反地,解密函数D作用于C产生M
D(C)=M.
先加密后再解密消息,原始的明文将恢复出来,下面的等式必须成立:
D(E(M))=M
(3) 鉴别、完整性和抗抵赖
除了提供机密性外,密码学通常有其它的作用:.
(a) 鉴别
消息的接收者应该能够确认消息的来源;入侵者不可能伪装成他人。
(b) 完整性检验
消息的接收者应该能够验证在传送过程中消息没有被修改;入侵者不可能用假消息代替合法消息。
(c) 抗抵赖
发送者事后不可能虚假地否认他发送的消息。
(4) 算法和密钥
密码算法也叫密码,是用于加密和解密的数学函数。(通常情况下,有两个相关的函数:一个用作加密,另一个用作解密)
如果算法的保密性是基于保持算法的秘密,这种算法称为受限制的算法。受限制的算法具有历史意义,但按现在的标准,它们的保密性已远远不够。大的或经常变换的用户组织不能使用它们,因为每有一个用户离开这个组织,其它的用户就必须改换另外不同的算法。如果有人无意暴露了这个秘密,所有人都必须改变他们的算法。
更糟的是,受限制的密码算法不可能进行质量控制或标准化。每个用户组织必须有他们自己的唯一算法。这样的组织不可能采用流行的硬件或软件产品。但窃听者却可以买到这些流行产品并学习算法,于是用户不得不自己编写算法并予以实现,如果这个组织中没有好的密码学家,那么他们就无法知道他们是否拥有安全的算法。
尽管有这些主要缺陷,受限制的算法对低密级的应用来说还是很流行的,用户或者没有认识到或者不在乎他们系统中内在的问题。
现代密码学用密钥解决了这个问题,密钥用K表示。K可以是很多数值里的任意值。密钥K的可能值的范围叫做密钥空间。加密和解密运算都使用这个密钥(即运算都依赖于密钥,并用K作为下标表示),这样,加/解密函数现在变成:
EK(M)=C
DK(C)=M.
这些函数具有下面的特性(见图2-2):
DK(EK(M))=M.

图2-2 使用一个密钥的加/解密


图2-3 使用两个密钥的

加/解密
有些算法使用不同的加密密钥和解密密钥(见图2-3),也就是说加密密钥K1与相应的解密密钥K2不同,在这种情况下:
EK1(M)=C
DK2(C)=M
DK2 (EK1(M))=M
所有这些算法的安全性都基于密钥的安全性;而不是基于算法的细节的安全性。这就意味着算法可以公开,也可以被分析,可以大量生产使用算法的产品,即使偷听者知道你的算法也没有关系;如果他不知道你使用的具体密钥,他就不可能阅读你的消息。
密码系统由算法、以及所有可能的明文、密文和密钥组成的。
基于密钥的算法通常有两类:对称算法和公开密钥算法。下面将分别介绍:
2.2 对称密码算法
对称算法有时又叫传统密码算法,就是加密密钥能够从解密密钥中推算出来,反过来也成立。在大多数对称算法中,加/解密密钥是相同的。这些算法也叫秘密密钥算法或单密钥算法,它要求发送者和接收者在安全通信之前,商定一个密钥。对称算法的安全性依赖于密钥,泄漏密钥就意味着任何人都能对消息进行加/解密。只要通信需要保密,密钥就必须保密。
对称算法的加密和解密表示为:
EK(M)=C
DK(C)=M
对称算法可分为两类。一次只对明文中的单个比特(有时对字节)运算的算法称为序列算法或序列密码。另一类算法是对明文的一组比特亚行运算,这些比特组称为分组,相应的算法称为分组算法或分组密码。现代计算机密码算法的典型分组长度为64比特--这个长度大到足以防止分析破译,但又小到足以方便使用(在计算机出现前,算法普遍地每次只对明文的一个字符运算,可认为是序列密码对字符序列的运算)。
2.3 公开密码算法
公开密钥算法(也叫非对称算法)是这样设计的:用作加密的密钥不同于用作解密的密钥,而且解密密钥不能根据加密密钥计算出来(至少在合理假定的长时间内)。之所以叫做公开密钥算法,是因为加密密钥能够公开,即陌生者能用加密密钥加密信息,但只有用相应的解密密钥才能解密信息。在这些系统中,加密密钥叫做公开密钥(简称公钥),解密密钥叫做私人密钥(简称私钥)。私人密钥有时也叫秘密密钥。为了避免与对称算法混淆,此处不用秘密密钥这个名字。
用公开密钥K加密表示为
EK(M)=C.
虽然公开密钥和私人密钥是不同的,但用相应的私人密钥解密可表示为:
DK(C)=M
有时消息用私人密钥加密而用公开密钥解密,这用于数字签名(后面将详细介绍),尽管可能产生混淆,但这些运算可分别表示为:
EK(M)=C
DK(C)=M
当前的公开密码算法的速度,比起对称密码算法,要慢的多,这使得公开密码算法在大数据量的加密中应用有限。


2.4 单向散列函数
单向散列函数 H(M) 作用于一个任意长度的消息 M,它返回一个固定长度的散列值 h,其中 h 的长度为 m 。
输入为任意长度且输出为固定长度的函数有很多种,但单向散列函数还有使其单向的其它特性:
(1) 给定 M ,很容易计算 h ;
(2) 给定 h ,根据 H(M) = h 计算 M 很难 ;
(3) 给定 M ,要找到另一个消息 M' 并满足 H(M) = H(M') 很难。
在许多应用中,仅有单向性是不够的,还需要称之为"抗碰撞"的条件:
要找出两个随机的消息 M 和 M',使 H(M) = H(M') 满足很难。
由于散列函数的这些特性,由于公开密码算法的计算速度往往很慢,所以,在一些密码协议中,它可以作为一个消息 M 的摘要,代替原始消息 M,让发送者为 H(M) 签名而不是对 M 签名 。
如 SHA 散列算法用于数字签名协议 DSA中。
2.5 数字签名
提到数字签名就离不开公开密码系统和散列技术。
有几种公钥算法能用作数字签名。在一些算法中,例如RSA,公钥或者私钥都可用作加密。用你的私钥加密文件,你就拥有安全的数字签名。在其它情况下,如DSA,算法便区分开来了??数字签名算法不能用于加密。这种思想首先由Diffie和Hellman提出 。
基本协议是简单的 :
(1) A 用她的私钥对文件加密,从而对文件签名。
(2) A 将签名的文件传给B。
(3) B用A的公钥解密文件,从而验证签名。
这个协议中,只需要证明A的公钥的确是她的。如果B不能完成第(3)步,那么他知道签名是无效的。
这个协议也满足以下特征:
(1) 签名是可信的。当B用A的公钥验证信息时,他知道是由A签名的。
(2) 签名是不可伪造的。只有A知道她的私钥。
(3) 签名是不可重用的。签名是文件的函数,并且不可能转换成另外的文件。
(4) 被签名的文件是不可改变的。如果文件有任何改变,文件就不可能用A的公钥验证。
(5) 签名是不可抵赖的。B不用A的帮助就能验证A的签名。
在实际应用中,因为公共密码算法的速度太慢,签名者往往是对消息的散列签名而不是对消息本身签名。这样做并不会降低签名的可信性。
本章仅对密码学进行了一些简要的介绍,更多的请参阅参考文献[1]。

3 Windows 环境下 PE 文件简介
3.1 WIN32 与 PE 基本概念
只要用过电脑的人都知道什么是 Windows,Windows95 已经是过时的昨日黄花了,Windows98 也已推出将近四年了。2000 年又推出了Windows2000,今年又推出了 WindowsXP,微软的操作系统更新速度是如此的快,以至于昨天还在使用的东西,在今天看来就已经过时了。Windows98 以后,微软传言不在推出 9x 内核的操作系统,但是2000 年下半年却正式推出了 WindowsMillennium,简称 Win.Me 。然而从 WindowsXP 的

推出,可以断言,微软不会在升级 Win9x 操作系统了。Windows2000 和 WindowsXP 都是基于 NT 内核的。
所有这些操作系统都使用一种"可移植可执行文件格式"(Portable Executable File format),简称PE文件格式。
下面简短介绍一下 PE 文件的一些概念。详细内容请参阅参考文献[14]。
Windows NT 继承了 VAX? VMS? 和 UNIX? 的传统。许多 Windows NT 的创始人在进入微软前都在这些平台上进行设计和编码。当他们开始设计 Windows NT 时,很自然的,为了最小化工程的启动时间,他们会使用以前写好的并且已经测试过的工具。用这些工具生成并且工作的可执行文件和 OBJ 文件格式叫做 COFF(Common Object File format 的首字母缩写)。COFF 的年龄不超过八年。
COFF 本身是一个很好的起点,但是需要扩展到一个现代操作系统如 Windows 95 和 Windows NT 就要进行一些更新。其结果就是产生了(PE格式)可移植可执行文件格式。它被称为"可移植的"是因为在所有平台(如x86,Alpha,MIPS等等)上实现的WindowsNT 都使用相同的可执行文件格式。当然了,也有许多不同的东西如二进制代码的CPU指令。重要的是操作系统的装入器和程序设计工具不需要为任何一种CPU完全重写就能达到目的。
关于PE文件最重要的是,磁盘上的可执行文件和它被WINDOWS载入内存之后(PE文件载入内存之后称为PE映像)是非常相像的(如图 3-1)。WINDOWS载入器不必为从磁盘上载入一个文件而辛辛苦苦创建一个进程。载入器使用内存映射文件机制把文件中相似的块映射到虚拟空间中。构造式的进行分析,一个PE文件类似一个预制的屋子。它本质上开始于这样一个空间,这个空间后面有几个把它连到其余空间的机件(就是说,把它联系到它的DLL上,等等)。这对PE格式的DLL式一样容易应用的。一旦这个模块被载入,Windows 就可以有效的把它和其它内存映射文件同等对待。

图 3-1 PE文件和PE映像的布局很相似

对Win32来讲,模块所使用的所有代码,数据,资源,导入表,和其它需要的模块数据结构都在一个连续的内存块中。在这种形势下,你只需要知道载入器把可执行文件映射到了什么地方就可以了。通过作为映像的一部分的指针,你可以很容易的找到这个模块所有不同的块。
另一个你需要知道的概念是相对虚拟地址(RVA)。PE文件中的许多域都用术语RVA来指定。一个RVA只是一些项目相对于文件映射到内存的偏移。比如说,载入器把一个文件映射到虚拟地址0x10000开始的内存块。如果映像中一个实际的表的首址是0x10464,那么它的RVA就是0x464。
(虚拟地址 0x10464)-(基地址 0x10000)=RVA 0x00464
为了把一个RVA转化成一个有用的指针,只

需要把RVA值加到模块的基地址上即可。基地址是EXE和DLL内存映射文件的基址,这个基址在Win32中这是一个很重要的概念。为了方便起见,WindowsNT 和 Windows9x用模块的基址作为这个模块的实例句柄(HINSTANCE)。可以对任何DLL调用GetModuleHandle(dllname)得到一个指针去访问它的组件。如果 dllname 为 NULL,则得到执行体自己的模块句柄。这是非常有用的,如通常编译器产生的启动代码将取得这个句柄并将它作为一个参数hInstance传给WinMain 。
3.2 PE首部
和其它可执行文件格式一样,PE文件在众所周知的地方有一些定义文件其余部分面貌的域。首部就包含这样象代码和数据的位置和尺寸的地方,操作系统要对它进行干预,比如初始堆栈大小,和其它重要的块的信息。和微软其它执行体的格式相比,PE格式的执行体的主要的首部不是在文件的最开始。典型的PE文件最开始的数百个字节被DOS残留部分占用。这个残留部分是一个打印如"这个程序不能在DOS下运行!"这类信息的小程序。所以,你在一个不支持Win32的系统中运行这个程序,会得到这类错误信息。当载入器把一个Win32程序映射到内存,这个映射文件的第一个字节对应于DOS残留部分的第一个字节,这是无疑的。于是,和你启动的任一个基于Win32 的程序一起,都有一个基于DOS的程序连带被载入。
和微软的其它可执行格式一样,你可以通过查找它的起始偏移来得到真实首部,这个偏移放在DOS残留首部中。WINNT.H头文件包含了DOS残留程序的数据结构定义(注),使得很容易找到PE首部的起始位置。e_lfanew 域是PE真实首部的偏移。为了得到PE首部在内存中的指针,只需要把这个值加到映像的基址上即可。
// 忽略类型转化和指针转化
pNTHeader = dosHeader + dosHeader->e_lfanew;
注:为了不失简洁,这里未列出这些结构体的完整定义就直接引用,这里直接引用的结构体其定义都在winnt.h中,建议读者在读本章时参考Winnt.h 。
一旦你有了PE主首部的指针,游戏就可以开始了!PE主首部是一个IMAGE_NT_HEADERS的结构,在WINNT.H中定义。这个结构由一个双字(DWORD)和两个子结构组成,布局如下:
DWORD Signature; // 标志域
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
标志域用ASCII表示就是"PE\0\0"。
标志域之后的是结构 IMAGE_FILE_HEADER 。这个域只包含这个文件最基本的信息。这个结构看上去并未从它的原始COFF实现更改过。除了是PE首部的一部分,它还表现在微软Win32编译器生成的COFF OBJ 文件的最开始部分。
这个部分的详细说明请参阅参考文献[14](本人已翻译为中文)。

3.3 PE文件的导入表
因为导入表在该软件的设计中很关键,

后面壳程序导入表的构建,对客户程序导入表的载入等,都牵涉到导入表 。所以,在这里有必要说明一下,更详细的说明请参阅参考文献[14][15][16]。
导入表,简单的说,导入表的作用相当于 DOS 的系统中断功能。两者都是操作系统 API 。只不过DOS中断不需要操作系统在载入每个执行体时填入 API 的实际地址,并且,导入表还可以导入除操作系统 API 之外的其它模块中的函数。
在一个PE文件中,当你调用另一模块中的一个函数时(比如在USER32.DLL中的GetMessage ),编译器产生的CALL 指令并不把控制直接转移到在DLL中的这个函数。代替的,CALL 指令把把控制转移到一个也在 .text 中的
JMP DWORD PTR [XXXXXXXX]
指令处(如图3-2)。 这个 JMP 指令通过一个在导入表中的DWORD变量间接的转移控制。 导入表的这个DWORD包含操作系统函数入口的实际地址。为什么DLL调用用这种方式来实现呢?原来,通过一个位置传送所有的对一个给定的DLL函数的调用,载入器不需要改变每个调用DLL的指令。所有的PE载入器必须做的是把目标函数的正确地址放到导入表的一个 DWORD 中。不需要改变任何call指令本身。
如果你想通过函数指针调用一个函数,事情也会如你所预料的一样。但是,如果你想读取 GetMessage 开始的字节,你将不能如愿。后面讲到反 API 断点时会详细说明。

图3-2 一个导入函数调用的图示
前面描述了函数调用怎样到一个外部DLL中而不直接调用这个DLL 。代替的,在执行体中的 .text 块中(如果你用Borland C++ 就是 .icode 块),CALL指令到达一个
JMP DWORD PTR [XXXXXXXX]
指令处。
JMP指令寻找的地址把控制转移到实际的目标地址。PE文件的导入表会包含一些必要的信息,这些信息是载入器用来确定目标函数的地址以及在执行体映像中修正他们的。
导入表开始于一个IMAGE_IMPORT_DESCRIPTOR数组。每个DLL都有一个PE文件隐含链接上的IMAGE_IMPORT_DESCRIPTOR 。没有指定这个数组中结构的数目的域。代替的,这个数组的最后一个元素是一个全NULL的IMAGE_IMPORT_DESCRIPTOR 。IMAGE_IMPORT_DESCRIPTOR的格式显示在表3-1 。
表 3-1 IMAGE_IMPORT_DESCRIPTOR 的格式
DWORD Characteristics
在一个时刻,这可能已是一个标志集。然而,微软改变了它的涵义并不再糊涂地升级WINNT.H 。这个域实际上是一个指向指针数组的偏移(RVA)。其中每个指针都指向一个IMAGE_IMPORT_BY_NAME结构。现在这个域的涵义是 OriginalFirstThunk 。

DWORD TimeDateStamp
表示这个文件的创建时间。

DWORD ForwarderChain
这个域联系到前向链。前向链包括一个DLL函数向另一个DLL转送引用。比如,在WindowsNT中,NTDLL.DLL就出现了的一些前向的它向KERNEL32.DLL导出的函

数。应用程序可能以为它调用的是NTDLL.DLL中的函数,但它最终调用的是KERNEL32.DLL中的函数。这个域还包含一个FirstThunk数组的索引(即刻描述)。用这个域索引的函数会前向引用到另一个DLL 。不幸的是,函数怎样前向引用的格式没有文档,并且前向函数的例子也很难找。

DWORD Name
这是导入DLL的名字,指向以NULL结尾的ASCII字符串。通用例子是KERNEL32.DLL和USER32.DLL 。


PIMAGE_THUNK_DATA FirstThunk
这个域是指向IMAGE_THUNK_DATA联合的偏移(RVA)。几乎在任何情况下,这个域都解释为一个指向的IMAGE_IMPORT_BY_NAME结构的指针。如果这个域不是这些指针中的一个,那它就被当作一个将从这个被导入的DLL的导出序数值。如果你实际上可以从序数导入一个函数而不是从名字导入,从文档看,这是很含糊的。
IMAGE_IMPORT_DESCRIPTOR 的一个重要部分是导入的DLL的名字和两个IMAGE_IMPORT_BY_NAME指针数组。在EXE文件中,这两个数组(由Characteristics域和FirstThunk域指向)是相互平行的,都是以NULL指针作为数组的最后一个元素。两个数组中的指针都指向 IMAGE_IMPORT_BY_NAME 结构。图 3-3 显示了这种布局。

图 3-3 导入表中一个项的结构图示

PE文件导入表中的每一个函数有一个 IMAGE_IMPORT_BY_NAME 结构。IMAGE_IMPORT_BY_NAME结构非常简单,看上去是这样:
WORD Hint;
BYTE Name[?];
第一个域是导入函数的导出序数的最佳猜测值。和NE文件不同,这个值不是必须正确的。于是,载入器指示把它当作一个进行二分查找的建议开始值。下一个是导入函数的名字的ASCIIZ字符串。
为什么有两个平行的指针数组指向结构IMAGE_IMPORT_BY_NAME ?第一个数组(由Characteristics域指向的)单独的留下来,并不被修改。经常被称作提名表。第二个数组(由FirstThunk域指向的)将被PE载入器覆盖。载入器在这个数组中迭代每个指针,并查找每个IMAGE_IMPORT_BY_NAME结构指向的函数的地址。载入器然后用找到的函数地址覆盖这个指向IMAGE_IMPORT_BY_NAME结构的指针。
JMP DWORD PTR [XXXXXXXX] 中的 [XXXXXXXX] 指向 FirstThunk 数组的一个条目。因为由载入器覆盖的这个指针数组实际上保持所有导入函数的地址,叫做"导入地址表"。
在优化上无止境的探索中,微软在WindowsNT中"优化"了系统DLL(KERNEL32.DLL等等)的thunk数组。在这个优化中,这个数组中的指针不再指向IMAGE_IMPORT_BY_NAME结构,它们已经包含了导入函数的地址。换句话说,载入器不需要去查找函数的地址并用导入函数的地址覆盖thunk数组(译注)。
译注:这就是 Bound Import,关于Bound Import,参考文献[15][16],有详细介绍。不过,在我的软件中,忽略了对Bound Import 的处理,这样会造成一些程序

载入速度的减小。但使问题简化了许多。
因为导入地址表在一个可写的块中,拦截一个EXE或DLL对另一个DLL的调用就相对容易。只需要修改适当地导入地址条目去指向希望拦截的函数。不需要修改调用者或被调者的任何代码。
注意微软产生的PE文件的导入表并不是完全被连接器同步的,这一点很有趣。所有对另一个DLL中的函数的调用的指令都在一个导入库中。当你连接一个DLL时,库管理器(LIB32.EXE或LIB.EXE)扫描将要被连接的OBJ文件并且创建一个导入库。这个导入库完全不同于16位NE文件连接器使用的导入库。32位库管理器产生的导入库有一个.text块和几个.idata$块。导入库中的.text块包含 JMP [XXXX] 指令,这条指令的标号在OBJ文件的符号表中用一个符号名来存储。这个符号名对将要从DLL中导出的所有函数名讲都是唯一的(例如:_Dispatch_Message@4)。导入库中的一个.idata$块包含一个从导入库中引用的地址,即导入库的 .text 中的指令:
JMP [XXXX]
中的XXXX 。
另一个.idata$块有一个提示序号(hint ordinal)的空间。这两个域就组成了IMAGE_IMPORT_BY_NAME结构。当你晚期连接一个使用导入库的PE文件时,导入库的块被加到连接器需要处理的块的列表中,这个列表在你的OBJ文件中。一旦导入库中的这个xxxx的名字和和要导入的函数名相同,连接器就假定这条jmp [xxxx]指令就是这个导入函数,并修正其中的xxxx ,使其指向这个.idata$中的一个存储导入函数地址的空间。导入库中的这条jmp [xxxx]指令在本质上就被当作这个导入函数本身了。
除了提供一个导入函数的指令jmp [xxxx],导入库还提供PE文件的.idata块(或称导入表)的片断。这些片断来自于库管理器放入导入库中的不同的.idata$块。简而言之,连接器实际上不知道出现在不同的OBJ文件中的导入函数和普通函数之间的不同。连接器只是按照它的内部规则去建立并结合块,于是,所有的事情就自然顺理成章了。
本文有关导入表的内容,基本上就这么多,要得到更多的信息,请参阅参考文献[14][15][16]。

4 当前流行的一些软件保护技术
4.1 序列号保护
数学算法一项都是密码加密的核心,但在一般的软件加密中,它似乎并不太为人们关心,因为大多数时候软件加密本身实现的都是一种编程的技巧。但近几年来随着序列号加密程序的普及,数学算法在软件加密中的比重似乎是越来越大了。
看看在网络上大行其道的序列号加密的工作原理。当用户从网络上下载某个shareware--共享软件后,一般都有使用时间上的限制,当过了共享软件的试用期后,你必须到这个软件的公司去注册后方能继续使用。注册过程一般是用户把自己

的私人信息(一般主要指名字)连同信用卡号码告诉给软件公司,软件公司会根据用户的信息计算出一个序列码,在用户得到这个序列码后,按照注册需要的步骤在软件中输入注册信息和注册码,其注册信息的合法性由软件验证通过后,软件就会取消掉本身的各种限制,这种加密实现起来比较简单,不需要额外的成本,用户购买也非常方便,在互联网上的软件80%都是以这种方式来保护的。
软件验证序列号的合法性过程,其实就是验证用户名和序列号之间的换算关系是否正确的过程。其验证最基本的有两种,一种是按用户输入的姓名来生成注册码,再同用户输入的注册码比较,公式表示如下:
序列号 = F(用户名)
但这种方法等于在用户软件中再现了软件公司生成注册码的过程,实际上是非常不安全的,不论其换算过程多么复杂,解密者只需把你的换算过程从程序中提取出来就可以编制一个通用的注册程序。
另外一种是通过注册码来验证用户名的正确性,公式表示如下:
用户名称 = F逆(序列号) (如ACDSEE)
这其实是软件公司注册码计算过程的反算法,如果正向算法与反向算法不是对称算法的话,对于解密者来说,的确有些困难,但这种算法相当不好设计。
于是有人考虑到以下的算法:
F1(用户名称) = F2(序列号)
F1、F2是两种完全不同的的算法,但用户名通过F1算法计算出的特征字等于序列号通过F2算法计算出的特征字,这种算法在设计上比较简单,保密性相对以上两种算法也要好的多。如果能够把F1、F2算法设计成不可逆算法的话,保密性相当的好;可一旦解密者找到其中之一的反算法的话,这种算法就不安全了。一元算法的设计看来再如何努力也很难有太大的突破,那么二元呢?
特定值 = F(用户名,序列号)
这个算法看上去相当不错,用户名称与序列号之间的关系不再那么清晰了,但同时也失去了用户名于序列号的一一对应关系,软件开发者必须自己维护用户名称与序列号之间的唯一性,但这似乎不是难以办到的事,建个数据库就可以了。当然也可以把用户名称和序列号分为几个部分来构造多元的算法。
特定值 = F(用户名1,用户名2,...序列号1,序列号2...)
现有的序列号加密算法大多是软件开发者自行设计的,大部分相当简单。而且有些算法作者虽然下了很大的功夫,效果却往往得不到它所希望的结果。
4.2 时间限制
有些程序的试用版每次运行都有时间限制,例如运行10分钟或20分钟就停止工作,必须重新运行该程序才能正常工作。这些程序里面自

然有个定时器来统计程序运行的时间。
这种方法使用的较少。
4.3 Key File 保护
Key File(注册文件)是一种利用文件来注册软件的保护方式。Key File一般是一个小文件,可以是纯文本文件,也可以是包含不可显示字符的二进制文件,其内容是一些加密过或未加密的数据,其中可能有用户名、注册码等信息。文件格式则由软件作者自己定义。试用版软件没有注册文件,当用户向作者付费注册之后,会收到作者寄来的注册文件,其中可能包含用户的个人信息。用户只要将该文件放入指定的目录,就可以让软件成为正式版。该文件一般是放在软件的安装目录中或系统目录下。软件每次启动时,从该文件中读取数据,然后利用某种算法进行处理,根据处理的结果判断是否为正确的注册文件,如果正确则以注册版模式来运行。
这种保护方法使用也不多,但是,我个人认为,比时间限制要好。
4.4 CD-check
即光盘保护技术。程序在启动时判断光驱中的光盘上是否存在特定的文件,如果不存在则认为用户没有正版光盘,拒绝运行。在程序运行的过程当中一般不再检查光盘的存在与否。Windows下的具体实现一般是这样的:先用GetLogicalDriveStrings( )或GetLogicalDrives( )得到系统中安装的所有驱动器的列表,然后再用GetDriveType( )检查每一个驱动器,如果是光驱则用CreateFileA( )或FindFirstFileA( )等函数检查特定的文件存在与否,并可能进一步地检查文件的属性、大小、内容等。
4.5 反跟踪技术(Anti-Debug)
好的软件保护都要和反跟踪技术结合在一起。如果没有反跟踪技术,软件等于直接裸露在 Cracker 面前。这里说的反跟踪,指的是反动态跟踪。即防止 Cracker 用 SoftICE 之类的调试器动态跟踪,分析软件。当前的这类软件还有如 TRW 、ICEDUMP 等等。反跟踪技术一般是具有针对性的,即针对某种调试器的反跟踪,而不能防止所有的调试器跟踪,如果有新的破解工具出现,就需要相应的反跟踪技术 。
这种技术一般是检测这些特定的调试器是否驻留内存,如果驻留内存,就认为被跟踪,从而拒绝执行,或进行一些惩罚性措施 。还有一些检测方法,如假设这些调试器在内存中,软件和这些调试器通信,如果结果合乎这些调试器的输出 。就认为被跟踪 。或者在内存中搜寻这些调试器的特征串,如果找到,就认为被跟踪 。有的甚至用中断钩子、SEH(Structural Exception Handle,即结构化异常处理)检测调试器。
4.6 反-反汇编技术(Anti- Disassmbly)
即 Anti-Disassmbly 。可针对专门的反汇编软件设计的"陷阱",让反汇编器陷入死循环,但这种方法没有通用性。
一般是使用花指令 。这种方法

有通用性,即所有的反汇编器都可以用这种方法来抵挡 。这种方法主要是利用不同的机器指令包含的字节数并不相同,有的是单字节指令,有的是多字节指令。对于多字节指令来说,反汇编软件需要确定指令的第一个字节的起始位置,也就是操作码的位置,这样才能正确地反汇编这条指令,否则它就可能反汇编成另外一条指令了。并且,多字节,指令长度不定,使得反汇编器在错误译码一条指令后,接下来的许多条指令都会被错误译码 。所以,这种方法是很有效的 。
实施方法:在程序中加入一些无用的字节来干扰反汇编软件的判断,从而使得它错误地确定指令的起始位置,那么也就达到了干扰反汇编器工作的目的。一般形式如下:


....
....
jmp L1
dd 012344578h ;这里是一些随机数,用来干扰反汇编器
;对指令的译码
L1:
....
...
4.7 软件狗
软件狗是一种智能型加密工具。它是一个安装在并口、串口等接口上的硬件电路,同时有一套使用于各种语言的接口软件和工具软件。当被狗保护的软件运行时,程序向插在计算机上的软件狗发出查询命令,软件狗迅速计算查询并给出响应,正确的响应保证软件继续运行。如果没有软件狗,程序将不能运行,复杂的软硬件技术结合在一起防止软件盗版。真正有商业价值得软件一般都用软件狗来保护。
平时常见的狗主要有"洋狗"(国外狗)和"土狗"(国产狗)。这里"洋狗"主要指美国的彩虹和以色列的HASP,"土狗"主要有金天地(现在与美国彩虹合资,叫"彩虹天地")、深思、尖石。总的说来,"洋狗"在软件接口、加壳、反跟踪等"软"方面没有"土狗"好,但在硬件上绝对无法破解(应当说破解难度非常大);而"土狗"在软的方面做的很好,但在硬件上不如"洋狗",稍有单片机功力的人,都可以复制。
4.8 Vbox 保护技术
Vbox 是一个软件 。它是用来保护其它软件的 。凡被 Vbox 保护的软件,一旦过了试用期,就不能再使用了,删了重装也没用,除非删除整个操作系统再重装。
4.9 SalesAgent 保护技术
SalesAgent 保护的软件一般具有 x 天试用再购买的接口,是一种时间限制保护方式。才用这种保护方式的软件主要有 Macromedia Flash 4 、DreameWaver 等等。
4.10 SecuROM 保护技术
SecuROM ()是Sony 开发的一种商业光盘加密技术,它可以阻止用户对加密光盘的复制,被保护的光盘上有 CMS16.dll 、cms32_95.dll 、cms32_nt.dll 这几个文件。很多游戏光盘才用这种保护技术。
4.11 软盘加密
通过在软盘上格式化一些非标准磁道,在这些磁道上写入一些数据,如软件的解密密钥等等。这种软盘成为"钥匙盘"。软

件运行时用户将软盘插入,软件读取这些磁道中的数据,判断是否合法的"钥匙盘"。
软盘加密还有其它一些技术,如弱位加密等等。
随着近年来软盘的没落,这种方法基本上退出了历史舞台。
4.12 将软件与机器硬件信息结合
用户得到(买到或从网上下载)软件后,安装时软件从用户的机器上取得该机器的一些硬件信息(如硬盘序列号、BOIS序列号等等),然后把这些信息和用户的序列号、用户名等进行计算,从而在一定程度上将软件和硬件部分绑定。在我的加壳程序中将使用这种方法和其它方法的结合,后面会详细说明。
4.13 加壳
就是在完整的软件上--已编译连接,可以运行的程序上,加上一个"壳",这个"壳",对软件进行保护,这些壳一般综合运用了4.1~4.6 节所述的软件保护技术。因为我的设计方案中使用了加壳技术,后面将会详细说明,这里不再赘述。
5 该软件的设计思想
5.1 传统保护的不足
上一章介绍了当前流行的一些软件保护技术,其中有些工作得非常好,如序列号技术,几乎所有的软件都使用了这种技术。
这里我不会指出这些技术如何会被破解,因为前面已经说明了,软件保护都可以被破解。只说出这些方案的"非技术"的缺点。
可以看出,这些技术中,软件和硬件仍然是分离的。
在软件狗保护中,软件和硬件有了一定的结合,但是,还没有把软件和一台特定的机器绑定 。在软盘保护中,软件和硬件也有一定结合。但是用户仍然可以在多台机器上安装同一套软件。只在需要这些硬件的时候时候,如插上软件狗,插入钥匙盘,就可以在多台机器上使用同一套软件了 。并且,要正版用户在使用软件时要插上软件狗,插入钥匙盘,造成很多不必要的麻烦 。如用户的并行口可能用户打印机……还有,由于加入了硬件,这种保护方法的成本较高,对一些比较小的软件,这种方法是不实用的。
而 CD-Check 等光盘加密技术,有个缺点是使用中用户必须插入光盘,而现在的硬盘技术的发展,使得存储容量不再是一个问题,用户往往把光盘上的所有东西都装入硬盘,而要用户在每次运行软件时都插入光盘,有点难以接受。
而比较看好的序列号保护技术,则存在一个通病--算法要软件开发者自己设计,而且,如果一对(序列号,用户名)被 Craker 从 Internet 上发布出去,所有的用户都可以用这对(序列号,用户名)来"注册"软件,从而非法使用。
5.2 网络的流行
现在,我想没有哪一个使用过计算的人没有使用过 Internet。全世界有几亿人在使用 Internet,我们中国有几千万万人在使用 Internet 。许多商业软件也都有 Internet 试用版,共享软

件(这里不讨论免费软件)甚至都是在 Internet 上发布的 。而几乎所有的软件在 Internet 上都有破解版 。于是,把软件保护和 Internet 结合起来是自然而然的事 。
要把软件保护和 Internet 结合起来,就自然要保证安全 。要保证安全就离不开密码学,在第一章已经简要介绍了密码学的一些概念 。网络上信息传输的安全很重要,特别是一些敏感信息,如用户资料,密码等等 。
5.3 我的方案
针对前面的一些问题,通过各方面比较,权衡,我提出了这套软件保护方案 。
这套方案集传统的序列号保护 、利用硬件信息保护 、加壳保护 、反跟踪 、反-反汇编 、反 Dump 、反 API 断点等于一身。又加入了密码学中的数字签名 、散列 、密钥交换等等 。形成了我自己独特的 、加密强度更高 、使用上更方便(现在只是作为一个演示,使用上还谈不上方便)、更合用户口味的反盗版 、电子注册解决方案 。
该方案的具体细节将在接下来的两章中进行描述。
5.4 该方案的可行性分析
可以说,没有密码学和网络的话,这个方案只能是纸上谈兵 。我正是看到了这两点,才萌发了这个方案的设计思想 。这个设想在 3 个月之前有了一个模糊的想法,在随后,经过查阅大量相关资料,加上自己多方面的考虑,逐渐地就在我的脑海里有了一个清晰的轮廓,这三个月的时间,只是要把这个设想变成现实 。
现代密码学(特别是非对称密码学),是直到 70 年代才初现端倪的,但是发展很快,到现在,我们已经可以使用许多现成的密码算法 。这些算法甚至成为 Windows 不可分割的一部分,称为 CryptoAPI,如果没有 CryptoAPI,我将不得不把庞大的算法库包括在我的源代码内,甚至有许多算法要自己编写代码实现 。这对我将是一个灾难 !
为了简化设计,并突出主要问题,不拘泥于花俏的外表,我不会去做图形用户接口,而使用简单的 Consol 控制台用户接口 。
踩在巨人的肩膀上,将看的更高,更远。正是有了这么多前人的努力,我才能在此之上进行自己的创新。
Internet 的流行,盗版软件的泛滥,几乎成为我们中国民族软件企业的灾难 。使得该软件具有很积极的现实意义 。--只需要极少的修改,增加图形界面,本系统即可作为商业应用。甚至不需要修改,只编写一些简单的批处理文件,都可以让软件开发者和普通用户方便的使用(后面的演示就是使用简单的批处理文件来简化用户界面的)。
正是基于这些原因,比起传统方案,本方案有以下优点:
(1) 当前许多软件保护技术,大多只求精于使用操作系统本身的特点 、沉溺于繁杂的技术细节,而不考虑使用更好的密码学协

议 、算法。本方案不同,首要的是使用可靠的密码学协议 、算法,使加密强度得到了保证。
(2) 通过网络对软件授权 、是当前流行软件保护技术的一个盲点,而综合使用数字签名 、散列技术 、密钥交换等技术的,更是少之又少。据我所知,有一个俄罗斯人编写的非常有名的加壳软件,使用了很多的密码算法--被人们戏称:"用光了世界上所有的密码算法!"但是他仍然没有数字签名以及密钥交换,并且也是单机的。
(3) 通过网络实现软件授权,成本很低,用户使用又非常方便,软件开发者管理软件的销售 、代理 、等等也很方便 。
(4) 本方案不光在密码算法 、协议上设计得很合理,而且,在最终受保护程序执行时,也比传统方案要好,这些将在详述壳程序主要技术时具体说明 。
(5) 本方案有很强的可扩展性,在软件授权 、密码协议的实现上使用了面向对象方法 。在壳程序的开发中使用了结构清晰 、分层次分明的分析、设计 。几个模块相对独立,在此基础上,各个模块在遵循一套规则的前提下可以相对独立设计,如相同的 Merge 、Register 程序,可以用不同的 Shield 壳程序来保护客户软件 。
(6) 本软件使用 VC 和汇编语言组合开发,使得开发成本降低了许多,软件的可靠性 、可用性 、可测试性 、可维护性都有很大的提高 。而当前几乎所有的同类软件都是用汇编语言开发,成本很高 。--如果本软件完全使用汇编语言,在这短短的三个月之间是不可能完成的 。
(7) 使用 CryptoAPI,使该软件的整体设计独立于具体的密码学算法,为了使用不同的算法,只需要修改相关参数即可,而不需要重新编写大量代码;并且,使用 CryptoAPI,使得该软件的体积大大缩小 --如果自己编写密码算法,软件的体积将异常庞大,至少是目前的 3倍以上 。
6 该软件的整体构架、开发工具及方法
6.1 需求分析
本软件的需求,就是一个软件的授权协议,及保护壳应该完成的保护功能,下面将简单说明:
(1) 角色说明(软件授权协议中的各个角色):
P :是一个软件产品;
A :是 P 的开发者;
B :是 A 的一个代理商(也可以是 A 自己) ;
C :是 P的最终用户。
(2) 软件授权的协议执行过程
约定:
(a) Server 、Merge 、Register 都运行在不同的机器上 。Server 运行在 A 的机器上,Merge 运行在 B 的机器上,Register 运行在 C 的机器上 。
(b) 服务器对开发者卖出的软件进行授权----当然他的软件是用我的软件保护过了的 。
(c) Server 是昼夜不停一直运行着的,它接收来自 Merge 和 Register 的请求 。
(d) 在下面的协议叙述过程中,Server 和 A 、Merge 和 B 、Register 和 C 交叠使用 。它们多

数时刻是同义词 。
协议开始执行:
这里对授权协议的说明只是简单的说明该软件的需求 。后面章节将详细地说明软件的授权过程,并给出协议所用的算法 。
(a) A把他的软件 P 给 B,A 运行 Server 。
(b) B 要卖出一套软件 P,就运行 Merge 程序。
Merge 程序产生一个随机的 SN,将此 SN 发往 Server (即 A )。
(c) Server 收到 SN,从注册数据库中查找 SN,如果找到(能找到该 SN 的概率的数量级在 以下),就发回信息给 Merge,它产生了重复的 SN,要它再重新计算一个 SN 。如果未找到(几乎总是找不到的),就把 SN 登进注册数据库,用自己的私人密钥 ASK对 SN 进行数字签名,得到 K1,把 K1 作为解密密码发给B , 同时也将自己的公钥 APK 发给 B 。B 用 K1 对 P 进行加密 。
(d) 软件卖给 C 后,C 可以在本地主机(用户自己的计算机)上,通过网络(拨号上网,或 ADSL,或其它方式)向开发者的网络服务器注册软件,然后才能使用 。C 运行注册程序 Register,Register 从 Q 中取得序列号SN(serial number) , 再取得本地主机的硬件信息 ,计算并存储该硬件信息 HD 的散列值 SAC (System Autentication Code)。 将它发送给服务器 Server 。服务器 Server 从数据库中查找这个 SN,如果找到 , 并且这个序列号的拷贝已经注册 , 并且 它收到的 SAC和以前注册的 SAC相同--相同拷贝可以在同一台被授权的计算机上多次安装/注册--或者找到了 SN,但该 SN 还未注册,就将随 SN 一起发来的 SAC 存入数据库,待以后再验证这台计算机。Server把SN对应的解密密码K1发给用户 C,同时将自己的公钥 APK 也发给 C 。
(e) Register 程序用K1解密 Q,同时用本地主机硬件信息HD的另一个散列值 K2 作为密钥加密 Q,最终得到 R 。
现在,用户C 可以运行软件 R 了(R 就是经过注册的P),R具有了一些反Cracker 功能及其它保护功能。
(3) 最终受保护的软件 P (即上面授权协议执行到最后产生的可执行程序 R )应该具有的功能:
(a) 只能在注册的那太机器上运行(反非法使用) ;
(b) 病毒检测和 Cracker 更改检测(保护功能) ;
(c) 反跟踪功能(Anti-Debug,或称 Anti-Trace );
(d) 反 Dump 功能(Anti-Dump);
(e) 反反汇编功能(Anit-Disassembler);
(f) 其它反 Crack 功能 。
(4) 协议的图示:
图6-1中方框表示处理的文件,Shield 是"壳程序", 椭圆表示"处理过程",也即该软件的模块 。
该图在后面的章节中还要引用到 。

6.2 整体框架
从上一节的需求分析可以看到,本软件至少要三个独立的模块:
(1) Server 服务器模块 。
(2) Merge 产生软件拷贝的模块 。
(3) Register 最终用户注册软件的模块 。
然而,还差一个模块,即保护软件的"外壳"

模块 。这个模块就叫做 Shield,意思是保护壳 。
整个软件就划分为这四个模块 !
它们的关系如图 6-1,授权协议的过程同时也指出了这四个模块之间的关系。
6.3 各取所长(汇编与 C/C++ 各取所长)
从图中可以看出,Server 、Merge 、Register 模块都没有涉及到底层的操作 。而 Shield 有没有涉及到操作系统底层,由前面的图形及协议过程还不能得出 。
但是,从需求可以知道:Shield 是将要附加到受保护的软件上的,运行受保护的软件时,Shield 程序将首先运行,从文件中提取一些数据,做一些必要的检查,还要解密,还要进行反跟踪,反-反汇编,最终还要在比较容易控制的情况下跳转到原程序的入口等等 。
由于 Shield 程序要进行这些底层的操作,使得它不适用高级语言开发,甚至不可能用高级语言开发 。这时,不得不使用汇编语言--最强大 、最高效,同时也是最难使用的语言 。
但是,从需求中也可以看到,Shield 程序也要使用一些高级的算法,如密码学算法,检验文件完整性算法等等 。这使得如果 Shield 程序完全用汇编语言开发,这么多复杂的,需要较高技巧的算法,可能都要用汇编语言写 。而这些并不是汇编语言所特长的,用汇编语言写的结果只能是:代价高昂,并且可扩展性,可维护性都很差 !
经过认真的考虑,阅读大量的资料,我终于找到了一个折衷的办法,把必须用 、不得不用汇编语言写的部分,如要从文件中提取数据的部分,以及反跟踪,反-反汇编,内存布局设计部分,用汇编语言写,其余部分,涉及到复杂算法的,用 C++ 写 。然后把两部分结合编译。
而 Server 、Merge 、Register,由于不涉及到这些底层的操作,可以全部用高级语言写,我用的是 C++,主要是因为:虽然这三个模块不涉及到最低层的操作,然而它们仍要和 Shield 进行通信(Server 不和 Shield通信),有些算法它们(Merge 、Register和Shield) 还要共用,于是高级语言选择 C++ 是理所当然的 。
至于开发工具,C/C++ 开发工具我选用 VC6.0,不用多说,汇编语言开发工具选用 MASM,版本是7.0,这是程序员使用最多的汇编语言。
6.4 C/C++ 与汇编语言混合编程时的互调协议
既然 Shield 是用 C++ 和汇编语言混合编写的,那么它们之间通信(即互相调用)就是不可避免的。要通信,就要服从共同的协议,经过查阅大量的资料 。我对 C/C++ 和汇编语言之间的通信终于了如指掌 。下面将详细说明:
(1) 命名约定:
VC 编译 C 文件(不是C++文件),产生的目标文件(.obj文件,也即编译器产生的C和可执行文件"之间"的中间文件)中,每个全局符号(函数名、全局变量名)前面都加了

一个下划线"_"。也就是说,如果 C 文件中有一个全局函数(C中也只有全局函数)"fun1",经过编译,在目标文件中,该函数的符号名就是"_fun1"。全局变量也相同。
对 Cpp 文件(C++语言源文件,后面将称为 C++文件)中符号的处理要复杂的多,这里我不打算介绍对类名 、类变量(即类中定义的 static 变量)、类方法(类中的static函数),对象名 、对象变量(类中定义的非 staitc 变量)、对象方法(类中的非 statioc函数)……,这些太多太多,不可能介绍完,并且因为我的设计中也未牵涉到这些方面。下面仅对 C++ 源程序中的全局非重载函数的命名约定进行说明 。
一般的说,C++ 源程序中一个完整的全局函数的声明应该是这样的:
[extern "C"] returntype calling_convension fun_name(paramtype1 [param1] , paramtype2 [param2] ,...);
函数的定义必须和声明完全一致 。对这个函数,定义应该如下:
[extern "C"] returntype calling_convension fun_name(paramtype1 [param1] , paramtype2 [param2] ,...)
{
.....
return v; // v 的类型应是returntype
}
extern "C" 表示目标文件中函数的命名将按 C 语言的协议,为求简化,我的代码中所有C++ 和汇编语言互相调用的函数都加了 extern "C" 。下面也是假定函数有 extern "C" 声明的 。
对不同的 calling_convension,即调用协议,在目标文件中产生的函数名、以及参数传递的次序,一般都不同。calling_convension 将在下面详细说明。现在只说函数名。
如果 calling_convension 是 __stdcall,产生的函数名,在源程序的函数名之前加一个下划线,在函数名之后加一个"@",后面在加上该函数形式参数区的字节数(10进制表示)。如对该函数,假设该函数有三个long类型(C语言标准规定long类型在所有的机器中都必须是32位)的形式参数,目标文件中的函数名将是 "_fun_name@12"。
如果 calling_convension 是 __cdecl,产生的函数名,在源程序的函数名之前加一个下划线,在函数名之后没有 "@",后面也没有该函数形式参数区的字节数(10进制表示)。如对该函数,假设该函数有三个 long形式参数,目标文件中的函数名将是 "_fun_name"。这个命名协议实际上就是C 的标准命名协议 。
其它的 calling_convension,在后面的一个表(表6-1)中示出。
(2) 参数传递协议
对 extern "C" __cdecl 函数,参数传递是从右向左压入堆栈,并且堆栈的恢复由调用者(caller)完成,即对上述函数,如果有以下调用:
fun_name(x,y,z);
产生的汇编指令将是:
push z
push y
push x
call _fun_name
add esp , 12
fun_name 的函数体如下:
_fun_name proc
push ebp
mov ebp , esp
....
ret ;这里未跳过参数区(未恢复堆栈)
_fun_name endp
这里只举这个例

相关文档
最新文档