Get username (or profile path) of current logged on user from app running as admin or service even if running in RDP

Darren Rose 281 Reputation points
2022-05-13T15:39:06.783+00:00

Hi

I need to be able to get the username (or profile path) of the currently logged on user from my app which may be running either as admin or a service running under system account

I have tried all the various methods mentioned found on google to varying degrees of success, but none of them work in all circumstances e.g. WMI "SELECT UserName FROM Win32_ComputerSystem" doesn't work if running in RDP, or WMI get owner of a process such as explorer.exe is too slow.

I have also tried looking in registry, but a lot of the locations where path / name is shown e.g. HKEY_CURRENT_USER\Volatile Environment is then not available when accessing registry as admin or system, it only shows when accessing it as the user.

Any ideas please?

Windows API - Win32
Windows API - Win32
A core set of Windows application programming interfaces (APIs) for desktop and server applications. Previously known as Win32 API.
2,420 questions
VB
VB
An object-oriented programming language developed by Microsoft that is implemented on the .NET Framework. Previously known as Visual Basic .NET.
2,569 questions
{count} vote

Accepted answer
  1. RLWA32 40,286 Reputation points
    2022-05-15T10:20:52.287+00:00

    The following code can be used in a service running as LocalSystem to retrieve the user name and profile path for the logged on interactive user. The service used for this code was written using .Net Framework 4.8. Its not guaranteed to be bug free so you should test thoroughly.

    The code is written so that it accepts a custom command that can be issued to the service by an interactive user. The command code is integer value 142 and can be issue to the service by using the sc.exe command. For example, at a command prompt enter "sc control <servicename> 142". The service will report the information by writing an entry in the Application Event Log.

    Code in VB.Net service -

        Protected Overrides Sub OnCustomCommand(command As Integer)
            If command = 142 Then
                ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf LoggedOnUserInfo))
            End If
        End Sub
    
        Private Structure USER_INFO
            Public username As String
            Public profilepath As String
        End Structure
    
        Private Sub LoggedOnUserInfo()
            Dim interactiveuser As USER_INFO = New USER_INFO()
            Try
                If GetLoggedOnUser(interactiveuser) Then
                    EventLog1.WriteEntry(String.Format("Username: {0}, Profile path: {1}", interactiveuser.username, interactiveuser.profilepath))
                Else
                    EventLog1.WriteEntry("Failed to retrieve information for logged on user", EventLogEntryType.Error)
                End If
            Catch ex As Exception
                EventLog1.WriteEntry(ex.Message)
            End Try
        End Sub
    
        Private Function GetLoggedOnUser(ByRef pUinfo As USER_INFO) As Boolean
            Dim sessions() As Win32.WTS_SESSION_INFO = New Win32.WTS_SESSION_INFO() {}
            Dim Retval As Boolean = False
    
            If GetSessions(sessions) Then
                Dim ui As New USER_INFO()
                If GetActiveSessionInfo(sessions, ui) Then
                    pUinfo = ui
                    Retval = True
                End If
            End If
            Return Retval
        End Function
    
        Private Function GetSessions(ByRef pSessions As Win32.WTS_SESSION_INFO()) As Boolean
            Dim iSessions As IntPtr = IntPtr.Zero
            Dim count As UInteger
            Dim Retval As Boolean = False
            Try
                If (Win32.WTSEnumerateSessions(IntPtr.Zero, 0, 1, iSessions, count)) Then
                    ReDim pSessions(count - 1)
                    Dim cbytes = Marshal.SizeOf(Of Win32.WTS_SESSION_INFO)()
                    Dim p As IntPtr = iSessions
                    Dim offset As Integer = 0
                    For i = 0 To count - 1 Step 1
                        pSessions(i) = Marshal.PtrToStructure(Of Win32.WTS_SESSION_INFO)(p + offset)
                        offset += cbytes
                    Next
                    Retval = True
                Else
                    Throw New Win32Exception(Marshal.GetLastWin32Error())
                End If
            Finally
                If (iSessions <> IntPtr.Zero) Then
                    Win32.WTSFreeMemory(iSessions)
                End If
            End Try
    
            Return Retval
        End Function
    
        Private Function GetActiveSessionInfo(sessions As Win32.WTS_SESSION_INFO(), ByRef pUinfo As USER_INFO) As Boolean
            Dim Retval As Boolean = False
            For Each sess As Win32.WTS_SESSION_INFO In sessions
                If (sess.State = Win32.WTS_CONNECTSTATE_CLASS.WTSActive) Then
                    Dim iBuffer As IntPtr = IntPtr.Zero
                    Dim bytesreturned As UInteger
                    Try
                        If Win32.WTSQuerySessionInformation(IntPtr.Zero, sess.Sessionid, Win32.WTS_INFO_CLASS.WTSSessionInfo, iBuffer, bytesreturned) Then
                            Dim activesession As Win32.WTSINFO = Marshal.PtrToStructure(Of Win32.WTSINFO)(iBuffer)
                            pUinfo.username = activesession.UserName
                            pUinfo.profilepath = GetLoggedOnProfilePath(activesession.SessionId)
                            Retval = True
                        Else
                            Throw New Win32Exception(Marshal.GetLastWin32Error())
                        End If
                        Exit For
                    Finally
                        If (iBuffer <> IntPtr.Zero) Then
                            Win32.WTSFreeMemory(iBuffer)
                        End If
                    End Try
                End If
            Next
    
            Return Retval
        End Function
        Private Function GetLoggedOnProfilePath(SessionId As UInteger) As String
            Dim strprofile As String = String.Empty
            Dim token As IntPtr = IntPtr.Zero
            Try
                If Win32.WTSQueryUserToken(SessionId, token) Then
                    Dim cch As Integer = 260
                    Dim strpath As StringBuilder = New StringBuilder(cch)
                    If Not Win32.GetUserProfileDirectory(token, strpath, cch) Then
                        strpath.Capacity = cch
                        If Not Win32.GetUserProfileDirectory(token, strpath, cch) Then
                            Throw New Win32Exception(Marshal.GetLastWin32Error())
                        End If
                    End If
                    strprofile = strpath.ToString()
                Else
                    Throw New Win32Exception(Marshal.GetLastWin32Error())
                End If
            Finally
                If token <> IntPtr.Zero Then
                    Win32.CloseHandle(token)
                End If
            End Try
            Return strprofile
        End Function
    

    Stuff for P/Invoke -

    Public Class Win32
    
        Public Enum WTS_CONNECTSTATE_CLASS As UInteger
            WTSActive              ' User logged On To WinStation
            WTSConnected           ' WinStation connected To client
            WTSConnectQuery        ' In the process Of connecting To client
            WTSShadow              ' Shadowing another WinStation
            WTSDisconnected        ' WinStation logged On without client
            WTSIdle                ' Waiting For client To connect
            WTSListen              ' WinStation Is listening For connection
            WTSReset               ' WinStation Is being reset
            WTSDown                ' WinStation Is down due To Error
            WTSInit                ' WinStation In initialization
        End Enum
    
        Public Enum WTS_INFO_CLASS As UInteger
            WTSInitialProgram
            WTSApplicationName
            WTSWorkingDirectory
            WTSOEMId
            WTSSessionId
            WTSUserName
            WTSWinStationName
            WTSDomainName
            WTSConnectState
            WTSClientBuildNumber
            WTSClientName
            WTSClientDirectory
            WTSClientProductId
            WTSClientHardwareId
            WTSClientAddress
            WTSClientDisplay
            WTSClientProtocolType
            WTSIdleTime
            WTSLogonTime
            WTSIncomingBytes
            WTSOutgoingBytes
            WTSIncomingFrames
            WTSOutgoingFrames
            WTSClientInfo
            WTSSessionInfo
            WTSSessionInfoEx
            WTSConfigInfo
            WTSValidationInfo   'Info Class value used To fetch Validation Information through the WTSQuerySessionInformation
            WTSSessionAddressV4
            WTSIsRemoteSession
        End Enum
    
        <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode)>
        Public Structure WTS_SESSION_INFO
            Public Sessionid As UInteger
            <MarshalAs(UnmanagedType.LPWStr)>
            Public pWinStationName As String
            Public State As WTS_CONNECTSTATE_CLASS
        End Structure
    
        <StructLayout(LayoutKind.Explicit)>
        Public Structure LARGE_INTEGER
            <FieldOffset(0)>
            Public LowPart As UInteger
            <FieldOffset(4)>
            Public HighPart As Integer
            <FieldOffset(0)>
            Public QuadPart As Long
        End Structure
    
        <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode)>
        Public Structure WTSINFO
            Public State As WTS_CONNECTSTATE_CLASS ' connection state (see Enum)
            Public SessionId As UInteger             ' session id
            Public IncomingBytes As UInteger
            Public OutgoingBytes As UInteger
            Public IncomingFrames As UInteger
            Public OutgoingFrames As UInteger
            Public IncomingCompressedBytes As UInteger
            Public OutgoingCompressedBytes As UInteger
            <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=32)>
            Public WinStationName As String '[WINSTATIONNAME_LENGTH]
            <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=17)>
            Public Domain As String '[DOMAIN_LENGTH]
            <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=21)>
            Public UserName As String '[USERNAME_LENGTH+1]
            Public ConnectTime As LARGE_INTEGER
            Public DisconnectTime As LARGE_INTEGER
            Public LastInputTime As LARGE_INTEGER
            Public LogonTime As LARGE_INTEGER
            Public CurrentTime As LARGE_INTEGER
        End Structure
    
        <DllImport("wtsapi32.dll", CallingConvention:=CallingConvention.StdCall, CharSet:=CharSet.Unicode, SetLastError:=True)>
        Public Shared Function WTSEnumerateSessions(hServer As IntPtr, reserved As UInteger,
                               version As UInteger, ByRef ppSessionInfo As IntPtr, ByRef count As UInteger) As Boolean
        End Function
    
        <DllImport("wtsapi32.dll", CallingConvention:=CallingConvention.StdCall)>
        Public Shared Sub WTSFreeMemory(pmemory As IntPtr)
        End Sub
    
        <DllImport("wtsapi32.dll", CallingConvention:=CallingConvention.StdCall, CharSet:=CharSet.Unicode, SetLastError:=True)>
        Public Shared Function WTSQueryUserToken(Sessionid As UInteger, ByRef htoken As IntPtr) As Boolean
        End Function
    
        <DllImport("kernel32.dll", CallingConvention:=CallingConvention.StdCall, SetLastError:=True)>
        Public Shared Function CloseHandle(handle As IntPtr) As Boolean
        End Function
    
        <DllImport("wtsapi32.dll", CallingConvention:=CallingConvention.StdCall, CharSet:=CharSet.Unicode, SetLastError:=True)>
        Public Shared Function WTSQuerySessionInformation(hServer As IntPtr, SessionId As UInteger, WTSInfoClass As WTS_INFO_CLASS,
                                                          ByRef ppBuffer As IntPtr, ByRef BytesReturned As UInteger) As Boolean
        End Function
    
        <DllImport("userenv.dll", CallingConvention:=CallingConvention.StdCall, CharSet:=CharSet.Unicode, SetLastError:=True)>
        Public Shared Function GetUserProfileDirectory(htoken As IntPtr, lpProfileDir As StringBuilder, ByRef cch As UInteger) As Boolean
        End Function
    End Class
    
    1 person found this answer helpful.

1 additional answer

Sort by: Most helpful
  1. RLWA32 40,286 Reputation points
    2022-05-14T16:36:21.243+00:00

    Following VB.Net console application when run as Administrator (manifest for requireAdministrator to be sure) should return the desired information when run in the interactive user's session. Expect an unhandled exception if you don't use the manifest and run it as a standard user.

    Module Module1
        Dim TOKEN_DUPLICATE As UInteger = &H2
        Dim TOKEN_IMPERSONATE As UInteger = &H4
        Dim TOKEN_QUERY As UInteger = &H8
        Dim PROCESS_QUERY_INFORMATION As UInteger = &H400
    
        <DllImport("user32.dll", CallingConvention:=CallingConvention.StdCall, SetLastError:=True)>
        Public Function GetShellWindow() As IntPtr
        End Function
    
        <DllImport("user32.dll", CallingConvention:=CallingConvention.StdCall, SetLastError:=True)>
        Public Function GetWindowThreadProcessId(hwnd As IntPtr, ByRef processid As UInteger) As UInteger
        End Function
    
        <DllImport("kernel32.dll", CallingConvention:=CallingConvention.StdCall, SetLastError:=True)>
        Public Function OpenProcess(processaccess As UInteger, inherit As Boolean, processid As UInteger) As SafeProcessHandle
        End Function
    
        <DllImport("advapi32.dll", CallingConvention:=CallingConvention.StdCall, SetLastError:=True)>
        Public Function OpenProcessToken(handle As SafeProcessHandle, desiredaccess As UInteger, ByRef token As SafeAccessTokenHandle) As Boolean
        End Function
    
        Sub Main()
            Console.WriteLine("Running as user {0}", Environment.UserName)
            Dim processid As UInteger
            Dim hwnd As IntPtr = GetShellWindow()
            Dim tid As UInteger = GetWindowThreadProcessId(GetShellWindow(), processid)
            Dim explorerhandle As SafeProcessHandle = OpenProcess(PROCESS_QUERY_INFORMATION, False, processid)
            If (Not explorerhandle.IsInvalid) Then
                Dim token As SafeAccessTokenHandle = New SafeAccessTokenHandle(IntPtr.Zero)
                If (OpenProcessToken(explorerhandle, TOKEN_IMPERSONATE Or TOKEN_QUERY Or TOKEN_DUPLICATE, token)) Then
                    WindowsIdentity.RunImpersonated(token, Sub() GetUserInfo())
                    token.Dispose()
                    Console.ReadKey()
                Else
                    Console.WriteLine(String.Format("OpenProcessToken failed error code is {0}", Marshal.GetLastWin32Error()))
                End If
                explorerhandle.Dispose()
            Else
                Console.WriteLine(String.Format("OpenProcess failed error code is {0}", Marshal.GetLastWin32Error()))
            End If
    
        End Sub
        Sub GetUserInfo()
            Console.WriteLine("Logged on user: {0}, pofile path: {1}", Environment.UserName, Environment.GetFolderPath(Environment.SpecialFolder.UserProfile))
        End Sub
    
    
    End Module
    
    1 person found this answer helpful.