几种常用的保护应用程序的方法 ( 作者:老实和尚 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、基于以上几种手段的编写的一个类
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
上传的附件: