C# - How To Get Logged In User's Username and Track Activities from Windows Service

If you've ever looked at Windows Event Logs, you must have noticed that it logs too many information, most of which are not useful when you're actually trying to do some kind of forensics. To reduce this noise, I went to build a proof-of-concept Windows Service whose only task is to log users’ session activities (Lock, Unlock, Logout, Login, etc) in a different place, thus giving me a much smaller log file to dig through.

Typically, the main class in a Windows Service project is the class that inherits from ServiceBase. Luckily, ServiceBase has an override-able method that can be made to fire when a user's session activity changes. Add the code below in the class to override the method

protected override void OnSessionChange(SessionChangeDescription changeDescription)
{
    //TODO: 
}

Unfortunately, the only information about the logged-on user that the incoming parameter carries is the Session ID, which is just an integer. To be useful, we have to find a way to get the username corresponding to that ID. In earlier versions of Windows, we would have easily written the code to run the powershell quser command to get us what we want; but that command is no longer available in Windows 10. To make matters a little worse, there is no (at least, I wasn't able to find any) .NET API to call to retrieve that info. So, I was left with no other option than to go old-school.

Add this code to a class inheriting from ServiceBase

[DllImport("Wtsapi32.dll")]
private static extern bool WTSQuerySessionInformation(IntPtr hServer, int sessionId, WtsInfoClass wtsInfoClass, out IntPtr ppBuffer, out int pBytesReturned);
[DllImport("Wtsapi32.dll")]
private static extern void WTSFreeMemory(IntPtr pointer);
 
private enum WtsInfoClass
{
    WTSUserName = 5, 
    WTSDomainName = 7,
}
 
private static string GetUsername(int sessionId, bool prependDomain = true)
{
    IntPtr buffer;
    int strLen;
    string username = "SYSTEM";
    if (WTSQuerySessionInformation(IntPtr.Zero, sessionId, WtsInfoClass.WTSUserName, out buffer, out strLen) && strLen > 1)
    {
        username = Marshal.PtrToStringAnsi(buffer);
        WTSFreeMemory(buffer);
        if (prependDomain)
        {
            if (WTSQuerySessionInformation(IntPtr.Zero, sessionId, WtsInfoClass.WTSDomainName, out buffer, out strLen) && strLen > 1)
            {
                username = Marshal.PtrToStringAnsi(buffer) + "\\" + username;
                WTSFreeMemory(buffer);
            }
        }
    }
    return username;
}

If you don't have one already, add a constructor to the class; and add this line to it:

CanHandleSessionChangeEvent = true;

Now you're good to go! Access the username from the overridden method like this:

protected override void OnSessionChange(SessionChangeDescription changeDescription)
{
    string username = GetUsername(changeDescription.SessionId);
    //continue with any other thing you wish to do
}

Content originally appeared in Code Drum

Did you find this useful? Did I miss anything? Please share your thoughts. I'll appreciate your feedback.

comments powered by Disqus

Related