C/C++ 使用CRC检测磁盘文件完整性

当软件被开发出来时,为了增加软件的安全性,防止被破解,通常情况下都会对自身内存或磁盘文件进行完整性检查,以防止解密者修改程序,我们可以将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结构中留下一些记号,当我们的程序被脱壳后程序中的判断语句将会起作用,从而让脱壳后的程序无法正常运行,也是一种思路。

posted @ 2020-09-12 12:58  lyshark  阅读(2024)  评论(0编辑  收藏  举报

loading... | loading...
博客园 - 开发者的网上家园