自举的代码幽灵——反射DLL注入(Reflective DLL Injection)
一天 渗透测试 544浏览 · 2025-02-28 12:59

一、前言

反射注入揭示了计算机安全最深邃的哲学命题:防御者固守的"进程-文件-注册表"三维认知框架,便暴露了对抗高维攻击的致命缺陷,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 函数

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

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

三、ReflectiveLoader的原理和代码实现

VS中调试DLL工程的正确方法_vs debug dll-CSDN博客

反射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 中,通过 Inject 来传递DLL的基址也是一种比较常规的做法,而我将跟随Stephen Fewer的思想,自实现暴力搜索DLL的基址。

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



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

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



怎么验证DOS头和NT头呢?DOS头的签名是 0x5A4D(小端序),即 “MZ” 字符串;NT头的签名是 0x00004550(小端序),即 “PE00”,可以用010 editor随便看一个pe文件



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

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_towlowerMyCompareStringWMyCompareStringAExtractDllName

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

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` 函数

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

ASCII字符串比较函数 MyCompareStringA

提取 DLL 名称的函数ExtractDllName

GetApiAddressByName的实现如下

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

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



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





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

3.3 加载 PE 文件节到内存

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

补充 SizeOfHeaders:这是 PE 文件头(IMAGE_OPTIONAL_HEADER 结构)中的一个字段,表示 所有头部结构的总大小SizeOfImage:这是 IMAGE_OPTIONAL_HEADER 中的另一个字段,表示 整个 PE 映像(Image)加载到内存后的总大小

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

1 PointerToRawData:节区数据在 磁盘文件 中的偏移量

2 SizeOfRawData:节区数据在 磁盘文件 中占用的实际大小

3 VirtualAddress:节区加载到内存后的 相对虚拟地址(RVA)

4 VirtualSize:节区在 内存 中占用的实际大小

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

3.4 修复重定位表

大致步骤

1 计算基址偏移量

2定位重定位表

3遍历重定位块

4处理重定位条目

5地址修正逻辑

(一)重定位表

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

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

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

(二)重定位块

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

(三)重定位条目(TypeOffset)

TypeOffset的每个元素都是一个自定义类型结构,每个重定位条目为 16位(WORD),其组成如下:

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

数据结构定义

若条目值为0x3012(十六进制),则:

Type = 0x3 → IMAGE_REL_BASED_HIGHLOW

Offset = 0x012 → 偏移18字节

TypeOffset的元素个数 = (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循环遍历也是可以的,感兴趣的读取可以自己去实现。

(四)地址修正

简单举一个例子

公式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,使其指向实际加载后的正确位置。



完整代码如下

3.5 修复导入表

大致思路

1 获取导入表中的每一个导入描述符(PIMAGE_IMPORT_DESCRIPTOR),导入描述符中存放着需要导入的DLL的名称的RVA。

2 使用在 获取所需要的Windows API 获取的LoadLibraryA的函数指针加载相应的DLL

3 IMAGE_IMPORT_DESCRIPTOROriginalFirstThunk 作为导入名称表(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

1将解析得到的函数地址写入IAT对应位置

2程序执行时通过IAT中的地址调用API

完整代码

3.6 获取dllmain的地址,执行dllmain

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

四、完整代码

4.1 Inject

4.2 ReflectiveLoader

测试



五、尾语

在末尾在说几句话吧

1 Inject中你是可以先扩展到节区表,然后找到 ReflectiveLoader 的地址,再创建线程去调用它,与上文提到的思路大差不差,这样做的好处就是不用再从RVA转到文件偏移了

2 CreateRemoteThread 是可以向被创建的线程传递一个参数的,如果不想在 ReflectiveLoader 中实现暴力搜索DLL的基址的话,可以向 ReflectiveLoader 传递DLL的基址

3 如果想实现类似CobaltStrike的有阶段beacon,你可以参考 oldboy21/RflDllOb: 反射式 DLL 注入制作 Bella 这个项目去修改 InjectReflectiveLoader,做到类型下图所示的功能



参考资料

1 最知名的Reflective DLL Injection项目,也算是技术起源了: GitHub - stephenfewer/ReflectiveDLLInjection:反射式DLL注入是一种库注入技术,其中采用反射式编程的概念来执行将库从内存加载到主机进程中。

2oldboy21/RflDllOb: 反射式 DLL 注入制作 Bella (github.com)

3GitHub - rapid7/ReflectiveDLLInjection at 6bad4c49327ad3b7d9cce6e280d034b76dbec928

4深入理解反射式dll注入技术 - FreeBuf网络安全行业门户

5反射DLL注入原理解析 - 先知社区 (aliyun.com)

6Windows Shellcode 注入姿势 | MYZXCG

7圣诞节我想要的只是反光 DLL 注射 :: Vincenzo — 博客

8GitHub - dismantl/ImprovedReflectiveDLLInjection: An improvement of the original reflective DLL injection technique by Stephen Fewer of Harmony Security

1 条评论
某人
表情
可输入 255