1. 程式人生 > 實用技巧 >轉載:CRC檢測的實現與對抗

轉載: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("\n------程式已經被修改,準備退出-----\n");
            getchar();
            return 0;
        }
        printf("程式正常執行\n");
        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\n", 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("\n------程式已經被修改,準備退出-----\n");
return 0;
}
printf("程式正常執行\n");
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