有些硬盘防拷贝是通过对硬盘FAT表中的簇链的检测来实现的,这与本文所推荐的方法原理一样,都是通过检测文件位置信息是否被改变来判断文件是否已被移动的。但前者涉及到定位FAT表及文件所在目录等繁琐工作,而且还和硬盘型号及格式化信息有关,编程很不方便。本文所介绍的方法则不然,它直接与操作系统打交道,取出操作系统内核中现成的数据,即利用DOS系统文件表(SYSTEM FILE TABLE 简称SFT)来实现防拷贝。
当您打开硬盘上的一个文件并移动读写指针时,DOS会时刻记录读写指针所对应的硬盘位置。一个稍大些的文件在硬盘上要占用多个簇,这些簇并不是连续的,而是由操作系统一般按照“就近循环分配”原则为该文件分配的,这些簇的绝对簇号具有一定的随机性,在FAT表中形成一个簇链。移动读写指针超过一个簇的大小时,则所对应的绝对簇号也要变化。只要该文件没有被移动过(拷贝或整理硬盘),这些簇号就是固定的。一旦该文件被移动了,根据DOS簇分配原则,这些对应的簇号就会改变。由此,我们可以检查特定的文件指针所对应的绝对簇号是否改变了来判断该文件是否已被非法移动。
SFT是DOS在文件管理上的一个很重要的数据结构。只要DOS开始引用一个文件或设备,它必然要建立一个SFT。该表记录了文件设备名、目录特性、设备特性、文件大小和位置、DDPB(块设备)或设备驱动程序标题(字符设备)的地址,以及打开模式等有关文件的存储、访问和操作的管理信息。其中有几项数据可以用来确定文件在硬盘上的位置,它们是:文件读写指针所对应的绝对簇号,目录项的相对索引号(即目录项在目录扇区中的序号),目录项所在扇区的扇区号。
那么怎样利用这几项数据防拷贝呢?这里所说的防拷贝是指可以拷贝但拷贝后的文件不能执行或不能被使用,包括数据文件和可执行文件。若是可执行文件,则其本身包括一段检测代码,用以判断其本身是否已被非法移动。若是数据文件,则在使用该数据文件的可执行文件中包括一段检测代码,来判断该数据文件是否已被非法移动。当第一次将一个文件装入硬盘后,它所占有的硬盘的簇号已是固定的了。这样我们就可以作一个加密程序,在该程序里,约定一个或多个秘密的值N,N1,N2…,然后将文件指针分别移动到N或N1,N2…,在SFT中取出那几项硬盘位置数据,再用约定的秘密算法加以运算,就得到了包含被加密文件位置信息的“钥匙”,将它存于另一个文件中或干脆附加在被加密文件的末尾。在使用该文件的可执行文件的检测代码中,按同样的算法取得当前的“钥匙”,与先前保存的“钥匙”相比较,若相等,则说明文件没有被移动过,程序正常执行,否则,说明文件已不在先前的位置了,即已被非法移动过了,则程序异常执行。
我们先看看DOS系统中SFT的具体结构,系统有多个SFT,这个数目取决于CONFIG.SYS文件中的FILES=N的N值。在DOS4.0,5.0,6.0中,每个SFT的长度是3BH。多个SFT形成SFT数组,并不是整个系统的所有SFT组成一个连续的大数组,而是可能有多个SFT数组,每个数组由一个控制块管理,它们之间由指针连接。每个SFT数组的结构如下:
偏移量 长度 说明
00H DWORD 指向下一个SFT数组的指针
04H WORD 本SFT数组内的SFT数目
06H nBYTEs 由SFT组成的数组(n=本数组中SFT数目*每个SFT所占字节数)
我们还必须知道第一个SFT数组所在的地址。这可由DOS功能调用52H先取得DOS的多重表指针,在多重表偏移量04H处即是一个长指针,它即指向第一个SFT数组的头部。这样剩下的SFT数组的位置也就知道了。
每当DOS打开一个新文件时,DOS就从这些SFT数组中找一个空闲的SFT项,分配给该文件。以后DOS就用此SFT控制访问该文件。那么怎样确定一个刚打开的文件所对应的SFT项是SFT数组中的第几项呢?我们利用文件句柄和文件句柄表。每个进程都有一个文件句柄表,当我们打开一个文件时可得到该文件的句柄,它是一个整数,设为M,则在文件句柄表中的第M项即为该文件的SFT表项在SFT数组中的位置。
那么怎样找到文件句柄表呢?我们先用DOS功能调用51H得到程序段前缀(PSP)的地址,在PSP偏移34H处即是指向该进程文件句柄表的双字指针。
附图给出怎样找一个已打开文件的SFT。
@@02A08500.GIF;附图@@
打开文件取得文件句柄是3,所对应的SFT的序号是6。
下面就给出一个可执行文件防拷贝的例子。
假设我们用C++语言编一个完成某一功能的程序NO-COPY.CPP,经过编译连接生成可执行文件NO-COPY.EXE,要对这个可执行文件实行硬盘防拷贝。我们先编写一个设置“钥匙”的程序PUT-KEY,在该程序中按上述方法取得NO-COPY.EXE的“原钥匙”(它包含了NO-COPY.EXE初次装入硬盘时的位置信息),存于一个专用文件LOC-KEY.DAT中(当然也可直接附加在NO-COPY.EXE的末尾,取时也到文件末尾去取),在NO-COPY.EXE中必须包含一段检测代码。
它也用PUT-KEY的方法取出自己的“当前钥匙”,与存于LOC-KEY.DAT中的“原钥匙”比较,相等则说明文件还在原处,正常执行。否则,说明正在执行的NO-COPY.EXE是被拷贝的副本,停止执行或异常执行。
这里,不用考虑“钥匙”的安全性,“钥匙”是可以公开的,只要取得“钥匙”的方法是保密的,非法拷贝者就无可奈何。可以这样理解这个防拷贝方法:可执行文件本身具有自锁性,在它每拷贝到一个新地点时就按文件位置上了一把新锁,当它执行时,它到一个固定的地方去取“钥匙”,打开本身的锁执行。当它被移动后,可理解为锁换了(因为位置变了)。用“原钥匙”打不开,所以“原钥匙”是可以公开的,PUT-KEY.EXE就是用来设置“原钥匙”的。当合法用户想把NO-COPY.EXE拷贝到一个新地方,只需运行一次PUT-KEY.EXE就可以使用新的NO-COPY.EXE了。所以PUT-KEY.EXE必须由合法用户保管。
先编写一个取“钥匙”的子程序,放在MAKE-KEY.CPP文件中,在该子程序中,分别取目录项的相对索引号,目录项所在扇区的扇区号,文件指针所对应的绝对簇号。采用相加的算法,形成“钥匙”。这里只用了一个文件指针6000所对应的簇号,当然可以使用多个文件指针,采用更复杂的算法,如相乘、相除、异或、相减等。所用的文件指针和所用的算法是保密的。而算出的“钥匙”可以是公开的。
MAKE-KEY.CPP的内容如下(本文件用来被包含在PUT-KEY.CPP和NO-COPY.CPP中)。
/*本子程序用来取filename的"当前钥匙"*/
unsigned long curentlocation-key(char filename)
{
unsigned long secc;
int filehandle;
int i,iii,jjj1,jjj2;
unsigned char u-char=0;
unsigned int u-int=0;
unsigned long u-long=0;
unsigned int offst1,segmnt1,offst2,segmnt2;
void psp-ptrr;
unsigned char far ptrr1, ptrr2;
FILE Stream;
Stream=fopen(filename,"r+b");
if(Stream==NULL)
{cputs("open error");exit(1);}
filehandle=fileno(Stream); //取文件句柄;
fseek(Stream,6000,0); //将文件指针置于6000处;
fread(&i,2,1,Stream); //读一次,使DOS按此文件指针修正SFT;
asm push es
asm mov ah,51h
asm int 21h //取程序段前缀(PSP)段地址;
asm mov es,bx
asm mov ax,word ptr es:[52] //偏移量34H为文件句柄表(FHT)地址;
asm mov bx,ax
axm mov ax,word ptr es:[54]
asm mov es,ax
asm add bx,filehandle //文件句柄表中偏移filehandle 处的一字节;
asm mov al,byte ptr es:[bx] //为该文件的SFT在SFT数组中的序号;
asm xor ah,ah
asm pop es
asm mov iii,ax //iii中存放文件的SFT在SFT数组中的序号;
asm push es
asm mov ah,52h //取DOS多重表指针;
asm int 21h //es为段地址,bx为偏移量;
asm mov ax,word ptr es:[bx+4] //多重表中偏移04H为第一个SFT数组控制块指针
;
asm mov offst1,ax //offset1为第一个SFT数组控制块偏移量;
asm mov ax,word ptr es:[bx+6]
asm mov segmnt1,ax//segment1为第一个SFT数组控制块段地址;
asm mov es,segmnt1
asm mov bx,offst1
asm mov ax,word ptr es:[bx]
asm mov offst2,ax//offset2为第二个SFT数组控制块偏移量;
asm mov ax,word ptr es:[bx+2]
asm mov segmnt2,ax //segment2为第二个SFT数组控制块段地址;
asm mov ax,word ptr es:[bx+4]
asm mov jjj1,ax //jjj1中存放第一个SFT数组的项数,一般为五
asm mov es,segmnt2 //个(系统保留的五个常用文件的SFT);
asm mov bx,offst2
asm mov ax,word ptr es:[bx+4]
asm mov jjj2,ax //jjj2中存放第二个SFT数组的项数,它一般等于
asm pop es //CONFIG.SYS中FILES=N的N值减去第一个SFT
//数组中SFT项数jjj1;
ptrr1=(unsigned char far *)MK-FP(segmnt1,offst1+6);
//ptrr1指向第一个SFT数组中第一个SFT
Ptrr2=(unsigned char far *)MK-FP(segmnt2,offst2+6);
//ptrr2指向第二个SFT数组中第一个SFT
if(iii<jjj1) //若该文件的SFT序号小于第一个SFT数组的项数,则它的SFT
{ //一定在第一个SFT数组中;
// for(i=0;i<0x3b;i++) //这两行可用来显示该文件的SFT;
// printf(" %2x",ptrr1[iii*0x3b+i]); //在DOS4.0-6.0中共0x3b个字节;
asm push es
asm push di
asm les bx,ptrr1 //第0个SFT的地址;
asm mov ax, iii
asm mov cx,3bh
asm mul cx
asm mov di,ax //第iii个SFT的地址,即要找的本文件的SFT;
asm mov ax, word ptr es:[bx+di+1bh] //SFT中偏移1bH处是目录项所在扇区的
asm mov word ptr u-long,ax //扇区号,可用于定位文件,取出存于
asm mov ax, word ptr es:[bx+di+1dh] //u-long中;
asm mov word ptr u-long+2,ax
asm mov al,byte ptr es:[bx+di+1fh] //偏移1fH处是目录项的相对索引号,即
目
asm mov u-char,al //录项在目录扇区中的序号,也可用于定位
//文件的位置,取出存于u-char中;
asm mov ax,word ptr es:[bx+di+35h]//SFT中偏移35H处是文件读写指针对应的
asm mov u-int,ax//绝对簇号(2字节),取出存于u-int中;
asm pop di
asm pop es
}
else
{
iii-=jjj1; //第二个SFT数组中的SFT项和第一个SFT数组中的项是统一编号的;
// for(i=0;i<0x3b;i++)
// printf(" %2x",ptrr2[(iii)*0x3b+i]);
asm push es
asm push di
asm les bx,ptrr2
asm mov ax,iii
asm mov cx,3bh
asm mul cx
asm mov di,ax
asm mov ax,word ptr es:[bx+di+1bh]
asm mov word ptr u-long,ax
asm mov ax,word ptr es:[bx+di+1dh]
asm mov word ptr u-long+2,ax
asm mov al, byte ptr es:[bx+di+1fh]
asm mov u-char,al
asm mov ax,word ptr es:[bx+di+35h]
asm mov u-int,ax
asm pop di
asm pop es
}
u-long+=u-int; //这里采用的秘密算法是相加;
u-long+=u-char;
fclose(Stream);
retrun u-long;
}
下面就是设置“钥匙”的PUT-KEY/CPP文件的内容。
#include <stdio.h>
#include <process.h>
#include <dos.h>
#include <io.h>
#include <conio.h>
#include "make-key.cpp" //将取钥匙子程序包函进来;
void main(int argc,char * argv[]) //命令行上是待加密的文件名;
{
unsigned long key=0;
FILE *stream;
if (argc<2)
{cputs("specify a file");exit(1);}
key=curentlocation-key(argv[1]); //取出文件当前“钥匙”;
stream=fopen("loc-key.dat","w+b"); //以写方式打开“钥匙”文件loc-key.da
t;
if(stream==NULL)
{cputs("can't open file loc-key.dat");exit(1);}
fwrite(&key,4,1,stream); //将当前“钥匙”(4字节)写入文件;
fclose(stream);
}
下面就是NO-COPY.CPP的内容:
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <dos.h>
#include <io.h>
#include <stddef.h>
#include <process.h>
#include "make-key.cpp" //将取钥匙子程序包函进来;
//该程序main()函数如下:
int main()
{
//下面是一段检测代码,(可放在程序中必要的地方);
unsigned long proto-key,curent-key;
FILE*Stream;
Stream=fopen ("loc-key.dat","rb");//以读方式打开“钥匙”文件;
if(Stream==NULL)//若因破坏而打不开则退出;
{cputs("loc-key.dat open error");exit(1);}
fread(&proto-key,4,1,Stream);//取存放于文件中的“原钥匙”(4字节);
fclose(Stream);
curent-key=curentlocation-key("d.exe");//按同样的方法取“当前钥匙”;
if(curent-key!=proto-key)//比较“当前钥匙”和“原钥匙”;
{
cputs("The file has been moved and is invalid!");exit(1);
}
//若不相等,说明当前的文件位置与装入时的位置不同,//文件已经被移动过,这里可异常执行;
cputs("The file is valid");//否则正常进行,完成程序的功能;
//以下是程序代码;
//...
//...
return 0;
}//为了防跟踪,当打不开“钥匙”文件或发现文件已被移动过时,可不退出,而是
//异常执行;
以上程序用Borlandc c++3.1编写,使用时,分别编译连接PUT-KEY.CPP和NO-COPY.CP
P形成PUT-KEY.EXE和NO-COPY.EXE。然后在DOS提示符下键入:
NO-COPY
就会得到loc-key.dat open error 的信息,程序退出,这是因为还没有运行PUT-KEY来设置“钥匙”。
若先键入PUT-KEY NO-COPY.EXE 来为NO-COPY.EXE设置“钥匙”
再键入NO-COPY
则得到The file is valid 说明正常执行。
若将NO-COPY.EXE拷贝到其它目录再执行就会得到loc-key.dat open error 的信息,程序退出。
若将loc-key.dat 一起拷贝到其它目录再执行
则得到The file has been moved and is invalid!
若先在新目录运行PUT-KEY NO-COPY.EXE 来为新的NO-COPY.EXE设置“钥匙”
再运行新的NO-COPY
则得到The file is valid 说明正常执行。
若要对一个或多个数据文件加密,可对PUT-KEY.CPP稍加修改,再在使用这些数据文件的可执行文件的原程序中加入检测代码即可。若要修改加密算法或调整“钥匙”的取出方
法,则只需修改curentlocation-key()函数。
作者:张安林