摘 要:本文详细介绍了Windows 95/98磁盘加密软件的程序设计方法。为了设计Windows 95/98加密软件,本文介绍了在应用程序中直接调用VxD服务的方法和未公开的KERNEL32.DLL API的调用方法,还介绍了通过VWIN32.VXD实现Windows 95/98 DPMI功能调用的方法,实现了加密软件程序设计过程中的两个重点——修改Windows 95/98物理内存和调用BIOS中断。
关键词:磁盘加密 VxD服务 Win32 VxD服务 未公开API DPMI功能调用
一、问题的提出
目前国内软件知识产权保护现状还不很令人满意,为了保护软件开发者和经营者的合法利益,防止非法盗版复制,较为通用的方法是对软件进行加密保护,加密方法可以分为磁盘加密、软件狗加密、加密卡加密等,其中磁盘加密以成本低、使用方便等优点成为目前加密的主要方法。目前操作系统平台已经由DOS转移到了Windows 95/98,磁盘加密也存在一个平台转移的问题,但是Windows 95/98平台是32位平台,磁盘加密软件程序设计方法不能完全照搬16位DOS平台下的程序设计方法,国内也几乎没有看到关于Windows 95/98磁盘加密软件程序设计这方面的资料,笔者经过了长时间的研究,终于研究出了一种设计Windows 95/98磁盘加密软件程序的方法,现介绍这种方法使用的主要技术。
二、磁盘加密软件设计基本思路
目前最常见的磁盘加密方法是磁道接缝加密法,这种方法利用了磁盘控制器只有读整条磁道的命令,而没有写整条磁道的命令的特点,用磁道头尾之间的接缝区域中的不可写入随机数据标识一张磁盘。为了读取磁道接缝,一般的方法是将磁道的最后一个扇区格式化成一个大扇区(超过512字节,达到1024字节以上),然后读取这个大扇区,就可以把磁道接缝读出来,然后对磁道接缝数据进行比较判断是否是正版磁盘。为了通过BIOS磁盘中断INT 13H读取大扇区,需要对磁盘基数表中的扇区大小字节(物理地址0000:0525H)进行修改,将扇区大小由2(512字节)修改为更大的值(3=1024字节,4=2048字节,5=4096字节,以此类推),然后调用INT 13H读取扇区。但是Windows 95/98工作在32位保护模式下,并采用了内存分页机制,不能直接访问物理地址,也不能直接调用BIOS中断,由此可见,设计Windows 95/98加密软件的重点在于以下两个方面:
1、如何在Windows 95/98下修改物理内存?
2、如何在Windows 95/98下调用BIOS中断?
笔者查阅了Win32API资料,并没有找到关于这两方面的API,尽管MSDN中有一篇文章“Using VWIN32 to Carry Out MS-DOS Functions”,该文章说可以通过DeviceIoControl函数调用VWIN32.VXD提供的功能来调用INT 13H,但笔者经过实验发现该方法调用的INT 13H不能完整地读取超过512字节的大扇区,笔者只好采用比API更为底层的编程方式——VxD服务调用和DPMI功能调用,下面分别讲述这两个重点的编程实现方法。
三、修改Windows 95/98物理内存
Windows 95/98内存管理使用了80386以上CPU的内存分页管理机制,应用程序使用的逻辑地址(称为线性地址)通过内存分页映射到物理地址,并不直接对应物理地址,所以不能在应用程序中直接通过指针修改物理内存,必须先将物理地址映射到线性地址才能进行修改。
Windows 95/98没有提供将物理地址映射到线性地址的API,不能直接使用API编程,不过Windows 95/98的VMM虚拟设备提供了VxD服务(VMM虚拟设备提供的VxD服务也可以称为VMM服务)_MapPhysToLinear(该VMM服务的参数请参见Windows 95 DDK中的帮助),该VMM服务可以将物理地址映射到线性地址。但是VxD服务必须在Ring 0上调用,而一般的应用程序工作在Ring 3上,一般不能直接调用VxD服务(VxD服务一般只能在另外一个VxD中调用),好在Intel 80386以上CPU提供了一种机制,可以通过保护模式下的中断从Ring 3转到Ring 0上的中断处理程序,这样可以通过编写自己的中断处理程序,在自己的中断处理程序中调用VxD服务的方法来在应用程序中直接调用VxD服务(据笔者研究,目前流行的CIH病毒正是通过这个方法调用VxD服务的)。下面给出了修改扇区大小字节的一个过程(ChangeSectorSize),该过程用汇编语言编写,可以在C语言中调用:
.386p
.model flat,stdcall
……
;修改的中断号,如果本中断号改成3则可以防止Soft-ICE跟踪!
HookExceptionNo EQU 05h
.data
IDTR_1 db 6 dup(0) ;保存中断描述符表寄存器
OldExceptionHook dd 0 ;保存原先的中断入口地址
.code
……
;修改扇区大小过程
;VC原型:void _stdcall ChangeSectorSize(BYTE SectorSize);
ChangeSectorSize PROC SectorSize:BYTE
push eax
;获取修改的中断的中断描述符(中断门)地址
sidt IDTR_1
mov eax,dword ptr IDTR_1+02h
add eax,HookExceptionNo*08h+04h
cli
;保存原先的中断入口地址
push ecx
mov ecx,dword ptr [eax]
mov cx,word ptr [eax-04h]
mov dword ptr OldExceptionHook,ecx
pop ecx
;设置修改的中断入口地址为新的中断处理程序1 入口地址
push ebx
lea ebx,NewExceptionHook1
mov word ptr [eax-04h],bx
shr ebx,10h
mov word ptr [eax+02h],bx
pop ebx
;执行中断,转到Ring 0(与CIH 病毒原理相似!)
push ebx
mov bl,byte ptr SectorSize ;扇区大小保存在bl寄存器中
int HookExceptionNo
pop ebx
;恢复原先的中断入口地址
push ecx
mov ecx,dword ptr OldExceptionHook
mov word ptr [eax-04h],cx
shr ecx,10h
mov word ptr [eax+02h],cx
pop ecx
;修改扇区大小过程结束
sti
pop eax
ret
ChangeSectorSize ENDP
……
;新的中断处理程序1
NewExceptionHook1 PROC
push eax
push ebx
push ecx
push edx
push esi
;修改扇区大小
push dword ptr 00000000h ;必须为0
push dword ptr 00000001h ;字节数
push dword ptr 00000525h ;物理地址0000:0525
;Windows 95/98调用VxD服务使用INT 20H指令后跟一个表示设备号(高字)和服务号(低字)的双字提供到VxD服务的动态链接
int 20h
dd 0001006ch ;以上两条指令相当于 VMMCall _MapPhysToLinear
pop esi
pop esi
pop esi
mov byte ptr [eax],bl ;修改扇区大小
;中断处理程序结束
pop esi
pop edx
pop ecx
pop ebx
pop eax
iretd
NewExceptionHook1 ENDP
……
end
将该程序用MASM 6.11以上版本汇编成OBJ文件,命令行如下:
ML /C /COFF <文件名>
然后将该OBJ文件插入Windows 95/98 C语言的工程文件中,然后在调用此过程的C程序中加入函数原型说明(见程序中的注释),就可以在C程序中调用该过程修改扇区大小了。该方法可以推广到在Windows 95/98应用程序中修改任意物理内存。
四、在Windows 95/98下调用BIOS中断
Windows 95/98工作在32位保护模式下,不能直接调用16位的BIOS中断,好在Windows 95/98支持32位DPMI(DOS保护模式接口),提供了调用16位实模式(在Windows 95/98中是V86模式)中断的功能,该功能使用中断INT 31H,调用如下:
INT 31H AX=0300H
入口参数:
BL=中断号
BH=标志(全为0)
CX=从保护模式栈拷贝到实模式栈的字数
ES:EDI=实模式(V86模式)寄存器数据结构的选择器:偏移量
出口参数
成功CF=0
ES:EDI=实模式(V86模式)寄存器数据结构的选择器:偏移量
失败CF=1
AX=错误代码
不过Windows 95/98应用程序并不能直接调用INT 31H,应该通过VWIN32.VXD提供的未公开接口——Win32 VxD服务调用,Win32 VxD服务是Windows 95/98应用程序与VxD通信的一个接口,微软公司没有公开这个接口(可能是考虑到不让程序员编写与Windows NT不兼容的代码),不过VWIN32.VXD还是提供了大量很有用的Win32 VxD服务,其中服务识别号为0x002a0029的Win32 VxD服务提供了对INT 31H DPMI功能的调用。调用Win32 VxD服务需要使用未公开的KERNEL32.DLL API——VxDCall函数,这是一个可变参数的函数,其中第一个参数是Win32 VxD服务的服务识别号,其余的参数是Win32 VxD服务的参数,通过Win32 VxD服务调用INT 31 DPMI功能的VxDCall函数的原型是:
DWORD WINAPI VxDCall(DWORD srvc,DWORD eax,DWORD ecx);
srvc——服务识别号(0x002a0029)
eax——DPMI功能入口参数中EAX的值
ecx——DPMI功能入口参数中ECX的值
其余的DPMI功能入口参数直接通过寄存器传递。
微软公司没有提供未公开API的头文件和引入库,不过一般情况下可以通过调用GetProcAddress函数,直接用函数序号获取API指针,但是微软公司出于某些未公开的原因,为了防止程序员调用KERNEL32.DLL中的未公开API,在KERNEL32.DLL中加入了Anti-Hacking代码,试图用函数序号调用GetProcAddress函数获取KERNEL32.DLL API指针只会导致调用失败。为了解决这一问题,可以采用自己建立引入库或者编写自己的GetProcAddress函数的方法,笔者采用通用性较好的后一种方法,通过Win32模块的格式获取KERNEL32.DLL API指针,程序如下:
//头文件(GETK32PA.H)
//获取KERNEL32.DLL引出函数指针(防止Microsoft Anti-Hacking代码)
#ifdef __cplusplus
extern "C"
{
FARPROC GetKernel32ProcAddress(DWORD OrdValue);
}
#else
FARPROC GetKernel32ProcAddress(DWORD OrdValue);
#endif
//源程序(GETK32PA.C)
//获取KERNEL32.DLL引出函数指针(防止Microsoft Anti-Hacking代码)
#include <windows.h>
#include "getk32pa.h"
typedef struct {
DWORD Signature;
//IMAGE_FILE_HEADER FileHeader;
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
//IMAGE_OPTIONAL_HEADER OptionalHeader;
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
//IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
DWORD EXPORT_VirtualAddress;
DWORD EXPORT_Size;
DWORD IMPORT_VirtualAddress;
DWORD IMPORT_Size;
DWORD RESORC_VirtualAddress;
DWORD RESORC_Size;
DWORD EXCEPT_VirtualAddress;
DWORD EXCEPT_Size;
DWORD SECURT_VirtualAddress;
DWORD SECURT_Size;
} PE;
FARPROC GetKernel32ProcAddress(DWORD OrdValue)
{
DWORD hModule;
PE *Hdr;
IMAGE_EXPORT_DIRECTORY *Exp;
PDWORD AddrFunc;
hModule=(DWORD)GetModuleHandle("KERNEL32");
Hdr=(PE *)(hModule+(*(PWORD)(hModule+0x3c)));
if(*(PDWORD)(Hdr)!=0x4550) return NULL;
Exp=(IMAGE_EXPORT_DIRECTORY *)(hModule+Hdr->EXPORT_VirtualAddress);
AddrFunc=(PDWORD)(hModule+(DWORD)Exp->AddressOfFunctions);
OrdValue--;
if(OrdValue<Exp->NumberOfFunctions)
return (FARPROC)(hModule+AddrFunc[OrdValue]);
else
return NULL;
}
该程序中的函数GetKernel32ProcAddress可以直接通过函数序号获取KERNEL32.DLL API,VxDCall函数在KERNEL32.DLL中的函数序号是1,当然也可以使用自己建立引入库的方法。
通过DPMI功能调用INT 13H读取磁盘扇区,需要V86模式的数据缓冲区,但是应用程序不能直接提供V86模式的数据缓冲区,好在Windows 95/98的V86MMGR虚拟设备提供了VxD服务V86MMGR_Allocate_Buffer和V86MMGR_Free_Buffer,VxD服务V86MMGR_Allocate_Buffer可以分配一段V86内存,同时将相同大小的一段32位内存中的内容复制到V86内存中,VxD服务V86MMGR_Free_Buffer则可以释放分配的V86内存,同时将被释放的V86内存中的内容复制到相同大小的一段32位内存中。在应用程序中直接调用这两个VxD服务的方法也可以通过编写自己的中断处理程序的方法调用,方法同上,因为篇幅所限,源程序较长,不能全部列出源程序,需要者可以直接与笔者联系。
五、完整的Windows 95/98磁盘加密函数
有了在Windows 95/98中修改物理内存和调用BIOS中断的方法以后,就可以按照编写DOS下磁盘加密软件程序的基本原理编写Windows 95/98下的磁盘加密程序了,笔者编写了一个完整的Windows 95/98磁盘加密函数,程序如下:
//头文件(W95ENC.H)
#ifdef __cplusplus
extern "C"
{
DWORD Win95DiskEncryption(BYTE Head,BYTE Track,BYTE Sector,BYTE SectorSize,void *Buffer);
}
#else
DWORD Win95DiskEncryption(BYTE Head,BYTE Track,BYTE Sector,BYTE SectorSize,void *Buffer);
#endif
//源程序(W95ENC.C)
#include <windows.h>
#include "w95enc.h"
#include "getk32pa.h"
//W95ENC.ASM汇编模块过程在VC中的函数原型
void _stdcall ChangeSectorSize(BYTE SectorSize);
DWORD _stdcall V86AllocateBuffer(DWORD FarPtrMem,BYTE SectorSize);
void _stdcall V86FreeBuffer(DWORD FarPtrMem,BYTE SectorSize);
static FARPROC VxDCall;
//实模式寄存器结构
typedef struct {
DWORD Reg_EDI;
DWORD Reg_ESI;
DWORD Reg_EBP;
DWORD Reserve;
DWORD Reg_EBX;
DWORD Reg_EDX;
DWORD Reg_ECX;
DWORD Reg_EAX;
WORD Reg_Flags;
WORD Reg_ES;
WORD Reg_DS;
WORD Reg_FS;
WORD Reg_GS;
WORD Reg_IP;
WORD Reg_CS;
WORD Reg_SP;
WORD Reg_SS;
} REAL_MODE_REGS;
DWORD Win95DiskEncryption(BYTE Head,BYTE Track,BYTE Sector,BYTE SectorSize,void *Buffer)
{
REAL_MODE_REGS Reg1;
DWORD V86Ptr1;
DWORD RegPtr1;
int BufSize;
int Sch1,Sch2;
//获取未公开API函数VxDCall的入口地址
if(!(VxDCall=GetKernel32ProcAddress(1))) return 0;
//缓冲区初始化
BufSize=(1<<(7+SectorSize));
for(Sch1=0;Sch1<BufSize;Sch1++) *((BYTE *)Buffer+Sch1)=0;
//分配V86 内存
if(!(V86Ptr1=V86AllocateBuffer((DWORD)Buffer,SectorSize))) return 0;
for(Sch2=0;Sch2<3;Sch2++)
{
Reg1.Reg_EAX=0x0201;
Reg1.Reg_EBX=(DWORD)LOWORD(V86Ptr1);
Reg1.Reg_ECX=(DWORD)MAKEWORD(Sector,Track);
Reg1.Reg_EDX=(DWORD)MAKEWORD(0,Head);
Reg1.Reg_ES=HIWORD(V86Ptr1);
RegPtr1=(DWORD)&Reg1;
//修改加密扇区大小
ChangeSectorSize(SectorSize);
_asm
{
mov bx,0013h
mov edi,RegPtr1
}
//调用DPMI功能,模拟INT 13H。
VxDCall(0x002a0029,0x0300,0);
//恢复加密扇区大小
ChangeSectorSize(2);
}
//释放V86 内存
V86FreeBuffer((DWORD)Buffer,SectorSize);
//返回EAX 寄存器的值
return Reg1.Reg_EAX;
}
该程序中的函数Win95DiskEncryption读取A:驱动器中密钥盘的指定磁头、磁道、扇区和扇区大小的密钥数据到指定数据缓冲区,为了保证读取稳定,在程序中用循环语句反复读取3次,然后返回读取后EAX寄存器的值。对于磁道接缝加密法,返回的EAX寄存器的值中的AH值一般是0(成功)或者0x10(CRC错误),不过无论是0还是0x10都能正确读出密钥数据,然后对数据缓冲区中的密钥数据进行检验就可以确定是否是正版磁盘。
六、结束语
综上所述,本文提供了一种Windows 95/98磁盘加密软件的程序设计方法,意在抛砖引玉,其实读者还可以参考本程序设计方法设计出更好的Windows 95/98磁盘加密程序,例如加入反跟踪功能,采用多密钥点判读,使用直接驱动器编程……等方法。本文中提供的在应用程序中直接调用VxD服务,未公开的KERNEL32.DLL API函数的调用以及DPMI功能调用等方法也可用于其他应用程序,为Windows 95/98应用程序的设计提供了一些新的思想和程序设计方法,可以提高Windows 95/98软件开发水准。
参考文献
[1]求伯君 深入DOS 编程 北京大学出版社 1993.1
[2]雷军 王全国 马贤亮 深入Windows 编程 清华大学出版社 1994.12
[3]王建文 程军 贺乐天 80386 扩展内存编程 西安电子科技大学出版社 1994.4
[4]Adrian King Windows 95技术内幕 清华大学出版社 1995.10
[5]Matt Pietrek Windows 95系统编程奥秘 电子工业出版社 1996.8
[6]刘涛涛 Win95编程专页 http://www.netease.com/~ayliutt
|