CRC32 检测

一、CRC32 检测

CRC32 也叫循环冗余校验(Cyclic Redundancy Check,CRC),是一种错误检测技术,广泛用于数据传输和存储领域。其原理是通过将数据表示为二进制多项式,并使用一个预定义的生成多项式进行模 2 除法运算,从而生成一个 32 位的校验值(CRC 码)。这个校验值可以用于在检测数据在传输或存储过程中是否发生错误。

因此我们可以通过 CRC32 检测来判断程序是否处于被调试状态。在程序执行之初将代码段的 CRC 码进行保存(这个时机越早越好),如果被加载到内存中的程序代码段发生更改(比如下软件段点、修改代码逻辑等),则检测到的 CRC 码会和之前保存的不一致,表示程序可能正在被调试。

我们先来看一下 CRC32 校验的代码:

#include <iostream>

// CRC32多项式,这里使用的是IEEE 802.3标准的多项式
#define POLYNOMIAL 0xEDB88320

// 预计算的CRC表
uint32_t crc_table[256];

// 初始化CRC表
void init_crc_table() {
    for (uint32_t i = 0; i < 256; i++) {
        uint32_t crc = i;
        for (uint32_t j = 0; j < 8; j++) {
            if (crc & 1) {
                crc = (crc >> 1) ^ POLYNOMIAL;
            }
            else {
                crc >>= 1;
            }
        }
        crc_table[i] = crc;
    }
}

// 计算CRC32
uint32_t make_crc32(const unsigned char* string, uint32_t size) {
    uint32_t crc = 0xFFFFFFFF; // 初始值

    for (uint32_t i = 0; i < size; i++) {
        uint8_t index = (crc ^ string[i]) & 0xFF;
        crc = (crc >> 8) ^ crc_table[index];
    }

    return ~crc; // 取反后的结果
}

int main() {
    // 初始化CRC表
    init_crc_table();

    // 测试字符串
    const char* test_string = "测试数据";
    uint32_t crc_value = make_crc32(reinterpret_cast<const unsigned char*>(test_string), strlen(test_string));

    // 输出CRC值
    printf("CRC32 value: %08X\n", crc_value);

    return 0;
}

运行结果如下:

CRC32 检测网址:https://www.lddgo.net/encrypt/crc

1 通过检测代码段

在程序加载之初,通过对 PE 结构中的节表进行遍历,找出带有执行属性的节,然后对齐进行 CRC32 计算进行保存,在需要校验的地方重新计算 CRC32 值与先前值进行比较,如果不一致则表示对应段的代码被更改:

#include <stdio.h>
#include <Windows.h>
#include <iostream>

int main()
{
	// 初始化CRC表
	init_crc_table();

	// 用于保存可执行段的 CRC32 校验值
	uint32_t value[10] = { 0 };
	
	// 返回用于创建调用进程的文件(.exe 文件)的句柄
	HMODULE fileHandle = GetModuleHandleA(NULL);

	// 获取 Dos 头
	PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)fileHandle;

	// 获取 Nt 头
	PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((ULONG_PTR)fileHandle + pDosHeader->e_lfanew);

	// 遍历 PE 节表并判断每个节是否为代码段(拥有可执行属性)
	PIMAGE_SECTION_HEADER pFirstHeader = IMAGE_FIRST_SECTION(pNtHeader);
	for (int i = 0; i < pNtHeader->FileHeader.NumberOfSections; i++)
	{
		// 当前节的属性
		DWORD Characteristics = pFirstHeader[i].Characteristics;
		
		// 如果为代码段(具有可执行属性)则进行 CRC32 校验并保存
		if (Characteristics & IMAGE_SCN_MEM_EXECUTE)
		{
			/*std::cout << "Section Name: " << pFirstHeader[i].Name
				<< " is executable." << std::endl;*/

			// 第 i 个元素保存第 i 个代码段的 CRC32 值
			value[i] = make_crc32((unsigned char*)((ULONG_PTR)fileHandle + pFirstHeader[i].VirtualAddress),
				pFirstHeader[i].Misc.VirtualSize);
		}
	}
	
	// 此处暂停代码,然后在调试器中更改代码段的代码
	system("pause");

	// 开始执行校验
	BOOL isDebugged = FALSE;
	for (int i = 0; i < pNtHeader->FileHeader.NumberOfSections; ++i)
	{
		// 当前节的属性
		DWORD Characteristics = pFirstHeader[i].Characteristics;

		// 如果为代码段(具有可执行属性)则进行 CRC32 校验
		if (Characteristics & IMAGE_SCN_MEM_EXECUTE)
		{
			uint32_t ret = make_crc32((unsigned char*)((ULONG_PTR)fileHandle + pFirstHeader[i].VirtualAddress),
				pFirstHeader[i].Misc.VirtualSize);
			if (ret != value[i])
			{
				isDebugged = TRUE;
				std::cout << "第 " << i << " 个节:" << pFirstHeader[i].Name <<
					" 的代码被改动!!!" << std::endl;
			}
		}
	}

	if (!isDebugged)
	{
		std::cout << "程序的代码段没有被更改" << std::endl;
	}

	system("pause");
}

运行程序,等执行到第一个 system("pause") 指定后已经将第一次计算的 CRC32 值进行了保存,此时我们在 x64dbg 中将第一个和第二个节进行更改(随意找代码 nop 掉即可):

然后再回到命令行窗口回车让程序执行 CRC32 校验,结果如下:

二、绕过检测

1 通过硬件断点绕过

了解到上面的关于 CRC32 的基本原理后,我们知道在计算 CRC32 的过程中,会逐字节的对校验的代码段进行访问,因此我们可以利用这个特点,在校验的代码段中找到正常流程不会走的一小段代码(四个字节即可),在我们的实验中,我们在代码段中填下以下未调用的代码:

void func()
{
	printf("111\r\n");
}

然后用 x64dbg 打开程序(运行到 mainCRTStartup 后),定位到该函数处,下一个四字节的数据访问硬件断点(不是执行断点):

然后运行程序,在程序执行 CRC32 初始化的时候就会在访问此处代码段数据的下一句代码处断下:

由于硬件访问断点是一个陷阱类异常,所以发生异常的时候,已经执行完发生异常的那条代码了。

我们知道此时这段代码正在进行 CRC32 校验,返回到上一层 make_crc32 函数的调用处:

可以看到计算完之后对 crc32 值进行了保存,value[10] 一共能保存 10 个 crc32 值,没有保存的位置全部初始化为 0。

在校验的代码段我们可以对校验代码逻辑进行更改,使其始终返回这个值,或者直接跳过校验代码。

posted @ 2025-02-23 09:41  lostin9772  阅读(7)  评论(0)    收藏  举报