Enumerating Logon Sessions
Introduction
For quite a number of applications or services, it might be useful to know, whether a user is currently interactively logged on to a machine or not. Depending on a user currently logged on, a service might, for instance, decide to do heavy computations because sluggish response times because of heavy CPU load won't affect a logged in interactive user's "user experience". Or a network administrator might decide to remotely shut down or power off all those machines where currently nobody is logged in in order to save power or because he just remotely deployed software such as an OS patch which requires a reboot. There are numerous reasons, why it would be quite interesting to know, either from within a service running on a particular machine, or remotely, if an interactive user is currently logged in on that particular machine. This article provides source code for an interactive service that can be used to see the result of a logon session enumeration in a message box on the interactive desktop (including the Winlogon desktop, so you can enumerate logon sessions after having logged out from your machine). Additionally, source code and implementation of a WMI instance provider is presented in this article. Both the service and the WMI provider give you an accurate list of (interactive) logon sessions on NT4, W2K, WXP and W2K3 Server, including all the flavors that allow Terminal Services running. While the service can more or less be considered an educative toy, the WMI provider could really be used in a productive environment, and has been implemented with all the required robustness and security in mind, that a WMI provider, which runs in the context of a system service, should have.
Background and historical perspective
Detecting and enumerating logon sessions on NTesque Operating Systems (NT3.x, NT4, W2K,WXP, W2K3Server) has always been a daunting task for developers, and was the subject of numerous discussions on the Usenet or in programmer magazines over the years. For interactive logon sessions (as opposed to network, batch, or service style logon sessions), one thing that immediately comes to mind for that purpose is to monitor logon and logoff events: just have a counter variable that initially starts with a value of zero and is incremented with each "logon event" (whatever that is) and that is decremented with each "logoff event" (again, whatever that is). If it is zero, nobody is interactively logged in, if it is non-zero, someone is logged in. Unfortunately, the reality is not even close to being that easy, because there is no such thing as a "logon event" or a "logoff" event on NT4. So, people started tinkering with companion applications started from the autorun-keys in the registry, registering registry key notifications, polling the input desktop, testing for an instance of the shell application, and so on. Way back in 1998/1999, there were a couple of "Windows Developer's Journal" issues, where Paula Tomlinson discussed various of these ways to detect user logoffs and logons, and their drawbacks and shortcomings on NT4. In one of her first articles on this issue, she concludes with the following: "Detecting logon events from a service is unfortunately not as straightforward as it could or should be". Subsequent articles of hers prove that the same is also true for "logoff-events". For NT4 Workstation or Server, all of this is still true today and none of the methods Paula investigates in her series of articles is really 100% reliable, let alone satisfactorily from an efficiency point of view, which she frankly admits in her articles. Additionally, with the proliferation of Terminal Services, starting with the release of Windows NT4 Terminal Server Edition and culminating in today's Windows XP Fast User Switching Feature, which is a crippled form of Terminal Services, the problem got even more complex. Now, just throw into the mix, one of the various SU implementations or W2K's runas functionality, which allow to start another interactive logon session from an existing interactive logon session. Or imagine that a service impersonates a logged on user, the user logs off and the service is still acting on behalf of that user. Then the question "Is an interactive user currently logged in on that machine?" gets kind of a philosophical dimension.
With the advent of Windows 2000, a new, much better way of detecting interactive logons or logoffs was introduced in the form of "Winlogon Notification Packages". This is just a fancy name for a DLL that has a number of special-named exported functions in conjunction with a registry key under HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Notify and associated named values under that key. If you write such a DLL and register it appropriately with the required registry values in a key under the one named above, you really can monitor interactive logons or logoffs effectively and reliably. Even the official documentation of Winlogon Notification Packages states about the possible use-cases of Winlogon Notification Packages:
"Winlogon notification packages are DLLs that receive and handle events generated by Winlogon [...] and respond to Winlogon events [...] for applications that need to perform additional processing during logon or logoff, or maintain state information that must be updated when Winlogon events occur."
But what happens if a Winlogon Notification Package dutifully records all logoff and logon events, and a service in the background that maintains all of the current state information based on information it gets from the Winlogon Notification Package crashes or gets stopped, while in the meantime users log on and off? What about the login sessions that are generated on-the-fly from an already logged-in user via runas or an SU utility? In these cases, all of that previously recorded state information is just useless or does not accurately reflect the real situation. And another disadvantage comes to mind immediately: you definitely need an additional service as an active component that runs in the background and "survives" the user logon or logoff events. And still there is no remoting functionality available that would allow the machine in question to be queried for interactive logon sessions, ala WMI. WMI? Can't we do that with WMI?
What about the built-in WMI functionality?
The problem with the built-in WMI functionality is, well, uh..., that it is sometimes just not built-in. While NT's and W2K's implementation of the WMI core contains no WMI classes at all to enumerate logon sessions, XP has four of them: Win32_Session
is the base class for sessions. The derivative Win32_LogonSession
is the class for all logon sessions and it contains information, such as the name of the Authentication package that was used, the logon time, the type of logon (interactive, service, batch, or network style) and as the key property, the "logon session ID". What is missing from this class is the Terminal Services session ID that this logon session is running under. Another problem with this class becomes apparent if you do a number of logins and logoffs: when enumerating instances of this class, all previous logon sessions will still be enumerated, so if you boot up your machine, log in with some user and do a number of logoffs and logins with that user, you will see among the instances of the Win32_LogonSession
class all those previously established and already destroyed interactive logon sessions. Obviously, the system still has some data structures allocated internally, although the associated users have already logged off for quite some time. I will show more about this phenomenon of "stale logon sessions", when talking about the implementation of the service and the WMI provider for this article later.
In order to solve the problem with stale logon session using WMI, the third and the fourth WMI class comes to the rescue: Win32_SessionResource
acts as the base class for the Win32_SessionProcess
class and is an "association WMI class (that) represents an association between a logon session and the processes associated with that session", as the Platform SDK describes. This means: Win32_SessionProcess
instances are currently running processes combined with the logon sessions of their process access tokens. When combining the information of Win32_LogonSession
instances and Win32_SessionProcess
instances, it would be possible, to first filter out the non-interactive sessions from the Win32_LogonSession
instances. From the remaining interactive sessions, filter out all those where there is no instance of Win32_SessionProcess
with the same logon session ID. You then get an accurate list of logon sessions. Well, uh, ..., if it weren't for the problem that WMI gives you only the list of processes that a particular user is allowed to open.
So, let's assume you have an XP box where two users A and B are logged in interactively in two "Fast User Switching" sessions. If you then query remotely from a second machine the instances of Win32_SessionProcess
and Win32_LogonSession
with the credentials of user A, you won't see the instances that pertain to user B. The reason for this is that these built-in WMI classes impersonate the logged on user and call the APIs that provide the information about logon sessions on behalf of that user. Internally, these APIs do an access check and simply return only the information the caller is allowed to see. As it will later turn out when we look at the APIs involved, only the LocalSystem account is allowed to enumerate all logon sessions and open all processes to compare their logon session ID with the ones from the logon session enumeration in order to filter out the stale sessions. I personally would not consider it to be a security breach, if these built-in classes would have been implemented in such a way, that if a query is done by a member of the Local Administrator's group, instead of impersonating this administrative user, LocalSystem would be used to enumerate all Win32_SessionProcess
and Win32_LogonSession
instances, and a complete list of sessions and processes would thus be returned to the caller. This is exactly the way I implemented the WMI provider that I present in this article, and I find that an Administrator is a legitimate bearer of this information. I am prepared to get flame emails from readers of this article for this last statement of mine.
APIs for acquiring logon session information
The two APIs that allow for enumerating logon sessions and retrieving information of logon sessions are LsaEnumerateLogonSessions
and LsaGetLogonSessionData
. Both of them are implemented in secur32.dll and are officially documented for Windows XP. Upon successful execution, LsaEnumerateLogonSessions
returns an array of locally unique IDs, or LUIDs, to the caller. A LUID is a 64-bit integer value that is issued by the system and that is guaranteed to be unique until the system is rebooted. This means that no two logon sessions can have the same LUID unless the computer is rebooted in between creation of the two logon sessions. In order to retrieve information about the individual logon sessions, you can iterate over the array of LUIDs returned by LsaEnumerateLogonSessions
and pass each LUID to LsaGetLogonSessionData
. Upon success, LsaGetLogonSessionData
creates a SECURITY_LOGON_SESSION_DATA
struct
for the caller. This struct
contains all necessary information about that particular logon session, such as the LUID itself, user name and logon domain, the authentication package used to authenticate the user, the logon type (interactive, service, batch, or network style), the Terminal Services session ID, the logon time, and the SID of the user under whose auspices the logon session is running. One particularly nice thing about these two APIs is that they also work on Windows 2000, although they are only documented for XP.
How to filter out the stale logon sessions
Fortunately, every process token contains a reference to its logon session ID. You can obtain a process' logon session ID by opening the process via OpenProcess
with PROCESS_QUERY_INFORMATION
, opening the process token via OpenProcessToken
with TOKEN_QUERY
followed by a GetTokenInformation
with TokenStatistics
. This will return a TOKEN_STATISTICS
struct
whose AuthenticationId
struct
member is the logon session ID, which is one of the LUIDs returned by LsaEnumerateLogonSessions
. So, if you retrieve the array of logon session LUIDs via LsaEnumerateLogonSessions
, and additionally a second array of LUIDs that can be found from enumerating all the processes and getting their logon session IDs, you have all the information you need to filter out the stale logon sessions: those are simply all the LUIDs in the array from LsaEnumerateLogonSessions
that can't be found in the array of logon session IDs you get from the process enumeration.
How can it be done on NT4?
On NT4, all the APIs that allow for logon session enumeration are either not documented or missing. In order to get a list of interactive logon sessions on NT4, a completely different approach is therefore necessary. Luckily, one person already found this approach: Felix Kasza, former Grandeur and Guru of the Microsoft kernel newsgroup, presents in one of his great code samples, a nice approach to identify interactive processes. Felix' basic idea is that you can identify an interactive process easily by enumerating the group SIDs of its process token: if it contains two well-known SIDs, namely the INTERACTIVE and the LOCAL group SID, and a logon SID, it is considered an interactive process. For the service and the WMI provider in this article, I adapted this idea and implemented it in the function EnumNT4StyleInteractiveSessions
.
Using the technique from Felix, it is possible to easily identify the user name, the logon domain, the user SID, and the logon session LUID of an interactive logon session. What cannot be done is retrieving the authentication package and the Terminal Services session ID. But at least, the Terminal Services session ID can be obtained using one of the WTS APIs that have been introduced with Service Pack 4 of Windows NT4 Terminal Server Edition: WTSEnumerateProcesses
returns an array of WTS_PROCESS_INFO
struct
s whose struct
members include the PID, the Terminal Services session ID, user name and SID, and the process name. By finding a match in PID, user name and SID, and the process name between the interactive session information you get from Felix' approach and the information you get from WTSEnumerateProcesses
, it is trivial to get the Terminal Services session ID for each logon session.
Using the code
The code for this article comes as a Visual Studio 6 Workspace for the service and the WMI provider. All of the code compiles cleanly under W4. Where the usage of STL or the WMI Provider Framework requires W3, my own code is wrapped in a #pragma warning (push,4) - #pragma warning (pop)
sequence. All of the code is entirely UNICODE because on the NT platform, using ANSI/MBCS is just plain dumb in my opinion. Unfortunately, I do not yet own a license for a newer Visual Studio version, but I don't expect, that you should encounter any difficulties in converting it into a Visual Studio .NET project. I suggest that you copy the content of the accompanied zip file into a directory on a local hard drive (that is not subst'ed), otherwise you might get into trouble getting the service or the WMI provider to run.
Using the service presented in this article
The service can be found in the project named "enumeratelogonsessions". Build the debug or the release version of the service, and open a command prompt where you navigate to the directory of the binary (lsd.exe) that must have been built for you. In order to install the service, log in as a member of the local administrator's group and type:
lsd.exe -install
followed by a:
net start logonsessiondumper
in order to start the service. If the service is running, hold down the F2 key for a few seconds, and watch if you can see a message box being created for you. The message box might be created somewhere in the background for you, so you might have to do some Alt-Tabbing or minimize or close other windows on your desktop in order to see it. The message box might also be quite large, depending on the number of logon sessions. It might be so large that you can't even see the OK-button at the bottom. In that case, simply hit the Enter key to dismiss that message box. This first message box, started by F2, will show all the logon sessions that come from a call to LsaEnumerateLogonSessions
, including the stale ones and service or network style logon sessions. After this first message box, a second one will appear, that contains all the interactive logon sessions obtained via the aforementioned EnumNT4StyleInteractiveSessions
function which uses Felix' approach. If you dismiss the second message box, no message box will appear unless you hold down F2 again for a few seconds. However, if instead of holding down F2, you hold down Shift-F2 for a few seconds, you will get as the first message box the logon sessions from LsaEnumerateLogonSessions
minus the stale ones, i.e., you will get only those logon sessions that are "backed" by a currently running process' access token. Now, log out from your machine and when on the Winlogon desktop, do the same experiment again. The first message box when invoked via F2 will probably still show the same list as before your logoff, but when invoked via Shift-F2, it should be one interactive logon session less than before. If there is now no other user logged in via Terminal Services or Fast User Switching, you will probably only get the first message box, because the enumeration via EnumNT4StyleInteractiveSessions
will return no logon session and therefore that message box won't appear at all. If you think that you now played enough with the service, log in again again as a member of the local administrator's group, open a command prompt, navigate to the directory where the service's binary resides in, and issue a:
net stop logonsessiondumper
which will stop the service, followed by a:
lsd.exe -remove
which will remove the service from the Service Control Manager's database.
Using the WMI provider presented in this article
The WMI provider for this article is based on the WMI provider framework. If you have only Visual Studio 6 without Platform SDK installed, you might get some problems when compiling this provider because of missing libraries and header files. If this is the case, you should download the most recent Platform SDK and install it appropriately. I expect that the newer Visual Studio versions already have the required header files and libraries, but nevertheless, it is always a good idea to have the latest and greatest Platform SDK installed. After a successful compilation of the WMI provider, you should have a DLL named ilogprov.dll in your debug or release subdirectory under the wmiprov subdirectory of the project. Now, you have to register the provider in your system, and for this purpose, start a command prompt and navigate to the directory that contains the freshly built provider DLL. For this purpose, type:
regsvr32.exe ilogprov.dll
in the command prompt. If you receive an error like "LoadLibrary failed.." when registering the debug build version, then it is very likely that the debug version of the WMI Provider Framework cannot be found. The name of that DLL is framedyd.dll, and it should be in the bin directory of your Platform SDK installation. Just copy it from there into your Windows directory or into a directory that is contained in your PATH environment variable, and you're probably done. A good idea might also be to copy it into %SYSTEMROOT%\SYSTEM32\WBEM, because this directory is normally contained in the PATH environment variable, and it is the directory where the release version of this DLL, framedyn.dll, resides. If you still have problems with the debug build, the debug version of the Visual C runtime might be missing, so you might need to find or reinstall an appropriate version of msvcrtd.dll for your development environment. If you now still have problems, it might be worthwhile to launch the Dependency Walker (depends.exe) tool which is very helpful in troubleshooting because it indicates in its left pane's tree view, those DLLs it can't find. On NT4, a missing psapi.dll might be a reason for failure, because the provider (as well as the service) requires PSAPI.DLL. PSAPI.DLL is available as a redistributable for NT4.
After having registered the provider as a COM component in the system, you have to add the classes of this provider into the local CIMOM repository of your machine. For this purpose, open a command prompt and navigate to the wmiprov subdirectory of this article's project. You should find a file named ILogonProv.MOF there. Now type:
mofcomp ILogonProv.MOF
on the command prompt, which should issue the following statement on stdout:
Parsing MOF file: ILogonProv.MOF
MOF file has been successfully parsed
Storing data in the repository...
Done!
Now, the provider should be fully functional on your machine, and you can verify this by starting the WMI CIM Studio. For older Platform SDKs, the WMI CIM Studio was a file named studio.htm in the BIN\WMI directory of the Platform SDK installation and is today available as a separate download from Microsoft, dubbed "WMI tools". In case you cannot find it in your Platform SDK installation, do a Google Search for "WMI CIM STUDIO", and one of the first hits should be a download link from Microsoft for wmitools.exe, which is the self-extracting executable installation for the WMI Tools. Note that you have to open the studio.htm file with Microsoft Internet Explorer. If your default web browser happens to be Mozilla or Opera, opening this file won't work as expected, so you may have to reside to launching this file with a right-mouse button click in Explorer and choosing "Open With"-"Internet Explorer" from the context menu.
After having started the WMI CIM Studio, you are presented with a dialog where you can provide the namespace to be examined. I decided to implement the class of my WMI provider in my own namespace named "Honeypie". So, in order to enumerate the instances of my class, you should provide the namespace root\honeypie in this dialog and click the OK button. Next comes another dialog box labeled "WMI CIM Studio Login". Just click OK here and you should get a tree control in the left pane of WMI CIM Studio. One of the tree items should be labeled "ILogonSession". Now, select this item. In the right pane, you should now see the class properties of the IlogonSession
WMI class that I defined in the ILogonProv.MOF file you previously added to the CIMOM repository. At the top of the right pane, you should find a row of toolbar buttons. Locate the toolbar button that has a tooltip labeled "Instances", and press it. You should now get a list of interactive logon sessions in the right pane. If you see only one row in this pane, you see the properties of your own current interactive logon session. If you get an access denied error, you are probably not logged in as a member of the local administrator's group. You can now double-click one of the rows in the right pane in order to get detailed information about this logon session. The class properties you can get here are the logon session LUID ("SessionLUID
", the key property), the user name ("User
") and SID ("UserSID
"), the logon domain ("Domain
"), and the Terminal Services session ID ("TSSessionID
"). If you have a second computer and your computers are connected via a network, it might be worthwhile to connect remotely with the WMI CIM Studio to a computer where the provider is properly installed. Now, login and logout there, or create new sessions either as XP Fast User Switching sessions, Terminal sessions or via runas, and watch what happens in WMI CIM Studio after each change on the remote machine (Note however, that WMI CIM Studio is not really a tool that updates its views instantly, so you have to refresh with F5 and do all the namespace selection and authentication stuff again after each change of the remote computer's state). Please consult the accompanying help files of WMI CIM Studio if you don't know how to remotely connect to another machine with WMI CIM Studio.
Points of Interest and Implementation Details
I think the code contains a lot of interesting stuff for people who are inclined into security and systems programming. Because the service itself is only meant to be a toy to play with in order to get a feeling for the subject, it is not really coded with the desire for catching all errors or exceptions in mind. The usage of STL without being prepared for catching exceptions only illustrates this sloppiness of mine. However, for the shared code between the service and the WMI provider, and the provider itself, I would be very grateful if readers would review my code and point out potential security problems or similar, like for instance, bad propagation of error codes up the call chain, memory leaks, or other nasty things. Since this is my first WMI provider, I would also be very grateful if readers with a solid background on WMI provider writing would take a critical look at my code. As a side note: I chose to implement my provider with the WMI Provider Framework because it was really easy to use this framework and I had my provider up and running almost instantly. I know that today Microsoft discourages the use of the WMI Provider Framework in favor of .NET based providers or ATL-based providers. However, the first option would probably not run at all on NT4, and the second option would require redistribution of the ATL runtime which might be too much of an intrusion on NT4 based systems. In contrast, the WMI Provider Framework based provider in this article works out of the box with NT4 SP6 and the WMIcore for NT4 installed (plus the psapi.dll redistributable, if not yet installed).
The WMI provider does a version check of the operating system and decides which functions to use for enumerating logon sessions. On NT4, it uses the aforementioned function EnumNT4StyleInteractiveSessions
, and on more recent operating systems, it uses the function EnumLogonSessions
which uses the more elegant Lsa... functions that are missing on NT4. Both EnumNT4StyleInteractiveSessions
and EnumLogonSessions
have the exact same prototype and usage semantic, and this is why the provider uses a function pointer (LOGON_SESSIONENUM CInteractiveLogonSession::m_pfEnumLogonSession
) for enumerating logon sessions. The function pointer is initialized according to the operating system version check, and points either to EnumNT4StyleInteractiveSessions
or to EnumLogonSessions
. Both functions work like the following: the caller passes to the function an array of class CLogonSessionData
as an in
-parameter and the number of array elements as an in
/out
-parameter. If the array is sufficiently large and everything goes well, the function will fill the array and return TRUE
as an indication of success. If the array is not large enough, the functions will return FALSE
and GetLastError()
will yield ERROR_MORE_DATA
. Additionally, the required size of the array will be returned in the array size in
/out
-parameter. If you don't have an NT4 machine available but you want the provider to work the way it works on NT4 also on more recent operating systems, then locate the following line in the file ILogonSession.cpp:
/// #define EMULATE_NT4_BEHAVIOUR
and remove the three slashes.