用C C++实现SMC动态代码加密技术.docx

上传人:b****2 文档编号:2862269 上传时间:2023-05-04 格式:DOCX 页数:25 大小:74.55KB
下载 相关 举报
用C C++实现SMC动态代码加密技术.docx_第1页
第1页 / 共25页
用C C++实现SMC动态代码加密技术.docx_第2页
第2页 / 共25页
用C C++实现SMC动态代码加密技术.docx_第3页
第3页 / 共25页
用C C++实现SMC动态代码加密技术.docx_第4页
第4页 / 共25页
用C C++实现SMC动态代码加密技术.docx_第5页
第5页 / 共25页
用C C++实现SMC动态代码加密技术.docx_第6页
第6页 / 共25页
用C C++实现SMC动态代码加密技术.docx_第7页
第7页 / 共25页
用C C++实现SMC动态代码加密技术.docx_第8页
第8页 / 共25页
用C C++实现SMC动态代码加密技术.docx_第9页
第9页 / 共25页
用C C++实现SMC动态代码加密技术.docx_第10页
第10页 / 共25页
用C C++实现SMC动态代码加密技术.docx_第11页
第11页 / 共25页
用C C++实现SMC动态代码加密技术.docx_第12页
第12页 / 共25页
用C C++实现SMC动态代码加密技术.docx_第13页
第13页 / 共25页
用C C++实现SMC动态代码加密技术.docx_第14页
第14页 / 共25页
用C C++实现SMC动态代码加密技术.docx_第15页
第15页 / 共25页
用C C++实现SMC动态代码加密技术.docx_第16页
第16页 / 共25页
用C C++实现SMC动态代码加密技术.docx_第17页
第17页 / 共25页
用C C++实现SMC动态代码加密技术.docx_第18页
第18页 / 共25页
用C C++实现SMC动态代码加密技术.docx_第19页
第19页 / 共25页
用C C++实现SMC动态代码加密技术.docx_第20页
第20页 / 共25页
亲,该文档总共25页,到这儿已超出免费预览范围,如果喜欢就下载吧!
下载资源
资源描述

用C C++实现SMC动态代码加密技术.docx

《用C C++实现SMC动态代码加密技术.docx》由会员分享,可在线阅读,更多相关《用C C++实现SMC动态代码加密技术.docx(25页珍藏版)》请在冰点文库上搜索。

用C C++实现SMC动态代码加密技术.docx

用CC++实现SMC动态代码加密技术

用C/C++实现SMC动态代码加密技术

摘要:

所谓SMC(SelfModifyingCode)技术,就是一种将可执行文件中的代码或数据进行加密,防止别人使用逆向工程工具(比如一些常见的反汇编工具)对程序进行静态分析的方法,只有程序运行时才对代码和数据进行解密,从而正常运行程序和访问数据。

计算机病毒通常也会采用SMC技术动态修改内存中的可执行代码来达到变形或对代码加密的目的,从而躲过杀毒软件的查杀或者迷惑反病毒工作者对代码进行分析。

由于该技术需要直接读写对内存中的机器码,所以多采用汇编语言实现,这使得很多想在自己的程序中使用SMC技术进行软件加密的C/C++程序员望而却步。

针对这种现状,本文提出了几种基于C/C++语言的机器指令定位方法,从而用C/C++语言实现了动态代码修改技术。

关键词:

SMC动态代码修改软件加密

窗体顶端

 

 

窗体底端

一、什么是SMC技术

   所谓SMC(SelfModifyingCode)技术,就是一种将可执行文件中的代码或数据进行加密,防止别人使用逆向工程工具(比如一些常见的反汇编工具)对程序进行静态分析的方法,只有程序运行时才对代码和数据进行解密,从而正常运行程序和访问数据。

计算机病毒通常也会采用SMC技术动态修改内存中的可执行代码来达到变形或对代码加密的目的,从而躲过杀毒软件的查杀或者迷惑反病毒工作者对代码进行分析。

现在,很多加密软件(或者称为“壳”程序)为了防止Cracker(破解者)跟踪自己的代码,也采用了动态代码修改技术对自身代码进行保护。

以下的伪代码演示了一种SMC技术的典型应用:

procmain:

............

IF.运行条件满足

 CALLDecryptProc(AddressofMyProc);对某个函数代码解密

 ........

 CALLMyProc                          ;调用这个函数

 ........

 CALLEncryptProc(AddressofMyProc);再对代码进行加密,防止程序被Dump

......

endmain

   在自己的软件中使用SMC(代码自修改)技术可以极大地提高软件的安全性,保护私有数据和关键功能代码,对防止软件破解也可以起到很好的作用。

但是,SMC技术需要直接读写对内存中的机器码,需要对汇编语言和机器码有相当的了解,具体的实现一般都是采用汇编语言。

由于汇编语言晦涩难懂,不容易掌握,这使得很多想在自己的程序中使用SMC技术进行软件加密的C/C++程序员望而却步。

难道只能用汇编语言实现SMC技术?

其实不然,从理论上讲,只要支持指针变量和内存直接访问,象C/C++这样的高级语言一样可以使用SMC技术。

本文就是利用C/C++语言的一些特性,比如函数地址和变量地址直接访问等特性,实现了几种对运行中的代码和数据进行动态加密和解密的方法。

首先是利用Windows可执行文件的结构特性,实现了一种对整个代码段进行动态加密解密的方法;接着又利用C/C++语言中函数名称就是函数地址的特性,实现了一种对函数整体进行加密解密的方法;最后采用在代码中插入特征代码序列,通过查找匹配特征代码序列定位代码的方式,实现了一种对任意代码片断进行解密解密的方法。

下面就分别介绍这几种方法。

二、对整个代码段使用SMC方式加密解密

   在程序中使用SMC最简单的方法就是修改(或加密)整个数据段或代码段,这里首先要讲一下“段”的概念。

这个“段”有两层含义,第一层含义是程序在内存中的分布,老的16位操作系统对内存使用分段映射的方式,使用不同的段分别存放代码、数据和堆栈,使用专用的基址寄存器访问这些段,于是就有了代码段、数据段和堆栈段等等区分。

随着32位Windows的兴起,一种新的32位平坦(Flat)内存模式被引入Windows内存管理机制,在平坦模式下对段的区分已经没有意义了,但是段的概念依然被保留下来,这些同名的基址寄存器现在被成为“段选择器”,只是它们的作用和普通的寄存器已经没有区别了。

段的另一层含义是指保存在磁盘上的Windows可执行文件中的数据结构(就是PE文件中的Section),是Windows在装载这个可执行文件时对代码和数据定位的参考。

不过要真正理解段的概念,还需要了解Windows可执行文件的结构和Windows将可执行文件加载到内存中的方式。

   Microsoft为它的32位Windows系统设计了一种全新的可执行文件格式,被成为“PortableExecutable”,也就是PE格式,PE格式的可执行文件适用于包括Windows9X、WindowsNT、Windows2000、WindowsXP以及Windows2003在内的所有32位操作系统,估计以后的Windows新版本也将继续支持PE格式。

PE文件格式将文件数据组织成一个线性的数据结构,图2-1展示了一个标准PE文件的映象结构:

图2-1WindowsPE文件映像结构

位于文件最开始部位的是一个MS-DOS头部和一段DOSstub代码,在PE文件中保留这一部分是为了DOS和Windows系统共存那一段时期设计的,当程序运行在DOS系统时,DOS系统按照DOS可执行文件的格式调用DOSstub代码,一个典型的DOSstub代码就是在控制台上输出一行提示:

“ThisprogramcannotberuninMS-DOSmode”,当然不同的编译器产生的DOSstub代码也各不相同。

曾经有一段时间很流行一种既可以在DOS系统上运行,又可以在Windows上运行的程序,其原理就是人为地替换这段DOSstub代码。

紧跟在DOSstub代码之后的就是PE文件的内容了,首先是一个PE文件标志,这个标志有4个字节,也就是“PE/0/0”。

这之后紧接着PE文件头(PEHeader)和可选头部(OptionalHeader,也可以理解为这个PE文件的一些选项和参数),这两个头结构存放PE文件的很多重要信息,比如文件包含的段(Sections)数、时间戳、装入基址和程序入口点等信息。

这些之后是所有的段头部,段头部之后跟随着所有的段实体。

PE文件的尾部还可能包含其它一些混杂的信息,包括重分配信息、调试符号表信息、行号信息等等,这些信息并不是一个PE文件必须的部分,比如正常发布的Release版本的程序就没有调试符号表信息和行号信息,所以图2-1表示的结构图中省略了这些信息。

   在整个头结构中,我们关心的仅仅是各个段的段头部,因为段头部包含这个段在文件中的起始位置、长度以及该段被映射到内存中的相对位置,在对内存中的代码修改时,需要这些信息定位内存读写地址和读写区域长度。

下面来看看winnt.h中对段首部的定义,

typedefstruct_IMAGE_SECTION_HEADER{

   BYTE   Name[IMAGE_SIZEOF_SHORT_NAME];

   union{

           DWORD  PhysicalAddress;

           DWORD  VirtualSize;

   }Misc;

   DWORD  VirtualAddress;

   DWORD  SizeOfRawData;

   DWORD  PointerToRawData;

   DWORD  PointerToRelocations;

   DWORD  PointerToLinenumbers;

   WORD   NumberOfRelocations;

   WORD   NumberOfLinenumbers;

   DWORD  Characteristics;

}IMAGE_SECTION_HEADER,*PIMAGE_SECTION_HEADER;

在这个头结构中我们关心的是Name、VirtualSize、VirtualAddress和Characteristics四个属性。

Name是这个段的名称,长度是8个字节,段名称一般以“.”开始,如“.text”,“.data”等等,但是这并不意味着段名称必须以“.”开始,这只是Microsoft的编译器的一个约定,很多编译器并不遵循这个约定。

段名称对直接修改内存代码和数据是一个很重要的属性。

因为在内存中定位段头部是通过搜索这个Name字符串来实现的。

VirtualSize是一个段的真实长度,它有别于SizeOfRawData,SizeOfRawData是文件对齐后的长度,通常PE文件是以200H字节对齐的,所以SizeOfRawData是200H的整数倍。

但是被Windows装入内存中就不一定是按照200H字节对齐了,所以要用VirtualSize来确定段的长度。

VirtualAddress是这个段在内存中的相对虚地址(RVA),这个相对虚地址加上程序加载的基地址就是这个段在内存中的真正地址。

最后是段属性Characteristics,操作这个段属性的目的是为这个段增加可写入的属性,因为Windows不允许向一个只读的段写数据。

段属性由一些标志位组成,各个常用标志位的含义以及它们的值如下表所示:

Flag                      意义

0x00000020            这是一个代码段

0x00000040            这个段包含已初始化数据

0x00000080            这个段包含未初始化数据

0x02000000            这个段的数据可被丢弃(EXE文件装载完成后,进程就不需要这些数据了)

0x10000000            该段可以执行

0x20000000            该段为共享段

0x40000000            该段可读

0x80000000            该段可写

表2-1常用段属性标志位

通常编译器生成的程序的代码段具有0x00000020、0x10000000和0x40000000属性,如果我们要修改代码段的代码,就需要为其添加0x80000000标志,否则会引起Windows报告非法访问的异常。

   PE格式文件的使用,使得Windows加载可执行文件不用再象以前一样将可执行文件拆开,在内存中东一块西一块地放置,取而代之的是一种简单的加载方式,就是按照顺序将PE文件读取到内存中,这也使得加载到内存中的PE文件和存放在磁盘上的PE文件具有相似的结构,只是各个段因为对齐方式的不同而导致偏移位置略有不同,下图演示了这种差别:

图2-2PE文件磁盘逻辑结构和那粗映象逻辑结构 

   上面只是简单介绍了PE文件的格式以及加载方式,如果想更加深入了解PE文件,可以查阅本文的参考文献[2],下面本文就通过一个简单的例子介绍一下如何通过直接访问内存实现对代码的动态加密和解密。

首先要说明的是不能对编译器生成的默认代码段进行全代码段加密,这是很显然的,因为整个程序的入口代码也在默认代码段,如果对整个默认代码段加密,你将没有机会对其解密,从而造成程序加载运行失败。

不同的编译器生成的默认代码名称是不一样的,一般Microsoft的编译器会将所有的代码放置在一个名为“.text”的默认代码段中,而Borland的编译器的默认代码段名为“CODE”,其它的编译器可能有其它的代码生成策略,不过有一点是相通的,就是不能对程序入口点所在的代码段实行整段加密。

针对这种情况,本文介绍的策略就是将需要加密的重要代码或数据放置在一个单独的代码段中,然后通过内存查找定位到这个段并对其进行加密解密操作。

首先是通知编译器在生成代码时生成一个新的代码段,并将我们指定的代码放置在这个代码段中,对于做到这一点,不同的编译器有不同的实现方法,本文的例子使用的编译器是VisualC++,可以使用预编译指令#pragma为程序添加一个代码段。

首先用VC的向导生成一个Win32应用程序框架,然后添加如下代码:

#pragmacode_seg(".scode")

intCalcRegCode(constchar*pszUserName,char*pCodeBuf,intnbufSize)

{

   if(!

pszUserName||!

pCodeBuf)

      return0;

   intnLength=strlen(pszUserName);

   if(nLength<=0||nLength>=nbufSize)

      return0;

   if(:

:

IsBadReadPtr(pszUserName,nLength)||:

:

IsBadWritePtr(pCodeBuf,nbufSize))

      return0;

   for(inti=0;i

      pCodeBuf[i]=pszUserName[i]+1;//为了演示,仅仅是作个移位变换

   pCodeBuf[nLength]=0;

   returnnLength;

}

#pragmacode_seg()

#pragmacomment(linker,"/SECTION:

.scode,ERW")

CalcRegCode()函数根据用户名生成一个合法的注册码,这是一个应该受到重点保护的函数,所以要对其进行加密,此处的CalcRegCode()函数代码非常简单,只是为了演示之用,其功能就是把用户名向后移一位形成注册码。

#pragmacode_seg(".scode")指令是告诉编译器为程序生成一个名为“.scode”的代码段,另一个不带参数的预编译指令#pragmacode_seg()告诉编译器此处是新代码段的结束位置,这两个预编译指令之间的代码将被编译器放置在这个名为“.scode”的新代码段中。

段的名称“.scode”可以根据自己的意愿随意命名,但是长度(不包括结尾的/0结束符)不能超过8个字节,这是由WindowsPE文件的结构所决定的。

最后一行#pragmacomment(linker,"/SECTION:

.scode,ERW")是告诉链接程序最终在生成代码时添加这个名为“.scode”的代码段,段属性为“ERW”,分别表示可执行、可读和可写。

也可以不使用预编译指令#pragmacomment,直接在编译选项中添加“/SECTION:

.scode,ERW”选项也可以达到相同的目的。

现在编译这个程序,使用PE文件查看工具可以看到程序中已经有了一个名为“.scode”的代码段,段属性为0xE0000020,也就是0x00000020(代码段)、0x10000000(可执行)、0x40000000(可读)和0x80000000(可写)四个属性的组合。

图2-3演示程序的SectionTable

 

   有了新的可读写代码段之后的问题就是如何在程序运行期间定位到这个段的位置,并对其进行修改,这就需要知道PE文件加载以后在内存中的位置。

当一个可执行程序被Windows加载以后,Windows的虚拟内存管理机制就为其映射了一个单独的4GB内存空间(当然应用程序只能使用其中的一部分,另一部分被操作系统占用),应用程序中的地址都被映射到这个虚拟的内存空间中,整个PE文件被映射到这个虚拟空间的某一段中,开始的位置就被称为映象基地址(ImageBase),这个地址当然也是一个“虚地址”(区别于在内存硬件中的真实地址)。

Windows提供了一个API用于获得应用程序的基地址,这个API就是GetModuleHandle(),它的函数原型是:

HMODULEGetModuleHandle(LPCTSTRlpModuleName);

参数lpModuleName用于指定模块的名字,如果是获得当前可执行文件加载的基地址,只需传递一个NULL就可以了,返回值类型HMODULE看起来有些神秘,其实可以将其强制转换成一个void类型的指针使用,它指向的位置就是我们需要的基地址。

找到映象基地址以后,就可以根据PE文件的结构依次遍历所有的Section(段)表,找到名为“.scode”的段,然后通过段表中的VirtualAddress属性得到“.scode”段在内存中的起始地址,实际上这个VirtualAddress只是相对于映象基地址的一个偏移量,,“.scode”段的真正位置要通过VirtualAddress加上映象基地址获得。

“.scode”段的大小通过VirtualSize属性得到,这个大小是对齐前的大小,也就是全部代码的真正大小,不包括为对齐而填充的0字节。

在前面对PE文件介绍的基础上,不难写出这个查找程序,下面就给出一个查找某个段的虚地址和大小的通用函数:

boolGetSectionPointer(void*pModuleBase,constchar*lpszSection,void**ppPos,LPDWORDlpSize)

{

   IMAGE_DOS_HEADER*pDosHead;

   IMAGE_FILE_HEADER*pPEHead;

   IMAGE_SECTION_HEADER*pSection;

   *ppPos=NULL;

   *lpSize=0;

   if(:

:

IsBadReadPtr(pModuleBase,sizeof(IMAGE_DOS_HEADER))||:

:

IsBadReadPtr(lpszSection,8))

      returnfalse;

   if(strlen(lpszSection)>=16)

      returnfalse;

   charszSecName[16];

   memset(szSecName,0,16);

   strncpy(szSecName,lpszSection,IMAGE_SIZEOF_SHORT_NAME);

   unsignedchar*pszModuleBase=(unsignedchar*)pModuleBase;

   pDosHead=(IMAGE_DOS_HEADER*)pszModuleBase;

   //跳过DOS头不和DOSstub代码,定位到PE标志位置

   DWORDSignature=*(DWORD*)(pszModuleBase+pDosHead->e_lfanew);

   if(Signature!

=IMAGE_NT_SIGNATURE)//"PE/0/0"

      returnfalse;

   //定位到PEheader

   pPEHead=(IMAGE_FILE_HEADER*)(pszModuleBase+pDosHead->e_lfanew+sizeof(DWORD));

   intnSizeofOptionHeader;

   if(pPEHead->SizeOfOptionalHeader==0)

      nSizeofOptionHeader=sizeof(IMAGE_OPTIONAL_HEADER);

   else

      nSizeofOptionHeader=pPEHead->SizeOfOptionalHeader;

   boolbFind=false;

   //跳过PEheader和OptionHeader,定位到Section表位置

   pSection=(IMAGE_SECTION_HEADER*)((unsignedchar*)pPEHead+sizeof(IMAGE_FILE_HEADER)+nSizeofOptionHeader);

   for(inti=0;iNumberOfSections;i++)

   {

      if(!

strncmp(szSecName,(constchar*)pSection[i].Name,IMAGE_SIZEOF_SHORT_NAME))//比较段名称

      {

         *ppPos=(void*)(pszModuleBase+pSection[i].VirtualAddress);//计算实际虚地址

         *lpSize=pSection[i].Misc.VirtualSize;//实际大小

         bFind=true;

         break;

      }

   }

   returnbFind;

}

   虽然对CalcRegCode()函数做了很多手脚,但是在程序中对CalcRegCode()函数的使用方式和调用其它的函数没有区别,只是需要在调用之前对“.scode”段解密。

由于本文介绍的方法需要较多的内存直接操作,特别是对程序要运行的代码进行读写操作,很可能会引起代码的异常,比如对代码解密失败将导致程序运行不可预料的指令,如果你不想让你的程序死的很难看,最好使用异常处理。

以下就是对CalcRegCode()函数的使用方法:

   try

   {

      boolbFind=GetSectionPointer((void*)hImageBase,".scode",&pSecAddr,&dwSecSize);

      if(!

bFind||!

pSecAddr)

         throw"Notfindspecialsection!

";

      //注意,解密和加密函数也是重要的函数,这两个函数的调用最好放在距离CalcRegCode()函数调用

      //远一点的位置,避免被发现

      DecryptBlock(pSecAddr,dwSecSize,0x5A);//首先解密代码段

      CalcRegCode("system",szBuff,128);//调用注册码计算函数

       

      EncryptBlock(pSecAddr,dwSecSize,0x5A);//调用后加密代码段

   }

   ....//异常处理

   到现在为止所有的动态准备工作已经做完,只差最后一道工序,那就是在程序生成之后对“.scode”代码段预先加密。

由于编译器生成的代码是不加密的代码,为了使本文介绍的方法能够正常使用,必须手工对PE文件中的“.scode”段进行加密处理。

本文的例子代码中有一个小程序CryptExe.exe,这是个命令行工具,可以加密指定PE文件的某个位置。

剩下的工作就是在磁盘文件中定位“.scode”段的偏移位置。

在磁盘文件中定位“.scode”段和在内存映象中定位“.scode”段的方法一样,也是查找Section表中的“.scode”段,然后通过段相应的属性定位这个段在文件中的偏

展开阅读全文
相关资源
猜你喜欢
相关搜索
资源标签

当前位置:首页 > 初中教育 > 语文

copyright@ 2008-2023 冰点文库 网站版权所有

经营许可证编号:鄂ICP备19020893号-2