在軟體保護中,增加對自身的完整性檢查,以防止解密者修改程式達到破解的目的。完整性檢查又包括磁盤檔案檢查和記憶體映像的檢查。完整性檢查的實作原理是用散列函數計算檔案的散列值,并将該值放在某處,以後檔案每次運作時,重新計算檔案的散列值,并與原散列值比較,以判斷檔案是否被修改。
1.磁盤檔案完整性檢查
打開需要完整性檢查的程式,用散列計算從PE檔案頭開始到檔案末尾的值,儲存在PE檔案頭得某個位置。在程式運作時取出存儲的值進行比較。需要一個額外的應用程式處理程式。
檔案處理程式:
打開目标程式,用散列函數計算出從PE檔案頭到檔案結束的散列值,并把散列值寫到PE檔案頭前那個空白處。
校驗程式:
(1)讀取自身檔案從PE頭開始到檔案結束的内容,用同樣的散列函數計算檔案内容的散列值,這時得到一個“原始“檔案的散列值。
(2) 讀取自身檔案先前存儲的散列值(PE檔案前),這個值時通過處理程式寫入的。
(3) 把步驟(1)和步驟(2)的值進行比較,如果相等,說明檔案沒有被修改過,反之,檔案被修改了。
缺陷:
解密者可能自己計算散列值,重新寫入檔案。
2.記憶體映像完整性檢查
磁盤檔案完整性校驗可以抵禦破解者修改磁盤檔案,但對記憶體更新檔卻沒有效果。是以對記憶體關鍵代碼也實行檢驗。每個程式都有資料塊和代碼塊,由于資料塊會動态變化,是以檢查這部分是沒有意義的。而代碼塊是隻讀的,存放的是程式代碼,在程式運作過程中是不會發生變化的,是以用這部分進行記憶體校驗是可行的。
實作過程:
(1) 從記憶體映像得到PE相關資料,入代碼區塊的RVA值和記憶體大小等。
(2) 根據得到的代碼區塊的RVA值和記憶體大小,計算其記憶體資料的散列值。
(3) 讀取自身檔案先前存儲的散列值。
(4) 比較兩個散列值。
這個方法還可以有效的抵抗調試器的普通斷點,因為調試器一般通過給應用程式下INT3指令來實作中斷,這樣就改變了代碼區塊的資料,計算散列值時會與原來的不同。
3.實作執行個體
對磁盤檔案完整性檢查主要檢視代碼段是否被修改,是以對磁盤檔案同樣可以隻檢查代碼段的MD5值。由于靜态磁盤檔案和記憶體映像中代碼段是相同的。是以隻需計算靜态檔案代碼段得MD5值,寫入PE檔案頭的某個位置。程式在運作時分别計算靜态磁盤檔案和記憶體鏡像代碼段的MD5值跟PE檔案頭預存放值進行比較。在計算MD5值時,最好加些特殊字元,以增加破解的難度。
3.1檔案處理程式
步驟:
1、打開檔案,擷取dos檔案頭。
2、通過dos頭的結構體内變量計算出PE檔案頭得位置。
3、通過PE檔案結構體變量計算出塊表位置。
4、通過塊表第一個區塊表計算出代碼段的起始位置和長度。
5、計算MD5值。
6、将計算出的MD5值寫入PE檔案頭之前的8個位元組中。
代碼:
size_t GetDiskFileCodeSectionEncrypt(const char* filename, unsigned char infor[8])
{
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNtHeader = NULL;
PIMAGE_SECTION_HEADER pCodeSection = NULL;
HANDLE hFile = NULL;
DWORD dwLowFileSize = 0, dwHighFileSize = 0,dwReturn = 0;
TCHAR *pBuffer = NULL;
encrypt e;
unsigned char en[16];
int i = 0;
TCHAR tcCodeSignature[] = ".text";
TCHAR *pCodeSectBuffer = NULL;
DWORD dwNumberOfSection = 0, j = 0;
TCHAR tcAdditional[] = "特殊字元";
//Open file need to calc code section MD5.
hFile = CreateFile(
filename,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
//Open file fail
return 0;
}
dwLowFileSize = GetFileSize(hFile, &dwHighFileSize);
if (dwLowFileSize == 0xFFFFFFFF)
{
CloseHandle(hFile);
return 0;
}
pBuffer = malloc(sizeof(TCHAR)*dwLowFileSize);//allocate buffer
if (ReadFile(hFile, pBuffer, dwLowFileSize, &dwReturn, NULL) == 0)
{
CloseHandle(hFile);
return 0;
}
pDosHeader = (PIMAGE_DOS_HEADER)pBuffer;
pNtHeader = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + (pDosHeader->e_lfanew));//DWORD must be exist in this place. if no DWORD, ocur fail.
dwNumberOfSection = pNtHeader->FileHeader.NumberOfSections;
pCodeSection = IMAGE_FIRST_SECTION(pNtHeader);
pCodeSectBuffer = (TCHAR *)pCodeSection;
for (j = 0; j < dwNumberOfSection; j++)
{
if (_strnicmp(pCodeSectBuffer,tcCodeSignature,8) ==0 ) // find ".text" section, .text section is the code section.
{
break;
}
pCodeSectBuffer += IMAGE_SIZEOF_SECTION_HEADER;
}
if (j == dwNumberOfSection)
{
CloseHandle(hFile);
return 0;
}
pCodeSection = (PIMAGE_SECTION_HEADER)pCodeSectBuffer;
//calc MD5 value
encryptInit(&e);
encryptUpdate(&e, pBuffer + pCodeSection->PointerToRawData, pCodeSection->Misc.VirtualSize);// 注意PointerToRawData和VirtualSize
encryptUpdate(&e, tcAdditional, strlen(tcAdditional));
encryptFinal(en, &e);
for (i=0; i < 8; i++)
{
infor[i] = en[i*2];
}
free(pBuffer);
CloseHandle(hFile);
return 1;
}
3.2.磁盤檔案校驗
步驟:
1、2、3、4、5與檔案處理程式步驟相同。
6、取出PE檔案頭之前8個位元組中的值。
7、将第5步與第6步的結果進行比較。若結果相同,說明檔案代碼段沒有被改動。若結果不同,說明檔案代碼段被修改。
代碼:
size_t CheckDiskFileCodeSectionEncrypt()
{
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNtHeader = NULL;
PIMAGE_SECTION_HEADER pCodeSection = NULL;
HANDLE hFile = NULL;
DWORD dwLowFileSize = 0, dwHighFileSize = 0,dwReturn = 0;
TCHAR *pBuffer = NULL;
encrypt e;
unsigned char en[16];
int i = 0;
TCHAR tcCodeSignature[] = ".text";
TCHAR *pCodeSectBuffer = NULL;
DWORD dwErrorLevel = 0, dwLength = 0;
TCHAR tcModuleName[MAX_PATH];
unsigned char ucOriginalMD5[8]="";
unsigned char ucNewMD5[8]="";
DWORD dwNumberOfSection = 0, j = 0;
TCHAR tcAdditional[] = "特殊字元";
dwErrorLevel = GetModuleFileName(NULL, tcModuleName, MAX_PATH);//get the module file name
if (dwErrorLevel == 0)
{
return 0;
}
//Open file need to calc code section MD5.
hFile = CreateFile(
tcModuleName,
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
//Open file fail
return 0;
}
dwLowFileSize = GetFileSize(hFile, &dwHighFileSize);
if (dwLowFileSize == 0xFFFFFFFF)
{
CloseHandle(hFile);
return 0;
}
pBuffer = malloc(dwLowFileSize * sizeof(TCHAR));//allocate buffer
if (ReadFile(hFile, pBuffer, dwLowFileSize, &dwReturn, NULL) == 0)
{
CloseHandle(hFile);
return 0;
}
CloseHandle(hFile);
pDosHeader = (PIMAGE_DOS_HEADER)pBuffer;
pNtHeader = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + (pDosHeader->e_lfanew));//DWORD must be exist in this place. if no DWORD, ocur fail.
dwNumberOfSection = pNtHeader->FileHeader.NumberOfSections;
pCodeSection = IMAGE_FIRST_SECTION(pNtHeader);
pCodeSectBuffer = (TCHAR *)pCodeSection;
for (j = 0; j < dwNumberOfSection; j++)
{
if (_strnicmp(pCodeSectBuffer,tcCodeSignature,8) ==0 ) // find ".text" section, .text section is the code section.
{
break;
}
pCodeSectBuffer += IMAGE_SIZEOF_SECTION_HEADER;
}
if (j == dwNumberOfSection)
{
CloseHandle(hFile);
return 0;
}
pCodeSection = (PIMAGE_SECTION_HEADER)pCodeSectBuffer;
for (i = 0; i < 8; i++)
{
ucOriginalMD5[i] = *(unsigned char*)((DWORD)pNtHeader - 8 +i);
}
//calc MD5 value
encryptInit(&e);
encryptUpdate(&e, (unsigned char*)(pBuffer + pCodeSection->PointerToRawData), pCodeSection->Misc.VirtualSize);//注意PointerToRawData和VirtualSize
encryptUpdate(&e, tcAdditional, strlen(tcAdditional));
encryptFinal(en, &e);
for (i=0; i < 8; i++)
{
ucNewMD5[i] = en[i*2];
}
//compare the value with new and original md5 value.
for (i = 0; i < 8; i++)
{
if (ucNewMD5[i] != ucOriginalMD5[i])
{
//printf("file has beed destoryed!\n");
return 0;
}
}
if (pBuffer != NULL)
{
free(pBuffer);
}
return 1;
}
3.3記憶體映像校驗
步驟:
1、取出記憶體中PE檔案的起始位址,擷取dos檔案頭。
2、通過dos頭的結構體内變量計算出PE檔案頭得位置。
3、通過PE檔案結構體變量計算出塊表位置。
4、通過塊表第一個區塊表計算出代碼段的起始位置和長度。
5、計算MD5值。
6、從記憶體中取出PE檔案頭之前位置的8個位元組值。
7、将第5步與第6步的結果進行比較。若結果相同,說明記憶體映像代碼段沒有被破壞,若結果不同說明記憶體映像中代碼段被修改。
提示:在内容映像檢驗程式中如果加斷點調式會使記憶體代碼段得MD5值與PE檔案頭之前取出的MD5值不同,正驗證前面提到的“這個方法還可以有效的抵抗調試器的普通斷點,因為調試器一般通過給應用程式下INT3指令來實作中斷,這樣就改變了代碼區塊的資料,計算散列值時會與原來的不同。”
代碼:
size_t CheckMemCodeSectionEncrypt()
{
int i = 0;
unsigned char ucOriginalMD5[8]="";
unsigned char ucNewMD5[8]="";
DWORD dwImageBase = 0;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNtHeader = NULL;
PIMAGE_SECTION_HEADER pCodeSection = NULL;
TCHAR *pCodeSectBuffer = NULL;
TCHAR tcCodeSignature[] = ".text";
encrypt e;
unsigned char en[16];
DWORD dwNumberOfSection = 0, j = 0;
TCHAR tcAdditional[] = "特殊字元";
dwImageBase = (DWORD)GetModuleHandle(NULL);//the module handle is the memory address
pDosHeader = (PIMAGE_DOS_HEADER)dwImageBase;
pNtHeader = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + pDosHeader->e_lfanew);
dwNumberOfSection = pNtHeader->FileHeader.NumberOfSections;
pCodeSection = IMAGE_FIRST_SECTION(pNtHeader);
pCodeSectBuffer = (TCHAR *)pCodeSection;
for (i = 0; i < 8; i++)//get the original MD5 value from memory ahead PE header
{
ucOriginalMD5[i] = *(unsigned char*)((DWORD)pNtHeader - 8 +i);
}
for (j = 0; j < dwNumberOfSection; j++)
{
if (_strnicmp(pCodeSectBuffer,tcCodeSignature,8) ==0 ) // find ".text" section, .text section is the code section.
{
break;
}
pCodeSectBuffer += IMAGE_SIZEOF_SECTION_HEADER;
}
if (j == dwNumberOfSection)
{
return 0;
}
pCodeSection = (PIMAGE_SECTION_HEADER)pCodeSectBuffer;
//calc code section MD5 value in memory
encryptInit(&e);
encryptUpdate(&e, (unsigned char *)(dwImageBase + pCodeSection->VirtualAddress), pCodeSection->Misc.VirtualSize);//注意VirtualAddress和VirtualSize
encryptUpdate(&e, tcAdditional, strlen(tcAdditional));
encryptFinal(en, &e);
for (i=0; i < 8; i++)
{
ucNewMD5[i] = en[i*2];
}
//compare the value with new and original md5 value.
for (i = 0; i < 8; i++)
{
if (ucNewMD5[i] != ucOriginalMD5[i])
{
//printf("file has beed destoryed!\n");
return 0;
}
}
return 1;
}