1. 程式人生 > 實用技巧 >C/C++ 使用 CRC32 檢測記憶體映像完整性

C/C++ 使用 CRC32 檢測記憶體映像完整性

前面的那一篇文章中所使用的技術只能有效抵抗解密者直接修改硬碟檔案,當我們使用動態補丁的時候,那麼記憶體中同樣不存在校驗效果,也就無法抵禦對方動態修改機器碼了,為了防止解密者直接對記憶體打補丁,我們需要在硬碟校驗的基礎上,增加記憶體校驗,防止動態補丁的運用。

僅對.text程式碼段進行校驗:

通常程式中至少包括了程式碼段,資料段,而資料段中所儲存的資料是經常會發生變動的,例如我們的全域性變數,靜態變數等都會預設儲存在資料段,而程式碼段則不會發生變化,我們在檢驗時只需要注重.text記憶體段中的資料完整性即可,針對記憶體的校驗同樣可以抵禦偵錯程式的CC斷點,該斷點原理就是在下端處寫入int3指令,同樣可以檢測得到。

校驗思路如下
1.首先從記憶體得到PE的程式碼節的RVA和節大小
2.根據得到的RVA和節大小計算出crc32或是RC4值
3.讀取自身儲存的原始CRC32值,與校驗結果進行比較

1.先來實現第一步,讀取記憶體映像的起始地址與大小,我們可以這樣做。

#include <stdio.h>
#include <windows.h>

int main(int argc, char *argv[])
{
	PIMAGE_DOS_HEADER pDosHeader = NULL;
	PIMAGE_NT_HEADERS pNtHeader = NULL;
	PIMAGE_SECTION_HEADER pSecHeader = NULL;
	DWORD ImageBase;

	// 獲取基地址
	ImageBase = (DWORD)GetModuleHandle(NULL);

	// 定位到PE頭結構
	pDosHeader = (PIMAGE_DOS_HEADER)ImageBase;

	// 定位到NT頭
	pNtHeader = (PIMAGE_NT_HEADERS32)((DWORD)pDosHeader + pDosHeader->e_lfanew);

	// 定位第一個區塊地址,因為預設的話第一個就是.text節
	pSecHeader = IMAGE_FIRST_SECTION(pNtHeader);

	// 取出節內偏移與節表長度
	DWORD va_base = ImageBase + pSecHeader->VirtualAddress;   // 定位程式碼節va基地址
	DWORD sec_len = pSecHeader->Misc.VirtualSize;             // 獲取程式碼節長度
	
	printf("映象基址(.text): %x --> 映象大小: %x \n", va_base, sec_len);

	system("pause");
	return 0;
}

2.第二部就是計算校驗和,然後計算該節的CRC32值,並存入全域性變數,也就是程式開啟後自動初始化計算一次記憶體crc32值並放入全域性變數中,然後開一個執行緒,每三秒檢測一次記憶體變化,如果變化則終止執行或彈窗提示,你也可以提前計算處校驗和並寫入PE空缺位置。

#include <stdio.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);
}

// 檢查記憶體中CRC32特徵值
DWORD CheckMemory()
{
	PIMAGE_DOS_HEADER pDosHeader = NULL;
	PIMAGE_NT_HEADERS pNtHeader = NULL;
	PIMAGE_SECTION_HEADER pSecHeader = NULL;
	DWORD ImageBase;

	// 獲取基地址
	ImageBase = (DWORD)GetModuleHandle(NULL);

	// 定位到PE頭結構
	pDosHeader = (PIMAGE_DOS_HEADER)ImageBase;
	pNtHeader = (PIMAGE_NT_HEADERS32)((DWORD)pDosHeader + pDosHeader->e_lfanew);

	// 定位第一個區塊地址,因為預設的話第一個就是.text節
	pSecHeader = IMAGE_FIRST_SECTION(pNtHeader);
	DWORD va_base = ImageBase + pSecHeader->VirtualAddress;   // 定位程式碼節va基地址
	DWORD sec_len = pSecHeader->Misc.VirtualSize;             // 獲取程式碼節長度
	//printf("映象基址(.text): %x --> 映象大小: %x \n", va_base, sec_len);

	DWORD CheckCRC32 = CRC32((BYTE*)(va_base), sec_len);
	// printf(".text節CRC32 = %x \n", CheckCRC32);
	return CheckCRC32;
}

int main(int argc,char *argv[])
{
	// 用於儲存初始化時 .text 節中的CRC32值
	DWORD OriginalCRC32 = 0;

	// 初始化時,給全域性變數賦值,記錄下初始的CRC32值
	OriginalCRC32 = CheckMemory();

	while (1)
	{
		Sleep(3000);
		DWORD NewCRC32 = CheckMemory();
		if (OriginalCRC32 == NewCRC32)
			printf("程式沒有被打補丁. \n");
		else
			printf("程式被打補丁 \n");
	}

	system("pause");
	return 0;
}

上方程式碼是保護了整個程式,在實際應用中,為了提高效率,有時我們只需要保護其中一個片段程式碼就好,這樣可以提高效率,所有我們對上面程式碼稍作修改即可實現針對特定片段的記憶體校驗。

#include <stdio.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);
}

// 檢查記憶體中CRC32特徵值
DWORD CheckMemory(DWORD va_base, DWORD sec_len)
{
	PIMAGE_DOS_HEADER pDosHeader = NULL;
	PIMAGE_NT_HEADERS pNtHeader = NULL;
	PIMAGE_SECTION_HEADER pSecHeader = NULL;
	DWORD ImageBase;
	ImageBase = (DWORD)GetModuleHandle(NULL);
	pDosHeader = (PIMAGE_DOS_HEADER)ImageBase;
	pNtHeader = (PIMAGE_NT_HEADERS32)((DWORD)pDosHeader + pDosHeader->e_lfanew);

	// 以下三條語句可用於確定位置
	// pSecHeader = IMAGE_FIRST_SECTION(pNtHeader);
	// DWORD va_base1 = ImageBase + pSecHeader->VirtualAddress;   // 定位程式碼節va基地址
	// DWORD sec_len1 = pSecHeader->Misc.VirtualSize;             // 獲取程式碼節長度
	// printf("映象基址(.text): %x --> 映象大小: %x \n", va_base1, sec_len1);

	DWORD CheckCRC32 = CRC32((BYTE*)(va_base), sec_len);
	return CheckCRC32;
}

int main(int argc, char *argv[])
{
	// 用於儲存初始化時 .text 節中的CRC32值
	DWORD OriginalCRC32 = 0;

	DWORD begin_addr, end_addr, size;
	// 獲取到兩個位置的偏移地址
	__asm mov begin_addr, offset begin;
	__asm mov end_addr, offset end;

	// 計算出 兩者記憶體差值
	size = end_addr - begin_addr;

	// 校驗指定記憶體位置
	OriginalCRC32 = CheckMemory(begin_addr, size);

	while (1)
	{
	begin: // 標記為需要保護的區域
		printf("hello lyshark \n");
		printf("hello lyshark \n");
		printf("hello lyshark \n");
	end:   // 保護區域宣告結束

		if (OriginalCRC32 == CheckMemory(begin_addr, size))
			printf("此區域沒有被破解 \n");
		else
			printf("此區域已被修改\n");

		Sleep(3000);
	}
	system("pause");
	return 0;
}

通過使用磁碟校驗結合記憶體校驗兩種方式綜合保護,可以極大的提高軟體的安全性,繞過方式則是找到哪兒跟全域性變數將其修正為正確的值即可,同樣的也可以更暴力一些直接將判斷條件改掉均可。