这是我写完代码后写的总结。重新梳理一下反射注入到底想干什么、以及怎么干。以及从中学到了什么。
首先是学习反射注入的收获,当然也可以作为学习的目标,同时也是反射注入实际做的东西。
我的理解是反射注入实际上就是手工加载模块。通过LoadLibrary加载dll会在peb中留下记录,通过手工实现加载过程,我们的dll能像正常dll那样工作,且不再peb中留下痕迹。
也就是说,通过学习反射注入,可以了解到windows系统加载一个pe文件的流程。其中涉及到了部分的peb以及大量的pe结构。
我们主要需要peb中的ldr结构,这个结构中保存了该进程已经加载了的dll。
既然我们的主要工作是手工加载pe文件,自然要对pe文件格式有一定了解。但笔记中不会多提pe文件结构,实际上只要大概了解pe文件格是是个什么,然后在写代码时多去看pe结构的定义,就可以对pe文件结构有一个更深的理解。
在学习过程中参考了许多资料,最主要的就是msf的反射注入payload的源码。其他如有不懂通过百度也可以找到详细的解释。相关文章比较多,dddd,就不一一列举了。
这篇东西由我阅读源码,查资料时做的笔记发展而来。国内虽然少但也有一些优秀的反射注入的文章,看雪中也有类似文章,但阅读门槛稍微有点高。因为是由笔记发展而来,这更像是一个零基础初学者的学习笔记(实际上在开始学反射注入之前,只知道pe文件格是是什么东西,几乎完全不了解。对windows的机制也完全不了解),希望能帮助到初学者,这项技术对我学习windows有很大帮助,虽然我只是一个初学者,但这项实践使我之后对书本、资料上的内容有了更深的了解。可能会有错误,希望发现错误的dalao可以帮帮我这个初学者纠正。
va、fa、rva这几个概念搞清楚好像就行。实际上就是对pe文件如何从文件映射到内存有一个大体的认识。我们的工作就是具体完成这个过程。可以参考下图。
pe文件的格式网上有比较多的图片,这里就不贴了。
首先打开dll文件,获取长度,并在堆中分配空间读取文件。
然后找到dll中ReflectiveLoader的入口点。
最后以RW申请空间,写入dll后改成RX,然后以ReflectiveLoader作为线程函数创建远程线程。
LoadLibraryR中有个函数Rva2Offset用于获取rva对应的fa。
原理是遍历区块获取区块的section_rva和section_fa,然后比较rva和section_rva找到rva所在的section,最后计算出fa。再用fa+baseAddr得到内存中的位置。
通过nt头,计算出sections的fa,以及通过nt头的optionalheader获取输出表的rva。
然后遍历section找到输出表的fa,接着遍历输出表的函数名字rva表,计算出rva对应fa得到导出函数名字,与需要的导出函数做对比,确定要找的函数在函数名表中的下标。用此下标在序号表中找到序号,最后再用序号去地址表找到地址。
首先获取代码的位置,然后再往前找dos头。
_ReturnAddress()返回当前调用函数的返回地址。所以在loader中调用一个函数,该函数再调用_ReturnAddress(),返回调用函数的返回地址,即loader中调用函数的下一条语句的地址。
其中 __declspec(noinline) 用于防止编译器优化该函数成内联函数,否则返回的就是loader的返回地址。
使用_ReturnAddress需要intrin.h,并使用#pragma intrinsic防止内联优化。
根据pe格式可知,dos头(IMAGE_DOS_HEADER)中有有一个e_magic标志,值是0x5A4D(MZ)。
所以向前遍历内存,直到找到MZ标志,再检查pe头的PE标志,这样就找到dos头了。
需要注意的是,检查PE标志时要检查pe头偏移是否正确,防止错误的内存访问。
这里也可以取巧,远程线程是可以传递一个参数的,对于我们这个简单的dll,imagebase实际上就是分配空间的首地址,可以作为参数传入。
接下来的步骤中需要用到一些ntdll.dll,kernel32.dll中的导出函数,所以需要先找到这些函数的va。这些系统模块都是已经加载了的,可以在peb中找到其加载的位置。
这里利用hash避免直接比较字符串。
我们需要LoadLibraryA、GetProcAddress加载导入表中的dll的对应的函数。
需要VirtualAlloc分配内存给我们把pe文件加载到其中。
需要NtFlushInstructionCache刷新指令缓存。
InMemoryOrderModuleList对应的链表是一个环形双向链表,且有一个头节点(或者说哨兵节点)。InMemoryOrderModuleList的Flink指向链表的第一个节点,Blink指向链表最后一个节点。头节点的Flink是第一个节点,可以以此为跳出条件遍历该链表。
这里借用一张网图。
首先从peb中找到ldr,然后遍历InMemoryOrderModuleList,通过hash(BaseName)找到kernel32.dll和ntdll.dll对应的LDR_DATA_TABLE_ENTRY结构。
找到dll对应的LDR_DATA_TABLE_ENTRY后,获取其imagebase,然后解析pe头,计算出导出表位置。同样利用hash比较字符串找到所需的导出函数,并计算出va。
新分配大小等于sizeOfImga的内存作为映像加载的空间,然后把pe头复制到新内存里,这里我只更新了新nt头的imagebase地址。太简单就不贴代码了。
遍历section_header获取fa和rva,计算出section在旧内存中的va和新内存中的va。然后复制section到新内存中的对应位置。
找到导入表,然后遍历导入表,依次加载对应的dll,及需要的dll的导出函数,并填写对应iat。
和导出表不同,导入表是一个结构体数组。它不提供结构体数量,最后一个结构体仅作为结束标志,不包含导入信息,其成员Characteristics为0,这可以作为遍历的退出条件。
对于每个导入表,在文件中时OriginalFirstThunk和FirstThunk都是RVA,指向同一个IMAGE_THUNK_DATA结构体数组。
当加载到内存时,FirstThunk改为函数的VA,即iat。
文件中时,OriginalFirstThunk和FirstThunk指向的结构体数组中,每一个IMAGE_THUNK_DATA的成员u1都被解释为Ordinal,若该函数应该通过序号导入,则Ordinal的最高位会被置为1。
见实现代码注释。
完成重定位过程。
重定位表是一个结构体数组,DataDirectory中的重定位表项保存着第一个重定位表的rva,遍历每一个重定位表,并遍历重定位表中的表项,根据其重定位类型,执行重定位操作。
其中每一个重定位表保存着一个rva,重定位实际上就是遍历IMAGE_BASE_RELOCATION的成员reloc,然后执行*(rva+baseAddr+reloc[i].offset) += baseAddr - ImageBase
。
两层循环,遍历重定位表,再遍历每个表的 RELOC reloc[]。然后根据重定位类型进行重定位。
跳转到dll的ep。实际上就是执行dll原本的_DllMainCRTStartup函数。该函数会完成一些初始化工作并转到dllMain,让我们的dllMain像正常dllmain那样运行,但又不在peb中留下dll加载的痕迹。
最后返回entrypoint。
https://github.com/rapid7/ReflectiveDLLInjection
(前置条件序号) 序号 流程内容
=
=
=
=
=
=
=
=
=
=
=
=
注入器:
=
=
=
=
=
=
=
=
=
=
=
=
(
0
)
1
打开dll文件(CreateFile),获取dll长度(GetFileSize)
(
1
)
2
分配内存(HealAlloc),读取文件(ReadFile)
(
0
)
3
打开目标进程(OpenProcess)
(
2
,
3
)
4
调用反射注入函数(LoadLibraryR.c>LoadRemoteLibraryR)
(
2
)
5
获取反射加载函数的文件偏移(LoadLibraryR.c>GetReflectiveLoaderOffset)
(
2
,
3
)
6
在目标进程中分配空间(VirtualAllocEx),写入dll(WriteProcessMemory)
(
6
)
7
修改目标进程中的空间为可执行(VirtualProtectEx)
(
5
,
7
)
8
创建远程线程,执行反射加载函数(CreateRemoteThread)
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
反射加载函数(运行在被注入进程的新建线程中):
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
1
获取基地址
2
获取需要的kernel32.dll及ntdll.dll的函数的va
3
分配空间作为映像空间,并复制pe头到新的位置
4
复制所有段到映像的对应位置
5
处理导入表,填写iat
6
重定位
7
跳转到ep(_DllMainCRTStartup)
8
返回entry point地址
(前置条件序号) 序号 流程内容
=
=
=
=
=
=
=
=
=
=
=
=
注入器:
=
=
=
=
=
=
=
=
=
=
=
=
(
0
)
1
打开dll文件(CreateFile),获取dll长度(GetFileSize)
(
1
)
2
分配内存(HealAlloc),读取文件(ReadFile)
(
0
)
3
打开目标进程(OpenProcess)
(
2
,
3
)
4
调用反射注入函数(LoadLibraryR.c>LoadRemoteLibraryR)
(
2
)
5
获取反射加载函数的文件偏移(LoadLibraryR.c>GetReflectiveLoaderOffset)
(
2
,
3
)
6
在目标进程中分配空间(VirtualAllocEx),写入dll(WriteProcessMemory)
(
6
)
7
修改目标进程中的空间为可执行(VirtualProtectEx)
(
5
,
7
)
8
创建远程线程,执行反射加载函数(CreateRemoteThread)
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
反射加载函数(运行在被注入进程的新建线程中):
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
=
1
获取基地址
2
获取需要的kernel32.dll及ntdll.dll的函数的va
3
分配空间作为映像空间,并复制pe头到新的位置
4
复制所有段到映像的对应位置
5
处理导入表,填写iat
6
重定位
7
跳转到ep(_DllMainCRTStartup)
8
返回entry point地址
hFile
=
CreateFileW(dllPathname, GENERIC_READ,
0
,
0
, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
0
);
dwDllLen
=
GetFileSize(hFile,
0
);
lpDll
=
HeapAlloc(GetProcessHeap(),
0
, dwDllLen);
hFile
=
CreateFileW(dllPathname, GENERIC_READ,
0
,
0
, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
0
);
dwDllLen
=
GetFileSize(hFile,
0
);
lpDll
=
HeapAlloc(GetProcessHeap(),
0
, dwDllLen);
DWORD Rva2Fa(DWORD rva, PIMAGE_SECTION_HEADER sections,
int
sectionNum)
{
for
(
int
i
=
0
; i < sectionNum; i
+
+
) {
int
sectionVa
=
sections[i].VirtualAddress;
if
((rva >
=
sectionVa) && ((sectionVa
+
sections[i].SizeOfRawData) > rva))
return
rva
-
(sectionVa
-
sections[i].PointerToRawData);
}
return
0
;
}
DWORD Rva2Fa(DWORD rva, PIMAGE_SECTION_HEADER sections,
int
sectionNum)
{
for
(
int
i
=
0
; i < sectionNum; i
+
+
) {
int
sectionVa
=
sections[i].VirtualAddress;
if
((rva >
=
sectionVa) && ((sectionVa
+
sections[i].SizeOfRawData) > rva))
return
rva
-
(sectionVa
-
sections[i].PointerToRawData);
}
return
0
;
}
__declspec(noinline) PVOID NextAddr()
{
return
(PVOID)_ReturnAddress();
}
__declspec(noinline) PVOID NextAddr()
{
return
(PVOID)_ReturnAddress();
}
while
(TRUE) {
if
(dosHeadAddr
-
>e_magic
=
=
0x5A4D
) {
LONG
e_lfanew
=
dosHeadAddr
-
>e_lfanew;
if
(e_lfanew >
=
sizeof(IMAGE_DOS_HEADER) && e_lfanew <
1024
) {
ntHeadAddr
=
(PIMAGE_NT_HEADERS)((PVOID)dosHeadAddr
+
(PVOID)e_lfanew);
if
(ntHeadAddr
-
>Signature
=
=
0x4550
)
break
;
}
}
dosHeadAddr
-
-
;
}
while
(TRUE) {
if
(dosHeadAddr
-
>e_magic
=
=
0x5A4D
) {
LONG
e_lfanew
=
dosHeadAddr
-
>e_lfanew;
if
(e_lfanew >
=
sizeof(IMAGE_DOS_HEADER) && e_lfanew <
1024
) {
ntHeadAddr
=
(PIMAGE_NT_HEADERS)((PVOID)dosHeadAddr
+
(PVOID)e_lfanew);
if
(ntHeadAddr
-
>Signature
=
=
0x4550
)
break
;
}
}
dosHeadAddr
-
-
;
}
/
/
找pLdrDataTableEnrty
DWORD pPeb
=
__readfsdword(
0x30
);
DWORD pLdr
=
*
(DWORD
*
)(pPeb
+
0xc
);
DWORD pInMemoryOrderModuleList
=
pLdr
+
0x14
;
/
/
第一个节点的二级指针
DWORD pLdrDataTableEnrty
=
*
(DWORD
*
)(pInMemoryOrderModuleList
+
0
);
/
/
遍历LdrDataTableEnrty
do{
WCHAR
*
name
=
(WCHAR
*
)
*
(DWORD
*
)(pLdrDataTableEnrty
+
0x24
+
0x4
);
hash
=
YourHashFun(name);
/
/
使用你自己的函数计算
hash
值
if
(
hash
=
=
DLLHASH) {
/
/
DLLHASH由你自己的函数计算得出
DWORD baseAddr
=
*
(DWORD
*
)(pLdrDataTableEnrty
+
0x10
);
/
/
解析pe头过程省略
for
(
int
i
=
0
; i < funcNum; i
+
+
) {
/
/
funcNum是导出函数的个数
char
*
name
=
(char
*
)(baseAddr
+
((DWORD
*
)nameRvas)[i]);
DWORD
hash
=
YourHashFun(name);
if
(
hash
=
=
FUNCHASH) {
pFunc
=
(FUNC)(baseAddr
+
((DWORD
*
)funcRvas)[((WORD
*
)ordRvas)[i]]);
}
}
}
}
while
(
*
(DWORD
*
)(pLdrDataTableEnrty) !
=
*
(DWORD
*
)(pInMemoryOrderModuleList))
/
/
找pLdrDataTableEnrty
DWORD pPeb
=
__readfsdword(
0x30
);
DWORD pLdr
=
*
(DWORD
*
)(pPeb
+
0xc
);
DWORD pInMemoryOrderModuleList
=
pLdr
+
0x14
;
/
/
第一个节点的二级指针
DWORD pLdrDataTableEnrty
=
*
(DWORD
*
)(pInMemoryOrderModuleList
+
0
);
/
/
遍历LdrDataTableEnrty
do{
WCHAR
*
name
=
(WCHAR
*
)
*
(DWORD
*
)(pLdrDataTableEnrty
+
0x24
+
0x4
);
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!