How to Ruin Your Weekend: Building a DIY EDR
作者通过开发名为“RottenTomato”的端点检测与响应(EDR)工具,展示了从简单Windows驱动到功能齐全安全工具的构建过程。该工具利用内核回调机制监控系统事件,并结合静态分析和远程注入功能实现对可疑进程的检测与拦截。 2025-9-5 05:55:26 Author: infosecwriteups.com(查看原文) 阅读量:6 收藏

Itz.sanskarr

Press enter or click to view image in full size

Photo by Henry Hustava on Unsplash

We’ve all been there. A simple question pops into your head. “I wonder how that works?” Before you know it, your browser has 50 tabs open, your coffee is cold, and your entire weekend has vanished into a coding rabbit hole. That was me, trying to figure out how EDRs — Endpoint Detection and Response tools — actually do their magic.

An EDR is like a super-powered security guard for your computer. It doesn’t just check a list of known troublemakers at the door; it watches what every program does, looking for suspicious behavior. But the internet, while full of information, was short on simple, step-by-step guides showing how these complex beasts are built.

So, I decided to build one myself. This is the story of how I built “RottenTomato,” a journey from a simple Windows driver to a (mostly) working security tool. To truly understand what a program is doing, we can’t just look at it from the outside; we need to get access to the heart of the operating system.

I. Entering the VIP Section: Kernel Space

Think of your computer’s operating system as having two areas. There’s user space, where your applications like Discord, Chrome, and Word live. Each runs in its own little sandbox. If Discord crashes, Word doesn’t care.

Then there’s kernel space. This is the OS’s control room. It’s where the core system operations, hardware drivers, and critical services run. The kernel has access to everything and is the ultimate source of truth for what’s happening on the system. These two spaces are isolated from each other for security and stability.

So, if a security tool wants to monitor for new processes or file modifications, it can’t do it from the user space sandbox. It needs a way to run code inside the kernel space. The most common way to do that? A kernel driver.

II. The Old “Hacky” Way: Messing with the SSDT

To understand how security tools first got this kernel-level view, let’s trace what happens when you do something as simple as opening a file.

  1. Your user-space program (like Notepad) calls a friendly, well-documented function from a Windows library (API), like CreateFileA.
  2. Behind the scenes, CreateFileA calls a more complex, lower-level function in a library called ntdll.dll. This library is the bridge between user space and kernel space.
  3. Finally, to actually get the kernel to do the work, ntdll.dll uses a special instruction called a syscall. This is the formal request to switch from user mode to kernel mode.

But how does the kernel know which function to run? It uses the SSDT (System Service Dispatch Table). Think of the SSDT as the kernel’s private phone directory. The syscall comes with a number, and the CPU looks up that number in the SSDT to find the exact memory address of the kernel function it needs to execute (like the kernel's version of NtCreateFile).

Security vendors realized they could modify this table. They would use a driver to sneak into the kernel and change the address for, say, NtCreateFile to point to their own monitoring function first. Their function would inspect the request, decide if it was safe, and then pass it along to the original kernel function. This was called SSDT hooking.

This was powerful, but it was also dangerous. A buggy security driver could crash the whole system. Worse, malware authors realized they could use the exact same technique (called a rootkit) to hide their own malicious activity!

To stop this, Microsoft introduced Kernel Patch Protection (KPP), better known as PatchGuard. This feature periodically checks critical kernel structures like the SSDT. If it detects any unauthorized modifications, it immediately crashes the system with a Blue Screen of Death (BSOD). Just like that, SSDT hooking was dead.

III. The New “Official” Way: Kernel Callbacks

Security vendors were furious, but Microsoft eventually provided a new, official way for them to monitor the system: Callbacks.

A callback is essentially a notification system. A driver can now officially register with the kernel and say, “Hey, please notify me every time a certain event happens.” The kernel keeps a list of these registered drivers, and when an event occurs — like a new process being created — it dutifully notifies every driver on the list.

Some of the most useful callbacks for an EDR are:

  • PsSetCreateProcessNotifyRoutine: Get notified when a process is created or terminated.
  • PsSetLoadImageNotifyRoutine: Get notified when a DLL is loaded.
  • CmRegisterCallback: Get notified of changes to the Windows Registry.

This is the mechanism we’ll use for our EDR. No more sketchy patching, just a clean, Microsoft-approved subscription to system events.

IV. Let’s Get Our Hands Dirty: The Setup

Okay, theory time is over. Let’s start building. This is the part where the weekend starts to slip away. To develop a kernel driver, you’ll need to set up a specific environment.

  1. Install the Tools: You’ll need Visual Studio and the Windows Driver Kit (WDK).
  2. Enable Test Signing: By default, modern Windows will only load drivers that are digitally signed by Microsoft. Since we’re making our own, we need to put our system into a special “test mode” that disables this check.
  3. Create the Project: In Visual Studio, create a new “Kernel Mode Driver, Empty” project.
bcdedit /set testsigning on
bcdedit -debug on

Now we have a blank slate, ready for our driver code.

V. Our First Driver: The Kernel Spy

A kernel driver, like any program, has a main entry point. Here, it’s called DriverEntry. When our driver is loaded, this is the first code that runs.

In our initial DriverEntry, we need to do two basic things:

  1. IoCreateDevice: This officially creates a "device object" for our driver, putting it on the map for the OS.
  2. IoCreateSymbolicLink: This creates a user-friendly name, or a "symlink," so that other programs can find and communicate with our driver.

We also need to define an Unload routine. This is the cleanup crew. When we stop the driver, this function deletes the device and symbolic link we created.

RottenTomato.c

#include <Ntifs.h>
#include <ntddk.h>
#include <wdf.h>

// Define the device name and symbolic link for the RottenTomato driver
UNICODE_STRING DEVICE_NAME = RTL_CONSTANT_STRING(L"\\Device\\RottenTomato"); // Kernel-visible device name
UNICODE_STRING SYM_LINK = RTL_CONSTANT_STRING(L"\\??\\RottenTomato"); // User-visible symbolic link

// Unload routine called when the driver is being unloaded
void UnloadRottenTomato(_In_ PDRIVER_OBJECT DriverObject) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "RottenTomato: Unloading driver...\n");

// Delete the device object created by the driver
IoDeleteDevice(DriverObject->DeviceObject);

// Remove the symbolic link to the device
IoDeleteSymbolicLink(&SYM_LINK);
}

// Entry point for the driver — called when the driver is loaded
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
UNREFERENCED_PARAMETER(RegistryPath); // Not used in this driver

DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "RottenTomato: Initializing driver...\n");

NTSTATUS status; // Status variable for function return codes
PDEVICE_OBJECT DeviceObject; // Pointer to created device object

// Create a device object for communication
status = IoCreateDevice(
DriverObject, // Driver object passed by system
0, // No additional device extension space needed
&DEVICE_NAME, // Name of the device
FILE_DEVICE_UNKNOWN, // Device type is unspecified
0, // No special characteristics
FALSE, // Not exclusive; multiple handles allowed
&DeviceObject // Output: created device object
);

// Check if device creation failed
if (!NT_SUCCESS(status)) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "RottenTomato: Failed to create device\n");
return status;
}

// Create a symbolic link so user-mode apps can access the driver
status = IoCreateSymbolicLink(&SYM_LINK, &DEVICE_NAME);

// Check if symbolic link creation failed
if (!NT_SUCCESS(status)) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "RottenTomato: Failed to create symbolic link\n");
IoDeleteDevice(DeviceObject); // Clean up created device
return status;
}

// Set the driver's unload routine
DriverObject->DriverUnload = UnloadRottenTomato;

return status; // Return the final status (should be STATUS_SUCCESS)
}

After compiling this and loading it with a few command-line instructions, we have a running kernel driver! It does absolutely nothing useful yet, but it’s alive. Success!

sc.exe create rottenTomato type=kernel binPath=rottenTomato.sys
sc.exe start rottenTomato

Press enter or click to view image in full size

VI. Implementing Callbacks: The EDR’s Eyes and Ears

Now let’s give our spy a mission. We’ll use the PsSetCreateProcessNotifyRoutineEx callback. This function lets us register our own custom function that will be executed every single time a new process is about to be created.

The real power here is in the information the kernel gives our callback function. For every new process, we get a structure (PPS_CREATE_NOTIFY_INFO) containing its command line, parent process ID, and, most importantly, a variable called CreationStatus.

This is our chance to act as judge, jury, and executioner. After analyzing the process info, we can set CreationStatus:

  • STATUS_SUCCESS: The process looks fine. Let it run.
  • STATUS_ACCESS_DENIED: This looks suspicious. Block the process from ever starting.

Let’s implement a simple rule: if the process command line contains “mimikatz”, block it. Otherwise, allow it.

RottenTomato.c

#include <Ntifs.h>
#include <ntddk.h>
#include <wdf.h>

// Global variables
UNICODE_STRING DEVICE_NAME = RTL_CONSTANT_STRING(L"\\Device\\rottenTomato"); // Internal device name
UNICODE_STRING SYM_LINK = RTL_CONSTANT_STRING(L"\\??\\rottenTomato"); // Symlink

// Handle incoming notifications about new/terminated processes
void CreateProcessNotifyRoutine(PEPROCESS process, HANDLE pid, PPS_CREATE_NOTIFY_INFO createInfo) {
UNREFERENCED_PARAMETER(process);
UNREFERENCED_PARAMETER(pid);

// Never forget this if check because if you don't, you'll end up crashing your Windows system ;P
if (createInfo != NULL) {
// Compare the command line of the launched process to the notepad string
if (wcsstr(createInfo->CommandLine->Buffer, L"notepad") != NULL) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "rottenTomato: Process (%ws) allowed.\n", createInfo->CommandLine->Buffer);
// Process allowed
createInfo->CreationStatus = STATUS_SUCCESS;
}

// Compare the command line of the launched process to the mimikatz string
if (wcsstr(createInfo->CommandLine->Buffer, L"mimikatz") != NULL) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "rottenTomato: Process (%ws) denied.\n", createInfo->CommandLine->Buffer);
// Process denied
createInfo->CreationStatus = STATUS_ACCESS_DENIED;
}
}
}

void UnloadrottenTomato(_In_ PDRIVER_OBJECT DriverObject) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "rottenTomato: Unloading routine called\n");
// Unset the callback
PsSetCreateProcessNotifyRoutineEx(CreateProcessNotifyRoutine, TRUE);
// Delete the driver device
IoDeleteDevice(DriverObject->DeviceObject);
// Delete the symbolic link
IoDeleteSymbolicLink(&SYM_LINK);
}

NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
// Prevent compiler error in level 4 warnings
UNREFERENCED_PARAMETER(RegistryPath);

DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "rottenTomato: Initializing the driver\n");

// Variable that will store the output of WinAPI functions
NTSTATUS status;

// Setting the unload routine to execute
DriverObject->DriverUnload = UnloadrottenTomato;

// Initializing a device object and creating it
PDEVICE_OBJECT DeviceObject;
UNICODE_STRING deviceName = DEVICE_NAME;
UNICODE_STRING symlinkName = SYM_LINK;
status = IoCreateDevice(
DriverObject, // our driver object,
0, // no need for extra bytes,
&deviceName, // the device name,
FILE_DEVICE_UNKNOWN, // device type,
0, // characteristics flags,
FALSE, // not exclusive,
&DeviceObject // the resulting pointer
);

if (!NT_SUCCESS(status)) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "rottenTomato: Device creation failed\n");
return status;
}

// Creating the symlink that we will use to contact our driver
status = IoCreateSymbolicLink(&symlinkName, &deviceName);
if (!NT_SUCCESS(status)) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "rottenTomato: Symlink creation failed\n");
IoDeleteDevice(DeviceObject);
return status;
}

// Registers the kernel callback
PsSetCreateProcessNotifyRoutineEx(CreateProcessNotifyRoutine, FALSE);

DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "rottenTomato: Driver created\n");
return STATUS_SUCCESS;
}

We recompile, load the driver, and try to run a file named mimikatz.exe. Access Denied. We try to run notepad.exe. It opens just fine. It works! Our EDR has taken its first breath.

Press enter or click to view image in full size

VII. From Kernel Spy to Full-Blown EDR

Our current logic is trivial to bypass (just rename the file). Real detection logic is complex. And running complex code in the kernel is a terrible idea — one small bug and you get a BSOD.

This is why every real EDR has two parts:

  1. A lightweight kernel driver (our spy) that gathers events.
  2. A robust user-space agent (the brain) that receives data from the driver and performs the heavy analysis.

Our driver’s job will be to forward process creation events to our user-space agent. To make things interesting for our RottenTomato EDR, we’ll build two agent components: a Static Analyzer and a Remote Injector.

The Static Analyzer (The Profiler)

This agent receives the filename of a new process from our driver. Before the process runs, it does a quick background check:

  1. Is it signed? Unsigned binaries are more suspicious.
  2. What functions does it import? A program that imports OpenProcess, VirtualAllocEx, WriteProcessMemory, and CreateRemoteThread is highly suspicious, as this is the classic recipe for injecting code into another process.
  3. Does it contain suspicious strings? The string SeDebugPrivilege is often used by malware to get powerful permissions.

If an unsigned binary trips one of these checks, our analyzer will tell the driver to block it.

staticAnalyzer.cpp


#include <stdio.h>
#include <windows.h>
#include <dbghelp.h>
#include <wintrust.h>
#include <Softpub.h>
#include <wincrypt.h>

#pragma comment (lib, "wintrust.lib")
#pragma comment(lib, "dbghelp.lib")
#pragma comment(lib, "crypt32.lib")

#define MESSAGE_SIZE 2048

BOOL VerifyEmbeddedSignature(const wchar_t* binaryPath) {
LONG lStatus;
WINTRUST_FILE_INFO FileData;
memset(&FileData, 0, sizeof(FileData));
FileData.cbStruct = sizeof(WINTRUST_FILE_INFO);
FileData.pcwszFilePath = binaryPath;
FileData.hFile = NULL;
FileData.pgKnownSubject = NULL;
GUID WVTPolicyGUID = WINTRUST_ACTION_GENERIC_VERIFY_V2;
WINTRUST_DATA WinTrustData;

memset(&WinTrustData, 0, sizeof(WinTrustData));
WinTrustData.cbStruct = sizeof(WinTrustData);
WinTrustData.pPolicyCallbackData = NULL;
WinTrustData.pSIPClientData = NULL;
WinTrustData.dwUIChoice = WTD_UI_NONE;
WinTrustData.fdwRevocationChecks = WTD_REVOKE_NONE;
WinTrustData.dwUnionChoice = WTD_CHOICE_FILE;
WinTrustData.dwStateAction = WTD_STATEACTION_VERIFY;
WinTrustData.hWVTStateData = NULL;
WinTrustData.pwszURLReference = NULL;
WinTrustData.dwUIContext = 0;
WinTrustData.pFile = &FileData;

lStatus = WinVerifyTrust(NULL, &WVTPolicyGUID, &WinTrustData);

BOOL isSigned;
switch (lStatus) {
case ERROR_SUCCESS:
isSigned = TRUE;
break;
case TRUST_E_SUBJECT_FORM_UNKNOWN:
case TRUST_E_PROVIDER_UNKNOWN:
case TRUST_E_EXPLICIT_DISTRUST:
case CRYPT_E_SECURITY_SETTINGS:
case TRUST_E_SUBJECT_NOT_TRUSTED:
isSigned = TRUE;
break;
case TRUST_E_NOSIGNATURE:
isSigned = FALSE;
break;
default:
isSigned = FALSE;
break;
}

WinTrustData.dwStateAction = WTD_STATEACTION_CLOSE;
WinVerifyTrust(NULL, &WVTPolicyGUID, &WinTrustData);

return isSigned;
}

BOOL ListImportedFunctions(const wchar_t* binaryPath) {
BOOL isOpenProcessPresent = FALSE;
BOOL isVirtualAllocExPresent = FALSE;
BOOL isWriteProcessMemoryPresent = FALSE;
BOOL isCreateRemoteThreadPresent = FALSE;

HMODULE hModule = LoadLibraryEx(binaryPath, NULL, DONT_RESOLVE_DLL_REFERENCES);
if (hModule != NULL) {
IMAGE_NT_HEADERS* ntHeaders = ImageNtHeader(hModule);
if (ntHeaders != NULL) {
IMAGE_IMPORT_DESCRIPTOR* importDesc = (IMAGE_IMPORT_DESCRIPTOR*)((BYTE*)hModule + ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
while (importDesc->Name != 0) {
const char* moduleName = (const char*)((BYTE*)hModule + importDesc->Name);

IMAGE_THUNK_DATA* thunk = (IMAGE_THUNK_DATA*)((BYTE*)hModule + importDesc->OriginalFirstThunk);
while (thunk->u1.AddressOfData != 0) {
if (!(thunk->u1.Ordinal & IMAGE_ORDINAL_FLAG)) {
IMAGE_IMPORT_BY_NAME* importByName = (IMAGE_IMPORT_BY_NAME*)((BYTE*)hModule + thunk->u1.AddressOfData);

if (strcmp("OpenProcess", importByName->Name) == 0)
isOpenProcessPresent = TRUE;
if (strcmp("VirtualAllocEx", importByName->Name) == 0)
isVirtualAllocExPresent = TRUE;
if (strcmp("WriteProcessMemory", importByName->Name) == 0)
isWriteProcessMemoryPresent = TRUE;
if (strcmp("CreateRemoteThread", importByName->Name) == 0)
isCreateRemoteThreadPresent = TRUE;
}
thunk++;
}
importDesc++;
}
}
FreeLibrary(hModule);
}

return (isOpenProcessPresent && isVirtualAllocExPresent && isWriteProcessMemoryPresent && isCreateRemoteThreadPresent);
}

BOOL lookForSeDebugPrivilegeString(const wchar_t* filename) {
FILE* file;
_wfopen_s(&file, filename, L"rb");
if (file != NULL) {
fseek(file, 0, SEEK_END);
long file_size = ftell(file);
rewind(file);
char* buffer = (char*)malloc(file_size);
if (buffer != NULL) {
if (fread(buffer, 1, file_size, file) == file_size) {
const char* search_string = "SeDebugPrivilege";
size_t search_length = strlen(search_string);
for (int i = 0; i <= file_size - search_length; i++) {
int j = 0;
for (; j < search_length; j++) {
if (buffer[i + j] != search_string[j]) break;
}
if (j == search_length) {
free(buffer);
fclose(file);
return TRUE;
}
}
}
free(buffer);
}
fclose(file);
}
return FALSE;
}

int main() {
LPCWSTR pipeName = L"\\\\.\\pipe\\rottentomato-analyzer";
DWORD bytesRead = 0;
wchar_t target_binary_file[MESSAGE_SIZE] = { 0 };

printf("Launching analyzer named pipe server\n");

HANDLE hServerPipe = CreateNamedPipe(
pipeName,
PIPE_ACCESS_DUPLEX,
PIPE_TYPE_MESSAGE,
PIPE_UNLIMITED_INSTANCES,
MESSAGE_SIZE,
MESSAGE_SIZE,
0,
NULL
);

while (TRUE) {
BOOL isPipeConnected = ConnectNamedPipe(hServerPipe, NULL);

if (isPipeConnected) {
ReadFile(
hServerPipe,
&target_binary_file,
MESSAGE_SIZE,
&bytesRead,
NULL
);

printf("~> Received binary file %ws\n", target_binary_file);

BOOL isSeDebugPrivilegeStringPresent = lookForSeDebugPrivilegeString(target_binary_file);
if (isSeDebugPrivilegeStringPresent)
printf("\t\033[31mFound SeDebugPrivilege string.\033[0m\n");
else
printf("\t\033[32mSeDebugPrivilege string not found.\033[0m\n");

BOOL isDangerousFunctionsFound = ListImportedFunctions(target_binary_file);
if (isDangerousFunctionsFound)
printf("\t\033[31mDangerous functions found.\033[0m\n");
else
printf("\t\033[32mNo dangerous functions found.\033[0m\n");

BOOL isSigned = VerifyEmbeddedSignature(target_binary_file);
if (isSigned)
printf("\t\033[32mBinary is signed.\033[0m\n");
else
printf("\t\033[31mBinary is not signed.\033[0m\n");

wchar_t response[MESSAGE_SIZE] = { 0 };
if (isSigned) {
swprintf_s(response, MESSAGE_SIZE, L"OK\0");
printf("\t\033[32mStaticAnalyzer allows\033[0m\n");
}
else {
if (isDangerousFunctionsFound || isSeDebugPrivilegeStringPresent) {
swprintf_s(response, MESSAGE_SIZE, L"KO\0");
printf("\n\t\033[31mStaticAnalyzer denies\033[0m\n");
}
else {
swprintf_s(response, MESSAGE_SIZE, L"OK\0");
printf("\n\t\033[32mStaticAnalyzer allows\033[0m\n");
}
}

DWORD bytesWritten = 0;
WriteFile(hServerPipe, response, MESSAGE_SIZE, &bytesWritten, NULL);
}

DisconnectNamedPipe(hServerPipe);
printf("\n\n");
}

return 0;
}

The Remote Injector & Hooking (The Undercover Agent)

This is where things get really cool. Instead of just blocking a process, what if we could monitor it from the inside? This agent’s job is to inject a DLL (a small library of our own code) into every new process.

This injected DLL will perform user-space hooking. Since we can’t touch the kernel’s SSDT, we’ll modify the process’s own copy of ntdll.dll. We'll use a brilliant library called MinHook to make this easy.

Our goal is to hook the NtAllocateVirtualMemory function. This function is used to allocate memory. The hook works like this:

  1. Our injected DLL finds the NtAllocateVirtualMemory function in the process's memory.
  2. It overwrites the first few bytes of the function with a JMP (jump) instruction that points to our own custom function.
  3. Now, whenever the process tries to call NtAllocateVirtualMemory, our custom function runs first.

Our custom function will check one thing: is the program trying to allocate memory that is Read, Write, and Execute (RWX)? This is a massive red flag. Legitimate programs rarely need memory that is simultaneously writable and executable. It’s the hallmark of shellcode injection. If we detect an RWX allocation, our hook will immediately terminate the process.

rottenTomatoDLL.dll

#include "pch.h"
#include <MinHook.h>

// Defines the prototype of the NtAllocateVirtualMemoryFunction
typedef DWORD(NTAPI* pNtAllocateVirtualMemory)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
);

// Pointer to the trampoline function used to call the original NtAllocateVirtualMemory
pNtAllocateVirtualMemory pOriginalNtAllocateVirtualMemory = NULL;

// This is the function that will be called whenever the injected process calls
// NtAllocateVirtualMemory. This function takes the arguments Protect and checks
// if the requested protection is RWX (which shouldn't happen).
DWORD NTAPI NtAllocateVirtualMemory(
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
) {

// Checks if the program is trying to allocate some memory and protect it with RWX
if (Protect == PAGE_EXECUTE_READWRITE) {
// If yes, we notify the user and terminate the process
MessageBox(NULL, L"Dude, are you trying to RWX me ?", L"Found u bro", MB_OK);
TerminateProcess(GetCurrentProcess(), 0xdeadb33f);
}

//If no, we jump on the originate NtAllocateVirtualMemory
return pOriginalNtAllocateVirtualMemory(ProcessHandle, BaseAddress, ZeroBits, RegionSize, AllocationType, Protect);
}

// This function initializes the hooks via the MinHook library
DWORD WINAPI InitHooksThread(LPVOID param) {
if (MH_Initialize() != MH_OK) {
return -1;
}

// Here we specify which function from wich DLL we want to hook
MH_CreateHookApi(
L"ntdll", // Name of the DLL containing the function to hook
"NtAllocateVirtualMemory", // Name of the function to hook
NtAllocateVirtualMemory, // Address of the function on which to jump when hooking
(LPVOID*)(&pOriginalNtAllocateVirtualMemory) // Address of the original NtAllocateVirtualMemory function
);

// Enable the hook on NtAllocateVirtualMemory
MH_STATUS status = MH_EnableHook(MH_ALL_HOOKS);
return status;
}

// Here is the DllMain of our DLL
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH: {
// This DLL will not be loaded by any thread so we simply disable DLL_TRHEAD_ATTACH and DLL_THREAD_DETACH
DisableThreadLibraryCalls(hModule);

// Calling WinAPI32 functions from the DllMain is a very bad practice
// since it can basically lock the program loading the DLL
// Microsoft recommends not using any functions here except a few one like
// CreateThread IF AND ONLY IF there is no need for synchronization
// So basically we are creating a thread that will execute the InitHooksThread function
// thus allowing us hooking the NtAllocateVirtualMemory function
HANDLE hThread = CreateThread(NULL, 0, InitHooksThread, NULL, 0, NULL);
if (hThread != NULL) {
CloseHandle(hThread);
}
break;
}
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

RemoteInjector.cpp

#include <stdio.h>
#include <windows.h>

#define MESSAGE_SIZE 2048
#define MAX_PATH 260

int main() {
LPCWSTR pipeName = L"\\\\.\\pipe\\rottentomato-analyzer";
DWORD bytesRead = 0;
wchar_t target_binary_file[MESSAGE_SIZE] = { 0 };

// Full path to your DLL
const char dll_full_path[] = "C:\\Users\\vboxuser\\source\\repos\\rottenTomatoDLL\\x64\\Debug\\rottenTomatoDLL.dll";

printf("Launching injector named pipe server, injecting %s\n", dll_full_path);

// Creates a named pipe
HANDLE hServerPipe = CreateNamedPipe(
pipeName,
PIPE_ACCESS_DUPLEX,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
MESSAGE_SIZE,
MESSAGE_SIZE,
0,
NULL
);

if (hServerPipe == INVALID_HANDLE_VALUE) {
printf("Failed to create named pipe, error: %lu\n", GetLastError());
return 1;
}

while (TRUE) {
BOOL isPipeConnected = ConnectNamedPipe(hServerPipe, NULL);
if (!isPipeConnected) {
printf("Failed to connect to named pipe, error: %lu\n", GetLastError());
continue;
}

wchar_t message[MESSAGE_SIZE] = { 0 };

if (!ReadFile(hServerPipe, &message, MESSAGE_SIZE, &bytesRead, NULL)) {
printf("Failed to read from pipe, error: %lu\n", GetLastError());
DisconnectNamedPipe(hServerPipe);
continue;
}

DWORD target_pid = _wtoi(message);
printf("~> Received process id %d\n", target_pid);

HANDLE hProcess = OpenProcess(
PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION |
PROCESS_VM_WRITE | PROCESS_VM_READ,
FALSE,
target_pid
);

if (hProcess == NULL) {
printf("Can't open handle, error: %lu\n", GetLastError());
DisconnectNamedPipe(hServerPipe);
continue;
}
printf("\tOpen handle on PID: %d\n", target_pid);

FARPROC loadLibAddress = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryA");
if (loadLibAddress == NULL) {
printf("Could not find LoadLibraryA, error: %lu\n", GetLastError());
CloseHandle(hProcess);
DisconnectNamedPipe(hServerPipe);
continue;
}
printf("\tFound LoadLibraryA function\n");

LPVOID vae_buffer = VirtualAllocEx(hProcess, NULL, MAX_PATH, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (vae_buffer == NULL) {
printf("Can't allocate memory, error: %lu\n", GetLastError());
CloseHandle(hProcess);
DisconnectNamedPipe(hServerPipe);
continue;
}
printf("\tAllocated: %d bytes\n", MAX_PATH);

SIZE_T bytesWritten;
if (!WriteProcessMemory(hProcess, vae_buffer, dll_full_path, strlen(dll_full_path) + 1, &bytesWritten)) {
printf("Can't write into memory, error: %lu\n", GetLastError());
VirtualFreeEx(hProcess, vae_buffer, 0, MEM_RELEASE);
CloseHandle(hProcess);
DisconnectNamedPipe(hServerPipe);
continue;
}
printf("\tWrote %zu bytes into PID %d memory\n", bytesWritten, target_pid);

HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)loadLibAddress,
vae_buffer, 0, NULL);

if (hThread == NULL) {
printf("Can't launch remote thread, error: %lu\n", GetLastError());
VirtualFreeEx(hProcess, vae_buffer, 0, MEM_RELEASE);
CloseHandle(hProcess);
DisconnectNamedPipe(hServerPipe);
continue;
}

printf("\tLaunched remote thread\n");

// Wait for thread to complete (optional)
WaitForSingleObject(hThread, INFINITE);

VirtualFreeEx(hProcess, vae_buffer, 0, MEM_RELEASE);
CloseHandle(hThread);
CloseHandle(hProcess);
printf("\tClosed handle\n");

wchar_t response[MESSAGE_SIZE] = { 0 };
swprintf_s(response, MESSAGE_SIZE, L"OK");

DWORD pipeBytesWritten = 0;
WriteFile(hServerPipe, response, sizeof(response), &pipeBytesWritten, NULL);

DisconnectNamedPipe(hServerPipe);
printf("\n\n");
}

return 0;
}

Final Driver Code (RottenTomato.c)

#include <ntddk.h>

// Global variables
UNICODE_STRING DEVICE_NAME = RTL_CONSTANT_STRING(L"\\Device\\rottenTomatoEDR");
UNICODE_STRING SYM_LINK = RTL_CONSTANT_STRING(L"\\??\\rottenTomatoEDR");

// Forward declaration of the unload routine and the callback
void UnloadRottenTomatoEDR(_In_ PDRIVER_OBJECT DriverObject);
void CreateProcessNotifyRoutine(PEPROCESS process, HANDLE pid, PPS_CREATE_NOTIFY_INFO createInfo);

// The main entry point for the driver
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
UNREFERENCED_PARAMETER(RegistryPath);
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "rottenTomatoEDR: Initializing the driver\n");
NTSTATUS status;
PDEVICE_OBJECT DeviceObject = NULL;

// Set the function to be called when the driver is unloaded
DriverObject->DriverUnload = UnloadRottenTomatoEDR;

// Create a device object for our driver
status = IoCreateDevice(DriverObject, 0, &DEVICE_NAME, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject);
if (!NT_SUCCESS(status)) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "rottenTomatoEDR: Device creation failed: 0x%X\n", status);
return status;
}

// Create a symbolic link so user-mode applications can communicate with the driver
status = IoCreateSymbolicLink(&SYM_LINK, &DEVICE_NAME);
if (!NT_SUCCESS(status)) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "rottenTomatoEDR: Symlink creation failed: 0x%X\n", status);
IoDeleteDevice(DeviceObject);
return status;
}

// Register our process creation callback routine
status = PsSetCreateProcessNotifyRoutineEx(CreateProcessNotifyRoutine, FALSE);
if (!NT_SUCCESS(status)) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "rottenTomatoEDR: Failed to set callback routine: 0x%X\n", status);
IoDeleteSymbolicLink(&SYM_LINK);
IoDeleteDevice(DeviceObject);
return status;
}

DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "rottenTomatoEDR: Driver loaded successfully.\n");
return STATUS_SUCCESS;
}

// The routine that is called when a process is created or terminated
void CreateProcessNotifyRoutine(PEPROCESS process, HANDLE pid, PPS_CREATE_NOTIFY_INFO createInfo) {
UNREFERENCED_PARAMETER(process);

if (createInfo) {
createInfo->CreationStatus = STATUS_SUCCESS;

HANDLE hPipeAnalyzer, hPipeInjector;
OBJECT_ATTRIBUTES objAttr;
IO_STATUS_BLOCK ioStatusBlock;
NTSTATUS status;

UNICODE_STRING pipeNameAnalyzer = RTL_CONSTANT_STRING(L"\\??\\pipe\\rottenTomato-analyzer");
UNICODE_STRING pipeNameInjector = RTL_CONSTANT_STRING(L"\\??\\pipe\\rottenTomato-injector");

// Connect to Static Analyzer Agent
InitializeObjectAttributes(&objAttr, &pipeNameAnalyzer, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
status = ZwCreateFile(&hPipeAnalyzer, GENERIC_WRITE | GENERIC_READ | SYNCHRONIZE, &objAttr, &ioStatusBlock, NULL,
FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_OPEN,
FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0);
if (!NT_SUCCESS(status)) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "rottenTomatoEDR: Failed to open analyzer pipe: 0x%X\n", status);
return;
}

DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "rottenTomatoEDR: Sending '%wZ' to Static Analyzer\n", createInfo->ImageFileName);

status = ZwWriteFile(hPipeAnalyzer, NULL, NULL, NULL, &ioStatusBlock,
createInfo->ImageFileName->Buffer, createInfo->ImageFileName->Length, NULL, NULL);
if (!NT_SUCCESS(status)) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "rottenTomatoEDR: Failed to write to analyzer pipe: 0x%X\n", status);
ZwClose(hPipeAnalyzer);
return;
}

wchar_t response[10] = { 0 };
status = ZwReadFile(hPipeAnalyzer, NULL, NULL, NULL, &ioStatusBlock,
response, sizeof(response) - sizeof(WCHAR), NULL, NULL);
if (!NT_SUCCESS(status)) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "rottenTomatoEDR: Failed to read from analyzer pipe: 0x%X\n", status);
ZwClose(hPipeAnalyzer);
return;
}
ZwClose(hPipeAnalyzer);

if (wcscmp(response, L"KO") == 0) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "rottenTomatoEDR: Static Analyzer denied process. Blocking.\n");
createInfo->CreationStatus = STATUS_ACCESS_DENIED;
return;
}

DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "rottenTomatoEDR: Static Analyzer allowed process. Forwarding to Injector.\n");

// Connect to Remote Injector Agent
InitializeObjectAttributes(&objAttr, &pipeNameInjector, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
status = ZwCreateFile(&hPipeInjector, GENERIC_WRITE | SYNCHRONIZE, &objAttr, &ioStatusBlock, NULL,
FILE_ATTRIBUTE_NORMAL, 0, FILE_OPEN, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0);
if (!NT_SUCCESS(status)) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "rottenTomatoEDR: Failed to open injector pipe: 0x%X\n", status);
return;
}

WCHAR pidBuffer[20];
UNICODE_STRING pidUnicodeString;
pidUnicodeString.Buffer = pidBuffer;
pidUnicodeString.Length = 0;
pidUnicodeString.MaximumLength = sizeof(pidBuffer);

status = RtlIntegerToUnicodeString((ULONG)(ULONG_PTR)pid, 10, &pidUnicodeString);
if (!NT_SUCCESS(status)) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "rottenTomatoEDR: Failed to convert PID to string: 0x%X\n", status);
ZwClose(hPipeInjector);
return;
}

DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "rottenTomatoEDR: Sending PID %wZ to Remote Injector\n", &pidUnicodeString);

status = ZwWriteFile(hPipeInjector, NULL, NULL, NULL, &ioStatusBlock,
pidUnicodeString.Buffer, pidUnicodeString.Length, NULL, NULL);
if (!NT_SUCCESS(status)) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "rottenTomatoEDR: Failed to write to injector pipe: 0x%X\n", status);
}

ZwClose(hPipeInjector);
}
}

// The routine that is called when the driver is unloaded
void UnloadRottenTomatoEDR(_In_ PDRIVER_OBJECT DriverObject) {
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "rottenTomatoEDR: Unloading routine called\n");
PsSetCreateProcessNotifyRoutineEx(CreateProcessNotifyRoutine, TRUE);
IoDeleteSymbolicLink(&SYM_LINK);
IoDeleteDevice(DriverObject->DeviceObject);
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, "rottenTomatoEDR: Driver unloaded successfully.\n");
}

Press enter or click to view image in full size

VIII. Conclusion

Building this project taught me three things. First, I now have a much deeper respect for how EDRs are architectured. Second, I hope this article demystifies the process for others and maybe even gives you a few ideas on how to bypass them. And third, I realized just how incredibly difficult it is to build a production-ready security tool.

As penetration testers, it’s easy to brag, “Haha, I bypassed that EDR.” But remember the teams of developers and blue teamers who work tirelessly to build and manage these complex systems. It’s a constant cat-and-mouse game, and they deserve a huge thumbs-up.

Happy hacking!


文章来源: https://infosecwriteups.com/how-to-ruin-your-weekend-building-a-diy-edr-a8f6dc6f8da4?source=rss----7b722bfd1b8d---4
如有侵权请联系:admin#unsafe.sh