在<一步一步简单的保护我们的源码>中,提到了几种保护源码的方式,本文再新增一种方式。
众所周知,PE文件有诺干节区(至少有.text/.data节),而一般源文件经过编译生成PE文件后,其可执行码就存放在PE文件的.text节区。并且节中可执行的指令集在磁盘中的分布和加载到内存后的分布是一样的。当调试程序而在某代码地址上下断点时,调试器会用int 3(0xcc)替换改地址上的指令,然后在异常中恢复。很明显,在下断点调试这个过程中内存上的指令集和磁盘上的指令集已经存在差异了。如果添加一种机制:程序运行时,时不时的检测一下内存中.text节的内容是否不同于磁盘上的,如果不同,直接退出程序,通过这种方式至少可以防止第二个人调试自己的代码。看雪<加密与解密>的14章提到了这种方法,可以参考其实现。
这里有2个问题:1,什么时候记录文件中.text节的校验值?程序发布前,.text节区中的内容已经确定不变了,因此这个时候计算并记录.text节的内容比较适宜。2,校验值存放在哪?PE文件中有大量冗余内容,我记录在IMAGE_DOS_HEADER!e_lfanew之后的4个字节,记录于此不影响程序运行。
以下开始贴代码
#include "stdafx.h"
#include <windows.h>
#include <assert.h>
#include <stdio.h>
DWORD PeHdrOff = 0;
DWORD PeDataTabOff = 0;
DWORD PeSectTabOff = 0;
DWORD PeSectionCount = 0;
DWORD PeMemoruAil = 0;
DWORD gRVA = 0; //节区的起始RVA
void LocatePeStruct(PVOID lpFileBuf);
DWORD RVA2FOA(PIMAGE_SECTION_HEADER section);
DWORD CRC32(BYTE* ptr,DWORD Size);
int main(int argc, char* argv[])
{
DWORD lastErr;
int idx;
HANDLE imageHd = CreateFile("C:\\studio\\checkdebug\\Debug\\checkdebug.exe",
GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL,OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
if(imageHd == INVALID_HANDLE_VALUE)
{
lastErr = GetLastError();
assert(0);
}
HANDLE mapHd = CreateFileMapping (
imageHd, NULL,
PAGE_READWRITE,
0,0,NULL);
if(mapHd == NULL)
{
lastErr = GetLastError();
assert(0);
}
DWORD dwCurAddr = 0, dwCurPart = 0;
LPVOID mapAddr = 0;
DWORD txtOff;
char* txtBase;
mapAddr = MapViewOfFile (mapHd, FILE_MAP_READ|FILE_MAP_WRITE, 0, dwCurAddr, dwCurPart ) ;
if(mapAddr == NULL)
{
lastErr = GetLastError();
assert(0);
}
LocatePeStruct(mapAddr);
PIMAGE_SECTION_HEADER sectPtr = (PIMAGE_SECTION_HEADER)(((char*)mapAddr)+PeHdrOff+PeSectTabOff);
//定位.text节
for(idx=0;idx<PeSectionCount;idx++)
{
if(strcmp((char*)sectPtr[idx].Name,".text") == 0)
{
//获得.text节在文件中的偏移
gRVA = sectPtr[idx].VirtualAddress;
txtOff = RVA2FOA(sectPtr+idx);
break;
}
}
//获得.text在内存映射中的地址
txtBase = (char*)mapAddr+txtOff;
DWORD crcSum = CRC32((BYTE*)txtBase,sectPtr[idx].Misc.VirtualSize);
//获得IMAGE_DOS_HEADER!e_lfanew偏移
DWORD patchOff = FIELD_OFFSET(IMAGE_DOS_HEADER, e_lfanew);
//在IMAGE_DOS_HEADER!e_lfanew+0x04的位置处写入CRC值
patchOff += sizeof(long);
memcpy(((char*)mapAddr+patchOff),&crcSum,sizeof(long));
UnmapViewOfFile(mapHd);
CloseHandle (mapHd);
CloseHandle (imageHd);
return 0;
}
void LocatePeStruct(PVOID lpFileBuf)
{
//e_lfanew:PE头距离MZ的偏移
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpFileBuf;
PeHdrOff = pDos->e_lfanew;
//DataDirectory:映像读到内存后,数据目录数组在内存中的地址
//下面计算DataDirectory与PE标志之间的偏移
PIMAGE_NT_HEADERS32 pNT = (PIMAGE_NT_HEADERS32)((char*)lpFileBuf+pDos->e_lfanew);
PeMemoruAil = pNT->OptionalHeader.SectionAlignment;
PeDataTabOff = (DWORD)pNT->OptionalHeader.DataDirectory-(DWORD)pNT;
//区段数
PeSectionCount = pNT->FileHeader.NumberOfSections;
//映像文件读到内存后,首个节表在内存中的地址
//下面计算pSection与PE标志之间的偏移
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNT);
PeSectTabOff = (DWORD)pSection-(DWORD)pNT;
return;
}
DWORD fileOffset;
char* gSectName = NULL;
DWORD RVA2FOA(PIMAGE_SECTION_HEADER section)
{
//距离命中节的起始虚拟地址的偏移值。
DWORD dwDiffer = 0;
//模拟内存对齐机制
DWORD dwBlockCount = section->SizeOfRawData/PeMemoruAil;
dwBlockCount += section->SizeOfRawData%PeMemoruAil? 1 : 0;
DWORD dwBeginVA = section->VirtualAddress;
DWORD dwEndVA = section->VirtualAddress + dwBlockCount * PeMemoruAil;
//rva与节表在内存中的起始地址的偏移
dwDiffer = gRVA - dwBeginVA;
if(dwDiffer > section->SizeOfRawData)
{
char errBuffer[1024] = {0};
sprintf(errBuffer,"节在文件中大小:0x%x,节在内存中起址:0x%08x",section->SizeOfRawData,dwBeginVA);
::MessageBoxA(NULL,errBuffer,"Exceed",MB_OK);
return 0;
}
else
{
//PointerToRawData:在整个文件中的偏移
fileOffset = section->PointerToRawData + dwDiffer;
gSectName = (char*)section->Name;
return fileOffset;
}
}
DWORD CRC32(BYTE* ptr,DWORD Size)
{
DWORD crcTable[256],crcTmp1;
//动态生成CRC-32表
for (int i=0; i<256; i++)
{
crcTmp1 = i;
for (int j=8; j>0; j--)
{
if (crcTmp1&1) crcTmp1 = (crcTmp1 >> 1) ^ 0xEDB88320L;
else crcTmp1 >>= 1;
}
crcTable[i] = crcTmp1;
}
//计算CRC32值
DWORD crcTmp2= 0xFFFFFFFF;
while(Size--)
{
crcTmp2 = ((crcTmp2>>8) & 0x00FFFFFF) ^ crcTable[ (crcTmp2^(*ptr)) & 0xFF ];
ptr++;
}
return (crcTmp2^0xFFFFFFFF);
}
这段代码是计算需要保护的PE文件.text节的校验值。
#include "stdafx.h"
#include <windows.h>
BOOL CodeSectionCRC32( );
DWORD CRC32(BYTE* ptr,DWORD Size);
int main(int argc, char* argv[])
{
if(CodeSectionCRC32())
MessageBox(NULL,TEXT ("CRC32 Check OK!"),TEXT ("OK"),MB_ICONEXCLAMATION);
else
MessageBox(NULL,"File corrupted! !","CRC error",MB_ICONEXCLAMATION);
return 0;
}
// 计算代码区块的CRC32值
//
BOOL CodeSectionCRC32( )
{
PIMAGE_DOS_HEADER pDosHeader=NULL;
PIMAGE_NT_HEADERS pNtHeader=NULL;
PIMAGE_SECTION_HEADER pSecHeader=NULL;
DWORD ImageBase,OriginalCRC32;
ImageBase=(DWORD)GetModuleHandle(NULL);//取基址,其实本例也可直接用0x4000000这个值
pDosHeader=(PIMAGE_DOS_HEADER)ImageBase;
pNtHeader=(PIMAGE_NT_HEADERS32)((DWORD)pDosHeader+pDosHeader->e_lfanew);
//定位到PE文件头(即字串“PE\0\0”处)前4个字节处,并读出储存在这里的CRC-32值:
DWORD checkSumOff = FIELD_OFFSET(IMAGE_DOS_HEADER,e_lfanew)+sizeof(long);
OriginalCRC32 = *((DWORD*)((char*)ImageBase+checkSumOff));
//OriginalCRC32 =*((DWORD *)((DWORD)pNtHeader-4));
pSecHeader=IMAGE_FIRST_SECTION(pNtHeader); //得到第一个区块的起始地址
//假设第一个区块就是代码区块
//是从磁盘文件得到代码块的地址和大小来计算CRC32值
if(OriginalCRC32==CRC32((BYTE*) (ImageBase+pSecHeader->VirtualAddress),pSecHeader->Misc.VirtualSize))
return TRUE;
else
return FALSE;
//如果程序从磁盘文件获取第一区块的数据,加壳后,数据将可能不准确,因此对于要加壳的程序来说,
//直接填上代码区块的地址和大小,方法是先随便填一数据,编译后用PE工具查看生成文件的代码区块的RVA和大小,
//再填进来重新编译
/*
if(OriginalCRC32==CRC32((BYTE*) 0x401000,0x36AE))
return TRUE;
else
return FALSE;
*/
}
// 计算字符串的CRC32值
// 参数:欲计算CRC32值字符串的首地址和大小
// 返回值: 返回CRC32值
DWORD CRC32(BYTE* ptr,DWORD Size)
{
DWORD crcTable[256],crcTmp1;
//动态生成CRC-32表
for (int i=0; i<256; i++)
{
crcTmp1 = i;
for (int j=8; j>0; j--)
{
if (crcTmp1&1) crcTmp1 = (crcTmp1 >> 1) ^ 0xEDB88320L;
else crcTmp1 >>= 1;
}
crcTable[i] = crcTmp1;
}
//计算CRC32值
DWORD crcTmp2= 0xFFFFFFFF;
while(Size--)
{
crcTmp2 = ((crcTmp2>>8) & 0x00FFFFFF) ^ crcTable[ (crcTmp2^(*ptr)) & 0xFF ];
ptr++;
}
return (crcTmp2^0xFFFFFFFF);
}
这段代码正是需要保护的假想代码,首先得到IMAGE_DOS_HEADER!e_lfanew+4处校验码,然后计算内存中.text节区代码,两者相比较,结果不同说明代码至少被下了断点。