C/C++ 實現PE檔案特徵碼識別

lyshark發表於2020-09-14

PE檔案就是我們常說的EXE可執行檔案,針對檔案特徵的識別可以清晰的知道該程式是使用何種程式語言實現的,前提是要有特徵庫,PE特徵識別有多種形式,第一種是靜態識別,此方法就是隻針對磁碟中檔案的特徵碼欄位進行檢測來判斷編譯器版本,此種方式優點是快,缺點是識別不準確,第二種則是動態識別,當程式被裝入記憶體解碼後在嘗試對其進行識別,此方法最為準確,我們經常使用的PEID查殼工具是基於靜態檢測的方法。


開啟PE檔案對映: 在讀取PE結構之前,首先要做的就是開啟PE檔案到記憶體,這裡開啟檔案我們使用了CreateFile()函式該函式可以開啟檔案並返回檔案控制程式碼,接著使用CreateFileMapping()函式建立檔案的記憶體映像,最後使用MapViewOfFile()讀取對映中的記憶體並返回一個控制程式碼,後面的程式就可以通過該控制程式碼操作開啟後的檔案了.

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

#pragma comment(lib,"Imagehlp.lib")

// 讀取PE結構的封裝
HANDLE OpenPeFile(LPTSTR FileName)
{
	HANDLE hFile, hMapFile, lpMapAddress = NULL;
	DWORD dwFileSize = 0;

	// CreateFile 既可以建立檔案,也可以開啟檔案,這裡則是開啟檔案的含義
	hFile = CreateFile(FileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE)
		return 0;

	// 獲取到檔案大小
	dwFileSize = GetFileSize(hFile, NULL);

	// 建立檔案的記憶體映像
	// 此方法非常靈活,其不需要將程式完全讀入記憶體中,節約空間。
	hMapFile = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, dwFileSize, NULL);
	if (hMapFile == NULL)
		return 0;

	// 讀取對映中的記憶體並返回一個控制程式碼
	lpMapAddress = MapViewOfFile(hMapFile, FILE_MAP_READ, 0, 0, dwFileSize);
	if (lpMapAddress != NULL)
		return lpMapAddress;
	return 0;
}

int main(int argc, char * argv[])
{
	HANDLE lpMapAddress = NULL;

	lpMapAddress = OpenPeFile("c://lyshark.exe");
	printf("開啟檔案控制程式碼: %d \n", lpMapAddress);

	system("pause");
	return 0;
}

判斷是否為PE檔案: 當檔案已經開啟後,接下來就要判斷檔案是否為有效的PE檔案,這裡我們首先將映象轉換為PIMAGE_DOS_HEADER格式並通過pDosHead->e_magic屬性找到PIMAGE_NT_HEADERS結構,然後判斷其是否符合PE檔案規範即可,這裡需要注意32位於64位PE結構所使用的的結構定義略有不同,程式碼中已經對其進行了區分.

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

#pragma comment(lib,"Imagehlp.lib")

// 讀取PE結構的封裝
HANDLE OpenPeFile(LPTSTR FileName)
{
	HANDLE hFile, hMapFile, lpMapAddress = NULL;
	DWORD dwFileSize = 0;

	// CreateFile 既可以建立檔案,也可以開啟檔案,這裡則是開啟檔案的含義
	hFile = CreateFile(FileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE)
		return 0;

	// 獲取到檔案大小
	dwFileSize = GetFileSize(hFile, NULL);

	// 建立檔案的記憶體映像
	// 此方法非常靈活,其不需要將程式完全讀入記憶體中,節約空間。
	hMapFile = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, dwFileSize, NULL);
	if (hMapFile == NULL)
		return 0;

	// 讀取對映中的記憶體並返回一個控制程式碼
	lpMapAddress = MapViewOfFile(hMapFile, FILE_MAP_READ, 0, 0, dwFileSize);
	if (lpMapAddress != NULL)
		return lpMapAddress;
	return 0;
}

// 判斷是否為PE檔案
BOOL IsPeFile(HANDLE ImageBase, BOOL Is64 = FALSE)
{
	PIMAGE_DOS_HEADER pDosHead = NULL;
	if (ImageBase == NULL)
		return FALSE;

	// 將對映檔案轉為DOS結構,並判斷開頭是否為MZ
	pDosHead = (PIMAGE_DOS_HEADER)ImageBase;
	if (IMAGE_DOS_SIGNATURE != pDosHead->e_magic)
		return FALSE;

	if (Is64 == TRUE)
	{
		// 根據 IMAGE_DOS_HEADER 的 e_lfanew 的值得到 64位 NT 頭的位置
		PIMAGE_NT_HEADERS64 pNtHead64 = NULL;
		pNtHead64 = (PIMAGE_NT_HEADERS64)((DWORD64)pDosHead + pDosHead->e_lfanew);
		if (pNtHead64->Signature != IMAGE_NT_SIGNATURE)
			return FALSE;
	}
	else if (Is64 == FALSE)
	{
		// 根據 IMAGE_DOS_HEADER 的 e_lfanew 的值得到 32位 NT 頭的位置
		PIMAGE_NT_HEADERS pNtHead32 = NULL;
		pNtHead32 = (PIMAGE_NT_HEADERS)((DWORD)pDosHead + pDosHead->e_lfanew);
		if (pNtHead32->Signature != IMAGE_NT_SIGNATURE)
			return FALSE;
	}
	return TRUE;
}

int main(int argc, char * argv[])
{
	HANDLE lpMapAddress = NULL;

	// 開啟檔案拿到PE控制程式碼
	lpMapAddress = OpenPeFile("c://lyshark.exe");

	// 判斷是否為PE檔案,這裡定義的為真返回1,為假返回0
	BOOL ret = IsPeFile(lpMapAddress, 0);
	printf("是否為PE檔案: %d \n", ret);

	system("pause");
	return 0;
}

判斷PE檔案特徵碼: 判斷程式使用了何種編譯器編寫,通常情況是要用檔案的入口處程式碼和特徵碼進行匹配,通常情況下我們只需要匹配程式開頭的前32個位元組就差不多了,當然為了匹配精度更高,我們也可以對多個欄位進行驗證,這裡就只寫出大體輪廓吧.

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

#pragma comment(lib,"Imagehlp.lib")

// 讀取PE結構的封裝
HANDLE OpenPeFile(LPTSTR FileName)
{
	HANDLE hFile, hMapFile, lpMapAddress = NULL;
	DWORD dwFileSize = 0;

	// CreateFile 既可以建立檔案,也可以開啟檔案,這裡則是開啟檔案的含義
	hFile = CreateFile(FileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE)
		return 0;

	// 獲取到檔案大小
	dwFileSize = GetFileSize(hFile, NULL);

	// 建立檔案的記憶體映像
	// 此方法非常靈活,其不需要將程式完全讀入記憶體中,節約空間。
	hMapFile = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, dwFileSize, NULL);
	if (hMapFile == NULL)
		return 0;

	// 讀取對映中的記憶體並返回一個控制程式碼
	lpMapAddress = MapViewOfFile(hMapFile, FILE_MAP_READ, 0, 0, dwFileSize);
	if (lpMapAddress != NULL)
		return lpMapAddress;
	return 0;
}

// 判斷是否為PE檔案
BOOL IsPeFile(HANDLE ImageBase, BOOL Is64 = FALSE)
{
	PIMAGE_DOS_HEADER pDosHead = NULL;
	if (ImageBase == NULL)
		return FALSE;

	// 將對映檔案轉為DOS結構,並判斷開頭是否為MZ
	pDosHead = (PIMAGE_DOS_HEADER)ImageBase;
	if (IMAGE_DOS_SIGNATURE != pDosHead->e_magic)
		return FALSE;

	if (Is64 == TRUE)
	{
		// 根據 IMAGE_DOS_HEADER 的 e_lfanew 的值得到 64位 NT 頭的位置
		PIMAGE_NT_HEADERS64 pNtHead64 = NULL;
		pNtHead64 = (PIMAGE_NT_HEADERS64)((DWORD64)pDosHead + pDosHead->e_lfanew);
		if (pNtHead64->Signature != IMAGE_NT_SIGNATURE)
			return FALSE;
	}
	else if (Is64 == FALSE)
	{
		// 根據 IMAGE_DOS_HEADER 的 e_lfanew 的值得到 32位 NT 頭的位置
		PIMAGE_NT_HEADERS pNtHead32 = NULL;
		pNtHead32 = (PIMAGE_NT_HEADERS)((DWORD)pDosHead + pDosHead->e_lfanew);
		if (pNtHead32->Signature != IMAGE_NT_SIGNATURE)
			return FALSE;
	}
	return TRUE;
}

// 掃描特徵碼,對比
void GetPeSignature(LPCWSTR FilePath)
{
	typedef struct _SIGN
	{
		char FileName[64];         // 儲存檔名或特徵描述
		LONG FileOffset;           // 儲存檢測檔案偏移地址
		BYTE VirusSign[32 + 1];    // 儲存特徵碼大小32,其中的1是結束符.
	}SIGN, *pSIGN;

	// 定義特徵碼與特徵描述資訊,你可以自己去提取一段特徵碼
	SIGN Sign[2] = {
		{
			"Microsoft Visual C/C++ x86 (2013)",
			0x8a0,
		"\x55\x8B\xEC\x81\xEC\xC4\x00\x00\x00\x53\x56\x57\x8D\xBD\x3C\xFF" \
		"\xFF\xFF\xB9\x31\x00\x00\x00\xB8\xCC\xCC\xCC\xCC\xF3\xAB\x8B\x45" \
		},
		{
			"Microsoft Visual C/C++ x64 (2013)",
			0x400,
		"\xCC\xCC\xCC\xCC\xCC\xE9\x86\x02\x00\x00\xE9\x31\x05\x00\x00\xE9" \
		"\x6C\x01\x00\x00\xE9\x57\x03\x00\x00\xE9\x22\x00\x00\x00\xCC\xCC" \
		}
	};

	DWORD dwNum = 0;
	BYTE buffer[32 + 1];
	HANDLE hFile = NULL;

	// 獲取到FilePath路徑下檔案的控制程式碼資訊
	hFile = CreateFile(FilePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ,
		NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

	// 我們有兩段待檢測特徵,這裡迴圈兩次從零開始
	for (int x = 0; x <= 2; x++)
	{
		// 將待檢測程式的檔案指標指向特徵碼的偏移位置
		SetFilePointer(hFile, Sign[x].FileOffset, NULL, FILE_BEGIN);
		// 讀取目標程式指定位置的特徵碼到記憶體中
		ReadFile(hFile, buffer, sizeof(buffer), &dwNum, NULL);
		// 對比記憶體中兩個特徵碼是否相等
		if (memcmp(Sign[x].VirusSign, buffer, 32) == 0)
		{
			printf("檢測結果: %s \n", Sign[x].FileName);
		}
	}
	CloseHandle(hFile);
}

int main(int argc, char * argv[])
{
	GetPeSignature(L"c://lyshark.exe");
	system("pause");
	return 0;
}

你需要自己提取不同編譯器的特徵欄位,然後按照我寫好的格式進行增加,例如我是用vs2013編譯的,那麼檢測結果就可能會是vs2013,特徵碼的提取應儘量保證一致性。

相關文章