I was initially intending on writing a blog post on bypassing EDR solutions using a method I have not seen before. But after some thought of how I would demonstrate this, I decided on actually creating a basic EDR first. Hopefully this will share some insights on how they work in addition to methods of bypass.
There are a range of methods that Anti Virus and EDR solutions use to detect malicious programs or behaviors.
Signature Detection
As the name implies, signature detection is the process of analyzing the signatures of files for known malware previously seen before. This can be something as simple as comparing a SHA1 hash of a file with a known database of malware. Generally signature detection is implemented at kernel level using file system filters. For example, Microsoft have a specific technology built into Windows for this very purpose. Files are scanned during the opening phase and if the file is deemed malicious, the filter driver will block access to the file from the calling application. Signature detection is usually the first line of defense employed by AV’s and EDR’s.
Sandboxing
If signature detection fails to stop the latest and greatest malware from running, the EDR’s next line of defence is sandboxing. Prior to allowing any executable from running, EDR’s will run the potentially malicious program inside a virtual machine. Not the kind we run our own virtual machines inside, but one designed to analyze program flow and look for known malware through dynamic analysis. Some vendors claim to have special AI engines that analyse program flow etc…, so how these are implemented are somewhat of a black box. I personally believe they are done through control flow graph (CFG) analysis. The advantage of CFG analysis means the re-compilation of binaries or other small changes will still produce the same CFG regardless of the resulting executable being different. Whilst the EDR sandbox is a decent line of defence, it can succumb to various bypasses.
The first is time. The virtual machine will only spend a finite amount of time analyzing executables. So if there is enough complexity in the program being virtualized, eventually the sandbox will give up if it hasn’t found the binary to be malicious in nature in that time.
The second bypass method that is commonly used to thwart the sandbox is breaking up control flow to prevent further analysis. As mentioned earlier, the sandbox is essentially a virtual machine, but there will be certain OS API calls that it cannot really virtualize successfully. When this occurs, the sandbox will not be able to carry on analyzing program flow and generally give up and let the program continue on it’s merry path.
Active Protection
The next stage of protection employed by these solutions is something I call active protection. Different products market this under different names. It’s generally implemented through loading a DLL into each process and implementing API function hooks (more on this later) to analyse and monitor for suspicious behaviors that are not generally found when running benign programs.
A simplified version of active protection is what I will be looking to implement as part 1 of this series. In part 2 I’ll cover bypassing this method using a technique I’ve not seen used before, but with the same ultimate goal. Making the malware API calls invisible to the active protection.
Event Tracing
I’d be remiss if I did not mention event tracing. All the methods above are proactive forms of protection, designed to stop malware from executing. Event tracing is the reactive component to an EDR’s arsenal. When all proactive protections have failed, event tracing is used to analyze a series of events that have occurred and tie these to behaviors found when executing malware. Some of you maybe familiar with sysmon, and essentially EDR vendors implement their own implementation of sysmon to achieve the same goals.
So with the various methods covered, lets dive in.
SylantStrike
Welcome to SylantStrike, the most sophisticated open source Windows EDR solution available on the market…. hmm, well, not quite.
OK, so as mentioned earlier in the article, the area that we will be looking to implement in our super cool EDR solution is the active protection component. Generally this is implemented using function hooks. So what is a function hook? A function hook is essentially a method of changing the behavior of a pre-existing API by redirecting execution of the function to user controlled code.
Take a look at the jsfiddle below. We essentially replace the default window.alert
function and add some custom behavior to it. The concept is no different to this.
It would be awesome if native API’s could be replaced as easy as the fiddle above, but at a native level it’s a little more complex. Hooking native API’s involve patching the first instruction of the API we are looking to intercept with a JMP instruction to our intercepted version of the API call. If you are interested in more technical details regarding native inline function hooks, head over to MalwareTech’s blog, he has a great post describing Inline Hooking.
Luckily for us there are an array of libraries already available that make the hooking processes a breeze. Check out the listing here on GitHub for some of the libraries available.
For SylantStrike I decided to go with MinHook. It supports both x86 and AMD64 architectures and is fairly minimal in terms of source files to include into our uber cool EDR solution.
Active Protection DLL
Now that we have decided on a hooking library, lets take a look at our basic DLL entry point for implementing our active protection DLL. The active protection is implemented as a DLL since it will eventually be loaded into all of the processes we are looking to protect.
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include "minhook/include/MinHook.h"
#include "SylantStrike.h"
DWORD WINAPI InitHooksThread(LPVOID param) {
//MinHook itself requires initialisation, lets do this
//before we hook specific API calls.
if (MH_Initialize() != MH_OK) {
OutputDebugString(TEXT("Failed to initalize MinHook library\n"));
return -1;
}
//Now that we have initialised MinHook, lets prepare to hook NtProtectVirtualMemory from ntdll.dll
MH_STATUS status = MH_CreateHookApi(TEXT("ntdll"), "NtProtectVirtualMemory", NtProtectVirtualMemory,
reinterpret_cast<LPVOID*>(&pOriginalNtProtectVirtualMemory));
//Enable our hooks so they become active
status = MH_EnableHook(MH_ALL_HOOKS);
return status;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH: {
//We are not interested in callbacks when a thread is created
DisableThreadLibraryCalls(hModule);
//We need to create a thread when initialising our hooks since
//DllMain is prone to lockups if executing code inline.
HANDLE hThread = CreateThread(nullptr, 0, InitHooksThread, nullptr, 0, nullptr);
if (hThread != nullptr) {
CloseHandle(hThread);
}
break;
}
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
The DllMain
function above is the entry point to our protection DLL. Whenever the DLL is loaded using the LoadLibrary API, the function is called automatically along with the DLL_PROCESS_ATTACH
reason. The code then creates a separate thread to setup our hooked functions. Ideally we should do this inline and not within a separate thread, but there is very little that can executed inside DllMain
without creating deadlocks, so we delegate to a thread instead.
The responsibility of the InitHooksThread
function is to initialise the MinHook library itself, setting up each individual function we are interested in hooking, then finally enabling the hooks.
One of the many API’s that EDR solutions will hook is the NtProtectVirtualMemory
API. This function enables an application to change the memory protection options for a specific range of memory. Memory can be marked as read only (RO), read write (RW), read execute (RX) or read/write execute (RWX). RWX is what our trusty EDR solution will be looking for. RWX is seldom required for generic application code, but malware will quite often request this mode of protection due to the nature of how malicious code functions. A typical example is AMSI bypass. AMSI bypasses usually involve patching the AmsiScanBuffer
API at runtime to change its behaviour to always return a clean result. This will usually involve a call to NtProtectVirtualMemory
to change the memory protection from RX to RWX for a short period of time whilst the patch is applied. Ergo, an update of a memory range’s protection flags to RWX is what we are going to implement within our EDR.
// SylantStrike.cpp : Hooked API implementations
//
#include "pch.h"
#include "framework.h"
#include "SylantStrike.h"
//Pointer to the trampoline function used to call the original API.
//This will be initialised by MinHook during initialisation.
pNtProtectVirtualMemory pOriginalNtProtectVirtualMemory = nullptr;
DWORD NTAPI NtProtectVirtualMemory(IN HANDLE ProcessHandle, IN OUT PVOID* BaseAddress, IN OUT PULONG NumberOfBytesToProtect, IN ULONG NewAccessProtection, OUT PULONG OldAccessProtection) {
//Check to see if the calling application is requesting RWX
if ((NewAccessProtection & PAGE_EXECUTE_READWRITE) == PAGE_EXECUTE_READWRITE) {
//It was, so notify the user of naughty behaviour and terminate the running program
MessageBox(nullptr, TEXT("You've been a naughty little hax0r, terminating program"), TEXT("Hax0r Detected"), MB_OK);
TerminateProcess(GetCurrentProcess(), 0xdead1337);
//Unreachable code
return 0;
}
//No it wasn't, so just call the original function as normal
return pOriginalNtProtectVirtualMemory(ProcessHandle, BaseAddress, NumberOfBytesToProtect, NewAccessProtection, OldAccessProtection);
}
The hooked version of NtProtectVirtualMemory
checks the NewAccessProtection
flag to determine if the application is requesting the RWX protection level on the memory range. If RWX is requested then we simply alert the user to the suspicious activity and terminate the offending process. If RWX is not requested, the code simply calls the original NtProtectVirtualMemory
API and returns the result.
And that is it, excluding third party code, in little more than 75 lines of code, we have implemented a very basic EDR solution that will terminate any program that tries to change the protection mode of any memory to RWX.
Injector
With the active protection DLL in place, we now need a way to have each process load the DLL on startup. SylantStrikeInject to the rescue, a C# program that will monitor and load our protection DLL into foreign processes. There are various ways EDR’s will do this. Many will chose the PsSetCreateProcessNotifyRoutine
to monitor for process creation, but the drawback is that this API can only be used from within a driver, and since we are implementing a simple EDR, we don’t want to be going down that route. So instead we can use WMI to monitor for new process events.
static void WaitForProcess()
{
try
{
var startWatch = new ManagementEventWatcher(new WqlEventQuery("SELECT * FROM Win32_ProcessStartTrace"));
startWatch.EventArrived += new EventArrivedEventHandler(startWatch_EventArrived);
startWatch.Start();
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"+ Listening for the following processes: {string.Join(" ", processList)}\n");
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(ex);
}
}
When a new process is launched, the startWatch_EventArrived
function will be called with details of the process that has started.
static void startWatch_EventArrived(object sender, EventArrivedEventArgs e)
{
try
{
var proc = GetProcessInfo(e);
if (processList.Contains(proc.ProcessName.ToLower()))
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($" Injecting process {proc.ProcessName}({proc.PID}) with DLL {dllPath}");
BasicInject.Inject(proc.PID, dllPath);
}
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(ex);
}
}
We then check that the process that has just started is on our list of names to inject our protection DLL, afterall, we don’t want to inject everything for our demo EDR. If the process is on our list of processes to inject, the code calls BasicInject.Inject
to handle the loading of the DLL into the foreign executable.
public static int Inject(int pid, string dllName) {
// geting the handle of the process - with required privileges
IntPtr procHandle = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, false, pid);
// searching for the address of LoadLibraryA and storing it in a pointer
IntPtr loadLibraryAddr = GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");
// alocating some memory on the target process - enough to store the name of the dll
// and storing its address in a pointer
IntPtr allocMemAddress = VirtualAllocEx(procHandle, IntPtr.Zero, (uint)((dllName.Length + 1) * Marshal.SizeOf(typeof(char))), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// writing the name of the dll there
UIntPtr bytesWritten;
WriteProcessMemory(procHandle, allocMemAddress, Encoding.ASCII.GetBytes(dllName), (uint)((dllName.Length + 1) * Marshal.SizeOf(typeof(char))), out bytesWritten);
// creating a thread that will call LoadLibraryA with allocMemAddress as argument
CreateRemoteThread(procHandle, IntPtr.Zero, 0, loadLibraryAddr, allocMemAddress, 0, IntPtr.Zero);
return 0;
}
The inject code allocates a block of memory to hold the path to our protection DLL and writes the full path into the remote process’s memory space. The code also determines where the address of the LoadLibraryA
function resides in memory. Once these steps have been performed, a thread is created in the remote process using the address of the LoadLibrary
API call, and the thread parameter is the address of where the active protection DLL path exists in memory. Once the thread is created, the active protection DLL will be loaded by the remote process.
And we are done, we have our active protection DLL and C# loader ready to rock.
Demo
So first things first is to start SylantStrikeInject to monitor for process creation and inject the protection DLL. Process monitoring using WMI relies on administrative permissions, so SylantStrikeInject should be launched from an elevated command prompt. For this demonstration we are only interested in protecting notepad and calc.
.\SylantStrikeInject.exe --process=notepad.exe --process=calc.exe --dll=c:\tools\SylantStrike\SylantStrike.dll
Waiting for process events
+ Listening for the following processes: notepad.exe calc.exe
Injecting process notepad.exe(22808) with DLL c:\tools\SylantStrike\SylantStrike.dll
Launching notepad will trigger the injection of SylantStrike and will protect notepad from any code that attempts to adjust memory protection to RWX.
To confirm our protection is active and working I’m going to use the awesome Donut tool from @TheRealWover. Donut can convert multiple types of EXE/DLL’s into position independant shellcode that can be injected into foreign processes.
C:\Tools\donut\donut.exe -a 2 DemoCreateProcess.dll -c TestClass -m RunProcess -p "calc.exe"
[ Donut shellcode generator v0.9.3
[ Copyright (c) 2019 TheWover, Odzhan
[ Instance type : Embedded
[ Module file : "DemoCreateProcess.dll"
[ Entropy : Random names + Encryption
[ File type : .NET DLL
[ Class : TestClass
[ Method : RunProcess
[ Parameters : calc.exe
[ Target CPU : amd64
[ AMSI/WDLP : continue
[ Shellcode : "loader.bin"
The command above uses Donut’s DemoCreateProcess library which will attempt to launch a process. This is then converted into shellcode and stored inside a file called loader.bin
Our next step is to load the generated shellcode into our notepad process that is actively protected by SylantStrike. There are loads of methods available to inject shellcode into a foreign process but for this demonstration I’m going to use CobaltStrike.
Using the shinject
beacon command, we choose our notepad PID and path to the donut generated shellcode.
Once the shellcode is loaded into notepad and begins executing, the malicious little blighter is stopped in its tracks by SylantStrike’s protection.
If you are interested in having a go yourself, head over to the SylantStrike repo on GitHub.
Conclusion
In this example here I am only demonstrating one particular function hook and suspicious behavior. In reality there are numerous API hooks and suspicious behaviours a fully fledged EDR will look out for.
In part 2 I will cover a few methods on how our new EDR solution can be bypassed, so until then, thanks for reading.
Acknowledgements
- Tsuda Kageyu‘s MinHook Library
- @TheRealWovers’s Donut project
- Injector code used from Dan Sporici – C# Inject a Dll into a Process
- WMI Process monitor based on Tim MalcomVetter’s WMIProcessWatcher