Pavel Yosifovich, Windows internals trainer at TrainSec Academy and co-author of the Microsoft Press Windows Internals book series, walks through one of the least visible but most important parts of the Windows security model: logon sessions. If you have spent time writing Windows code or analyzing Windows processes, you have almost certainly worked with access tokens. What is less obvious is where those tokens come from, and why they exist as separate objects at all. That answer is logon sessions.
What Does a Windows Logon Session Actually Hold?
A logon session is a kernel object that stores the complete security context of an entity that has authenticated to the system. The word “logged in” here does not mean an interactive desktop session necessarily. Windows creates a logon session for every type of authentication: interactive login, network logon, batch logon, and service logon. Each one produces a logon session object regardless of whether a human being was involved.
Each logon session holds:
- The principal’s username and SID (Security Identifier)
- The group SIDs the principal belongs to
- The privileges associated with that account
- A unique identifier called the LUID (Local Unique ID)
The LUID is a 64-bit value. It is unique for the lifetime of the running system. After a reboot, new logon sessions will receive different LUIDs. The identifier is local to the machine, not globally unique across systems.
Why Do Processes Use Tokens Instead of Accessing the Logon Session Directly?
A natural question is: if the logon session already holds all the security information, why not give every process a handle to that logon session and skip the extra layer?
The answer is that a logon session acts as a blueprint. Tokens are copies of that blueprint, and tokens can be slightly adjusted per-process. You can enable or disable privileges that are already present in the token. What you cannot do is add privileges that were never granted to the account, or change the SID. Those fixed attributes come from the logon session and are permanent for the lifetime of the session. A new login is required to change them.
This design means two processes running under the same user account can carry tokens that differ from each other. An elevated shell has more privileges enabled than a non-elevated shell. Both processes belong to the same user, but their tokens came from different logon sessions, which is why the logon session LUID differs. One logon session was created for the standard user context, another for the elevated context.
When a process tries to access a resource such as a file or a registry key, the kernel checks the primary token of the process against the security descriptor protecting that resource. The logon session itself is not consulted at that point. The token is the active security credential.
How Many Logon Sessions Are Running on a Typical Windows System?
More than most developers expect. Even before an interactive user logs in, Windows has already created at least three logon sessions for its built-in service accounts. These are fixed, well-known LUIDs:
- SYSTEM account (SID S-1-5-18): logon session LUID
0x3E7(decimal 999) - Local Service account (SID S-1-5-19): logon session LUID
0x3E5(decimal 997) - Network Service account (SID S-1-5-20): logon session LUID
0x3E4(decimal 996)
An interactive user who is also running an elevated instance adds at least two more: one for the standard user token and one for the elevated token. Those come from separate logon sessions even though they represent the same human user account. Virtual machines, Remote Desktop sessions, and background services with custom accounts each add further sessions.
In Process Explorer, the Security tab on any process shows the primary token of that process. The username, SID, group memberships, privilege list, and logon session LUID shown there all come from the token attached to the process, not from a generic security descriptor for the process object itself. The logon session ID listed here is the LUID of the session that originally produced this token.
To see all active logon sessions on a system, logonsessions.exe from Sysinternals is the simplest approach. It requires administrator privileges because the underlying API requires access to the LSASS database. On a developer machine with a few virtual machines running, 20 or more active logon sessions is typical. Pavel’s own System Explorer tool (available on his GitHub) shows the same data in a slightly different format.
How Do You Enumerate Logon Sessions Programmatically?
The LSA API exposes two functions for this. Include NTSecAPI.h and link against secur32.lib:
#include <Windows.h>
#include <NTSecAPI.h>
int main() {
ULONG count;
PLUID luids;
LsaEnumerateLogonSessions(&count, &luids);
for (ULONG i = 0; i < count; i++) {
PSECURITY_LOGON_SESSION_DATA data;
if (LsaGetLogonSessionData(&luids[i], &data) == 0) {
wprintf(L"LUID: %08X-%08X User: %.*s Session: %u Type: %u\n",
data->LogonId.HighPart, data->LogonId.LowPart,
data->UserName.Length / sizeof(WCHAR), data->UserName.Buffer,
data->Session, data->LogonType);
LsaFreeReturnBuffer(data);
}
}
LsaFreeReturnBuffer(luids);
return 0;
}LsaEnumerateLogonSessions allocates and returns an array of LUID values, one per active session. LsaGetLogonSessionData takes one of those LUIDs and returns a pointer to a SECURITY_LOGON_SESSION_DATA structure. That structure contains the username, domain, authentication package name, logon type, console session number, and logon time.
One subtlety: UserName in SECURITY_LOGON_SESSION_DATA is an LSA_UNICODE_STRING, which is a counted string and not necessarily null-terminated. Use the %wZ format specifier with the length divided by sizeof(WCHAR) rather than %ws, otherwise you risk reading past the end of the buffer.
Both the LUID array and each SECURITY_LOGON_SESSION_DATA buffer are allocated by the API. Call LsaFreeReturnBuffer on both when you are done with them. Both functions return an NTSTATUS value where zero indicates success. The process must run as administrator; LsaGetLogonSessionData returns access denied for all sessions if the caller does not have the required privilege, even though LsaEnumerateLogonSessions itself succeeds.
For developers who also want to understand how injecting into a process relates to the security context it carries, the DLL Injection with Windows Application Verifier post covers how to attach a DLL to a target process very early in startup, at the point where the primary token is already attached.
What Does a Logon Session Look Like in the Kernel Debugger?
The _TOKEN kernel structure has a field called AuthenticationId of type LUID. Despite the name, this is the logon session ID, not an authentication identifier in the Kerberos sense. The value matches what Process Explorer shows as the logon session LUID in the Security tab.
The _TOKEN structure also contains a LogonSession pointer of type _SEP_LOGON_SESSION. This is an undocumented kernel structure. It holds the LogonId, the account name, the authority name, and a reference to what is effectively the master token: the original token created when the logon session was first established by LSASS.
When a new process is created by an existing one, its token is duplicated as the primary token of the new process. The duplicate can be adjusted: virtualization state, per-privilege enable/disable status, and a handful of other fields can change per-process. The SID, the group list, and the privilege set are fixed and cannot be modified in any individual token.
Every token that belongs to the same logon session points to the same _SEP_LOGON_SESSION address. Two processes belonging to different logon sessions, even for the same user account, point to different addresses.
For privileged processes that need access to a user’s primary token, WTSQueryUserToken provides a handle to the logon session’s master token. Closing that handle carelessly could affect any processes still running under that session. This function is also relevant to EDR tooling that needs to spawn processes in the context of a specific logged-on user, which is covered in depth in the EDR Internals: Research and Development course.
What This Means Practically
- Audit active logon sessions with
logonsessions.exe -pto map each session to the processes running under it, and identify unexpected service or network logon sessions. - Check the logon session LUID in Process Explorer’s Security tab to confirm whether two elevated processes share a logon session or come from separate ones.
- Build your own enumeration tooling with
LsaEnumerateLogonSessionsandLsaGetLogonSessionDatato correlate sessions with process lists without depending on third-party utilities. - In WinDbg, follow
dt nt!_TOKENand then theLogonSessionpointer to trace any process’s security identity back to the originating authentication event. - When correlating malware behavior or EDR telemetry, compare
AuthenticationIdvalues across token objects to determine whether a suspicious process shares a logon session with a legitimate one or established its own session independently.
Keep Learning
Logon sessions, tokens, and privileges are covered in full in Windows Internals: Day 5, including how security descriptors work, how the kernel evaluates access checks, and how tools like Process Explorer surface these details. If you are working on EDR tooling or malware analysis, the EDR Internals: Research and Development course goes further into how logon sessions and token manipulation show up in both offensive techniques and detection logic.