LSASS Internals

I recently published the analysis article about Mimikatz. Mimikatz uses the LSASS process to dump credentials on the system. I analyzed the corresponding API calls and memory operations in that article. After that, I wanted to pursue it further, so I decided to analyze the LSASS process.
Disclaimer: This research is conducted purely for educational and ethical security research purposes. It aims to understand the internal mechanisms of LSASS and does not involve or endorse any malicious activity.
There are plenty of explanations on the internet about what LSASS is, so I won’t go into it here. The purpose of this analysis is to understand how it works internally.
LSASS Process Overview
Get-Process | Where-Object { $_.StartTime } | Sort-Object StartTime | Format-Table Id, Name, StartTime, CPU, WS -AutoSize
Id Name StartTime CPU WS
-- ---- --------- --- --
92 Registry 5/15/2025 10:41:35 PM 2.21875 104865792
4 System 5/15/2025 10:41:45 PM 73 135168
292 smss 5/15/2025 10:41:45 PM 0.4375 933888
404 csrss 5/15/2025 10:41:48 PM 1.140625 5210112
480 wininit 5/15/2025 10:41:48 PM 0.234375 6991872
488 csrss 5/15/2025 10:41:48 PM 6.234375 19906560
564 winlogon 5/15/2025 10:41:49 PM 0.421875 12443648
620 services 5/15/2025 10:41:49 PM 3.859375 9359360
640 lsass 5/15/2025 10:41:49 PM 9.046875 19632128
752 svchost 5/15/2025 10:41:49 PM 11.265625 26357760
As we can see in the process list above, the LSASS process is started almost immediately after the system boot.
When checking with Task Manager in my environment, it appears that three services, CNG Key Isolation, Credential Manager, and Security Accounts Manager. These services seem to be closely related to LSASS.
File Location
The LSASS process is started by the C:\Windows\System32\lsass.exe
file.
Memory Dump
We can dump the LSASS memory by clicking on the Create dump file
from the right-click menu in the Task Manager.
According to MITRE ATT&CK, we can also dump using the following commands:
# Method 1. Procdump
procdump -ma lsass.exe lsass_dump
# Method 2. Mimikatz
sekurlsa::Minidump lsassdump.dmp
sekurlsa::logonPasswords # optional: retrieves credentials from the dump file
# Method 3. comsvcs.dll
rundll32.exe C:\Windows\System32\comsvcs.dll MiniDump [PID] lsass.dmp full
Attackers try to obtain credentials with the dump file using tools such as Mimikatz, Impacket.
Analyzing Dump File in WinDbg
Generally, we can analyze the dump file using the !analyze
command in WinDbg.
.symfix; .reload /f
!analyze -v
*******************************************************************************
* *
* Exception Analysis *
* *
*******************************************************************************
KEY_VALUES_STRING: 1
Key : Analysis.CPU.mSec
Value: 578
Key : Analysis.Elapsed.mSec
Value: 745
...Omitted...
FILE_IN_CAB: lsass.dmp
...Omitted...
PROCESS_NAME: lsass.exe
...Omitted...
SYMBOL_NAME: lsass+2136
MODULE_NAME: lsass
IMAGE_NAME: lsass.exe
FAILURE_BUCKET_ID: MISSING_CRITICAL_SYMBOLS_ntdll.dll_80000003_lsass.exe!Unknown
OS_VERSION: 10.0.19041.1
BUILDLAB_STR: vb_release
OSPLATFORM_TYPE: x64
OSNAME: Windows 10
...Omitted...
How Mimikatz Extracts Credentials from Dump File
The mimikatz’s sekurlsa::logonPasswords
command can extract credentials from a dump file, but what does it do under the hood?
This will be revealed by observing the original source code. To summarize briefly, it performs the following processing on a dump file.
// 1. Open hte handle to the dump file
HANDLE hData = CreateFile("lsass.dmp", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
// 2. Locates the dump data into LSASS memory by mapping the file.
PKULL_M_MEMORY_HANDLE *hLsassMemory = (PKULL_M_MEMORY_HANDLE) LocalAlloc(LPTR, sizeof(KULL_M_MEMORY_HANDLE));
if((*hLsassMemory)->pHandleProcessDmp = (PKULL_M_MEMORY_HANDLE_PROCESS_DMP) LocalAlloc(LPTR, sizeof(KULL_M_MEMORY_HANDLE_PROCESS_DMP)))
status = kull_m_minidump_open(hData, &(*hLsassMemory)->pHandleProcessDmp->hMinidump); // map the dump file data
// 3. Extracts the logon session data with the offsets from the located address.
sessionData.LogonId = (PLUID) ((PBYTE) aBuffer.address + helper->offsetToLuid);
sessionData.LogonType = *((PULONG) ((PBYTE) aBuffer.address + helper->offsetToLogonType));
sessionData.Session = *((PULONG) ((PBYTE) aBuffer.address + helper->offsetToSession));
sessionData.UserName = (PUNICODE_STRING) ((PBYTE) aBuffer.address + helper->offsetToUsername);
sessionData.LogonDomain = (PUNICODE_STRING) ((PBYTE) aBuffer.address + helper->offsetToDomain);
sessionData.pCredentials= *(PVOID *) ((PBYTE) aBuffer.address + helper->offsetToCredentials);
sessionData.pSid = *(PSID *) ((PBYTE) aBuffer.address + helper->offsetToPSid);
sessionData.pCredentialManager = *(PVOID *) ((PBYTE) aBuffer.address + helper->offsetToCredentialManager);
sessionData.LogonTime = *((PFILETIME) ((PBYTE) aBuffer.address + helper->offsetToLogonTime));
sessionData.LogonServer = (PUNICODE_STRING) ((PBYTE) aBuffer.address + helper->offsetToLogonServer);
// 4. Extracts the protocol-specific credential data (LM, NTLM, SHA1) with the offsets from the located address.
// Omitted...
How LSASS Starts
The first thing I’m curious about is what starts the lsass.exe
process after the system boot.
Boot Logging
To find that, I can investigate the boot logs by enabling Boot Logging in Procmon with click on Options -> Enable Boot Logging
, and then reboot the system.
After rebooting, reopen Procmon, save the Bootlog.pml
file, and start investigating this file.
Now I opened the Procmon’s Process Tree window by clicking on the Tools -> Process Tree
menu and observed it.
As a result, I found that the lsass.exe
process is started as follows:
System
└─ smss.exe
└─ smss.exe (Session 1)
└─ wininit.exe
└─ lsass.exe
At this point, I know that lsass.exe
is launched by wininit.exe
, so apply the below filters in Procmon:
- Process Name is wininit.exe then Include
- Process Name is lsass.exe then Include
Then I found the line where the lsass.exe
process is started by wininit.exe
as below:
Kernel Debugging
Next, I perform kernel debugging to confirm the actual behavior from system boot to the point where the LSASS process is started.
1. Preparation for Kernel Debugging
To prepare for kernel debugging, add a serial port in the virtual machine settings and follow the steps:
-
Check on
Use named pipe
\\.\pipe\com_1
- This end is the server.
- The other end is an application.
After that, run the following commands in Command Prompt as Administrator:
bcdedit /debug on
bcdedit /dbgsettings serial debugport:1 baudrate:115200
Next, open WinDbg in the host machine and go to File -> Attach to Kernel -> COM
and set the following items:
Baud Rate: 115200
Port: \\.\pipe\com_1
And click OK
to start the kernel debugger.
2. Starting Kernel Debugging and Breaking Immediately
Now the WinDbg debugger is waiting to connect the guest VM, so restart the VM guest machine.
Note: The important thing at this point is to click on the Break
button in WinDbg to stop the process as soon as the guest machine restarts. That’s because the timing to start investigating should be before lsass.exe
is launched.
Once connecting the guest machine in WinDbg, the process stopped with no processes started yet as below:
kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
NULL value in PsActiveProcess List
3. Break just before the LSASS Process
At this point, I want to break just before or after the lsass.exe
process starts, so I looked for the functions in the kernel module (specifically, ntoskrnl
) which can be used for this purpose.
kd> x nt!Ps*
After some research, I felt that nt!PspInsertProcess
is one of the better options. PspInsertProcess
is a function that prepares the next process which is about to be launched. The EPROCESS structure is passed as the first argument, and the executable name of the target process is stored in the ImageFileName field of the structure.
According to Windows x64 function calling convention, the RCX
register is passed as the first argument to the function, so I observed how RCX
is used in the nt!PspInsertProcess
.
kd> uf nt!PspInsertProcess
nt!PspInsertProcess:
...Omitted...
fffff805`632302d9 4c8b9170050000 mov r10,qword ptr [rcx+570h]
fffff805`632302e0 488bd9 mov rbx,rcx
...Omitted...
fffff805`632302ee 8b8140040000 mov eax,dword ptr [rcx+440h]
...Omitted...
Checking the EPROCESS structure as shown below, there are members corresponding to the positions of each of the above offsets (rcx+570h
and rcx+440h
):
kd> dt _EPROCESS
ntdll!_EPROCESS
...Omitted...
+0x440 UniqueProcessId : Ptr64 Void
...Omitted...
+0x570 ObjectTable : Ptr64 _HANDLE_TABLE
...Omitted...
Also, the ImageFileName field in this structure stores the executable name of the process started by PspInsertProcess.
+0x5a0 ImageFilePointer : Ptr64 _FILE_OBJECT
+0x5a8 ImageFileName : [15] UChar
Based on this, I first set a breakpoint at nt!PspInsertProcess
to catch the creation of each process. When the breakpoint is hit, I run the !process @rcx 1
command to inspect the EPROCESS structure passed via the RCX
register and check the image name (ImageFileName) of the target process. In other words, I execute the following commands:
kd> bp nt!PspInsertProcess
kd> g
kd> !process @rcx 1
I repeated the above process until “lsass.exe” appears in the “Image:” field of the !process
command output.
In other words, I continued executing the g
command until just before the lsass.exe
process starts.
After repeating these commands several times, I was finally able to halt execution when the lsass.exe
process was prepared:
kd> !process @rcx 1
PROCESS ffffd58ca6e0a0c0
SessionId: 0 Cid: 0248 Peb: 8cf1985000 ParentCid: 01f0
DirBase: 130563000 ObjectTable: ffff810ec44862c0 HandleCount: 0.
Image: lsass.exe
We can see the following values from the above results:
- Process Name: lsass.exe
- PID: 0248
- PPID: 01f0
The PPID (Parent PID) points to the wininit.exe
process, as shown below:
kd> !process 1f0 0
Searching for Process with Cid == 1f0
PROCESS ffffd58ca66f7080
SessionId: 0 Cid: 01f0 Peb: 3ba5568000 ParentCid: 0180
DirBase: 1302f6000 ObjectTable: ffff810ec4352480 HandleCount: 88.
Image: wininit.exe
At this point, the lsass.exe
process has not yet started, as shown below:
kd> !process 0 0 lsass.exe
(Nothing found)
This means that the lsass.exe
process has been fully initialized but has not been started yet.
4. Entering the LSASS Process on User Mode
Now, at this point, I deleted the breakpoint at nt!PspInsertProcess
and set a new breakpoint at ntdll!LdrInitializeThunk
:
kd> bc 0
kd> bp ntdll!LdrInitializeThunk
By setting the breakpoint, the lsass.exe
process is stopped immediately after it starts (before the entry function is called).
By continuing the execution at this point, the lsass.exe
process has started:
kd> g
kd> !process 0 0 lsass.exe
PROCESS ffffd58ca6e0a0c0
SessionId: 0 Cid: 0248 Peb: 8cf1985000 ParentCid: 01f0
DirBase: 130563000 ObjectTable: ffff810ec44862c0 HandleCount: 0.
Image: lsass.exe
This section was a bit lengthy, but it clearly showed how the lsass.exe
process is started.
lsass.exe Basic Flow
Note: I named the non-Windows system API functions myself, and the names might not perfectly reflect their actual behavior since I’m not good at naming things.
Entry Point
Two functions are called at the entry point.
init_seeds
: Initializes the configurations for the Security Cookie.program_main
: The program main entry.
Since the contents of init_seeds
may be common to Windows applications not specific to LSASS, I decided to dig into program_main
.
program_main
performs the following actions:
- Calls two CRT startup functions:
_initterm_e
is called first, followed by_initterm
. - Calls
lsass_main
.
lsass_main
seems to be effectively the main function of lsass.exe
. Therefore, I analyzed this function to highlight some notable behaviors.
Error Handling
It calls SetErrorMode with SEM_FAILCRITICALERRORS to prevent Windows from displaying a popup and instead send the error to the calling process.
Unhandled Exception Filter
SetUnhandledExceptionFilter
is called with RtlUnhandledExceptionFilter
as its argument.
Since ntdll!RtlUnhandledExceptionFilter
is undocumented, I searched the function on GitHub and the React OS documentation, then I found the following code:
LONG
NTAPI
RtlUnhandledExceptionFilter(IN struct _EXCEPTION_POINTERS* ExceptionInfo)
{
/* This is used by the security cookie checks, and also called externally */
UNIMPLEMENTED;
PrintStackTrace(ExceptionInfo);
return ERROR_CALL_NOT_IMPLEMENTED;
}
Critical Process
RtlSetProcessIsCritical
is called to mark the current process as a critical process. When the process is terminated, it triggers a BSOD. Its arguments are set as follows:
- NewValue = 1 (True)
- OldValue = 0 (False)
- CheckFlag = 1 (True)
Reference: NtDoc
Windows Error Reporting (WER)
WerSetFlags
is called with the following flags:
- WER_FAULT_REPORTING_FLAG_QUEUE
- WER_FAULT_REPORTING_FLAG_DISABLE_THREAD_SUSPENSION
- WER_FAULT_REPORTING_DISABLE_SNAPSHOT_HANG
- WER_FAULT_REPORTING_CRITICAL
- WER_FAULT_REPORTING_DURABLE
IsProtectedProcess Flag
It checks whether the IsProtectedProcess
bit (bit 1) in PEB->BitField
is set. If so, it jumps to the set_mitigation_policy
label; Otherwise, it jumps to the harden_lsass_process
label.
In each label, the following actions may occur:
/*
* set_mitigation_policy
*/
// 1. Sets the mitigation policy for the current process.
NtSetInformationProcess(
// ProcessHandle: lsass.exe process itself
-1,
// ProcessInformationClass: ProcessMitigationPolicy
52,
// ProcessInformation:
// - ProcessMitigationPolicyInformation.Policy = ProcessDynamicCodePolicy
// - ProcessMitigationPolicyInformation.StrictHandleCheckPolicy = 1
procInfo,
// ProcessInformationLength: 8
8
);
// 2. If STATUS_NOT_SUPPORTED, go to the harden_lsass_process.
// 3. Otherwise, exit the thread.
/*
* harden_lsass_process
*/
// 1. Enables the ProcessHandleCheckingMode for the current process.
NtSetInformationProcess(
// ProcessHandle: lsass.exe process itself
-1,
// ProcessInformationClass: ProcessHandleCheckingMode
54,
// ProcessInformation: Enables the ProcessHandleCheckingMode
1,
// ProcessInformationLength: 4
4
);
// 2. If error, exit the thread.
// 3. Sets the priority to 9.
NtSetInformationProcess(
// ProcessHandle: lsass.exe process itself
-1,
// ProcessInformationClass: ProcessBasePriority
5,
// ProcessInformation: KPRIORITY = 9
9,
// ProcessInformationLength: 4
4
);
// 4. If error, exit the thread.
// 5. Proceed the process...
Settings SystemRoot to Environment Variable
The C:\\Windows
path is assigned to the “Path” environment variable. It also uses the \\System32
string in some way, but I could not figure out its purpose. It likely attempts to construct the full C:\Windows\System32
path instead.
Initializing SIDs
RtlLengthRequiredSid
, RtlInitializeSid
, and RtlSubAuthoritySid
are used to construct several SIDs with specific subauthorities. Then, the remaining SIDs are derived from the capability name “lpacIdentityServices” by calling RtlDeriveCapabilitySidsFromName
.
Dispatch to KsecDD or SSP Client Callback
Under certain conditions, one of the following actions is performed:
- Calls
sspi!SspiSrvClientCallback
. Although this function is poorly documented, it is likely related to a Security Support Provider (SSP) such as Kerberos or NTLM, and may perform operations related to authentication or session handling. - Calls
NtDeviceIoControlFile
to send a specific IOCTL code to the “\Device\KsecDD” device driver.
Initializing LPC Server/Client
1. Starting SeLsaCommandPort Server
It initializes a port object named “SeLsaCommandPort” by calling NtCreatePort
.
It initializes a LPC listener by calling NtListenPort
with the port object.
It establishes connection with a LPC client by calling NtAcceptConnectPort
, and then calls NtCompleteConnectPort
.
And it receives incoming LPC requests from a client by calling NtReplyWaitReceivePort
.
2. Establishing Connection with SeRmCommandPort Server
It establishes a connection with the LPC server named “SeRmCommandPort” by calling NtConnectPort
.
Resolving LSA APIs in LsaSrv
It retrieves module names stored in the Extensions value under the registry key HKLM\SYSTEM\CurrentControlSet\Control\LsaExtensionConfig\LsaSrv
by calling RegOpenKeyExW
and RegQueryValueExW
. lsasrv.dll
is typically included in this value.
Next, it loads those modules by calling LoadLibraryExW
, and retrieves the following function addresses by calling GetProcAddress
:
InitializeLsaExtension
QueryLsaInterface
These functions are stored in a global structure that holds module and API pointers.
Note: There is a persistence technique that involves adding malicious DLLs to the Extensions value. Resource: detection.fyi
Resolving LSA APIs in Interfaces
It enumerates subkeys under the registry path HKLM\SYSTEM\CurrentControlSet\Control\LsaExtensionConfig\Interfaces
by calling RegEnumKeyExW
. Those subkeys are interfaces named like “1001” or “1002”.
After that, the module names are obtained from the Extension value for each interface, and the addresses of the same two functions as before are retrieved.
Preparing RPC Server
It calls RpcServerUseProtseqEpW
to register two protocol/endpoint pairs for the RPC server:
- “ncacn_np” with “\pipe\lsass”
- “ncalrpc” with “lsapolicylookup”
After that, it calls RpcServerRegisterIf3
to register the interface for the RPC server.
Calling SspiSrvInitialize
It calls sspisrv!SspiSrvInitialize
with the global function table as the first argument.
Although this function is poorly documented, it may initialize a Security Support Provider Interface (SSPI) service or some other related component.
Establishing Connection to KsecDD
It calls NtDeviceIoControlFile
to send the IOCTL code IOCTL_KSEC_CONNECT_LSA (KsecDispatch) to the “\Device\KsecDD” kernel driver. This code is used to establish the initial connection.
Starting RPC Server
It calls RpcServerListen
to signal the RPC run-time library to start listening for incoming RPC requests. This call follows the configuration set earlier by RpcServerUseProtseqEpQ
and RpcServerRegisterIf3
.
Then it calls CreateEventW
and SetEventW
to notify that the LSA RPC server has been activated by setting the event to the signaled state.
Calling LSA Functions
It calls InitializeLsaExtension
and QueryLsaInterface
using the list of function pointers previously retrieved from the registry key HKLM\SYSTEM\CurrentControlSet\Control\LsaExtensionConfig
.
Setting the LSA Initialized Event to Signaled State
It sets the event object named “LSA_SUBSYSTEM_INITIALIZED” to signaled state by calling SetEvent
.
LSASS Memory Analysis
Since I was curious about where the credentials were stored in LSASS memory, I decided to look into it directly. I could have easily extracted them using Mimikatz, but I wanted to get my hands dirty to better understand the process. So, I started WinDbg again for kernel debugging.
In WinDbg, I switched to the context of the LSASS process with the following commands:
kd> !process 0 0 lsass.exe
PROCESS ffffdd0f7cb9d080
SessionId: 0 Cid: 027c Peb: 75aa5c2000 ParentCid: 01e4
DirBase: 1363e4000 ObjectTable: ffffb5040be9d900 HandleCount: 1007.
Image: lsass.exe
kd> .process /r /p ffffdd0f7cb9d080
Extracting Credentials
First, I obtained the address pointed to by the LogonSessionList
pointer, which references a LIST_ENTRY
structure. The first field in the LIST_ENTRY
structure is Flink
, so I retrieved the address stored in that Flink
field:
kd> r @$t0 = poi(lsasrv!LogonSessionList)
By tracing the offsets using the structure implemented in Mimikatz as a reference, I was able to obtain the username, domain, and the logon server as plaintext:
kd> dt _UNICODE_STRING @$t0+90
win32k!_UNICODE_STRING
"johndoee"
+0x000 Length : 0xe
+0x002 MaximumLength : 0x10
+0x008 Buffer : 0x000001c0`b72f6240 "johndoee"
kd> dt _UNICODE_STRING @$t0+a0
win32k!_UNICODE_STRING
"DESKTOP-XXXXXXX"
+0x000 Length : 0x1e
+0x002 MaximumLength : 0x20
+0x008 Buffer : 0x000001c0`b78856a0 "DESKTOP-XXXXXXX"
kd> dt _UNICODE_STRING @$t0+f8
win32k!_UNICODE_STRING
"DESKTOP-XXXXXXX"
+0x000 Length : 0x1e
+0x002 MaximumLength : 0x1e
+0x008 Buffer : 0x000001c0`b78857c0 "DESKTOP-XXXXXXX"
Then I obtained the Credentials
field to extract the NTLM hash using the specified offset:
kd> dd @$t0+108
000001c0`b7840698 b7885760 000001c0 00000e94 00000000
kd> dd 1c0b7885760
000001c0`b7885760 00000000 00000000 00000003 00000000
000001c0`b7885770 b7840340 000001c0 00490048 0008004b
The structure of the Credentials
field is as follows:
typedef struct _KIWI_MSV1_0_CREDENTIALS {
struct _KIWI_MSV1_0_CREDENTIALS *next;
DWORD AuthenticationPackageId;
PKIWI_MSV1_0_PRIMARY_CREDENTIALS PrimaryCredentials;
} KIWI_MSV1_0_CREDENTIALS, *PKIWI_MSV1_0_CREDENTIALS;
Therefore, I could see that the address in the second line of the dd
command above is a pointer to the PrimaryCredentials
field. So, I displayed the data at that address using the following command:
kd> dd 1c0b7840340
000001c0`b7840340 00000000 00000000 00080007 00000000
000001c0`b7840350 b7840368 000001c0 01b801b0 00000000
The structure of the PrimaryCredentials
field is as follows:
typedef struct _KIWI_MSV1_0_PRIMARY_CREDENTIALS {
struct _KIWI_MSV1_0_PRIMARY_CREDENTIALS *next;
ANSI_STRING Primary;
LSA_UNICODE_STRING Credentials;
} KIWI_MSV1_0_PRIMARY_CREDENTIALS, *PKIWI_MSV1_0_PRIMARY_CREDENTIALS;
Using the offsets in that structure, I extracted the Primary
and Credentials
fields:
0: kd> dt _UNICODE_STRING 1c0b7840340+8
win32k!_UNICODE_STRING
"牐浩牡y"
+0x000 Length : 7
+0x002 MaximumLength : 8
+0x008 Buffer : 0x000001c0`b7840368 "牐浩牡y"
0: kd> dt _UNICODE_STRING 0x1c0b7840340+18
win32k!_UNICODE_STRING
"㤮龂㓇䵧???"
+0x000 Length : 0x1b0
+0x002 MaximumLength : 0x1b8
+0x008 Buffer : 0x000001c0`b7840370 "㤮龂㓇䵧???"
However, the Buffer
fields appeared garbled, while I could display the Primary
field in ASCII:
kd> da poi(0x1c0b7840340+8+8)
000001c0`b7840368 "Primary"
On the other hand, the Credentials
field had a different value when compared to the NTLM hash of the actual password.
kd> dd poi(0x1c0b7840340+18+8) L4
000001c0`b7840370 9f82392e 4d6734c7 21662bff 2508b27d
When I looked at the source code of Mimikatz, it seemed that the NTLM hash was not stored directly at this location. It appears that the NT, NTLM, and SHA1 hashes can be obtained by further tracing the offset from the address pointed to by the Buffer
field, but I decided this was enough for the current investigation.
…To be honest, I gave up due to a lack of concentration and time.