C/C++ 使用 CRC32 檢測磁盤文件完整性


當軟件被開發出來時,為了增加軟件的安全性,防止被破解,通常情況下都會對自身內存或磁盤文件進行完整性檢查,以防止解密者修改程序,我們可以將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結構中留下一些記號,當我們的程序被脫殼后程序中的判斷語句將會起作用,從而讓脫殼后的程序無法正常運行,也是一種思路。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM