几种常用的保护应用程序的方法 ( 作者:老实和尚  coverlove@163.com )



1、前言
  目前很多程序员都没有软件安全的意识,对自己辛辛苦苦的劳动成果不加保护,而这些缺乏保护意识的程序在日益强大的SoftIce, OllyDBG(OllyICE), W32DASM等工具面前显得如此脆弱,一个稍微有点经验的Cracker可能在几分钟之内就可以轻易的突破防线;有些程序员虽然有了保护自己程序的意识,但是一般都完全依赖于专业的保护软件进行加壳保护,对自己的程序本身在编码阶段却完全不设防,而这些知名的壳也并非安全,目前对这些知名的壳都有专门的脱壳工具和脱壳教材,一旦这些壳被剥掉,那么呈现在Cracker面前的也完全是一些赤裸裸的代码。
  本文主要介绍在软件编码阶段进行一些必要的保护手段,在编译生成二进制代码后再进行外部加壳保护处理,这样应该更安全一些,一旦壳被攻破,还有内部的保护机制在起作用,已经写成类库,觉得有用的可以下载稍微修改即可使用。

2、常用的几种编码阶段的保护手段

A、检测SoftIce驱动是否安装

检测SoffIce的驱动是否存在来判断是否安装了SoftIce,代码如下:

if(CreateFile( "\\\\.\\NTICE", GENERIC_READ | GENERIC_WRITE, 
        FILE_SHARE_READ | FILE_SHARE_WRITE, 
        NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
        NULL)!=INVALID_HANDLE_VALUE) 
  { 
      There is SoftICE NT on your system; 
  } 
if(CreateFile( "\\\\.\\SICE", GENERIC_READ | GENERIC_WRITE, 
        FILE_SHARE_READ | FILE_SHARE_WRITE, 
        NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
        NULL)!=INVALID_HANDLE_VALUE) 
  { 
      There is SoftICE98 on your system; 
  }


B. 程序窗口句柄检测
  
检测是否存在窗口Ollyice或者OllyDbg等等,读者可以自行添加

char name1[]="OLLYDBG";

char name2[]="OLLYICE";

HWND hwnd=BlurFindWindow(name1); //循环检测windows所有顶层窗口,只要窗口名称中含有(模糊匹配)字符串name1,name2的窗口则说明存在Ollyice或者OllyDbg

if(hwnd!=NULL)
{
  There is OLLYDBG on your system;
}
hwnd=BlurFindWindow(name2);

if(hwnd!=NULL)
{
  There is OLLYICE on your system;
}

C. 用线程环境块检测

TEB(Thread Environment Block) 在 Windows 9x 系列中被称为 TIB(Thread Information Block),它记录了线程的重要信息,而且每一个线程都会对应一个 TEB 结构。在TEB结构的 30h 偏移处存放着另外一个重要的数据结构的首地址PPEB:
typedef struct _NT_TEB
{
 NT_TIB Tib; // 00h
 PVOID EnvironmentPointer; // 1Ch
 CLIENT_ID Cid; // 20h
 PVOID ActiveRpcInfo; // 28h
 PVOID ThreadLocalStoragePointer; // 2Ch
 PPEB Peb; // 30h
 ULONG LastErrorValue; // 34h
 ULONG CountOfOwnedCriticalSections; // 38h
 PVOID CsrClientThread; // 3Ch
 PVOID Win32ThreadInfo; // 40h
 ULONG Win32ClientInfo[0x1F]; // 44h
 PVOID WOW32Reserved; // C0h
 ULONG CurrentLocale; // C4h
 ULONG FpSoftwareStatusRegister; // C8h
 PVOID SystemReserved1[0x36]; // CCh
 PVOID Spare1; // 1A4h
 LONG ExceptionCode; // 1A8h
 ULONG SpareBytes1[0x28]; // 1ACh
 PVOID SystemReserved2[0xA]; // 1D4h
 GDI_TEB_BATCH GdiTebBatch; // 1FCh
 ULONG gdiRgn; // 6DCh
 ULONG gdiPen; // 6E0h
 ULONG gdiBrush; // 6E4h
 CLIENT_ID RealClientId; // 6E8h
 PVOID GdiCachedProcessHandle; // 6F0h
 ULONG GdiClientPID; // 6F4h
 ULONG GdiClientTID; // 6F8h
 PVOID GdiThreadLocaleInfo; // 6FCh
 PVOID UserReserved[5]; // 700h
 PVOID glDispatchTable[0x118]; // 714h
 ULONG glReserved1[0x1A]; // B74h
 PVOID glReserved2; // BDCh
 PVOID glSectionInfo; // BE0h
 PVOID glSection; // BE4h
 PVOID glTable; // BE8h
 PVOID glCurrentRC; // BECh
 PVOID glContext; // BF0h
 NTSTATUS LastStatusValue; // BF4h
 UNICODE_STRING StaticUnicodeString; // BF8h
 WCHAR StaticUnicodeBuffer[0x105]; // C00h
 PVOID DeallocationStack; // E0Ch
 PVOID TlsSlots[0x40]; // E10h
 LIST_ENTRY TlsLinks; // F10h
 PVOID Vdm; // F18h
 PVOID ReservedForNtRpc; // F1Ch
 PVOID DbgSsReserved[0x2]; // F20h
 ULONG HardErrorDisabled; // F28h
 PVOID Instrumentation[0x10]; // F2Ch
 PVOID WinSockData; // F6Ch
 ULONG GdiBatchCount; // F70h
 ULONG Spare2; // F74h
 ULONG Spare3; // F78h
 ULONG Spare4; // F7Ch
 PVOID ReservedForOle; // F80h
 ULONG WaitingOnLoaderLock; // F84h

 PVOID StackCommit; // F88h
 PVOID StackCommitMax; // F8Ch
 PVOID StackReserve; // F90h

 PVOID MessageQueue; // ???
} NT_TEB, *PNT_TEB; 


PPEB  Peb;       // 30h Pointer to owning process database

这个偏移地址处的内容非常有用,它指向本线程的拥有者的 PEB(Process Database) 的线性地址。这个结构的02h 偏移处存放着进程是否被调试的标志。当有调试器调试进程时这个BeingDebugged会被置1,否则为0,可以通过这个标志来检测是否被调试器调试。

typedef struct _PEB
{
      UCHAR InheritedAddressSpace;                     // 00h
      UCHAR ReadImageFileExecOptions;                  // 01h
      UCHAR BeingDebugged;                             // 02h
      UCHAR Spare;                                     // 03h
      PVOID Mutant;                                    // 04h
      PVOID ImageBaseAddress;                          // 08h
      PPEB_LDR_DATA Ldr;                               // 0Ch
      PRTL_USER_PROCESS_PARAMETERS ProcessParameters;  // 10h
      PVOID SubSystemData;                             // 14h
      PVOID ProcessHeap;                               // 18h
      PVOID FastPebLock;                               // 1Ch
      PPEBLOCKROUTINE FastPebLockRoutine;              // 20h
      PPEBLOCKROUTINE FastPebUnlockRoutine;            // 24h
      ULONG EnvironmentUpdateCount;                    // 28h
      PVOID* KernelCallbackTable;                      // 2Ch
      PVOID EventLogSection;                           // 30h
      PVOID EventLog;                                  // 34h
      PPEB_FREE_BLOCK FreeList;                        // 38h
      ULONG TlsExpansionCounter;                       // 3Ch
      PVOID TlsBitmap;                                 // 40h
      ULONG TlsBitmapBits[0x2];                        // 44h
      PVOID ReadOnlySharedMemoryBase;                  // 4Ch
      PVOID ReadOnlySharedMemoryHeap;                  // 50h
      PVOID* ReadOnlyStaticServerData;                 // 54h
      PVOID AnsiCodePageData;                          // 58h
      PVOID OemCodePageData;                           // 5Ch
      PVOID UnicodeCaseTableData;                      // 60h
      ULONG NumberOfProcessors;                        // 64h
      ULONG NtGlobalFlag;                              // 68h
      UCHAR Spare2[0x4];                               // 6Ch
      LARGE_INTEGER CriticalSectionTimeout;            // 70h
      ULONG HeapSegmentReserve;                        // 78h
      ULONG HeapSegmentCommit;                         // 7Ch
      ULONG HeapDeCommitTotalFreeThreshold;            // 80h
      ULONG HeapDeCommitFreeBlockThreshold;            // 84h
      ULONG NumberOfHeaps;                             // 88h
      ULONG MaximumNumberOfHeaps;                      // 8Ch
      PVOID** ProcessHeaps;                            // 90h
      PVOID GdiSharedHandleTable;                      // 94h
      PVOID ProcessStarterHelper;                      // 98h
      PVOID GdiDCAttributeList;                        // 9Ch
      PVOID LoaderLock;                                // A0h
      ULONG OSMajorVersion;                            // A4h
      ULONG OSMinorVersion;                            // A8h
      ULONG OSBuildNumber;                             // ACh
      ULONG OSPlatformId;                              // B0h
      ULONG ImageSubSystem;                            // B4h
      ULONG ImageSubSystemMajorVersion;                // B8h
      ULONG ImageSubSystemMinorVersion;                // C0h
      ULONG GdiHandleBuffer[0x22];                     // C4h

      PVOID ProcessWindowStation;                      
} PEB, *PPEB;
代码如下:
int exist=0;
__asm
  {
    pushad
    //在fs:[0x18]再加上0x30就是指向了TEB结构
    mov   eax,fs:[0x18]     //pTeb 线性地址
        mov   eax, dword ptr  [eax+0x30]    //pPeb的首地址
    //获取PEB偏移2h处BeingDebugged的值  
    movzx eax,byte ptr[eax+0x02]
    or al,al  
    jz No
    jnz Yes
     No:
     mov exist,0
     jmp Exit
     Yes:
     mov exist,1

     jmp Exit
     Exit:    
    popad
  }  
  if(exist)
  {
    // The process is debugged

  }

D、用API函数IsDebuggerPresent检测

if(IsDebuggerPresent())
{
  // The process is debugged

}

E、利用异常SEH机制

利用异常检测调试器,下面这段代码执行throw(&exce)
的时候会抛出异常,然后进入到我们设置好的异常处理例程将bIsInDebugger设置为FALSE,而一旦调试器运行这段代码的时候,异常首先被调试器截获,如果调试器截获后进行了处理而没有继续传递给我们的异常处理例程,那么将不会执行bIsInDebugger=FALSE从而达到检测调试器的目的。

BOOL bIsInDebugger = TRUE ;
 
CFileException exce;

try
{
     // 抛出异常 
    
    throw(&exce);

    //throw(1);

}
catch(int )
{
  bIsInDebugger = TRUE ;

}
catch(CFileException *)
{
  
  bIsInDebugger = FALSE ;
}

  
if( bIsInDebugger )
{
  // The process is debugged

}

F、使用自己的函数替代部分系统函数,完成同样的功能

  比如:替换上面的IsDebuggerPresent函数,拷贝IsDebuggerPresent函数的实现代码为自己所用,这样可以避免Cracker使用bpx IsDebuggerPresent断点来跟踪我们的程序。偷系统的代码实现如下:



 
(系统API IsDebuggerPresent实现的二进制代码 )

__declspec( naked )  BOOL CDebugProtect::IsDebuggerPresentEx()
{
  __asm
  {
    mov     eax, dword ptr fs:[18h]
    mov     eax, dword ptr [eax+30h]
    movzx   eax, byte ptr [eax+2h]
    retn
  }
}
这样在调用IsDebuggerPresent的地方都可以用IsDebuggerPresentEx替换掉。当然有些函数只能替换高层API,对于底层API还是无法替换的,这样Cracker可以在更底层的API上面下断点,再跟踪到调用的地方。


G、使用动态获取DLL函数地址替代使用隐式调用DLL
隐式调用DLL中函数的地方,很容易让Crack通过引用API参考找到下断点的地方,Cracker只需要简单的双击引用API地址的行就能定位到调用API的地方。

DWORD CDebugProtect::TerminateFun=GetProcAddr("kernel32.dll","TerminateProcess");

DWORD CDebugProtect::GetProcAddr(const char *dll,const char *fun)
{
  //const char * pszModName = "kernel32.dll";

  //const char * pszTerminatelName = "TerminateProcess";

  const char * pszModName=dll;

  const char * pszTerminatelName =fun;


  HMODULE hKernel = GetModuleHandle(pszModName);
  
  FARPROC proc=GetProcAddress(hKernel,pszTerminatelName);

  return (DWORD)proc;

}

如在类的静态变量中获取TerminateProcess函数的地址,然后在调用TerminateProcess的地方使用这个变量来终止进程,这样Cracker就不能简单的下断点bpx TerminateProcess。

如下:在类的静态变量初始化时候完成API地址的获取,在实际使用的时候使用这个地址直接CALL,下面是两个API地址隐藏的实现方法。

DWORD apiAddr=TerminateFun; // TerminateFun是一个静态变量,在类第一次使用前已经被初始化

HANDLE currentp=GetCurrentProcess(); //当前进程句柄

  _asm
{
      pushad

      push 0
      
      push currentp

      call apiAddr //形成的汇编代码就很难通过函数参考来找到

      popad


}

//获取硬盘ID,通过隐藏API地址调用
DWORD   GenerateID(void)
{
  const char * lpRootPathName="c:\\";   //取C盘的序列号

  char lpVolumeNameBuffer[12];  //磁盘卷标

  unsigned long int nVolumeNameSize=12;
  
  unsigned long int VolumeSerialNumber;  //磁盘序列号
  unsigned long int MaximumComponentLength;
  char  lpFileSystemNameBuffer[10];
  unsigned long int nFileSystemNameSize=10;
  unsigned long int FileSystemFlags;

  DWORD apiAddr=GetVolumeInformationFun;

  __asm
  {
    pushad  

    push nFileSystemNameSize

    lea eax, lpFileSystemNameBuffer
    
    push eax

    lea eax, FileSystemFlags  

    push eax

    lea eax, MaximumComponentLength 

    push eax

    lea eax, VolumeSerialNumber 

    push eax

    push nVolumeNameSize

    lea eax, lpVolumeNameBuffer 
    
    push eax

    push lpRootPathName //常量字符串变量本身就代表地址,直接使用即可不再使用lea

    call apiAddr

    popad 

  }

//原来的调用方式

//  GetVolumeInformation(lpRootPathName,
//    lpVolumeNameBuffer, nVolumeNameSize,
//    &VolumeSerialNumber, &MaximumComponentLength,
//    &FileSystemFlags,
//    lpFileSystemNameBuffer, nFileSystemNameSize);

  VolumeSerialNumber^=0x12345678;

  return  VolumeSerialNumber;  //给用户显示机器码使用,硬盘伪序列号
  
}

H、 检查程序的父进程是否为EXPLORER.EXE
一个正常的EXE文件应该是由EXPLORER.EXE来加载的,所以其父进程应该是EXPLORER.EXE,可以通过这个检测点来检测是否被调试。

char lpszSystemInfo[MAX_PATH];
HANDLE hSnapshot=NULL;

  DWORD PID_child;
  DWORD PID_parent,PID_explorer;
  HANDLE hh_parnet = NULL;
    PROCESSENTRY32  pe32 = {0};
  pe32.dwSize = sizeof(PROCESSENTRY32);//0x128;
    PID_child=GetCurrentProcessId();//getpid();
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);
  if (Process32First(hSnapshot, &pe32))
    {
    while (Process32Next(hSnapshot, &pe32))
    {
      GetFileNameFromPath(pe32.szExeFile);
            CharUpperBuff(pe32.szExeFile,strlen(pe32.szExeFile));
            if(strcmp(pe32.szExeFile,"EXPLORER.EXE")==0)
            {
                PID_explorer=pe32.th32ProcessID;
            }
      if(pe32.th32ProcessID==PID_child)
      {
        PID_parent=pe32.th32ParentProcessID;
      }
    }
  }
    if(PID_parent!=PID_explorer)
    {
        hh_parnet= OpenProcess(PROCESS_ALL_ACCESS, TRUE, PID_parent);
    res=TRUE;  
        //TerminateProcess(hh_parnet, 0);

     _asm
     {
      pushad
      push 0    
      push hh_parnet
      call apiAddr
      popad
     }
    }
  else
  {
    MODULEENTRY32  me32 = {0};
    me32.dwSize = sizeof(MODULEENTRY32);
    hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE,PID_explorer);
    if (Module32First(hSnapshot, &me32))
    {
        do
            {
          if(PID_explorer==me32.th32ProcessID)
          {
          GetWindowsDirectory(lpszSystemInfo, MAX_PATH+1);
          strcat(lpszSystemInfo,"\\");
          strcat(lpszSystemInfo,"EXPLORER.EXE");
          CharUpperBuff(me32.szExePath,strlen(me32.szExePath));
          if(strncmp(me32.szExePath,lpszSystemInfo,strlen(lpszSystemInfo)))
          {
            GetFileNameFromPath(me32.szExePath);
            if(strcmp(me32.szExePath,"EXPLORER.EXE")==0)
            {
              hh_parnet= OpenProcess(PROCESS_ALL_ACCESS, TRUE, PID_explorer);
              res=TRUE;
              //TerminateProcess(hh_parnet, 0);
                _asm
                 {
                  pushad
                  push 0      
                  push hh_parnet
                  call apiAddr
                  popad
                 }
            }
          }
                }
            }while (Module32Next(hSnapshot, &me32));
    }
  }

I、检查STARTUPINFO结构
Windows操作系统中的explorer.exe创建进程的时候会把STARTUPINFO结构中的值设为0,而非explorer.exe创建进程的时候会忽略这个结构中的值,也就是结构中的值不为0,所以可以利用这个来判断是否在调试程序
STARTUPINFO Info;
//通过检测STARTUPINFO结构来检测debugger
GetStartupInfo(&Info);
   if ( (Info.dwX != 0) || (Info.dwY !=0) || (Info.dwXCountChars != 0) || (Info.dwYCountChars !=0 ) || (Info.dwFillAttribute != 0) || (Info.dwXSize != 0) || (Info.dwYSize != 0) )
   {
   //TerminateProcess(GetCurrentProcess(), 0);
      _asm
     {
      pushad
      push 0  
      push currentp
      call apiAddr
      popad
     }
   }

J、 所有的检测函数返回值只作为参考,而更多的返回值可以通过unsigned char * output来判断

  在主程序中判断函数调用是否成功,可以通过返回值EAX来作初步判断,更准确的判断可以通过复杂的数据结构output来判断,而output的值在函数正常执行完成后进行填充,防止Cracker直接修改函数的返回值EAX来暴力破解。

K、敏感的字符串都进行异或或者可逆变换处理,再使用的地方动态解密出来
  对于敏感的字符串信息需要进行简单的加密处理,否则Cracker可以通过简单的字符串参考就能找到敏感信息的代码地址,在引用字符串的地方先解密再使用,也不要弹出对话框等暴露敏感字符串信息。
L、对文件进行代码段的CRC校验处理
  可以对内存中的PE文件的进行一些CRC校验处理而不要对文件系统中的文件进行CRC校验,因为访问文件本身就是CRC破解的一个突破口。由于编译后我们的二进制代码又进行了加壳等外部处理,整个PE文件的结构发生了重大变化,进行CRC校验已经非常困难,所以此种方法对加壳的PE文件处理比较麻烦。
M、使用花指令等方法防止静态分析
W32Dasm等工具反编译出来的汇编指令如果没有花指令干扰,反编译出来的结果非常容易让Cracker分析,因此可以故意设置一些花指令“陷阱”,让W32Dasm落入我们设下的“陷阱”了。花指令的实质是利用了反编译工具没有人脑那么智能的,它们往往会把人为加入的一些指令理解错,从而错误地确定了指令的起始位置,导致分析错误,而实质上加入花指令后并没有改变原来的程序执行流程,从而达到干扰目的。更详细的介绍请参看罗聪的博客:
(http://www.luocong.com/articles/show_article.asp?Article_ID=14)
3、基于以上几种手段的编写的一个类

上传的附件 DebugProtect.rar

  • 标 题:答复
  • 作 者:shangzh
  • 时 间:2008-06-27 15:11:43

为了防止这个类库将VC IDE也当作调试经常杀死,可以加#ifndef  _DEBUG宏,参考如下:尽量分散多个地方多调用几次:)

#ifndef  _DEBUG

  unsigned char output[100];

  BOOL resutl=FALSE;

  resutl=CDebugProtect::AntiDebug(output,100);

  if(resutl) return FALSE;

  //BOOL res=CDebugProtect::Ollydbg(output,10);

  srand( (unsigned)time( NULL ) );

  int index= rand() % 99 ; //rand() 0 -0x7fff

  if(output[index]!=index+1)  //随机检查返回缓冲区,用户可以自己定义,防止eax爆破
  {
    return FALSE;
  }
  
  resutl=CDebugProtect::Ollydbg(output,100);

  if(resutl) return FALSE;

  srand( (unsigned)time( NULL ) );

  index= rand() % 99 ; //rand() 0 -0x7fff

  if(output[index]!=index+1)
  {
    return FALSE;
  }


  resutl=CDebugProtect::SoftIceDeteck(output,100);

  if(resutl) return FALSE;

  srand( (unsigned)time( NULL ) );

  index= rand() % 99 ; //rand() 0 -0x7fff

  if(output[index]!=index+1)
  {
    return FALSE;
  }


#endif

  • 标 题:答复
  • 作 者:shellwolf
  • 时 间:2008-07-02 21:47:22

内容是不太丰富。楼主提到的方法,OD的应可全部通过。
IsdebugPresent,checkStartupinfo,无论是API还是自己代码,hideod修改几个标志就可通过了。
parentprocess,Hideod处理了Process32NextW,也可通过。
异常,只要设置OD忽略所有异常,也可通过。
推荐peter pierre的<unpacker tirck>里面的方法很全。