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