关于反射dll注入的一些概念和内容可以在我的文章里找到
dll的加载
loadlibrary
我们知道loadlibrary在kernelbase.dll中有很多个,loadlibraryA、loadlibraryW、loadlibraryExA、loadlibraryExW,实际上最终调用的都是loadlibraryExW,loadlibraryA是用于当传入的字符串为ascii编码时将字符串转换成unicode编码再传给loadlibraryW的,loadlibraryW再调用loadlibraryExW进行dll加载。
贴一段reactos的loadlibraryExW源码,在我的Win11下使用ida观察到的loadlibraryExW的逻辑和reactos中的基本一致,直接看带注释的reactos源码更方便理解,该函数检查dwflag的值进行一些分支行为,初始化unicode_string结构体,搜索dll路径,加载dll,最终返回dll句柄,在实际的windows中会多出一些安全检查。
LoadLibraryExW(LPCWSTR lpLibFileName,
HANDLE hFile,
DWORD dwFlags)
{
UNICODE_STRING DllName;
HINSTANCE hInst;
NTSTATUS Status;
PWSTR SearchPath;
ULONG DllCharacteristics = 0;
BOOL FreeString = FALSE;
/* Check for any flags LdrLoadDll might be interested in */
if (dwFlags & DONT_RESOLVE_DLL_REFERENCES)
{
/* Tell LDR to treat it as an EXE */
DllCharacteristics = IMAGE_FILE_EXECUTABLE_IMAGE;
}
/* Build up a unicode dll name from null-terminated string */
RtlInitUnicodeString(&DllName, (LPWSTR)lpLibFileName);
/* Lazy-initialize BasepExeLdrEntry */
if (!BasepExeLdrEntry)
LdrEnumerateLoadedModules(0, BasepLocateExeLdrEntry, NtCurrentPeb()->ImageBaseAddress);
/* Check if that module is our exe*/
if (BasepExeLdrEntry && !(dwFlags & LOAD_LIBRARY_AS_DATAFILE) &&
DllName.Length == BasepExeLdrEntry->FullDllName.Length)
{
/* Lengths match and it's not a datafile, so perform name comparison */
if (RtlEqualUnicodeString(&DllName, &BasepExeLdrEntry->FullDllName, TRUE))
{
/* That's us! */
return BasepExeLdrEntry->DllBase;
}
}
/* Check for trailing spaces and remove them if necessary */
if (DllName.Buffer[DllName.Length/sizeof(WCHAR) - 1] == L' ')
{
RtlCreateUnicodeString(&DllName, (LPWSTR)lpLibFileName);
while (DllName.Length > sizeof(WCHAR) &&
DllName.Buffer[DllName.Length/sizeof(WCHAR) - 1] == L' ')
{
DllName.Length -= sizeof(WCHAR);
}
DllName.Buffer[DllName.Length/sizeof(WCHAR)] = UNICODE_NULL;
FreeString = TRUE;
}
/* Compute the load path */
SearchPath = BaseComputeProcessDllPath((dwFlags & LOAD_WITH_ALTERED_SEARCH_PATH) ?
DllName.Buffer : NULL,
NULL);
if (!SearchPath)
{
/* Getting DLL path failed, so set last error, free mem and return */
BaseSetLastNTError(STATUS_NO_MEMORY);
if (FreeString) RtlFreeUnicodeString(&DllName);
return NULL;
}
_SEH2_TRY
{
if (dwFlags & LOAD_LIBRARY_AS_DATAFILE)
{
/* If the image is loaded as a datafile, try to get its handle */
Status = LdrGetDllHandleEx(0, SearchPath, NULL, &DllName, (PVOID*)&hInst);
if (!NT_SUCCESS(Status))
{
/* It's not loaded yet - so load it up */
Status = BasepLoadLibraryAsDatafile(SearchPath, DllName.Buffer, &hInst);
}
_SEH2_YIELD(goto done;)
}
/* Call the API Properly */
Status = LdrLoadDll(SearchPath,
&DllCharacteristics,
&DllName,
(PVOID*)&hInst);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
Status = _SEH2_GetExceptionCode();
} _SEH2_END;
done:
/* Free SearchPath buffer */
RtlFreeHeap(RtlGetProcessHeap(), 0, SearchPath);
/* Free DllName string if it was dynamically allocated */
if (FreeString) RtlFreeUnicodeString(&DllName);
/* Set last error in failure case */
if (!NT_SUCCESS(Status))
{
DPRINT1("LoadLibraryExW(%ls) failing with status %lx\n", lpLibFileName, Status);
BaseSetLastNTError(Status);
return NULL;
}
/* Return loaded module handle */
return hInst;
}微软文档中有关dwflag的含义
dll路径搜索顺序
微软官方文档中的描述:
如果启用了安全 DLL 搜索模式,则搜索顺序如下所示:
DLL 重定向。
API 集。
SxS 清单重定向。
Loaded-module 列表。
已知 DLL。
Windows 11 版本 21H2 (10.0;内部版本 22000)及更高版本。 进程的包依赖项关系图。 这是应用程序的包以及应用程序包清单
<Dependencies>部分中指定为<PackageDependency>的任何依赖项。 依赖项按它们在清单中显示的顺序进行搜索。从中加载应用程序的文件夹。
系统文件夹。 使用 GetSystemDirectory 函数检索此文件夹的路径。
16 位系统文件夹。 没有可获取此文件夹的路径的函数,但搜索该函数。
Windows 文件夹。 使用 GetWindowsDirectory 函数获取此文件夹的路径。
当前文件夹。
PATH环境变量中列出的目录。 这不包括由 应用路径 注册表项指定的每个应用程序路径。 计算 DLL 搜索路径时,不使用 应用路径 密钥。
如果 禁用安全 DLL 搜索模式,则搜索顺序相同,不同之处在于 当前文件夹 从序列中的位置 11 移动到位置 8(步骤 7 之后)。应用程序从中加载的文件夹。
ldrloaddll
ldrloaddll和ldrploaddll都是一些标志位和路径检查、初始化之类的措施,关键的操作在LdrpLoadDllInternal中,该函数的主要工作流程为:

ldrpProcessWork的流程如下,根据dll的标志位会进入不同的功能分支,而一般的加载都是进入右边这三类分支,这三类分支均进行一些检查之后去调用LdrpMapDllNtFileName来加载dll。

同样通过一系列检查以及对dll文件的打开还有内存操作之后,流程来到了LdrpMapDllWithSectionHandle,该函数将dll文件加载到内存镜像,然后将通过LdrpInsertDataTableEntry其挂载到InMemoryOrderLinks和InLoadOrderLinks上,至此在ldr的链表当中已经可以看到新的dll。

至此在ldr的链表当中已经可以看到新的dll。
新的问题:windbg与processhacker能在上链之前就发现dll
这里我写了一个简单的使用loadlibraryW加载Dll1.dll的小程序来做测试,在测试过程中我是用sxe ld来对我要加载的dll进行监测,windbg命中中断时的调用栈如下

a2hooks是我的杀软hook直接无视,可以看到这个调用栈还在执行dll文件映射到内存的阶段,但此时windbg已经能够检测到新的dll的加载了,虽然在!dlls中是看不到的,但是.imgscan可以发现目标dll,并且此时processhacker中也是可以看到目标dll的


说明windbg和processhacker都不是依赖遍历ldr来检测进程加载的dll的,那么他依赖的是什么呢?
VAD(Virtual Address Descriptor 虚拟地址描述符)树
Windows 内存管理器使用虚拟地址描述符树 (VAD) 来描述进程分配的内存范围。当进程使用 VirutalAlloc 分配内存时,内存管理器会在 VAD 树中创建一个条目。相应的页目录和页表条目只有在进程尝试引用该内存页面时才会创建,这可以为分配大量内存但访问稀疏的进程节省大量内存。
通过eprocess就可以找到vad树的根节点VadRoot,偏移量要看具体系统中的具体情况。
在windbg中也可以通过!vad直接列出vad树上的所有节点。

vad节点的数据结构为_MMVAD
typedef struct _MMVAD
{
_MMVAD_SHORT Core; // +0x000 VAD的核心结构,包含虚拟地址范围、保护属性、AVL树节点等基础信息
union {
// 匿名联合体,包含一些扩展信息或标志位,具体内容随Windows版本不同
} u2; // +0x040
struct _SUBSECTION* Subsection; // +0x048 指向映射的文件子段,间接关联到ControlArea和文件对象
struct _MMPTE* FirstPrototypePte; // +0x050 第一个原型页表项的指针,代表映射的起始页表项
struct _MMPTE* LastContiguousPte; // +0x058 最后一个连续页表项的指针,表示连续映射的结束页表项
LIST_ENTRY ViewLinks; // +0x060 双向链表,用于管理视图之间的链接
struct _EPROCESS* VadsProcess; // +0x070 该VAD所属的进程对象指针
union {
// 另一个匿名联合体或结构体,包含额外标志或辅助信息,随版本不同
} u4; // +0x078
struct _FILE_OBJECT* FileObject; // +0x080 指向该VAD对应的文件对象,直接获取被映射文件(如DLL)信息
} MMVAD, *PMMVAD;这里重点关注的是包含了controlarea的subsection
typedef struct _SUBSECTION {
struct _CONTROL_AREA* ControlArea; // +0x000 指向对应的 ControlArea 结构,管理整个映射区域的共享信息(文件映射核心)
struct _MMPTE* SubsectionBase; // +0x008 该子段中第一个页表项的起始地址
struct _SUBSECTION* NextSubsection; // +0x010 指向下一个子段,形成链表或树形结构
union {
struct _MI_FILE_EXTENTS* FileExtents; // +0x018 文件扩展信息(描述文件映射范围)
struct _RTL_AVL_TREE GlobalPerSessionHead; // +0x018 会话全局树根(多用途,根据上下文)
struct _MI_PER_SESSION_PROTOS* SessionDriverProtos; // +0x018 另一种会话驱动保护结构指针
};
union {
/* 未命名联合体,包含若干标志和属性 */
} u; // +0x020
unsigned int StartingSector; // +0x024 映射文件开始的扇区号(文件偏移量,扇区为单位)
unsigned int NumberOfFullSectors; // +0x028 文件映射中完整扇区的数量
unsigned int PtesInSubsection; // +0x02c 该子段内的页表项数量
union {
/* 未命名联合体,包含更多页表相关信息 */
} u1; // +0x030
unsigned int UnusedPtes; // +0x034 未使用的页表项数量
unsigned int AlignmentNoAccessPtes; // +0x034(与上字段共用偏移)对齐相关的不可访问页表项数
} SUBSECTION, *PSUBSECTION;到了controlarea这里就可以读取到包含file_object的filepointer了,其类型是_EX_FAST_REF。
typedef struct _CONTROL_AREA {
struct _SEGMENT* Segment; // +0x000 指向管理该映射区域的内存段结构
LIST_ENTRY ListHead; // +0x008 用于链接 ControlArea 结构的双向链表
unsigned __int64 NumberOfSectionReferences; // +0x018 当前 ControlArea 的引用计数(section 对象的引用数)
unsigned __int64 NumberOfPfnReferences; // +0x020 物理页框引用数,管理页面的映射计数
unsigned __int64 NumberOfMappedViews; // +0x028 由该 ControlArea 映射的视图数量
unsigned __int64 NumberOfUserReferences; // +0x030 用户模式对该 ControlArea 的引用数
union { // +0x038 未命名联合体,包含状态标志等(版本相关)
// 省略详细字段
} u;
union { // +0x03c 另一个未命名联合体,包含额外状态或标志
// 省略详细字段
} u1;
_EX_FAST_REF FilePointer; // +0x040 指向关联的 FILE_OBJECT(文件对象),是一个带引用计数的快速引用结构
int ControlAreaLock; // +0x048 控制区锁,保护 ControlArea 结构体的并发访问
unsigned int ModifiedWriteCount; // +0x04c 修改写计数,表示有多少未写回的页
struct _MI_CONTROL_AREA_WAIT_BLOCK* WaitList; // +0x050 等待该 ControlArea 操作完成的等待队列
union { // +0x058 未命名联合体,版本相关或扩展标志
// 省略详细字段
} u2;
unsigned __int64 LockedPages; // +0x068 被锁定的页面数量
_EX_PUSH_LOCK FileObjectLock; // +0x070 保护文件对象的推锁(轻量级锁)
} CONTROL_AREA, *PCONTROL_AREA;
_EX_FAST_REF是一个用来同时存储指针和少量标志/引用计数的特殊结构,他的低4位保存引用计数,实际指针需要把低4位清零才能得到准确地址,清除低4位后才能得到正确的file_object地址。
typedef struct _EX_FAST_REF {
union {
void* Object; // +0x000 实际指针地址,低4位可能用作引用计数等标志
struct {
unsigned __int64 RefCnt : 4; // +0x000 低4位,表示引用计数(4 bit)
unsigned __int64 Pointer : 60; // 剩余60位,表示指针的高位部分(地址,通常16字节对齐)
};
unsigned __int64 Value; // +0x000 整个64位值,包括指针和引用计数
};
} EX_FAST_REF, *PEX_FAST_REF;通过windbg双端内核调试,先使用!gflag +ksl开启记录内核栈调用信息,然后使用sxe ld来对目标dll的加载做中断,观察调用栈
[0x0] nt!DebugService2+0x5 0xffffd001ca262628 0xfffff8021434db86
[0x1] nt!DbgLoadUserImageSymbols+0x2a 0xffffd001ca262630 0xfffff802147aeb73
[0x2] nt!MiLoadUserSymbols+0x7f 0xffffd001ca262680 0xfffff802146ccb37
[0x3] nt!MiMapViewOfImageSection+0x9c7 0xffffd001ca2626e0 0xfffff80214619c2d
[0x4] nt!MiMapViewOfSection+0x33d 0xffffd001ca262830 0xfffff8021461f7e1
[0x5] nt!NtMapViewOfSection+0x2e1 0xffffd001ca2629a0 0xfffff8021436a263
[0x6] nt!KiSystemServiceCopyEnd+0x13 0xffffd001ca262a90 0x7fff84f337ca
[0x7] ntdll!NtMapViewOfSection+0xa 0x80b517f658 0x7fff84ee070e
[0x8] ntdll!LdrpMapViewOfSection+0xbe 0x80b517f660 0x7fff84ee0255
[0x9] ntdll!LdrpMapImage+0x75 0x80b517f700 0x7fff84ee0125
[0xa] ntdll!LdrpMapDllWithSectionHandle+0x2d 0x80b517f7a0 0x7fff84edefd8
[0xb] ntdll!LdrpMapDllNtFileName+0x130 0x80b517f7e0 0x7fff84ee263c
[0xc] ntdll!LdrpMapDllSearchPath+0x1c8 0x80b517f8b0 0x7fff84ed8e5c
[0xd] ntdll!LdrpProcessWork+0x70 0x80b517fa90 0x7fff84ec0911
[0xe] ntdll!LdrpLoadDllInternal+0x14d 0x80b517fae0 0x7fff84ec05ca
[0xf] ntdll!LdrpLoadDll+0xf2 0x80b517fb60 0x7fff84ebaf86
[0x10] ntdll!LdrLoadDll+0x96 0x80b517fd00 0x7fff8217ef1b
[0x11] KERNELBASE!LoadLibraryExW+0x17b 0x80b517fe00 0x7ff6282e11e3
[0x12] Project1!main+0x33 0x80b517fe70 0x7fff00000000
[0x13] 0x7fff00000000 0x80b517fe78 0x80b529b780
[0x14] 0x80b529b780 0x80b517fe80 0x7ff627c34000
[0x15] 0x7ff627c34000 0x80b517fe88 0x0 顺便贴一张NtMapviewOfSection的流程图

!vad可以看到在vad树上已经有目标dll的节点了

找到Subsection.ControlArea.FilePointer的值,去掉低4位用_FILE_OBJECT解析,在FileName就可以看到文件名了

这里我们把filename给修改掉,改成dll2.dll,这个时候lm就看不到这个模块了,说明windbg是通过遍历vad树中节点的fileObject来获得模块信息的

为什么反射注入dll在进程模块里面看不到
通过传统远程进程注入的dll在进程的模块中是可以看到该dll的信息的
而通过反射注入的dll在进程模块中是没有该模块的信息的,但是在内存中可以找到该dll


正常loadlibrary流程当中会通过LdrLoadDll将加载的dll注册到peb的ldr中,同时这个过程的文件内存镜像映射是通过NtMapViewOfSection来完成的,所以通过工具才能查看到进程加载的模块,而反射dll注入过程中并没有使用loadlibrary来加载反射dll本身,通过手动逐字节进行内存镜像映射,只是使用了loadlibrary来加载反射dll所需要的依赖dll。
反射注入的reflectiveLoader实现步骤:
定位去掉reflectiveLoader的真实dll的基址
读取进程的peb,遍历ldr来找到所需的dll和函数va
利用virtualalloc申请一段新的虚拟内存,手动逐字节将每个段映射过去
读取导入表,通过loadlibrary把所需dll加载,然后根据函数序号或者名称把所需的函数地址从导入的dll找到,写入iat表
检查重定向表,进行重定向操作
查找dll入口,调用入口函数
这样一来ldr上和vad中都没有和dll相关的信息,所以反射注入dll通过processhacker或者是windbg都在进程模块中看不到,但是windbg的imgscan还是可以扫出来,因为最后阶段的dll没做混淆,他的头部还是符合pe文件的。