如何不在编码时直接导入相关 API 的前提下进行攻击
从攻击者的角度来看,不在编码时直接导入相关 API 是一个核心的规避手段。这种技术通常被称为动态 API 调用或运行时 API 解析,其主要目的是:
- 绕过签名检测: 传统的杀毒软件和安全工具会扫描可执行文件中的导入表。如果导入表里有
CreateRemoteThread
、WriteProcessMemory
、LoadLibrary
等高危函数,文件就会被标记为可疑。动态调用 API 可以让导入表看起来非常“干净”,从而躲过静态扫描 - 增加逆向分析难度: 逆向工程师通常会从导入表入手,快速了解程序的功能。如果导入表是空的或只导入了少数几个基础函数,逆向分析师就必须花费大量时间去跟踪程序的运行时行为,才能发现其真正意图
- 支持多操作系统版本和架构: 有些 API 的地址在不同版本的 Windows 上可能会有细微差异。动态获取 API 地址可以确保代码在不同系统上都能正确运行,提高攻击的通用性
- 按需加载: 只有在需要执行特定恶意行为时才去获取和调用相应的 API,这可以减少不必要的代码和数据,使恶意程序更小、更精简
下面是几种具体的技术实现,从初级到高级:
1. 使用 LoadLibrary
和 GetProcAddress
这是最基础、最常见的方法。攻击者只需要在代码中静态导入 LoadLibraryA/W
和 GetProcAddress
这两个函数,然后用它们来动态获取所有其他需要的 API
实现步骤:
- 加载 DLL: 调用
LoadLibraryA
,传入需要加载的 DLL 名称(例如 "kernel32.dll")。这个函数会返回该 DLL 在内存中的基址 - 获取函数地址: 调用
GetProcAddress
,传入 DLL 的基址和需要获取的函数名称(例如 "CreateRemoteThread")。这个函数会返回该 API 的内存地址 - 函数指针调用: 将获取到的地址赋值给一个函数指针,然后通过这个指针像调用普通函数一样来调用它
// 伪代码
#include <windows.h>
#include <stdio.h>
int main() {
HMODULE hKernel32 = LoadLibraryA("kernel32.dll");
if (hKernel32 == NULL) {
// 处理错误
return 1;
}
// 定义一个函数指针类型
typedef HANDLE (WINAPI* CreateRemoteThread_t)(HANDLE, LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);
// 获取 CreateRemoteThread 的地址
CreateRemoteThread_t pCreateRemoteThread = (CreateRemoteThread_t)GetProcAddress(hKernel32, "CreateRemoteThread");
if (pCreateRemoteThread == NULL) {
// 处理错误
return 1;
}
// 现在可以使用 pCreateRemoteThread 来调用 CreateRemoteThread 函数了
// pCreateRemoteThread(..., ..., ...);
return 0;
}
这种方法虽然简单,但 LoadLibrary
和 GetProcAddress
依然会出现在程序的导入表中,因此安全软件仍然可以进行识别
2. 手动解析 PEB
这是更高级、更隐蔽的方法。其核心是完全不依赖任何静态导入,通过手动遍历内存中的数据结构来找到所需的 API 地址
实现步骤:
- 获取 PEB 地址: 在 32 位系统上,PEB 的地址可以通过
FS:[0x30]
寄存器来获取;在 64 位系统上,可以通过GS:[0x60]
来获取 - 遍历 PEB LDR 数据结构: PEB 结构体中包含一个指向已加载模块列表的指针(
LDR_DATA
)。攻击者可以遍历这个列表,找到ntdll.dll
、kernel32.dll
等已加载的 DLL 模块 - 解析 DLL 的导出表(EAT): 找到目标 DLL 的基址后,手动解析其导出地址表 (EAT)。EAT 是一个包含所有导出函数名称和地址的结构
- 哈希值匹配: 为了避免在代码中硬编码函数名称字符串(字符串会暴露恶意意图),攻击者通常会为每个函数名称计算一个哈希值。遍历 EAT 中的函数名称,计算哈希值,然后与预设的目标哈希值进行匹配。如果匹配成功,就找到了所需 API 的地址
- 函数指针调用: 获取到地址后,同样通过函数指针进行调用