转载:CRC检测的实现与对抗

本文转载于吾爱破解论坛:https://www.52pojie.cn/forum.php?mod=viewthread&tid=1150032

一直听说CRC可以校验代码是否被修改,最近研究了一下。
CRC的优点是代码量小,容易理解,在动态校验上应用比较广泛。
代码量确实是小,没毛病,但是看网上的资料我理解起来,还真有点费劲,下面详细讲一下。
1.CRC算法原理
数据发送过程:
多项式转化为二进制数,这个2进制数作为除数。
CRC校验码的位数=上面计算除数的位数-1
校验码的位数是多少,就把需要校验的数据左移多少位,得到的就是被除数
被除数 模二除 除数 = 商+余数
余数就是我们需要的CRC校验码
数据接收过程:0
多项式转化为二进制数,这个2进制数作为除数。
接收到的数据和CRC码拼接起来,作为被除数
除数确定了,被除数也确定了,接下来再次使用“模2除法”校验
结果为0,则接受的数据正确,结果不为0接收的数据不正确
还是不懂,很好,再翻译一遍
多项式就是一个指定的数值,用我们需要校验的数据模二除这个多项式的数值,得到的余数就是CRC校验码。
这样好理解很多了吧,模二除法想了解的话可以网上搜一下,大家动动手,我就懒一下了。2.CRC算法实现

首先生成CRC校验码,我们这里按字节计算CRC,不考虑网上的按位计算(都64位系统了,不差一张表的内存)
先写个函数生成一张字节CRC校验码的表,因为每个字节从00-FF有256个组合,所以每个字节有256种不同的校验码。

VOID GenerateByteCrc()
{
    unsigned int crc = 0;
    int i=0,j=0;
    for (j = 0; j < 256; j++)                       //一个byte有256种不同的值,计算所有可能值的crc码
    {                     
        crc = j;
        for (i = 0; i < 8; i = i++)                 //这个for循环生成crc码
        {   
            if (crc & 1)
                crc = (crc >> 1) ^ 0xEDB88320;             //根据多项式生成的除数
            else
                crc >>= 1;
        }
        crc_byte[j] = crc;                          //对应整型数值的crc码保存在该数组中
    }
    crc_byte_being = 1;        //通过这个值判断是否生成了这个表
}

有了校验表,就可以对数据进行校验,再写一个校验的函数:

DWORD GenerateDataCrc(char* data, int len)    //data是校验数据的起始地址,len是校验数据的长度
{
    unsigned int crc = 0xFFFFFFFF;
    unsigned int i;
    for (i = 0; i < len; i++)
    {
        crc = crc_byte[(crc ^ data[i]) & 0xFF] ^ (crc >> 8);
    }
    crc = ~crc;
    printf("当前代码段数据CRC校验码为0x%x ", crc);
    return crc;
}

起始看懂原理,再看这份实现的代码也有点晦涩,代码是借鉴加密与解密书中简化的代码,如果纯按照原理来实现的话,代码还有有点繁琐的。
至此功能基本实现,写一个程序使用CRC校验自身是否被修改。

int main()
{
    char* data=0x00401000;        //这里是该程序代码段的起始地址
    int len = 0x0e6c;        //这里是该程序代码段的长度
  
    if (!crc_byte_being)
        GenerateByteCrc();
    DWORD OriginalCrcCode = GenerateDataCrc(data, len);
  
    while (1)
    {
        DWORD CurrentCrcCode = GenerateDataCrc(data, len);
        if (OriginalCrcCode != CurrentCrcCode)
        {
            printf("
------程序已经被修改,准备退出-----
");
            getchar();
            return 0;
        }
        printf("程序正常运行
");
        Sleep(2000);
    }
    return 0;
}

看一下代码段的虚拟地址偏移和大小,虚拟地址偏移加上映像基址就是data的值,大小就是len的值。

程序正常执行如下:

3.过掉CRC检测
使用OD打开程序,F9跑起来:

我们可以在代码段随便修改一条指令,看到效果:程序已经检测到代码被修改。

 好,现在我们试着过掉检测,因为CRC会不断的读取要验证的代码,所以我们可以使用CE查看是哪些代码在读取我们的程序。CE附加进程后,先手动添加一条代码段的地址,这里我就添加了代码段的起始地址,然后查看什么访问了该地址。

 在OD中看一下这个地址,CE也可以看,不过用OD更直观。
这段代码就是我们计算CRC的代码,esi中保存了最后计算出的CRC码,传给eax作为函数的返回值。

 在这个函数中下个软件断点,因为软件断点会修改当前地址的指令为int 3,所以程序正常执行肯定会退出,我们单步跟着程序走,找到判断的代码

很快遇到一个跳转, 这个就很明显了,nop掉就可以了,当然绕过的方法还有很多,就不一一列举了。
4.CRC校验改进
经过上述过程,发现CRC检测程序很容易被发现,被发现就会被干掉。
如果创建一个线程专门用来CRC检测呢,通过内存访问断点还是会被定位到检测代码。
如果双层CRC嵌套检测呢,两处代码还是会访问被检测的地址,所以还是会被定位。
如果通过另一个进程来检测被保护进程呢?果断写份代码试一试。

int main()
{
SIZE_T* Real_len;
char* process_name = "CRC-verify.exe";
char* buff;
  
VirtualAlloc(&buff, 0x0e6c, MEM_RESERVE, PAGE_READWRITE);
int Pid = ProcesstoPid(process_name);
printf("%d
", Pid);
HANDLE hprocess = OpenProcess(PROCESS_ALL_ACCESS, NULL, (DWORD)Pid);
if (!hprocess)        //进程被od打开时,这里OpenProcess会返回0
{
printf("进程打开失败");
return 0;
}
ReadProcessMemory(hprocess, 0x00401000, &buff, 0x0e6c, &Real_len);
if (!crc_byte_being)
GenerateByteCrc();
DWORD OriginalCrcCode = GenerateDataCrc(&buff, 0x0e6c);
  
while (1)
{
ReadProcessMemory(hprocess, 0x00401000, &buff, 0x0e6c, &Real_len);
DWORD CurrentCrcCode = GenerateDataCrc(&buff, 0x0e6c);
if (OriginalCrcCode != CurrentCrcCode)
{
printf("
------程序已经被修改,准备退出-----
");
return 0;
}
printf("程序正常运行
");
Sleep(2000);
}
CloseHandle(hprocess);
return 0;
}

先运行前面校验自身的程序,再运行后面这个跨进程校验的程序,然后使用CE附加被保护的程序,再次查找一下是什么访问了内存地址。

发现只能看到自身校验的代码,随便修改一处代码段中的内容,再查看跨进程校验的程序。

 这个程序也发现代码段被修改了,说明思路没问题。
这样我们可以采用自身CRC校验全部代码,通过保护进程来校验CRC校验的代码的方案来达到检测的目的。
当然这种方式通过遍历进程句柄表查找哪些进程打开了被保护进程,也可以过掉,但是相比于CRC自校验来说,门槛一下就提高了。
今天就写到这里吧,新人第一次发帖,有不足的地方还望大佬们指正。

参考资料:
《加密与解密第四版》   段刚
网络游戏安全之实战某游戏厂商FPS游戏CRC检测的对抗与防护   https://bbs.pediy.com/thread-253552.htm
如何通俗的理解CRC校验并用C语言实现   https://zhuanlan.zhihu.com/p/77408094

原文地址:https://www.cnblogs.com/basstorm/p/13512685.html