当软件被开发出来时,为了增加软件的安全性,防止被破解,通常情况下都会对自身内存或磁盘文件进行完整性检查,以防止解密者修改程序,我们可以将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;
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; }
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);
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;
pDosHeader = (PIMAGE_DOS_HEADER)pBuffer;
pNtHeader = (PIMAGE_NT_HEADERS32)((DWORD)pDosHeader + pDosHeader->e_lfanew);
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;
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; } 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; pNtHeader = (PIMAGE_NT_HEADERS32)((DWORD)pDosHeader + pDosHeader->e_lfanew);
DWORD OriginalCRC32 = *(DWORD *)((DWORD)pNtHeader - 4); printf("读出节表值: %x \n", OriginalCRC32); 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结构中留下一些记号,当我们的程序被脱壳后程序中的判断语句将会起作用,从而让脱壳后的程序无法正常运行,也是一种思路。