← ← Back to all posts

Your First Kernel Driver for Application Modification

2025-11-04 · Benja

You will learn the basics of creating your first kernel-level driver for reading and modifying memory. NOTICE: WE DO NOT ENCOURAGE THE INVASIVE OR UNAUTHORIZED USE OF THIS TECHNOLOGY. EDUCATIONAL USE ONLY IN A SECURE ENVIRONMENT.

Your First Kernel Driver for Application Modification
Kernel Driver to Read and Write Process Memory on Windows

Step-by-step development of a kernel driver to read and write process memory on Windows

In this article we will walk, like a code review between a senior engineer and a junior developer, through the development of a C driver for Windows that:

  • Searches processes by name and obtains their PID.
  • Finds the base address of a DLL in that process.
  • Reads memory from that process.
  • Writes memory into that process.

First you will see the full code and then we will analyze it section by section, explaining why it is written that way and what each part does.

Complete driver code


#include <ntddk.h>
#include <wdm.h>
#include <string.h>

// ============================================================================
// DEFINITIONS AND CONSTANTS
// ============================================================================

#define POOL_TAG 'MPRC' // Tag for pool allocations
#define MAX_PROCESS_NAME 260 // Max process name length

// System process info structure (partial)
typedef struct _SYSTEM_PROCESS_INFO {
    ULONG NextEntryOffset;
    ULONG NumberOfThreads;
    LARGE_INTEGER Reserved[3];
    LARGE_INTEGER CreateTime;
    LARGE_INTEGER UserTime;
    LARGE_INTEGER KernelTime;
    UNICODE_STRING ImageName;
    KPRIORITY BasePriority;
    HANDLE ProcessId;
    HANDLE InheritedFromProcessId;
} SYSTEM_PROCESS_INFO, *PSYSTEM_PROCESS_INFO;

// LDR data table entry (module info)
typedef struct _LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY InLoadOrderLinks;
    LIST_ENTRY InMemoryOrderLinks;
    LIST_ENTRY InInitializationOrderLinks;
    PVOID DllBase;
    PVOID EntryPoint;
    ULONG SizeOfImage;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

// ============================================================================
// HELPERS
// ============================================================================

/**
 * Validate a user pointer is accessible
 */
BOOLEAN IsAddressValid(PVOID Address, SIZE_T Size, LOCK_OPERATION Mode) {
    if (!Address || Size == 0) {
        return FALSE;
    }

    __try {
        ProbeForRead(Address, Size, sizeof(UCHAR));
        if (Mode == IoWriteAccess) {
            ProbeForWrite(Address, Size, sizeof(UCHAR));
        }
        return TRUE;
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        return FALSE;
    }
}

/**
 * Case-insensitive wide string compare safe in kernel
 */
INT SafeWcsicmp(PCWSTR Str1, PCWSTR Str2) {
    if (!Str1 || !Str2) {
        return -1;
    }

    __try {
        return _wcsicmp(Str1, Str2);
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        return -1;
    }
}

// ============================================================================
// MAIN FUNCTIONS
// ============================================================================

/**
 * Get PID by process name
 */
NTSTATUS GetPidByName(PCWSTR TargetProcessName, HANDLE* Pid) {
    NTSTATUS status;
    PSYSTEM_PROCESS_INFO processInfo = NULL;
    PSYSTEM_PROCESS_INFO current = NULL;
    ULONG bufferSize = 0;
    BOOLEAN found = FALSE;

    if (!TargetProcessName || !Pid) {
        DbgPrint("[MemDriver] Error: Invalid parameters in GetPidByName\n");
        return STATUS_INVALID_PARAMETER;
    }

    status = ZwQuerySystemInformation(
        SystemProcessInformation,
        NULL,
        0,
        &bufferSize
    );

    if (status != STATUS_INFO_LENGTH_MISMATCH) {
        DbgPrint("[MemDriver] Error querying size: 0x%X\n", status);
        return status;
    }

    bufferSize += 0x1000;

    processInfo = (PSYSTEM_PROCESS_INFO)ExAllocatePoolWithTag(
        NonPagedPool,
        bufferSize,
        POOL_TAG
    );

    if (!processInfo) {
        DbgPrint("[MemDriver] Error: Could not allocate memory\n");
        return STATUS_INSUFFICIENT_RESOURCES;
    }

    status = ZwQuerySystemInformation(
        SystemProcessInformation,
        processInfo,
        bufferSize,
        NULL
    );

    if (!NT_SUCCESS(status)) {
        DbgPrint("[MemDriver] Error getting process info: 0x%X\n", status);
        ExFreePoolWithTag(processInfo, POOL_TAG);
        return status;
    }

    current = processInfo;
    while (TRUE) {
        if (current->ImageName.Buffer && current->ImageName.Length > 0) {
            if (SafeWcsicmp(current->ImageName.Buffer, TargetProcessName) == 0) {
                *Pid = current->ProcessId;
                found = TRUE;
                DbgPrint("[MemDriver] Found process: %ws (PID: %d)\n",
                         TargetProcessName, (ULONG)(ULONG_PTR)current->ProcessId);
                break;
            }
        }

        if (current->NextEntryOffset == 0) {
            break;
        }

        current = (PSYSTEM_PROCESS_INFO)((PUCHAR)current + current->NextEntryOffset);
    }

    ExFreePoolWithTag(processInfo, POOL_TAG);

    if (!found) {
        DbgPrint("[MemDriver] Process not found: %ws\n", TargetProcessName);
        return STATUS_NOT_FOUND;
    }

    return STATUS_SUCCESS;
}

/**
 * Get base address of a DLL inside a process
 */
NTSTATUS GetDllBaseAddress(HANDLE Pid, PCWSTR DllName, PVOID* BaseAddress) {
    PEPROCESS targetProcess = NULL;
    NTSTATUS status;
    KAPC_STATE apcState;
    PPEB peb = NULL;
    PPEB_LDR_DATA ldr = NULL;
    PLIST_ENTRY listHead = NULL;
    PLIST_ENTRY listEntry = NULL;
    BOOLEAN found = FALSE;

    if (!Pid || !DllName || !BaseAddress) {
        DbgPrint("[MemDriver] Error: Invalid parameters in GetDllBaseAddress\n");
        return STATUS_INVALID_PARAMETER;
    }

    status = PsLookupProcessByProcessId(Pid, &targetProcess);
    if (!NT_SUCCESS(status)) {
        DbgPrint("[MemDriver] Error finding process (PID: %d): 0x%X\n",
                 (ULONG)(ULONG_PTR)Pid, status);
        return status;
    }

    if (PsGetProcessExitStatus(targetProcess) != STATUS_PENDING) {
        DbgPrint("[MemDriver] Error: Process is terminating or has terminated\n");
        ObDereferenceObject(targetProcess);
        return STATUS_PROCESS_IS_TERMINATING;
    }

    KeStackAttachProcess(targetProcess, &apcState);

    __try {
        peb = PsGetProcessPeb(targetProcess);
        if (!peb) {
            DbgPrint("[MemDriver] Error: Could not get PEB\n");
            status = STATUS_UNSUCCESSFUL;
            __leave;
        }

        ldr = peb->Ldr;
        if (!ldr) {
            DbgPrint("[MemDriver] Error: Could not get Ldr\n");
            status = STATUS_UNSUCCESSFUL;
            __leave;
        }

        listHead = &ldr->InMemoryOrderModuleList;
        listEntry = listHead->Flink;

        while (listEntry != listHead) {
            PLDR_DATA_TABLE_ENTRY module = CONTAINING_RECORD(
                listEntry,
                LDR_DATA_TABLE_ENTRY,
                InMemoryOrderLinks
            );

            if (module->BaseDllName.Buffer && module->BaseDllName.Length > 0) {
                if (SafeWcsicmp(module->BaseDllName.Buffer, DllName) == 0) {
                    *BaseAddress = module->DllBase;
                    found = TRUE;
                    DbgPrint("[MemDriver] DLL found: %ws (Base: 0x%p, Size: 0x%X)\n",
                             DllName, module->DllBase, module->SizeOfImage);
                    status = STATUS_SUCCESS;
                    break;
                }
            }

            listEntry = listEntry->Flink;

            if (listEntry == NULL) {
                break;
            }
        }

        if (!found) {
            DbgPrint("[MemDriver] DLL not found: %ws\n", DllName);
            status = STATUS_NOT_FOUND;
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        DbgPrint("[MemDriver] Exception searching DLL: 0x%X\n", GetExceptionCode());
        status = STATUS_ACCESS_VIOLATION;
    }

    KeUnstackDetachProcess(&apcState);
    ObDereferenceObject(targetProcess);

    return status;
}

/**
 * Read memory from a target process
 */
NTSTATUS ReadProcessMemory(HANDLE Pid, PVOID Address, PVOID Buffer, SIZE_T Size) {
    PEPROCESS targetProcess = NULL;
    NTSTATUS status;
    SIZE_T bytesRead = 0;

    if (!Pid || !Address || !Buffer || Size == 0) {
        DbgPrint("[MemDriver] Error: Invalid parameters in ReadProcessMemory\n");
        return STATUS_INVALID_PARAMETER;
    }

    if (!IsAddressValid(Buffer, Size, IoWriteAccess)) {
        DbgPrint("[MemDriver] Error: Invalid destination buffer\n");
        return STATUS_ACCESS_VIOLATION;
    }

    status = PsLookupProcessByProcessId(Pid, &targetProcess);
    if (!NT_SUCCESS(status)) {
        DbgPrint("[MemDriver] Error finding process for read: 0x%X\n", status);
        return status;
    }

    if (PsGetProcessExitStatus(targetProcess) != STATUS_PENDING) {
        DbgPrint("[MemDriver] Error: Process not active\n");
        ObDereferenceObject(targetProcess);
        return STATUS_PROCESS_IS_TERMINATING;
    }

    __try {
        status = MmCopyVirtualMemory(
            targetProcess,
            Address,
            PsGetCurrentProcess(),
            Buffer,
            Size,
            KernelMode,
            &bytesRead
        );

        if (NT_SUCCESS(status) && bytesRead == Size) {
            DbgPrint("[MemDriver] Read successful: 0x%p (%zu bytes)\n", Address, Size);
            status = STATUS_SUCCESS;
        } else if (bytesRead < Size) {
            DbgPrint("[MemDriver] Warning: Partial read (%zu/%zu bytes)\n",
                     bytesRead, Size);
            status = STATUS_PARTIAL_COPY;
        } else {
            DbgPrint("[MemDriver] Read error: 0x%X\n", status);
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        DbgPrint("[MemDriver] Exception reading memory: 0x%X\n", GetExceptionCode());
        status = STATUS_ACCESS_VIOLATION;
    }

    ObDereferenceObject(targetProcess);
    return status;
}

/**
 * Write data into a target process memory
 */
NTSTATUS WriteProcessMemory(HANDLE Pid, PVOID Address, PVOID Buffer, SIZE_T Size) {
    PEPROCESS targetProcess = NULL;
    NTSTATUS status;
    SIZE_T bytesWritten = 0;

    if (!Pid || !Address || !Buffer || Size == 0) {
        DbgPrint("[MemDriver] Error: Invalid parameters in WriteProcessMemory\n");
        return STATUS_INVALID_PARAMETER;
    }

    if (!IsAddressValid(Buffer, Size, IoReadAccess)) {
        DbgPrint("[MemDriver] Error: Invalid source buffer\n");
        return STATUS_ACCESS_VIOLATION;
    }

    status = PsLookupProcessByProcessId(Pid, &targetProcess);
    if (!NT_SUCCESS(status)) {
        DbgPrint("[MemDriver] Error finding process for write: 0x%X\n", status);
        return status;
    }

    if (PsGetProcessExitStatus(targetProcess) != STATUS_PENDING) {
        DbgPrint("[MemDriver] Error: Process not active\n");
        ObDereferenceObject(targetProcess);
        return STATUS_PROCESS_IS_TERMINATING;
    }

    __try {
        status = MmCopyVirtualMemory(
            PsGetCurrentProcess(),
            Buffer,
            targetProcess,
            Address,
            Size,
            KernelMode,
            &bytesWritten
        );

        if (NT_SUCCESS(status) && bytesWritten == Size) {
            DbgPrint("[MemDriver] Write successful: 0x%p (%zu bytes)\n", Address, Size);
            status = STATUS_SUCCESS;
        } else if (bytesWritten < Size) {
            DbgPrint("[MemDriver] Warning: Partial write (%zu/%zu bytes)\n",
                     bytesWritten, Size);
            status = STATUS_PARTIAL_COPY;
        } else {
            DbgPrint("[MemDriver] Write error: 0x%X\n", status);
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        DbgPrint("[MemDriver] Exception writing memory: 0x%X\n", GetExceptionCode());
        status = STATUS_ACCESS_VIOLATION;
    }

    ObDereferenceObject(targetProcess);
    return status;
}

// ============================================================================
// USAGE EXAMPLE
// ============================================================================

NTSTATUS ExampleUsage() {
    NTSTATUS status;
    HANDLE pid = NULL;
    PVOID dllBase = NULL;
    PVOID finalAddress = NULL;
    INT32 valueToWrite = 100;
    INT32 currentValue = 0;
    ULONG_PTR offset = 0x1234; // example offset

    DbgPrint("[MemDriver] ===== Starting usage example =====\n");

    status = GetPidByName(L"example.exe", &pid);
    if (!NT_SUCCESS(status)) {
        DbgPrint("[MemDriver] Could not find process\n");
        return status;
    }

    status = GetDllBaseAddress(pid, L"mydll.dll", &dllBase);
    if (!NT_SUCCESS(status)) {
        DbgPrint("[MemDriver] Could not find DLL\n");
        return status;
    }

    finalAddress = (PVOID)((ULONG_PTR)dllBase + offset);
    DbgPrint("[MemDriver] Computed address: 0x%p (Base: 0x%p + Offset: 0x%lX)\n",
             finalAddress, dllBase, offset);

    status = ReadProcessMemory(pid, finalAddress, &currentValue, sizeof(INT32));
    if (NT_SUCCESS(status)) {
        DbgPrint("[MemDriver] Current value at 0x%p: %d\n", finalAddress, currentValue);
    } else {
        DbgPrint("[MemDriver] Warning: Could not read memory\n");
    }

    status = WriteProcessMemory(pid, finalAddress, &valueToWrite, sizeof(INT32));
    if (NT_SUCCESS(status)) {
        DbgPrint("[MemDriver] New value written: %d\n", valueToWrite);

        INT32 verifyValue = 0;
        if (NT_SUCCESS(ReadProcessMemory(pid, finalAddress, &verifyValue, sizeof(INT32)))) {
            DbgPrint("[MemDriver] Verification: current value is %d\n", verifyValue);
        }
    } else {
        DbgPrint("[MemDriver] Error writing memory\n");
    }

    DbgPrint("[MemDriver] ===== Example finished =====\n");
    return status;
}

// ============================================================================
// DRIVER ENTRYPOINT
// ============================================================================

VOID DriverUnload(PDRIVER_OBJECT DriverObject) {
    UNREFERENCED_PARAMETER(DriverObject);
    DbgPrint("[MemDriver] Driver unloaded successfully\n");
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
    UNREFERENCED_PARAMETER(RegistryPath);

    DbgPrint("[MemDriver] Driver loaded successfully\n");

    DriverObject->DriverUnload = DriverUnload;

    // ExampleUsage(); // commented out in production

    return STATUS_SUCCESS;
}
    

1. Overall driver design

Before diving into each function, it's useful to understand what problems we want to solve from kernel space and how that reflects in the code structure:

  1. We need a way to enumerate processes and find one by name → GetPidByName.
  2. Once we have the process, we want to find a specific DLL inside it → GetDllBaseAddress.
  3. With PID and addresses, we need to read memoryReadProcessMemory.
  4. And also write memoryWriteProcessMemory.
  5. Finally, an example that shows the whole flow → ExampleUsage, and the standard driver entry/unload routines.

Everything else (structures, helpers, validations) exists so those four operations are as safe and clear as possible.

2. Definitions, constants and internal structures

2.1 POOL_TAG and constants

#define POOL_TAG 'MPRC' defines a tag for all pool allocations made by this driver. It helps debugging and allows the kernel to associate memory blocks with your module.

MAX_PROCESS_NAME sets a reasonable limit for process name buffers when static buffers are used.

2.2 SYSTEM_PROCESS_INFO

SYSTEM_PROCESS_INFO is the format returned by ZwQuerySystemInformation(SystemProcessInformation, ...). We don't use every field, but we rely on:

  • NextEntryOffset: offset to the next element in the buffer.
  • ImageName: the executable name (UNICODE_STRING).
  • ProcessId: the PID we are looking for.

2.3 LDR_DATA_TABLE_ENTRY

This structure models each module loaded in a process (EXE or DLL). The process loader keeps doubly linked lists of these entries.

  • DllBase: base address of the image in memory.
  • BaseDllName: short DLL name like kernel32.dll.
  • InMemoryOrderLinks: linked-list node used to iterate modules.

3. Helper functions: sanitizing input

3.1 User pointer validation: IsAddressValid

IsAddressValid ensures, from kernel, that a pointer passed from user-mode is accessible before use. Basic logic:

  1. If the address is NULL or size is 0, fail.
  2. Inside __try / __except call ProbeForRead. If writing, call ProbeForWrite as well.
  3. If any call raises, catch in __except and return FALSE.

This prevents an invalid user-mode pointer from crashing the whole system.

3.2 Safe wide-string compare: SafeWcsicmp

SafeWcsicmp wraps _wcsicmp with __try / __except. If either string is invalid, return -1. Used both for process and DLL name comparisons.

4. Finding a process PID: GetPidByName

This function walks the system process list and returns the PID matching the provided name.

4.1 Reserving the correct buffer

  1. Validate TargetProcessName and Pid are not NULL.
  2. Call ZwQuerySystemInformation with a NULL buffer to get required bufferSize.
  3. Add a margin (+0x1000) to mitigate list growth between calls.
  4. Allocate NonPagedPool memory of that size.

4.2 Iterating the process list

After filling the buffer, iterate using current:

  1. If ImageName exists, compare with TargetProcessName using SafeWcsicmp.
  2. On match store ProcessId, set found = TRUE and exit.
  3. If NextEntryOffset == 0 reach the end.
  4. Otherwise advance via pointer arithmetic.

Free buffer with ExFreePoolWithTag. Return STATUS_NOT_FOUND if not found.

5. Finding a DLL base: GetDllBaseAddress

Given a PID, locate the base of a specific DLL inside the process by attaching to its address space and iterating the loader lists.

5.1 Getting EPROCESS and attaching

  1. Validate inputs.
  2. Obtain PEPROCESS via PsLookupProcessByProcessId.
  3. Check process is not terminating (PsGetProcessExitStatus).
  4. Attach to the process with KeStackAttachProcess.

5.2 PEB, Ldr and module list

  1. Get PEB with PsGetProcessPeb.
  2. From PEB get peb->Ldr and the InMemoryOrderModuleList.
  3. Iterate Flink and use CONTAINING_RECORD to get LDR_DATA_TABLE_ENTRY.
  4. Compare BaseDllName with target using SafeWcsicmp. On match store DllBase.

Wrap this block in __try / __except. Always call:

  • KeUnstackDetachProcess(&apcState);
  • ObDereferenceObject(targetProcess);

6. Read process memory: ReadProcessMemory

Copies memory from the target process into a buffer in the driver's context using MmCopyVirtualMemory.

6.1 Validations and obtaining process

  1. Verify Pid, Address, Buffer and Size.
  2. Validate destination buffer with IsAddressValid(..., IoWriteAccess).
  3. Lookup process and verify state.

6.2 Copy using MmCopyVirtualMemory


status = MmCopyVirtualMemory(
    targetProcess,           // source process
    Address,                 // source address
    PsGetCurrentProcess(),   // destination (this driver)
    Buffer,                  // destination buffer
    Size,                    // size
    KernelMode,              // access mode
    &bytesRead               // bytes read
);
    

If bytesRead == Size and status is success, the read is successful. Partial reads return STATUS_PARTIAL_COPY.

7. Write memory to a process: WriteProcessMemory

Symmetric logic to reading: source buffer is the driver, destination is the process.

7.1 Input validations

  1. Ensure parameters are non-null.
  2. Validate source buffer with IsAddressValid(..., IoReadAccess).
  3. Find process and check its state.

7.2 Copy into target process


status = MmCopyVirtualMemory(
    PsGetCurrentProcess(),   // source (this driver)
    Buffer,                  // source buffer
    targetProcess,           // destination process
    Address,                 // destination address
    Size,                    // size
    KernelMode,              // access mode
    &bytesWritten            // bytes written
);
    

If bytesWritten == Size and status is success, return STATUS_SUCCESS. Partial writes return STATUS_PARTIAL_COPY.

8. Bringing it together: ExampleUsage

ExampleUsage demonstrates the full flow:

  1. Find process "example.exe" via GetPidByName.
  2. Get base of "mydll.dll" via GetDllBaseAddress.
  3. Compute final address (base + fake offset).
  4. Read an INT32 using ReadProcessMemory.
  5. Write the value 100 with WriteProcessMemory.
  6. Verify by reading back.

This is the basic recipe to inspect and modify a process memory from kernel mode.

9. DriverEntry and DriverUnload

Finally, the driver entry point and unload routine.

9.1 DriverUnload

DriverUnload logs unload. In a real driver you should free global resources, unregister callbacks and stop threads here.

9.2 DriverEntry

DriverEntry is the driver "main":

  • Ignore RegistryPath if unused.
  • Log successful load.
  • Assign DriverUnload.
  • Keep ExampleUsage() commented out for production.

10. Conclusion

The driver analyzed shows a complete pipeline for process memory manipulation from kernel mode: locate the process, find modules, compute addresses, and read/write memory using kernel APIs in a controlled way.

Engineering takeaways:

  • Always validate pointers coming from user-mode.
  • Wrap accesses to internal structures (PEB, module lists) in __try / __except.
  • Use kernel primitives (ZwQuerySystemInformation, PsLookupProcessByProcessId, MmCopyVirtualMemory) instead of reinventing low-level logic.
  • Always clean up resources: pool memory and process references.

From this base you can add IOCTLs to expose these operations to a user-mode program in a controlled way, or integrate advanced analysis and monitoring logic.