65.9K
CodeProject is changing. Read more.
Home

Using the Local Security Authority to Enumerate User Sessions in .NET

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.17/5 (6 votes)

Mar 28, 2007

3 min read

viewsIcon

73622

downloadIcon

1213

An article on enumerating Windows user logon sessions in .NET

Introduction

The Local Security Authority, or LSA, has been included in the Windows operating systems since Windows 2000. LSA is responsible for account validation, management of local security policy, auditing, maintaining sessions and the generation of tokens for login and impersonation. At times you may want to determine what users are logged onto a machine, be it local, remote or system service accounts. LSA maintains a range of information, including usernames, domain information, login times, the authentication package used, SIDs and terminal services session information. Unfortunately, .NET doesn't provide a simple way to interact with LSA, so you have to Interop with the LSA functions directly to enumerate user sessions and extract data.

LSA User Session Enumeration

To get a list of all the current unique logins on a machine, LSA provides the LsaEnumerateLogonSessions function. This returns a pointer to an array of LUIDs, or locally unique identifiers. You need to iterate through and marshal this array to get access to the LUIDs. For each LUID, a call to LsaGetLogonSessionData will enable the extraction of all the login session information into the SECURITY_LOGON_SESSION_DATA struct.

Interop

Most of the interop declarations are straight forward. However, you will need to explicitly marshal most of the data out of the SECURITY_LOGON_SESSION_DATA struct to retrieve useful information. Below are the interop declarations required.

[DllImport("secur32.dll", SetLastError = false)]
private static extern uint LsaFreeReturnBuffer(IntPtr buffer);

[DllImport("Secur32.dll", SetLastError = false)]
private static extern uint LsaEnumerateLogonSessions
        (out UInt64 LogonSessionCount, out IntPtr LogonSessionList);

[DllImport("Secur32.dll", SetLastError = false)]
private static extern uint LsaGetLogonSessionData(IntPtr luid, 
    out IntPtr ppLogonSessionData);

[StructLayout(LayoutKind.Sequential)]
private struct LSA_UNICODE_STRING
{
    public UInt16 Length;
    public UInt16 MaximumLength;
    public IntPtr buffer;
}

[StructLayout(LayoutKind.Sequential)]
private struct LUID
{
    public UInt32 LowPart;
    public UInt32 HighPart;
}

[StructLayout(LayoutKind.Sequential)]
private struct SECURITY_LOGON_SESSION_DATA
{
    public UInt32 Size;
    public LUID LoginID;
    public LSA_UNICODE_STRING Username;
    public LSA_UNICODE_STRING LoginDomain;
    public LSA_UNICODE_STRING AuthenticationPackage;
    public UInt32 LogonType;
    public UInt32 Session;
    public IntPtr PSiD;
    public UInt64 LoginTime;
    public LSA_UNICODE_STRING LogonServer;
    public LSA_UNICODE_STRING DnsDomainName;
    public LSA_UNICODE_STRING Upn;
}

private enum SECURITY_LOGON_TYPE : uint
{
    Interactive = 2,        //The security principal is logging on 
                            //interactively.
    Network,                //The security principal is logging using a 
                            //network.
    Batch,                  //The logon is for a batch process.
    Service,                //The logon is for a service account.
    Proxy,                  //Not supported.
    Unlock,                 //The logon is an attempt to unlock a workstation.
    NetworkCleartext,       //The logon is a network logon with cleartext 
                            //credentials.
    NewCredentials,         //Allows the caller to clone its current token and
                            //specify new credentials for outbound connections.
    RemoteInteractive,      //A terminal server session that is both remote 
                            //and interactive.
    CachedInteractive,      //Attempt to use the cached credentials without 
                            //going out across the network.
    CachedRemoteInteractive,// Same as RemoteInteractive, except used 
                            // internally for auditing purposes.
    CachedUnlock            // The logon is an attempt to unlock a workstation.
}

Enumerate and Extract Logon Session Information

Below is the primary code required to enumerate users and extract information.

  DateTime systime = new DateTime(1601, 1, 1, 0, 0, 0, 0); //win32 systemdate

  UInt64 count;
  IntPtr luidPtr = IntPtr.Zero;
  LsaEnumerateLogonSessions(out count, out luidPtr);  //gets an array of 
                                                      //pointers to LUIDs

  IntPtr iter = luidPtr;                              //set the pointer to the
                                                      //start of the array

  for (ulong i = 0; i < count; i++)                   //for each pointer in the
                                                      //array
  {
      IntPtr sessionData;

      LsaGetLogonSessionData(iter, out sessionData);
      SECURITY_LOGON_SESSION_DATA data =(
          SECURITY_LOGON_SESSION_DATA)Marshal.PtrToStructure
                (sessionData, typeof(SECURITY_LOGON_SESSION_DATA));

      //if we have a valid logon
      if (data.PSiD != IntPtr.Zero)
      {
          //get the security identifier for further use
          System.Security.Principal.SecurityIdentifier sid = 
        new System.Security.Principal.SecurityIdentifier(data.PSiD);

          //extract some useful information from the session data struct
          string username = Marshal.PtrToStringUni(
              data.Username.buffer).Trim();
                                                    //get the account username
          string domain =  Marshal.PtrToStringUni(
              data.LoginDomain.buffer).Trim();
                                                     //domain for this account
          string authpackage = Marshal.PtrToStringUni(
              data.AuthenticationPackage.buffer).Trim();
                                                      //authentication package

          SECURITY_LOGON_TYPE secType = (SECURITY_LOGON_TYPE)data.LogonType;
          DateTime time = systime.AddTicks((long)data.LoginTime);
          //get the datetime the session was logged in

          //do something with the extracted data, ie, add to a display control
          listBox1.Items.Add(
            "User: " + username + " *** Domain: " + domain + " *** Login Type: 
        (" + data.LogonType + ") " + secType.ToString() 
            +" *** Login Time: "+time.ToLocalTime().ToString());

      }
      iter = (IntPtr)((int)iter + Marshal.SizeOf(typeof(LUID))); 
      //move the pointer forward
      LsaFreeReturnBuffer(sessionData);
      //free the SECURITY_LOGON_SESSION_DATA memory in the struct
  }
  LsaFreeReturnBuffer(luidPtr);       //free the array of LUIDs

SecurityIdentifier

Once you have populated the SECURITY_LOGON_SESSION_DATA, you can create a SecurityIdentifier class. This is the .NET representation of the SID, and when combined with other .NET classes, enables you to compare users, enum permissions, determine which user is executing a process, as well as other related tasks in the System.Security.Principal namespace. For example, to test if an enumerated user is the current interactive user on the machine;

   System.Security.Principal.WindowsIdentity currentUser = 
            System.Security.Principal.WindowsIdentity.GetCurrent();
   ...
   //if we have a valid logon
   if (data.PSiD != IntPtr.Zero)
   {
        System.Security.Principal.SecurityIdentifier sid = 
            new System.Security.Principal.SecurityIdentifier(data.PSiD);
        if (sid.CompareTo(currentUser.User) == 0 && 
            data.LogonType == (uint)SECURITY_LOGON_TYPE.Interactive)
        {
            // this is the current interactive user
            // be aware the same SID can be shared simultaneously by 
            //multiple logon sessions (for the same user, but in 
            //different logon modes)
        }
    }
    ...

Points of Interest

Some of the interop used in this process is quite complex, hence my decision to post this code for others to use. The marshalling and iteration through arrays-of-pointers-to-structs is complex to understand if you haven't had experience in C/C++, especially with array and pointer manipulation. Trying to then convert that into interop can become a little daunting.

The signed and unsigned integer and long data types in the structs were difficult to match to the definitions in Ntsecapi.h, and don't necessarily align to the suggested marshalling types. I have run this through FxCop, tested it under the 32 and 64 bit .NET Frameworks, and it executes correctly. I suggest caution if you intend to change the struct definitions.

Conclusion

The LSA functions are very powerful, however almost none of that functionality is exposed natively in the .NET Framework classes. This example illustrates some of the abilities of LSA, and how to Interop with LSA's functions and structures. There are also a range of good articles on codeproject covering other aspects of LSA, so have a look around for examples of impersonisation and terminal session manipulation.