技术社区
安全培训
技术社群
积分商城
先知平台
漏洞库
历史记录
清空历史记录
相关的动态
相关的文章
相关的用户
相关的圈子
相关的话题
注册
登录
自举的代码幽灵——反射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_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` 函数
不区分大小写的宽字符串比较函数
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
的四个字段,一个是
SizeOfRawData
、
PointerToRawData
、
VirtualAddress
、
VirtualSize
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_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
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
这个项目去修改
Inject
和
ReflectiveLoader
,做到类型下图所示的功能
参考资料
1
最知名的Reflective DLL Injection项目,也算是技术起源了:
GitHub - stephenfewer/ReflectiveDLLInjection:反射式DLL注入是一种库注入技术,其中采用反射式编程的概念来执行将库从内存加载到主机进程中。
2
oldboy21/RflDllOb: 反射式 DLL 注入制作 Bella (github.com)
3
GitHub - rapid7/ReflectiveDLLInjection at 6bad4c49327ad3b7d9cce6e280d034b76dbec928
4
深入理解反射式dll注入技术 - FreeBuf网络安全行业门户
5
反射DLL注入原理解析 - 先知社区 (aliyun.com)
6
Windows Shellcode 注入姿势 | MYZXCG
7
圣诞节我想要的只是反光 DLL 注射 :: Vincenzo — 博客
8
GitHub - dismantl/ImprovedReflectiveDLLInjection: An improvement of the original reflective DLL injection technique by Stephen Fewer of Harmony Security
2
人收藏
2
人喜欢
转载
分享
1
条评论
某人
表情
可输入
255
字
评论
发布投稿
热门文章
1
从零掌握java内存马大全(基于LearnJavaMemshellFromZero复现重组)
2
突破网络限制,Merlin Agent助你轻松搭建跳板网络!
3
从白帽角度浅谈SRC业务威胁情报挖掘与实战
4
基于规则的流量加解密工具-CloudX
5
从0到1大模型MCP自动化漏洞挖掘实践
近期热点
一周
月份
季度
1
从零掌握java内存马大全(基于LearnJavaMemshellFromZero复现重组)
2
突破网络限制,Merlin Agent助你轻松搭建跳板网络!
3
从白帽角度浅谈SRC业务威胁情报挖掘与实战
4
基于规则的流量加解密工具-CloudX
5
从0到1大模型MCP自动化漏洞挖掘实践
暂无相关信息
暂无相关信息
优秀作者
1
T0daySeeker
贡献值:38700
2
一天
贡献值:24800
3
Yale
贡献值:18000
4
1674701160110592
贡献值:18000
5
1174735059082055
贡献值:16000
6
Loora1N
贡献值:13000
7
bkbqwq
贡献值:12800
8
手术刀
贡献值:11000
9
lufei
贡献值:11000
10
xsran
贡献值:10600
目录
一、前言
二、Inject的原理和代码实现
三、ReflectiveLoader的原理和代码实现
3.1 暴力搜索DLL的基址
3.2 获取所需要的Windows API
3.3 加载 PE 文件节到内存
3.4 修复重定位表
(一)重定位表
(二)重定位块
(三)重定位条目(TypeOffset)
(四)地址修正
3.5 修复导入表
3.6 获取dllmain的地址,执行dllmain
四、完整代码
4.1 Inject
4.2 ReflectiveLoader
五、尾语
参考资料
转载
标题
作者:
你好
http://www.a.com/asdsabdas
文章
转载
自
复制到剪贴板