當軟件被開發出來時,為了增加軟件的安全性,防止被破解,通常情況下都會對自身內存或磁盤文件進行完整性檢查,以防止解密者修改程序,我們可以將exe與dll文件同時做校驗,來達到相互認證的目的,解密者想要破解則比較麻煩,當我們使用的互認證越多時,解密者處理的難度也就越大。
實現磁盤文件檢測,我們可以使用CRC32算法或者RC4算法來計算程序的散列值,以CRC32為例,其默認會生成一串4字節CRC32散列,我們只需要計算后將該值保存在文件或程序自身PE結構中的空缺位置即可。
具體實現:通過使用CRC32算法計算出程序的CRC字節,並將其寫入到PE文件的空缺位置,這樣當程序再次運行時,來檢測這個標志,是否與計算出來的標志一致,來決定是否運行程序,一旦程序被打補丁,其crc32值就會發生變化,一旦發生變化程序就廢了。
實現CRC32完整性檢查: 生成CRC32的代碼如下,其中的CRC32就是計算過程,這個過程是一個定式,我們只需要使用CreateFile
打開文件,並將文件字節數全部讀入到BYTE *pFile = (BYTE*)malloc(dwSize);
中,然后調用crc32計算其硬盤中的hash散列值即可。
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
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);
}
int main(int argc, char* argv[])
{
char *FileName = "c://test.exe";
// 驗證文件是否存在,不存在則退出
if (GetFileAttributes(FileName) == 0xFFFFFFFF)
return 0;
HANDLE hFile = CreateFile(FileName, GENERIC_READ, FILE_SHARE_READ,
0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
DWORD dwSize = GetFileSize(hFile, NULL);
// 開辟一段內存空間
BYTE *pFile = (BYTE*)malloc(dwSize);
// 將數據讀入文件
DWORD dwNum = 0;
ReadFile(hFile, pFile, dwSize, &dwNum, 0);
// 計算CRC32
DWORD dwCrc32 = CRC32(pFile, dwSize);
if (pFile != NULL)
{
printf("CRC32 = 0x%x \n", dwCrc32);
free(pFile);
pFile = NULL;
}
system("pause");
return 0;
}
1.我們將程序自身放入C://test.exe
中,然后計算其hash散列值,最終得到CRC32 = 0x70122091
,接着我們去找PE文件頭,其結構中有很多空字節可以使用,我我們就選擇PE頭之前的最后4個字節作為替換位置。
2.接着就是如何定位並讀出節表中是的數據了,讀取數據可以這樣寫。
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
int main(int argc, char* argv[])
{
char szFileName[MAX_PATH] = { 0 };
char *pBuffer;
DWORD pNumberOfBytesRead;
int FileSize = 0;
// 獲取自身文件,並打開文件
GetModuleFileName(0, szFileName, MAX_PATH);
HANDLE hFile = CreateFile(szFileName, GENERIC_READ, 1, 0, 3, FILE_ATTRIBUTE_NORMAL, 0);
// 為空則打開失敗,退出
if (hFile == INVALID_HANDLE_VALUE) return FALSE;
// 獲取文件大小讀入緩沖區
FileSize = GetFileSize(hFile, 0);
pBuffer = new char[FileSize];
ReadFile(hFile, pBuffer, FileSize, &pNumberOfBytesRead, 0);
CloseHandle(hFile);
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS32 pNtHeader = NULL;
// 獲取到DOS頭數據
pDosHeader = (PIMAGE_DOS_HEADER)pBuffer;
// 獲取到NT頭
pNtHeader = (PIMAGE_NT_HEADERS32)((DWORD)pDosHeader + pDosHeader->e_lfanew);
// 定位到PE文件頭前4字節處
DWORD OriginalCRC32 = *(DWORD *)((DWORD)pNtHeader - 4);
printf("讀出節表值: %x \n", OriginalCRC32);
system("pause");
return 0;
}
首先編譯器生成以上代碼片段,然后我們使用前面的CRC32計算工具計算出其hash散列值,CRC32 = 0x92e05c8a
將此地址,反寫到程序中。
會發現,當我們嘗試修改程序中的數據時,crc32散列值也會隨之變化,也就是說我們動了程序crc32也就重新就算了,這好像是一個死結無法被解開,那么該如何解決這個問題呢?
我們只需要更改以下CRC32計算程序,讓其跳過PE頭前面的DOS頭部分,不讓其參與到計算中,即可解決這個沖突問題,由於DOS頭沒什么實際作用,跳過也無妨,將計算代碼進行更改。
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
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);
}
BOOL CheckCRC32()
{
char szFileName[MAX_PATH] = { 0 };
char *pBuffer;
DWORD pNumberOfBytesRead;
int FileSize = 0;
// 獲取自身文件,並打開文件
GetModuleFileName(0, szFileName, MAX_PATH);
HANDLE hFile = CreateFile(szFileName, GENERIC_READ, 1, 0, 3, FILE_ATTRIBUTE_NORMAL, 0);
if (hFile == INVALID_HANDLE_VALUE) return FALSE;
FileSize = GetFileSize(hFile, 0);
pBuffer = new char[FileSize];
ReadFile(hFile, pBuffer, FileSize, &pNumberOfBytesRead, 0);
CloseHandle(hFile);
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS32 pNtHeader = NULL;
pDosHeader = (PIMAGE_DOS_HEADER)pBuffer;
// 獲取到NT頭
pNtHeader = (PIMAGE_NT_HEADERS32)((DWORD)pDosHeader + pDosHeader->e_lfanew);
// 定位到PE文件頭前4字節處
DWORD OriginalCRC32 = *(DWORD *)((DWORD)pNtHeader - 4);
printf("讀出節表值: %x \n", OriginalCRC32);
// 我們只需要計算PE結構的CRC32值,不需要計算DOS頭
FileSize = FileSize - DWORD(pDosHeader->e_lfanew);
DWORD CheckCRC32 = CRC32((BYTE*)(pBuffer + pDosHeader->e_lfanew), FileSize);
printf("計算出 CRC32 = %x \n", CheckCRC32);
if (CheckCRC32 == OriginalCRC32)
printf("程序沒有被破解 \n");
else
printf("程序被破解 \n");
}
int main(int argc, char* argv[])
{
CheckCRC32();
system("pause");
return 0;
}
編譯程序,並記下 CRC32 = 86906a18
hash數值。
寫入到文件中,即可實現磁盤文件的完整性檢測,注意寫入時應該是反寫,且前面要補0.
在此次打開會提示程序沒有被破解,當用戶認為的修改指令時,就會提示已破解,無法繼續運行下去。
如何破解: 如果目標磁盤文件進行了CRC32磁盤校驗,我們該如何破解呢?思路差不多就是找到CRC32算號位置,然后觀察其結果到底時與誰進行的比較,將指令取反,也可實現破解。
定位CRC32位置我們可以觀察期算法特征,首先他會用到0xEDB88320L,0xFFFFFFFF,0x00FFFFFF
這三個關鍵常數,我們可以將其作為識別條件的一部分。
其次CRC32會有一個256此的循環也可以作為識別條件,或者攔截ReadFile也可,因為計算之前必定會讀取,也是一個思路。
將對比過程取反,同樣可以過掉其磁盤CRC32的檢測。
MapFileAndCheckSum 校驗和: 通過使用系統提供的API實現反破解,該函數主要通過檢測,PE可選頭IMAGE_OPTIONAL_HEADER
中的Checksum
字段來實現的,一般的EXE默認為0而DLL中才會啟用,當然你可以自己開啟,讓其支持這種檢測.
#include <stdio.h>
#include <windows.h>
#include <Imagehlp.h>
#pragma comment(lib,"imagehlp.lib")
int main(int argc,char *argv[])
{
DWORD HeadChksum = 1, Chksum = 0;
char text[512];
GetModuleFileName(GetModuleHandle(NULL), text, 512);
if (MapFileAndCheckSum(text, &HeadChksum, &Chksum) != CHECKSUM_SUCCESS)
return 0;
if (HeadChksum != Chksum)
printf("文件校驗和錯誤 \n");
else
printf("文件正常 \n");
system("pause");
return 0;
}
在編譯上方代碼之前,需要將編譯器進行一定的設置,以確保支持校驗和。
C/C++ -> 常規 -> 調試信息格式 --> 程序數據庫
連接器 -> 常規 -> 啟用增量鏈接 -> 否
連接器 -> 高級 -> 設置校驗和 -> 是
啟用校驗和后,IMAGE_OPTIONAL_HEADER中的Checksum字段保存有該程序的hash數據。
磁盤校驗還可以用於反脫殼,我們可以加殼后在殼子的PE結構中留下一些記號,當我們的程序被脫殼后程序中的判斷語句將會起作用,從而讓脫殼后的程序無法正常運行,也是一種思路。