Loading a kernel driver leaves traces. Where and how many depends on which path you take.

There are two realistic options from user mode: go through the Service Control Manager, or call NtLoadDriver directly. Both require SeLoadDriverPrivilege. Both end with the driver running in kernel. The artifacts they leave behind are completely different.

The two paths

The SCM path uses documented Win32 APIs:

c
SC_HANDLE hSCM = OpenSCManagerA(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
SC_HANDLE hSvc = CreateServiceA(
    hSCM, "mydriver", "mydriver",
    SERVICE_START,
    SERVICE_KERNEL_DRIVER,
    SERVICE_DEMAND_START,
    SERVICE_ERROR_IGNORE,
    "C:\\path\\to\\driver.sys",
    NULL, NULL, NULL, NULL, NULL);
StartServiceA(hSvc, 0, NULL);

The NtLoadDriver path bypasses SCM entirely. You create the registry key yourself, then call the syscall directly via GetProcAddress into ntdll:

c
using NtLoadDriver_t = NTSTATUS(*)(PCUNICODE_STRING DriverServiceName);
using RtlAdjustPrivilege_t = NTSTATUS(*)(ULONG Privilege, BOOLEAN Enable,
                                          BOOLEAN Client, PBOOLEAN WasEnabled);

HMODULE ntdll = GetModuleHandleW(L"ntdll.dll");
auto NtLoadDriver     = (NtLoadDriver_t)GetProcAddress(ntdll, "NtLoadDriver");
auto RtlAdjustPrivilege = (RtlAdjustPrivilege_t)GetProcAddress(ntdll, "RtlAdjustPrivilege");

BOOLEAN old = FALSE;
RtlAdjustPrivilege(10L /* SE_LOAD_DRIVER_PRIVILEGE */, TRUE, FALSE, &old);

UNICODE_STRING svcPath;
RtlInitUnicodeString(&svcPath,
    L"\\Registry\\Machine\\SYSTEM\\CurrentControlSet\\Services\\mydriver");
NTSTATUS status = NtLoadDriver(&svcPath);

SE_LOAD_DRIVER_PRIVILEGE is privilege index 10. RtlAdjustPrivilege enables it in the current token. The registry key under HKLM\SYSTEM\CurrentControlSet\Services\<name> must exist before you call NtLoadDriver, with at least ImagePath (REG_EXPAND_SZ, global namespace path \\??\C:\...) and Type (REG_DWORD, 0x1 for kernel driver).

SCM artifacts

When you call CreateServiceA + StartServiceA, the following happens:

  1. SCM acquires its internal service database lock.
  2. SCM writes the service entry to HKLM\SYSTEM\CurrentControlSet\Services\<name>.
  3. SCM writes Windows Event Log event 7045 (“A new service was installed in the system”) to the System log.
  4. SCM starts the service. The driver loads.
  5. The service is now visible in the SCM database, sc query, and services.msc.

Event 7045 is logged by SCM unconditionally when a new service is created. It includes the service name, binary path, service type, and start type. You don’t get to suppress it without patching SCM or the event log subsystem, neither of which is subtle.

The service entry persists in the registry until you call DeleteService. The SCM in-memory database also holds it while the service exists.

Verify with Event Viewer: Open eventvwr.msc → Windows Logs → System → filter by Event ID 7045. A new entry appears within seconds of CreateServiceA. It shows the service name and binary path verbatim.

Verify with System Informer: Services tab → look for the service name. It appears immediately after CreateServiceA, before StartServiceA.

NtLoadDriver artifacts

The NtLoadDriver path never touches SCM. The artifacts are narrower:

  • The registry key under HKLM\SYSTEM\CurrentControlSet\Services\<name> must exist before the call. You created it.
  • No Event 7045. SCM was never involved.
  • No SCM database entry. sc query returns nothing. services.msc shows nothing.
  • The driver is loaded in kernel. It runs.

That’s it. The only user-mode-visible artifact at load time is the registry key you created.

The post-load registry deletion trick

Here’s the useful part: you can delete the registry key after NtLoadDriver succeeds.

c
/* Create key, set ImagePath and Type, then load */
setup_reg_key();
NtLoadDriver(&svcPath);

/* Now delete the key. The driver keeps running. */
RegDeleteTreeW(HKEY_LOCAL_MACHINE,
               L"SYSTEM\\CurrentControlSet\\Services\\mydriver");

This works because NtLoadDriver passes the registry path to the kernel as a UNICODE_STRING. The kernel reads the registry key, resolves ImagePath, maps the image, and runs DriverEntry. After that, the kernel holds a reference to the loaded image via PsLoadedModuleList. It does not keep a reference to the registry key. Deleting the key does not unload the driver.

After deletion:

  • sc query: nothing.
  • Registry: no entry under Services.
  • DriverView (and similar tools that enumerate loaded drivers via the registry): no entry.
  • The driver is still running.

NarniaLoader does exactly this. It generates a random 32-character service name, creates the service via SCM (which does fire Event 7045), starts it, then immediately deletes the registry key with RegDeleteTreeA:

c
/* NarniaLoader/NarniaLoader.c */
RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\services\\",
              0, DELETE | KEY_ENUMERATE_SUB_KEYS | KEY_QUERY_VALUE, &regHNDL);
RegDeleteTreeA(regHNDL, drvName);

That removes the registry persistence. The driver stays loaded. SCM still fired 7045 though, and the random name is in that log entry.

The NtLoadDriver path avoids 7045 entirely because SCM is never called. You get the same post-deletion state without the event log entry.

What persists after both paths

One place neither technique hides from: PsLoadedModuleList.

PsLoadedModuleList is a doubly-linked list in kernel memory (nt!PsLoadedModuleList). Every loaded kernel module, whether loaded via SCM, NtLoadDriver, or a raw MmLoadSystemImage call, gets a LDR_DATA_TABLE_ENTRY linked into it. The entry contains the module base address, size, and full path.

Enumerating it requires kernel access. Tools that can see it:

  • A kernel debugger (kd> lm lists all modules, which reads PsLoadedModuleList).
  • A driver with kernel privileges (most EDRs run one).
  • NtQuerySystemInformation(SystemModuleInformation) from user mode. This call returns the contents of PsLoadedModuleList without requiring SeDebugPrivilege. The driver shows up here regardless of what you did with the registry key.

NtQuerySystemInformation(SystemModuleInformation) is class 11. It returns an array of RTL_PROCESS_MODULE_INFORMATION structures with ImageBase, ImageSize, and FullPathName for every loaded module.

Verify with System Informer: Kernel → Modules tab (requires driver). Your driver appears in the list with its full path, even after registry key deletion.

SE_LOAD_DRIVER_PRIVILEGE: who has it

SeLoadDriverPrivilege (privilege constant 10, LUID {10, 0}) is what both paths require.

Default token assignments on Windows 10/11:

ContextHas privilegeEnabled by default
Local AdministratorsYesNo
SYSTEMYesYes
Standard userNoNo
Service (LocalService)NoNo
Service (NetworkService)NoNo

Local admins hold the privilege but it is disabled in the token by default. You need to enable it before calling NtLoadDriver. RtlAdjustPrivilege does this without needing AdjustTokenPrivileges and an explicit token handle:

c
BOOLEAN was_enabled = FALSE;
/* 10 = SE_LOAD_DRIVER_PRIVILEGE, TRUE = enable, FALSE = process token (not thread) */
NTSTATUS st = RtlAdjustPrivilege(10L, TRUE, FALSE, &was_enabled);

If you’re running as a medium-integrity admin, UAC has already stripped the privilege from the token. You need an elevated process (high integrity) for this to work. RtlAdjustPrivilege fails with STATUS_PRIVILEGE_NOT_HELD if the privilege isn’t present in the token at all.

Side-by-side comparison

SCM pathNtLoadDriver pathCreateServiceA + StartServiceAEvent 7045 written to System logSCM database entry createddriver loaded (kernel)registry key: persistsRegCreateKeyW + set ImagePath,Type manuallyno Event 7045 (SCM not involved)no SCM database entrydriver loaded (kernel)registry key: deletable post-load

What survives either way

After the registry key is deleted and SCM is out of the picture, the driver is still in PsLoadedModuleList. NtQuerySystemInformation(11) from any user-mode process returns it. No special privilege required.

c
ULONG size = 0;
NtQuerySystemInformation(11 /* SystemModuleInformation */, NULL, 0, &size);
PVOID buf = VirtualAlloc(NULL, size * 2, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
NtQuerySystemInformation(11, buf, size * 2, &size);

PRTL_PROCESS_MODULES mods = (PRTL_PROCESS_MODULES)buf;
for (ULONG i = 0; i < mods->NumberOfModules; i++) {
    RTL_PROCESS_MODULE_INFORMATION *m = &mods->Modules[i];
    printf("%p  %s\n", m->ImageBase, m->FullPathName + m->OffsetToFileName);
}

Every loaded driver shows up here. If you’re trying to hide at the kernel level you need DKOM on PsLoadedModuleList, which is a different problem.

The choice between SCM and NtLoadDriver is about user-mode artifact reduction. NtLoadDriver with post-load registry deletion gives you a driver running in kernel with no Event 7045, no SCM entry, and no registry key. The kernel still knows it’s there.