Qia's LabVIEW Station Virry Test & Control

We talk about LabVIEW and HVAC

如何在PB中通过读取硬盘序列号实现软件加密

大家知道,每当我们格式化软盘或硬盘时系统都会给它分配一个序列号,即用DOS命令dir显示出的"Volume Serial Number is 0A41-0E0A"。该序列号是随机产生的,且具有唯一性。也就是因为这个原因许多软件的测试版本利用该项技术使测试版一旦过了限定期限就不能再使用,即使将该软件重新安装也无济于事。另外,有些共享软件的注册码也是通过这
个序列号来生成的。同样的方法我们也可以运用到软件的防拷贝技术。那么如何才能实现上述功能呢?

  为了达到目的我们必须首先能够得到硬盘的序列号,其次,为了安全起见我们最好再选择一种加密算法,将加密后的硬盘的序列号作为密文公开存放,软件通过解密得到明文,即硬盘的序列号,通过将解密后的硬盘序列号和实际的硬盘序列号相比较得出程序是否合法。这一步当然是由应用程序秘密运行,用户根本不知道,从而达到软件的二次加密目的,同时也隐藏了软件的合法性识别过程,使破译者无从下手。下面就来谈谈如何具体实现。

一、如何读取硬盘序列号

  要读取硬盘序列号我们可以用汇编来实现,但毕竟不容易,况且也不能有效的结合到PB脚本中。在PB中我们可以通过调用Windows提供的外部函数GetVolumeInformationA()来实现。这相对来说比较简单。

  该函数的原型为:

  BOOL GetVolumeInformation(

  LPCTSTR lpRootPathName,

  LPTSTR lpVolumeNameBuffer,

  DWORD nVolumeNameSize,

  LPDWORD lpVolumeSerialNumber,

  LPDWORD lpMaximumComponentLength,

  LPDWORD lpFileSystemFlags,

  LPTSTR lpFileSystemNameBuffer,

  DWORD nFileSystemNameSize

  上述原型中,参数类型只要是以"LP-"开头的表明该参数用的是长指针(Long Pointer)类型,即在PB中调用时的参数传递是通过引用传递。在8个参数中对我们真正有用的只有两个LPCTSTR lpRootPathName和LPDWORD lpVolumeSerialNumber。其中参数lpRootPathName是指向文件系统根目录的地址,我们需要用它来指明所要获取序列号的硬盘盘符;参数lpVolumeSerialNumber是返回的硬盘序列号的地址,这正是我们需要的。

  众所周知,PB在调用任何外部函数前都要首先进行函数声明,可以将声明放在全局或局部函数声明中。具体声明如下:

  Function Boolean GetVolumeInformationA( &

  ref String ls_Rootpath, &

  ref String ls_volumnename, &

  Ulong lul_VolumeNameSize, ref Ulong lul_VolumeSerialNumber, &

  ref Ulong lul_MaximumComponentLength, &

  ref Ulong lul_FileSystemFlags, &

  ref String ls_FileSystemNameBuffer, &

  Ulong lul_FileSystemNameSize &

  ) Library "Kernel32.dll"



  上述声明中,"ref"指明是该参数是通过引用传递的,有关函数引用的详细内容请参见有关教程。声明完毕我们不能马上进行调用,还必需确保已为它分配足够的内存空间,即使是参数引用传递也是这样,否则的话将会出现调用错误,这跟C语言的引用调用不同,这一点往往被忽视,希望读者能够注意。也就是为什么我在调用该函数前将有些字符串参数给它预先分配了多达256个字符空间以及给一些整型类型的参数赋初始值256。完整的读取硬盘序列号的程序代码如下:

/******************* 程序代码 ************************/

String ls_Rootpath, ls_volumnename

ls_Rootpath = "C:" // 指定要得到序列号的硬盘,

// 一般情况都是C盘,除非你能保证用户存在其它逻辑盘或物理盘

ls_volumnename = Space(256) // 分配足够的空间,下同

Ulong lul_VolumeNameSize

lul_VolumeNameSize = 256

Ulong lul_VolumeSerialNumber, lul_MaximumComponentLength, lul_FileSystemFlags

lul_MaximumComponentLength = 256

String ls_FileSystemNameBuffer

ls_FileSystemNameBuffer = space(256)

Ulong lul_FileSystemNameSize

lul_FileSystemNameSize = 256

beep(1)

boolean lb_rtn

lb_rtn = False

lb_rtn = GetVolumeInformationA(ls_Rootpath, ls_volumnename, lul_VolumeNameSize,

lul_VolumeSerialNumber, lul_MaximumComponentLength, lul_FileSystemFlags,

ls_FileSystemNameBuffer, lul_FileSystemNameSize)

if lb_rtn = true then

MessageBox("提示","函数调用成功!")

else

MessageBox("提示","函数调用失败!")

end if

sle_1.text = String(lul_VolumeSerialNumber) // 得到硬盘序列号

/********************* 结束 *************************/

  一旦读取成功我们的任务也就完成了近一半,接下来要做的是怎样选择一个合适的加密算法。

二、选取一个优秀的加密算法

  1、数据加密概述

  早在几千年前人类就已经有了通信保密的思想和方法。但直到1949年,信息论创始人香农发表著名文章,论证了一般经典加密方法得到的密文几乎都是可破译的。密码学才得以进入了一个新的发展时期。70年代后期,美国的数据加密标准DES和公开密钥密码体制的出现成为近代密码学发展史上的两个重要里程碑。

  公开密钥密码体制的概念是由Difie与Hellman于1976年提出。所谓公开密钥密码体制就是加密密钥与解密密钥不同,是一种由已知加密密钥推导出解密密钥在计算上是不可行的密码体制。其中,基于数论中大数分解问题的RSA体制曾被ISO/TC97的数据加密技术委员会SC20推荐为公开密钥数据加密标准。

  2、RSA体制的基本原理

  该体制是根据寻求两个大素数比较简单,而将它们的乘积分解开则极其困难这一原理来设计的。在已提出的公开密钥算法中它是最容易理解和实现的。RSA在世界上许多地方已成事实上的标准。ISO几乎(但没有明确)已指定RSA用作数字签名标准。该算法已经经受住了多年深入的密码分析,虽然密码分析者既不能证明也不能否定RSA的安全性,但这恰恰说明了该算法有一定的可信度。它的安全性是与大数分解密切相关的。我想通过下表你将会对它的安全性有一个较好的认识,它给出了在计算机每一微妙做一次操作的假定下分解不同大小的N所需要的时间。

  N的十进位数  50    75    100   200

  时间      3.9小时  104天  74年  3.8X1015年

  RSA加密算法具体如下:

  (1)选取两个大素数,p和q。为了获得最大程序的安全性,两个素数的长度一样。并计算乘积N(N=pq)。

(2) 随后计算出N的欧拉函数ф(N)=(p-1)(q-1),ф(N)定义为不超过N并与N互素的数的个数。

(3)从[0,ф(N)- 1]中随机选取加密密钥e,使得e和ф(N)互为素数。 

  (4)计算出满足公式ed=1 modф(N)的d,d为解密密钥。 

  (5)若用整数X表示明文,整数Y表示密文(X,Y均小于N),则加解密运算为:

  加密:Y = Xe mod N 

  解密:X = Yd mod N 

  注意,其中的d和N也互素。e和N是公开密钥,d是秘密密钥。两个素数p和q应舍弃,但千万不要泄密哦。
3、相关数学背景知识

  (1)素数:素数是一个比1大,其因子只有1和它本身,没有其它数可以整除它的数。素数是无限的。例如,2,3,5,7……等。

(2)两个数互为素数:指的是它们除了1之外没有共同的因子。也可以说这两个数的最大公因子是1。例如,4和9,13和27等。

(3)模变换:两个数相模,
如A模N运算,它给出了A的余数,余数是从0到N-1的某个整数,这种运算称为模运算。

  4、算法的具体实现

  从RSA的基本原理我们得知,对明文进行加密选择一个合适的e很重要,如果你选择合适的话,RSA的加密速度将快得多,并且也不会因为用户机器的限制而要做更多的变换(指在计算中为了避免数据的溢出所进行的转换,毕竟我们用的是PC机再说也用不着很高的安全性)。最常用的三个e值是3,17,65537。在这里我们取的e等于3,当然到底选取哪个e值并没有规定,这里只是为了演示方便罢了。

  根据算法定义,

  (1)为了方便起见我们选取素数p = 3和q = 11,则N = pq = 3 * 11 = 33。 

  (2)ф(N)=(p-1)(q-1)= 2 * 10 = 20。 

  (3)从[0,ф(N) - 1]中,即,[0, 19]之间任意选取加密密钥e = 3,且e和ф(N)互素。

  (4)如何从公式ed=1 modф(N)求出解密密钥d?

  由模的定理我们可以将公式ed=1 modф(N)转换成形式ed= k * ф(N)+ 1,即3d = k * 20 + 1,将0,1,2,3…依次代入k,求出d。取k = 1,得d = 7。

  读者可以通过编程实现随机选取p和q来求出相应的N,e,d。

  (5)进行加解密。

  对明文进行加密

  根据定义,我们首先要根据N的值对明文进行分组,每个分组的值应小于N。如果要加密固定的消息分组,那么可以在它的左边填充一些0(零)并确保该值比N小。例如,我们要对数据X=172035594进行加密(在我的计算机上C盘的序列号是0A41-0E0A,转换成十进制就是172035594),我们首先要将它分成小于N(N=33)的若干小组。可以分成,X1=17,X2=20,X3=3,X4=5,X5=5,X6=9,X7=4。对第一分组X1运用加密公式得到加密密文Y1=X1e mod N = 173 mod 33 = 29,依次将其余分组进行加密得到,Y2=14,Y3=27,Y4=26,Y5=26,Y6=3,Y7=31。即密文Y= 2914272626331。我们可以将密文存储在文件或注册表中,每当应用程序启动时先读取密文,并将其解密,再将解密后的结果与硬盘序列号进行比较,以此来判断软件是否合法。在实际运用中我们可以随时通过程序修改密文,比如,将密文去掉一位或将密文颠倒等,就可以实现诸如测试版软件的使用限制问题,

  对密文进行解密

  对密文进行解密同样要首先对密文进行分组,使每个分组都小于N。将密文Y=2914272626331分组成:

  Y1=29,Y2=14,Y3=27,Y4=26,Y5=26,Y6=3,Y7=31

  这时我们一定要注意,不要急于将将各分组代入解密公式X=Yd mod N,如果这样做了我们所得到的明文将是X=1202811913,并不是加密时的明文!是不是加密算法有错?绝对不是。回顾加解密的公式,我们不难发现它们做的都是先将一个数进行n次方运算然后在做模运算。问题就出在"n次方运算"上,千万不要忽略PowerBuilder中数值的取值范围,在其它的编程语言中也是如此。在本例中我给明文和密文用的都是unsigned long类型,它的32位所允许最大值是4294967295,的确很大,但我们不能保证一个数在进行了7次方后不超过该最大值。其实,这种情况在对明文加密时也是会发生的,只是33的3次方是35937,远小于最大值,我们将其忽略罢了。

  好在问题并不像我们想象的那么复杂。由模的运算规律得知,模运算像普通的运算一样,它是可交换的、可结合的、可分配的。而且,简化运算每一个中间结果的模n运算,其作用与先进行全部运算,然后再简化模n运算是一样的。比如,

  (A * B) mod N = ((A mod N) * (B mod N)) mod N。

  因此,

  X = Y7 mod N

  = (Y3 * Y4)mod N

  = ((Y3 mod N)*(Y4 mod N))mod N

  当然,我们也可以将Y7分解成更多项的乘积。将分组后的密文Y1至Y7依次代入上式得出密文为X = 17 20 3 5 5 9 4。即为正确明文,解密成功。

  在实际的运用中考虑到PB没有现成的乘方运算函数,为了便于读者理解原程序是如何实现RSA加密算法的本文所采用的方法是通过FOR…NEXT语句循环来实现乘方运算,读者可以将其做成一个函数,在使用的时候调用。RSA加解密算法的完整程序代码如下:

/**************** 加密程序代码 ***************/

// 以下参数由RSA加密算法得来

integer li_e, li_d, li_n

li_e = 3 // 设置指数e,加密密钥

li_d = 7 // 设置指数d,解密密钥

li_n = 33 // 设置N:两个素数得乘积

string ls_str

ls_str = Trim(sle_1.text) // 将明文转换成字符串,以便随后进行分组

ulong lul_temp

lul_temp = 0

ulong lul_x, lul_y // lul_x: 加密明文; lul_y: 加密密文

int I

do until ls_str = ""

lul_temp = Integer(left(ls_str, 2))

if lul_temp >= li_n then // 将明文分组,且每组均小于N(N=33)

lul_temp = Integer(left(ls_str, 1))

ls_str = right(ls_str, len(ls_str)-1)

else

ls_str = right(ls_str, len(ls_str)-2)

end if

lul_y = 1

for I = 1 to li_e // 进行乘方运算

lul_y = lul_y * lul_temp

next

lul_y = mod( lul_y, 33) // 根据加密公式计算密文

sle_2.text = trim(sle_2.text) + string(lul_y) // sle_2.tex中存放的是加密后的密文

loop

/******************** 结束 *****************/

/**************** 解密程序代码 ***************/

ls_str = Trim(sle_2.text) // 与加密同理,将密文转换成字符串,以便随后进行分组

ulong lul_x0, lul_x1

do until ls_str = ""

lul_temp = Integer(left(ls_str, 2))

if lul_temp >= li_n then // 将密文分组,且每组均小于N(N=33)

lul_temp = Integer(left(ls_str, 1))

ls_str = right(ls_str, len(ls_str)-1)

else

ls_str = right(ls_str, len(ls_str)-2)

end if

// 由于考虑到乘方运算得结果可能会超出数值所允许得最大取值,

// 因此对解密公式进行适当转换,lul_x = lul_x0 * lul_x1

lul_x0 = 1

lul_x1 = 1

// 假如解密密钥是7,则先进行数的4次方运算取模,在进行数的3次方运算取模

for I = 1 to 4

lul_x0 = lul_x0 * lul_temp

next

lul_x0 = mod( lul_x0, 33)

for I = 1 to li_d - 4

lul_x1 = lul_x1 * lul_temp

next

lul_x1 = mod( lul_x1, 33)

lul_x = mod(lul_x0 * lul_x1, 33) // 根据解密公式计算明文

sle_3.text = trim(sle_3.text) + string(lul_x) // sle_3.tex中存放的是解密后的明文

loop

/******************** 结束 *****************/

  总结,文中所提供的程序代码已经在PowerBuilder 7.0,Windows 98环境下运行通过。本文所讨论的通过读取硬盘序列号方法来实现程序加密技术同样可以运用到其它程序语言中,只是具体的实现方法略有差异罢了。由于本人水平有限,错误和不足之处再所难免,因此还敬请同行们给予指教

posted on 2006-01-22 23:27  LabVIEW开发者  阅读(1710)  评论(0编辑  收藏  举报

导航