建立自己的PE Protector (部分1): Your first EXE Protector
By Ashkbiz Danehkar 

原文:http://www.codeproject.com/cpp/peprotector1.asp
译者:arhat
时间:2006年4月18日
关键词:PE Protector 调试器 反汇编器 OEP SEH 导入表

本文主要是描述在Visual C++编译器下,开发属于自己的PE Protector。

Yoda's Protector v1.01 - Free EXE Protector

序言

本文是为那些还不熟悉本领域的人写的。在此,我不准备介绍PE的结构,因为MSDNMatt Pietrek所写的文章已经解释得非常详细了。如果你不熟悉PE结构,我建议你在阅读本文之前先读Matt Pietrek的文章。他在文章结尾还提供了许多有用的连接。

本文将分成三个部分:

  1. 第一个EXE Protector

这部分介绍用Visual C++ Win32 Programming开发一个强大的EXE protectorpacker

  1. 支持OCXDLL,和SCR

这部分介绍除EXE文件之外的其它类型的PE文件。会介绍一些你应该知道的、保护OCXDLL的小技巧。

  1. 使用Cryptography API

这部分将演示怎样用Microsoft Cryptography API函数加密/解密PE区段信息。

本文包含yoda’s protector 1.011.03版本的源码。它在网站上出现6个月后,人们才能看到它的源码。它以[6] Danilo Bzdok 所写的yoda’s Crypter汇编源码,[7] Markus F.X.J. Oberhumer & László Molnár所写的UPX library中的压缩源码,[9] Joergen Ibsen所写的aPLib压缩源码为基础。因此,非常感谢他们为我们提供了这些工具。我也要感谢那些在不同版本Windows上努力测试它的朋友。我想把本文做一个小礼物,送给所有关心爱护yoda’s protector的人们。

Contents

介绍

PEMicrosoft Windows NT ®操作系统下标准的可执行文件格式。它包含代码,数据,资源,DLLdynamic link libraries)等信息。通过那些强大的调试器(例如OllyDbgSoftICE),加上一些汇编语言的知识,可以修改它们。但这样做,软件开发公司将会延长产品开发周期,浪费盈利的时间。因此,他们一般购买类似于EXE protector之类的工具,从而阻止用户非法拷贝软件。我想,如果每一个人都有自己的EXE Protector,世界将会怎样。Cracker将面对不同类型的EXE protector(这样的日子怎么过呀!)。因此,我想最好是我们每人都有属于自己的EXE protector

PE结构概述

PE包括MS-DOSWindows NT,和区段信息。Windows操作系统通过这些信息来分配内存,导入DLL,执行代码。

Table 1

MS-DOS information

IMAGE_DOS_HEADER

MS-DOS Stub Program

Windows NT information

PE Signature ("PE")

IMAGE_FILE_HEADER

IMAGE_OPTIONAL_HEADER

Sections information

IMAGE_SECTION_HEADER[0]

IMAGE_SECTION_HEADER[n]

SECTION[0]

SECTION[n]

你可以[1] “Microsoft Portable Executable and Common Object File Format Specification”里找到PE文件格式的详细描述。Matt Pietrek[2] “Peering Inside the PE: A Tour of the Win32 Portable Executable File Format”[3a/b] “An In-Depth Look into the Win32 Portable Executable File Format”里也讲解的十分清楚。

此外,Wayne J. RadburnPEView [4] 将帮助你看清楚PE文件格式的方方面面。

Figure 1

PEView

PE中所有的数据结构都是在Visual C++winnt.h中定义的。要与PE共事,了解IMAGE_DOS_HEADER, IMAGE_NT_HEADERS, IMAGE_SECTION_HEADER就差不多了这些结构的信息可以在[5] MSDN library中找到。

打开PE文件

为了使用这些信息,我们必须把PE文件格式载入内存。某些Windows API函数会帮我们做这些事情:CreateFile(), GetFileSize(), GlobalAlloc(), ReadFile(), CloseHandle()

我为了和PE文件共事,专门编写了一个类。它帮助我打开文件,把DOS头,NT头,区段头,和区段放到内存中分开的地方,然后把它们重建成新的PE文件。

class PEStructure

{

private:

      DWORD ReservedHeaderSize;

      DWORD ReservedHeaderRO;

public:

      DWORD                   dwRO_first_section;

      IMAGE_DOS_HEADER        image_dos_header;

      char                    *reservedheader;

      IMAGE_NT_HEADERS        image_nt_headers;

      IMAGE_SECTION_HEADER    image_section_header[MAX_SECTION_NUM];

      char                    *image_section[MAX_SECTION_NUM];

      void OpenFileName(char* FileName);

      void UpdateHeaders(BOOL bSaveAndValidate);

      void UpdateHeadersSections(BOOL bSaveAndValidate);

      void Free();

};

OpenFileName() 将打开PE文件,放入image_dos_header, reservedheader, image_nt_headers, image_section_header[], image_section[]。在我的工具中,所有的PE结构都被这个函数抽象出来。

void PEStructure::OpenFileName(char* FileName)

{

      hFile=CreateFile(FileName,

                       GENERIC_READ,

                       FILE_SHARE_WRITE | FILE_SHARE_READ,

                       NULL,OPEN_EXISTING,

                       FILE_ATTRIBUTE_NORMAL,NULL);

      if(hFile==INVALID_HANDLE_VALUE)

      {

            ShowErr(FileErr);

            return;

      }

      dwFsize=GetFileSize(hFile,0);

      if(dwFsize == 0)

      {

            CloseHandle(hFile);

            ShowErr(FsizeErr);

            return;

      }

      dwOutPutSize=

          dwFsize+IT_SIZE+DEPACKER_CODE_SIZE+ALIGN_CORRECTION;

      pMem=(char*)GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT,

                                             dwOutPutSize);

      if(pMem == NULL)

      {

            CloseHandle(hFile);

            ShowErr(MemErr);

            return;

      }

      ReadFile(hFile,pMem,dwFsize,&dwBytesRead,NULL);

      CloseHandle(hFile);

      CopyMemory(&image_dos_header,pMem,sizeof(IMAGE_DOS_HEADER));

      ReservedHeaderRO=sizeof(IMAGE_DOS_HEADER);

      ReservedHeaderSize=

             image_dos_header.e_lfanew-sizeof(IMAGE_DOS_HEADER);

      reservedheader=new TCHAR[ReservedHeaderSize];

      CopyMemory(&image_nt_headers,

                 pMem+image_dos_header.e_lfanew,

                 sizeof(IMAGE_NT_HEADERS));

      dwRO_first_section =

           image_dos_header.e_lfanew + sizeof(IMAGE_NT_HEADERS);

      UpdateHeadersSections(TRUE);

}

检验PE文件是否正确

为了防止不可预知的错误,需要检查image_nt_headerimage_dos_headerSignaturee_magic来检验文件是否是Win32 PE文件。

if(PEfile.image_dos_header.e_magic!='ZM')

{

      GlobalFree(pMem);

      CloseHandle(hFile);

      if(MakeBackup) DeleteFile(szFnameBackup);

      ShowErr(PEErr);

      return;

}

if(PEfile.image_nt_headers.Signature!='EP')

{

      GlobalFree(pMem);

      CloseHandle(hFile);

      if(MakeBackup) DeleteFile(szFnameBackup);

      ShowErr(PEErr);

      return;

}

生成额外的区段

为了保护和unpacking代码,yoda’s Protector中用了一个小技巧――生成额外的区段。Visual C++帮它做这些事情,不需要用汇编的编译器和连接器。如果你查看CryptStuff.cpp中的子例程――PE_LOADER_CODE(),你就会发现我正在介绍的内容。Danilo Bzdok[6] yoda’s Crypter也用了这个方法。当然,他只用了汇编语言,而没有用任何其它的高级语言。我从PE_LOAD_CODE() 中抠出了一些代码,编成GetFunctionRVA(), GetFunctionSize() ,CopyFunction(),用它们在目标PE文件中生成额外的区段。

DWORD GetFunctionRVA(void* FuncName)

{

      void *_tempFuncName=FuncName;

      char *ptempFuncName=PCHAR(_tempFuncName);

      DWORD _jmpdwRVA,dwRVA;

      CopyMemory(&_jmpdwRVA,ptempFuncName+1,4);

      dwRVA=DWORD(ptempFuncName)+_jmpdwRVA+5;

      return(dwRVA);

}

DWORD GetFunctionSize(void* FuncName)

{

      DWORD dwRVA=GetFunctionRVA(FuncName);

      char* pFuncBody=PCHAR(dwRVA);

      UCHAR _temp;

      bool notEnd=TRUE;

      char *DepackerCodeEnd=new TCHAR[10];

      DWORD l=0;

      do

      {

            CopyMemory(&_temp,pFuncBody+l,1);

            if(_temp==0xC3)

            {

                  CopyMemory(DepackerCodeEnd,pFuncBody+l+0x01,10);

                  DepackerCodeEnd[9]=0x00;

                  if(strcmp(DepackerCodeEnd,"ETGXZKATZ")==0)

                  {

                        notEnd=FALSE;

                  }

            }

            l++;

      }while(notEnd);

      return(l);

}

GetFunctionRVA() 将搜索GetFunctionSize() CopyFunction()使用的具体例程的RVAGetFunctionSize() 将返回被CopyFunction()使用的例程的大小。它通过搜索关键字(“ETGXZKATZ”)来计算例程的大小。最后,CopyFunction() 是从PE_LOADER_CODE()子程序中借用的完整的例程,放置在打包的PE文件中。

char* CopyFunction(void* FuncName)

{

    DWORD dwRVA=GetFunctionRVA(FuncName);

    DWORD dwSize=GetFunctionSize(FuncName);

    char* pFuncBody=PCHAR(dwRVA);

    char* filebuff=new TCHAR[dwSize+1];

    CopyMemory(filebuff,pFuncBody,dwSize);

    return(filebuff);

}

下面的代码可以说明这个方法:

char    *pDepackerCode;

DWORD    DEPACKER_CODE_SIZE;

DEPACKER_CODE_SIZE=GetFunctionSize(PE_LOADER_CODE);

pDepackerCode=new TCHAR[DEPACKER_CODE_SIZE];

pDepackerCode=CopyFunction(PE_LOADER_CODE);

 

void PE_LOADER_CODE()

{

      _asm

      {

      //----------------------------------------------------------

      //-------------- START OF THE PE LOADER CODE ---------------

DepackerCode:

      …

      …

      …

DepackerCodeEND:

    RET

    //"ETGXZKATZ" <<-- KEY WORD

    INC EBP     //'E'

    PUSH ESP    //'T'

    INC EDI     //'G'

    POP EAX     //'X'

    POP EDX     //'Z'

    DEC EBX     //'K'

    INC ECX     //'A'

    PUSH ESP    //'T'

    POP EDX     //'Z'

    }

}

打包并加密区段

UPX compressor [7]的源码是打包PE区段的另一选择。我用[8] Markus F.X.J. OberhumerLZO data compression library来打包代码段和数据段。Danilo Bzdok的多形加密和解密方法[6] 比较简单,把它们略作修改(细节查看PER.CPP中的EncryptBuff() DecryptBuff()),对加密PE区段来说足够了

这个protector把区段分别放在不同内存单元。然后,通过CompressPE() CryptPE()打包并加密区段。

//------ ENCRYPT THE SECTIONS -----

// generate PER

PEfile.UpdateHeadersSections(TRUE);

SecEncryptBuff=new TCHAR[SEC_PER_SIZE];

SecDecryptBuff=new TCHAR[SEC_PER_SIZE];

MakePER(SecEncryptBuff,SecDecryptBuff,SEC_PER_SIZE);

CopyMemory(pDepackerCode+dwRO_SEC_DECRYPT,

           SecDecryptBuff,

           SEC_PER_SIZE);   

// encrypt !

CompressPE(pMem);

CryptPE(pMem);

RemoveSectionNames(pMem);

newsection.Misc.VirtualSize=DepackCodeVirtualSize+0x2000;

PEfile.image_section_header[

        PEfile.image_nt_headers.FileHeader.NumberOfSections-1]

        .Misc.VirtualSize = newsection.Misc.VirtualSize;

PEfile.UpdateHeadersSections(FALSE);

//---------------------------------

LZO data compression library [8]compressor源码是C++的,decompressor源码有C++和汇编两种。因此,我们打包时可以用高级语言,解包时用低级语言。我在CompressPE() 内用来自LZO librarylzo1x_999_compress_level() 压缩区段,在_DecompressPE() 内用PE_LOADER_CODE()lzo1f_decompress_asm_fast_safe()汇编源码解压缩区段。

Windows98版本以上,要着重注意image_nt_headers.OptionalHeader.SectionAlignment image_nt_headers.OptionalHeader.FileAlignment,防止发生Win32不兼容的文件错误。IMAGE_SECTION_HEADER 中的变量应当适应文件对齐和区段对齐。我保留了UpdateHeadersSections() 函数,用它找回并重建所有的PE文件格式结构。

void PEStructure::UpdateHeadersSections(BOOL bSaveAndValidate)

{

    DWORD i;

    if(bSaveAndValidate)//TRUE = data is being retrieved

    {

        DWORD SectionNum = PEfile.image_nt_headers

                           .FileHeader.NumberOfSections;

        CopyMemory(&image_dos_header,pMem,sizeof(IMAGE_DOS_HEADER));

        ReservedHeaderSize = image_dos_header.e_lfanew –

                             sizeof(IMAGE_DOS_HEADER);

        if((ReservedHeaderSize&0x80000000)==0x00000000)

        {

            CopyMemory(reservedheader,

                       pMem+ReservedHeaderRO,

                       ReservedHeaderSize);

        }

        CopyMemory(&image_nt_headers,

             pMem+image_dos_header.e_lfanew,

             sizeof(IMAGE_NT_HEADERS));

     dwRO_first_section = image_dos_header.e_lfanew +

                            sizeof(IMAGE_NT_HEADERS);

       CopyMemory(&image_section_header,

                  pMem+dwRO_first_section,

                  SectionNum*sizeof(IMAGE_SECTION_HEADER));

       for(i=0;i< SectionNum;i++)

       {

           image_section[i] = (char*)GlobalAlloc(

                GMEM_FIXED | GMEM_ZEROINIT,

            PEAlign(image_section_header[i].SizeOfRawData,

                PEfile.image_nt_headers.OptionalHeader.FileAlignment));

           CopyMemory(image_section[i],

               pMem + image_section_header[i].PointerToRawData,

               image_section_header[i].SizeOfRawData);

       }

    }

   else//FALSE = data is being initialized

   {

       DWORD SectionNum = PEfile.image_nt_headers

                          .FileHeader.NumberOfSections;

       CopyMemory(pMem,

            &image_dos_header,sizeof(IMAGE_DOS_HEADER));

            ReservedHeaderSize=image_dos_header.e_lfanew –

                               sizeof(IMAGE_DOS_HEADER);

       if((ReservedHeaderSize&0x80000000)==0x00000000)

       {

          CopyMemory(pMem + ReservedHeaderRO,

                     reservedheader,

                     ReservedHeaderSize);

       }

       CopyMemory(pMem+image_dos_header.e_lfanew,

            &image_nt_headers,

            sizeof(IMAGE_NT_HEADERS));

       dwRO_first_section = image_dos_header.e_lfanew +

                            sizeof(IMAGE_NT_HEADERS);

       CopyMemory(pMem+dwRO_first_section,

                 &image_section_header,

                 SectionNum*sizeof(IMAGE_SECTION_HEADER));

       for(i=0;i< SectionNum;i++)

       {

           CopyMemory(pMem+image_section_header[i].PointerToRawData,

                      image_section[i],

                      image_section_header[i].SizeOfRawData);

       }

    }

}

建立Import Table Directory

PE解包区段需要导入两个原始的API函数来动态加载需要的API函数。Kernel32.dll LoadLibraryA() GetProcAddress() 是导入其它API函数所必需的函数,当然,还需要点小技巧。保留的AssembleIT() 就是用来完成这个任务的。为了转向额外区段里的新导入表目录,我们应当改变导入表的地址和大小。为了更正老的导入表,必须在下一步里重载并重建导入表目录来完成程序代码。加载器区段使用LoadLibraryA() GetProcAddress()搜索DLL的导入函数的RVA

Table 2

Import Table Address ->

"Kernel32.dll", 0x00

 

LoadLibrary_RVA

 

GetProcessAddres_RVA

 

0x00,0x00

LoadLibrary_RVA ->

0x00,0x00,"LoadLibraryA"

GetProcessAddres_RVA ->

0x00,0x00,"GetProcessAddress"

重载Import TableAPI Redirection

使导入表远离逆向工程的魔爪很重要。Danilo Bzdok[6] yoda’s Crypter里使用的技术包括销毁导入块数据,加密导入信息。加载器代码区段可以再次恢复这个部分。我在我的PE Protector中使用了他的方法,部分代码用C,剩下的部分用汇编。ProcessOrgIT() 为我们提供了实现我们目的所需要的东西。在加载器区段里,INIT_IMPORT_TABLE() 例程帮我们实现了API重定向及导入表重建工作。

反调试的方法

PE Protector应该能检测到程序是否被调试,并阻止被调试。OllyDbgSoftICE是使用最多的两个调试,它们可以绕过许多阻碍调试器的技巧。尽管如此,我还是要介绍一些检测调试器的简单方法。当然,这些方法对这些使用最新插件的调试器来说,没有任何效果。

  1. IsDebuggerPresent Windows API: 无论如何,只要当前的进程运行在调试器的上下文中,IsDebuggerPresent() 总是返回非零值。
  2. SoftICE detection:通过检查windows NT下的NTICE 驱动及windows 98下的SICE 驱动是否正在使用来发现SoftICE调试器。

3.            if(CreateFile( "\\\\.\\NTICE", GENERIC_READ | GENERIC_WRITE,

4.                  FILE_SHARE_READ | FILE_SHARE_WRITE,

5.                  NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,

6.                  NULL)!=INVALID_HANDLE_VALUE)

7.            {

8.                There is SoftICE NT on your system;

9.            }

10.        if(CreateFile( "\\\\.\\SICE", GENERIC_READ | GENERIC_WRITE,

11.              FILE_SHARE_READ | FILE_SHARE_WRITE,

12.              NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,

13.              NULL)!=INVALID_HANDLE_VALUE)

14.        {

15.            There is SoftICE98 on your system;

16.        }

  1. Probe Processes: 有时候,它需要搜索具体的进程或隔离具体的进程。在例子源码里,演示了它怎样寻找父进程,检查它是否是EXPLORER.EXE ,杀死除了Explorer 窗口之外的所有父进程。在CryptStuff.CPP 里有C++和汇编两种代码,说明了怎样实现这个任务。

18.        void GetFileNameFromPath(char* szSource)

19.        {

20.              char *szTemp=strrchr(szSource,'\\');   

21.              if(szTemp!=NULL)

22.              {

23.                    szTemp++;

24.                    DWORD l=DWORD(strlen(szTemp))+1;

25.                    CopyMemory(szSource,szTemp,l);

26.              }

27.        }

28.         

29.        void AntiDebug()

30.        {

31.              char lpszSystemInfo[MAX_PATH];

32.              HANDLE hSnapshot=NULL;

33.              DWORD PID_child;

34.              DWORD PID_parent,PID_explorer;

35.              HANDLE hh_parnet = NULL;

36.              PROCESSENTRY32    pe32 = {0};

37.              pe32.dwSize = sizeof(PROCESSENTRY32);//0x128;

38.              PID_child=GetCurrentProcessId();//getpid();

39.              hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);

40.              if (Process32First(hSnapshot, &pe32))

41.              {

42.                    while (Process32Next(hSnapshot, &pe32))

43.                    {

44.                          GetFileNameFromPath(pe32.szExeFile);

45.                          CharUpperBuff(pe32.szExeFile,strlen(pe32.szExeFile));

46.                          if(strcmp(pe32.szExeFile,"EXPLORER.EXE")==0)

47.                          {

48.                                PID_explorer=pe32.th32ProcessID;

49.                          }

50.                          if(pe32.th32ProcessID==PID_child)

51.                          {

52.                                PID_parent=pe32.th32ParentProcessID;

53.                          }

54.                    }

55.              }

56.              if(PID_parent!=PID_explorer)

57.              {

58.                    hh_parnet= OpenProcess(PROCESS_ALL_ACCESS,

59.                                            TRUE, PID_parent);

60.                    TerminateProcess(hh_parnet, 0);

61.              }

62.              else

63.              {

64.                    MODULEENTRY32    me32 = {0};

65.                    me32.dwSize = sizeof(MODULEENTRY32);

66.                    hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,

67.                                PID_explorer);

68.                    if (Module32First(hSnapshot, &me32))

69.                    {

70.                          do

71.                          {

72.                                if(PID_explorer==me32.th32ProcessID)

73.                                {

74.                                      GetWindowsDirectory(lpszSystemInfo,

75.                                       MAX_PATH+1);

76.                                      strcat(lpszSystemInfo,"\\");

77.                                      strcat(lpszSystemInfo,"EXPLORER.EXE");

78.                              CharUpperBuff(me32.szExePath,

79.                                            strlen(me32.szExePath));

80.                                      if(strncmp(me32.szExePath,

81.                                                  lpszSystemInfo,

82.                                            strlen(lpszSystemInfo)))

83.                                      {

84.                                         GetFileNameFromPath(me32.szExePath);

85.                                         if(strcmp(me32.szExePath,

86.                                            "EXPLORER.EXE")==0)

87.                                         {

88.                                            hh_parnet=

89.                                                OpenProcess(PROCESS_ALL_ACCESS,

90.                                                  TRUE, PID_explorer);

91.                                            TerminateProcess(hh_parnet, 0);

92.                                         }

93.                                      }

94.                                }

95.                          }while (Module32Next(hSnapshot, &me32));

96.                    }

97.              }

98.        }

因为缺少CreateToolhelp32Snapshot(), Process32First(), Process32Next(), Module32First(), Module32Next(),这段代码在Windows NT 4.0下并不能使用。但这些API函数可以用来自NTDLL.DLL的未公开的API函数来构建。你可以阅读Gary Nebbett[10] ‘Windows NT (2000) Native API reference’

此外,我推荐你用Mark Russinovich[11] Process Explorer探究你系统中正在运行的进程。它可以帮助你更好的理解它们。

Figure 2

Process Explorer

删除不必要的数据

有时候,你可能会想着删除所有不必要的数据,例如调试信息,重定位区段,减小DOS头,删除MS-DOS stub Program等。但基于一些考虑,你应该在你的PE Protector中保留这些数据。例如,重定位区段在运行EXE文件中没有任何作用,但它在OLE-Active ControlsDLL中扮演着重要的角色。

例子代码

这个项目可以在Visual C++ .NET 2003下编译,而不需要任何其它的工具。除了Windows NT 4.0Windows 95外,它可以在任何版本的Windows系统下工作。

结论

本文及它的源码演示了PE protector工具的工作原理,可以作为它们的入门读物。我希望它覆盖了开源领域有关此主题所缺少的内容。

参考资料

[1] "Microsoft Portable Executable and Common Object File Format Specification", Microsoft Corporation, Revision 6.0, February 1999.

[2] " Peering Inside the PE: A Tour of the Win32 Portable Executable File Format", Matt Pietrek, MSDN Library, March 1994.

[3a] "An In-Depth Look into the Win32 Portable Executable File Format", part 1, Matt Pietrek, MSDN Magazine, February 2002.

[3b] "An In-Depth Look into the Win32 Portable Executable File Format", part 2, Matt Pietrek, MSDN Magazine, March 2002.

[4] PEview Version 0.67, Wayne J. Radburn.

[5] MSDN Library, Microsoft Corporation, April 2003.

[6] yoda’s Crypter, Danilo Bzdok.

[7] UPX, the Ultimate Packer for eXecutables, Markus F.X.J. Oberhumer & László Molnár.

[8] LZO real-time data compression library, Markus F.X.J. Oberhumer.

[9] aPLib compression library, Joergen Ibsen.

[10] "Windows NT (2000) Native API reference", Gary Nebbet.

[11] Process Explorer, Mark Russinovich.

Ashkbiz Danehkar

Click here to view Ashkbiz Danehkar's online profile.

 

Other popular articles: