Neutering the EDR
EDR (Endpoint Detection and Response) products attempt to detect misbehavior that slightly deviates from the baseline, by continuously analyzing the memory for inter-process interactions. While a few so-called EDRs are still strongly based on signatures for detection, others opt for behavioral analysis only. For memory-based detections, an EDR will need to inspect process actions and will inject a custom DLL into every process Attackers have the possibility to detect and block this process injection. Based on the same legitimate techniques that sensitive processes use to prevent the injections, the malware running in memory makes the detection engine blind. We’re going to debug the process that we’re using, the child process will be named the same name as the parent process. There are some events that are exposed by the Windows OS that allow us to trigger on certain events – for the purpose of this blog we’ll be looking at OnLoad. We’re going to prevent the DLL from being loaded into the process by patching the DLL as it ‘s loading. We will specify that we will be using the Windows SDK for Windows 10 – that means that this code may not run on previous versions of Windows. When DEBUG is commented out, it the printf will not show in the compiled binary’s main: When DEBUG is commented out, it the printf will not show in the compiled binary’s main:Uncommenting DEBUG will allow the disassembler to see the printf: This is important because an attacker can hide from security products that are using string-based detection. If you’ve performed a penetration test in the last 10 years, you’ve probably renamed mimikatz to mimidogz and zipped right by a poorly engineered security product. The idea is that we’re going to reload the binary itself, but we need to reload it in debug mode.This code will check to detemrine if this is the first or second time this process has ran: So if the argument is smaller than 2, it means that there’s no argument. But if there’s one more than argument, it means that it’s a second instance. MessageBox is a Microsoft function that we’re going to be using for debuggin purposes. After compiling we won’t see MessageBox because we don’t have any arguments: We’re now going to create a process which is itself, we’re going to set this process to DEBUG mode. We’re not going to create process, a great learning resource for Window’s APIs is MSDN located here. The lpProcessAttributes and lpThreadAtteributes are not important, so those are both set to NULL. bInheritHandles will be set to TRUE because we need to inherit the handle from the parent process because we’re going to be debugging them. We will be setting the dwCreationFlags argument to DEBUG_ONLY_THIS_PROCESS. The lpEnvironment and lpCurrentDirectory arguments will be set to NULL. According to the MSDN documentation, the NULL value cannot be passed for the lpStartupInfo or lpProcessInformation arguments. Therefore, lpStartupInfo and lpProcessInformation structures are needed in our code – we’ll create the structures but not use them. The MSDN documentation for STARTUPINFOA states that you need to specify the size of structure so that the process does not crash. However, the PROCESS_INFORMATION structure does not need a size argument. After compiling, our code fails. Microsoft to the rescue! Windows has a built-in API called GetLastError that retrieves the calling thread’s last-error code value. So let’s add that to to our code and recompile: Although our code does not run, we now have an error code – we can investigate the bug. Looks like we’re trying to use code that we cannot access. Let’s add some debugging capability into our code to assess where the bug is. Ok, looking at our code, we never initialize the si function. We need to zero out the memory where that structure resides. We also need to clarify the size of the space on the stack that we need. And now GetLastError returns a ‘1’, which means the process returns. Now we’re ready to create an infinite debug while loop. We initialize DEBUG_EVENT by setting it to zero. WaitforDebug Event takes two arguments: lpDebugEvent and dwMilliseconds. The ‘&’ in front of the event variable is actually a pointer to DEBUG_EVENT. So this loop will wait forever for a debugging event. If there is no debugging event, we continue in the loop. We’re going to use a switch case multiway branch statement that triggers on two of the DEBUG_EVENT API arguments, CREATE_PROCESS_DEBUG_EVENT and LOAD_DLL_DEBUG_EVENT. We use CREATE_PROCESS_DEBUG_EVENT to confirm that the EDR process was actually spawned. To confirm that this code works as expected, we’ll set a debugger state that will be used for the process. We’re going to add a default statement to end our switch case that lets the debugger know that if CREATE_PROCESS_DEBUG_EVENT and LOAD_DLL_DEBUG_EVENT are not hit, the debugger should continue. We’ll now add a handle to the process to inspect each process. We have to do this because the switch case statement needs to have the ability to determine whether or not an actionable event is taking place. We’ll also debug this area to ensure that we have a valid handle. Then we’ll compile and run: And the screenshot below confirms that we did trigger CREATE_PROCESS_DEBUG_EVENT. Let’s add a ContinueDebugEvent to the while loop so that the rest of our code is reached. We need to pass the PID, TID, and status as arguments. Let’s add the same functionality to the LOAD_DLL_DEBUG_EVENT case statement. We’ll add our #ifdef directive that allows for conditional compilation. We do this and compile to make sure our code still works. There are multiple DLLs that load for the MessageBox process. The next step is to map the file based on the handle, this is called file mapping.3 We’re going to map the file according to what we have in the handle. Now that we have a handle to the file mapping, now we need to go through it and retrieve the file map name. There is a Microsoft API