I'm working on a service (C#) that receives session-change notifications (specifically SessionLogon). The only piece of information I get with that notification is SessionId.
My ultimate goal is to check the logon user's profile (local/roaming AppData/MyCorp/MyApp folder) for a particular setting, and perform a task if it's there.
I need to go from SessionId to something I can map to a local user profile, either directly to a User Account SID or to something that can be mapped to a SID, (e.g. "<domain>\<username>", etc).
The solutions I've found on SO depend upon Windows Terminal Services (WTS) APIs (e.g. WTSQuerySessionInformation), but Remote Desktop Services isn't available on Windows 10 Home edition, so that's a non-starter.
Does anyone know how to map from SessionId to a local user account that doesn't involve WTS APIs?
(EDIT #1) CLARIFICATION:
In .NET, the ServiceBase class has an OnSessionChange override that gets called for login/logout/unlock/lock events. I was originally thinking this was for all such events (from physical machine or Terminal Server).
It looks like this only applies to Terminal Server sessions(?) So, apparently, the sessionId that I get back is a TerminalServer-specific thing. As #RbMm points out below, this override probably wouldn't get called in the first place on Windows Home edition. It's a moot point, though, because it was the local (physical) logon events I was interested in, and that's completely different from Terminal Service sessions.
It seems odd to me that the service base class would have a useful event like this, but have it tied to Terminal Services, rather than work for all cases. Maybe someone has some insight into this?
(EDIT #2) REALIZATION:
#RbMm's comments have cleared up some misconceptions that I started with. Here's a update:
The OnSessionChange event is only for Terminal Services, and has nothing to do with local (physical) logon sessions (I was conflating the two).
I'm only interested in the local logon sessions, so I'll be looking for a way to get notified about them inside my service. If no such notification is available, I'll have to set up a timer and poll.
I'll need to derive a user account SID from whatever piece of information I receive along with such a notification (or periodic call to LsaGetLogonSessionData)
I think you can retrieve this information via WMI
Win32_LogonSession
class Win32_LogonSession : Win32_Session
{
string Caption;
string Description;
datetime InstallDate;
string Name;
string Status;
datetime StartTime;
string AuthenticationPackage;
string LogonId;
uint32 LogonType;
};
Furthermore:
LogonId
Data type: string
Access type: Read-only
Qualifiers: key
ID assigned to the logon session.
Example
var scope = new ManagementScope(ManagementPath.DefaultPath);
var query = new SelectQuery($"Select * from Win32_LogonSession where LogonId = {SessionId}");
var searcher = new ManagementObjectSearcher(scope, query);
var results = searcher.Get();
foreach (ManagementObject mo in results)
{
}
Note this is fully untested
You can resolve this info via processes instead of tokens.
I dont have a c# sample here, but here is PS-snippet to demo the concept:
# https://learn.microsoft.com/en-us/windows/win32/devnotes/getting-the-active-console-session-id
$sessionId = [System.Runtime.InteropServices.Marshal]::ReadByte(0x7ffe02d8)
$pList = (Get-CimInstance Win32_Process -Filter "name = 'explorer.exe' and SessionId=$sessionId")
$winEx = $pList | where {$_.Path -eq 'C:\WINDOWS\explorer.exe'} | sort CreationDate | select -First 1
$sid = (Invoke-CimMethod -InputObject $winEx -MethodName GetOwnerSid).sid
Related
How to get a unique identification from harddisk, motherboard etc from a web-application. I know that this is not possible to do directly.
I´m working in a company with a lot of personal information and the employees use a web-application, that I have developed in aspx.net, of course with login and https, but because of new rules from the government, this is not secure enough anymore.
So to solved this, the company have decided that we have to make some kind of whitelisting of computer, that means that every computer have to use a unique key (GUID) based on the computer's hardware to login to our web application.
But how to do that from web (aspx.net, javascript, jquery), can I make some kind of App (or a Winform program), and next time our employees try to login, they have to install the App/application, and then the aspx.net application can call this program and return the GUID to the login service (WCF-service), how to do that?
I have been working with c#, Winforms, Silverlight, aspx.net, javasript, jquery etc. for many years, but this year I have to find out how to developing Apps (cross platform), so if you have an example/link in other than those mentioned language, this would be fine too
I suggest taking a look at this Retrieving Hardware Identifiers in C# with WMI.
By using classes such as ManagementObject and ManagementClass you will be able to get the properties of your computer's hardware.
Note that the processor ID itself is not unique, you might want to consider a combination of hardware IDs as your unique key.
ex:
/// <summary>
/// Return processorId from first CPU in machine
/// </summary>
/// <returns>[string] ProcessorId</returns>
public string GetCPUId()
{
string cpuInfo = String.Empty;
string temp=String.Empty;
ManagementClass mc = new ManagementClass("Win32_Processor");
ManagementObjectCollection moc = mc.GetInstances();
foreach(ManagementObject mo in moc)
{
if(cpuInfo==String.Empty)
{// only return cpuInfo from first CPU
cpuInfo = mo.Properties["ProcessorId"].Value.ToString();
}
}
return cpuInfo;
}
Then you can create your own browser application to access your company's website. Fetch these IDs and pass them for security purposes.
I'm writing a service and I am trying to get the logged in User's sid and for whatever reason it is not working. It only returns {S-1-5-18}. Yet if I create a quick console application, it works just fine.
I've tried 2 methods:
WindowsIdentity usr = WindowsIdentity.GetCurrent();
return usr.User
as well as:
UserPrincipal.Current.Sid
They both have the same affect in my service. They both only return {S-1-5-18}. Yet in a console app, they both return the full user sid.
What could be causing this?
I suppose you are running your service-process as NT AUTHORITY\SYSTEM or .\LOCALSYSTEM.
Please see KB 243330 for more detail:
SID: S-1-5-18
Name: Local System
Description: A service account that is used by the operating system.
If you want to get the SID from the desktop-session, you could eg go for (by utilizing cassia - nuget-package available) :
ITerminalServicesSession GetActiveSession()
{
var terminalServicesSession = default(ITerminalServicesSession);
var terminalServicesManager = new TerminalServicesManager();
using (var server = terminalServicesManager.GetLocalServer())
{
foreach (var session in server.GetSessions())
{
if (session.ConnectionState == ConnectionState.Active)
{
// yep, I know ... LINQ ... but this is from a plain .NET 2.0 source ...
terminalServicesSession = session;
break;
}
}
}
return terminalServicesSession;
}
The ITerminalServiceSession-instance does contain the property SessionId which should work as needed. But please, be aware that there are caveats associated with state of the session - I do not guarantee that my condition suffices, you may need to adapt the condition on ConnectionState as needed.
Those APIs will return the SID of the user executing the current process, in your case your service. S-1-5-18 is NT AUTHORITY\SYSTEM.
There can be anything from zero to many users logged on to a Windows system (for interactive use: either locally or remotely): there is no singular "logged on user".
You need to refine your requirements: why do you want to know the logged on user?
I am editing a c# WinForm solution and I do not understand the code that gets the user account name. The code is shown below.
The application shows a customized form for each user account and the user account name is needed to get user-specific configuration values from an SQL database.
What happens, to the best I can tell, is the returned user name is correct for the first user account accessed, but after switching to a different user account, the returned user account name is not updated and the initial user account name continues to be returned.
#region "Function to retrieve LoggedIn user"
/// <summary>
/// "Function to retrieve LoggedIn user"
/// </summary>
/// <returns></returns>
private string GetLoggedInUserName()
{
ManagementClass objManClass = new ManagementClass("Win32_Process");
ManagementObjectCollection arrManObjects = objManClass.GetInstances();
foreach (ManagementObject objMan in arrManObjects)
{
if (objMan["Name"].ToString().Trim().ToLower() == "explorer.exe")
{
string[] arrArgs = { "", "" };
try
{
objMan.InvokeMethod("GetOwner", arrArgs);
sUserName = arrArgs[0];
break;
}
catch (Exception lExp)
{
BusinessObject.Logger.Logger.Log(lExp);
}
}
}
return sUserName;
}
#endregion
This application is to run on XP, Vista and 7.
My instinct is to just use something like...
string sUserName = Environment.UserName;
...but my knowledge of the Windows OS is poor and the people who wrote the original code are much smarter than me.
So my two questions are:
(1) Why does this code appear to not update to the new user name when I change user accounts?
(2) why use the 'explore.exe' method instead of simply using 'Environment.UserName'?
Also, two projects in my solution have a GetLoggedInUserName()method. One project runs in the background with a timer that calls the other project, and that project generates the user-customized form.
I have another related question about why the form fails to appear for all user accounts except the admin account that I will post as a separate question once I figure out this question.
If you want the currently logged in user, use can use the WindowsIdentity object:
string currentUser = System.Security.Principal.WindowsIdentity.GetCurrent().Name;
The Explorer process is always running when you log onto a Windows box, so it will always be found. If you open Task Manager and view the processes you will see it, and the account that started it. It looks like a throw back to VBScript, although I'm sure that there is an easier way to it with that too.
There is no good reason to use WMI to get the current user account on a local machine over other simpler methods.
For the user name bit try ...
string username = System.Security.Principal.WindowsIdentity.GetCurrent().Name;
I would like to be able to retrieve the SID of a local Machine like the PSGetSID
utility from Sysinternals but using C#.
Is this possible?
Edit:
I am looking for a solution that will work for computers that may or may not be members of a Domain.
This has good helper class to use lookupaccountname win32 api call.
get machine SID (including primary domain controller)
I did not find a way to do this with native C#
You could do it via pinvoke and just get it from the Win32 API.
http://www.pinvoke.net/default.aspx/advapi32.lookupaccountname
There may also be a way to get it in "pure" .NET.
Another SO post from user ewall has a pure .NET solution with examples in both C# and PowerShell. It uses the DirectoryEntry and SecurityDescriptor objects. See:
How can I retrieve a Windows Computer's SID using WMI?
SIDs documentation:
https://learn.microsoft.com/en-us/windows/security/identity-protection/access-control/security-identifiers
Local user account SID form: S-1-5-21-xxxxxxxxx-xxxxxxxxx-xxxxxxxxxx-yyyy
System specific part: xxx...xxx
User account specific part: yyyy
So you just need to remove the last group of digits from a user account SID to get the system SID.
If your code is a Windows application, you can get the system SID this way:
using System.Security.Principal;
string systemSid;
using (WindowsIdentity windowsIdentity = WindowsIdentity.GetCurrent())
{
systemSid = windowsIdentity.User.Value.Substring(0, windowsIdentity.User.Value.LastIndexOf('-'));
}
If your code is a web application it usually runs in the context of an application pool identity and if your code is a Windows service it usually runs in the context of a system account (system, local service, network service). None of these identities are user accounts. So you cannot use the code above. You need to either know a user account name, or list user accounts.
If you know a user account name, you can get the system SID this way:
using System.Security.Principal;
NTAccount ntAccount = new NTAccount("MACHINE_NAME\\UserAccountName");
SecurityIdentifier sid = (SecurityIdentifier)ntAccount.Translate(typeof(SecurityIdentifier));
string systemSid = sid.Value.Substring(0, sid.Value.LastIndexOf('-'));
But you cannot assume the name of a standard user like "guest" because standard local user accounts names are localized, because this standard user account may have been deleted, and because a standard user account may be suppressed in future Windows releases.
So most of the time, from a web application or from a Windows service, you need to list user accounts by means of WMI:
// your project must reference System.Management.dll
using System.Management;
SelectQuery selectQuery = new SelectQuery("SELECT * FROM Win32_UserAccount");
ManagementObjectSearcher managementObjectSearcher = new ManagementObjectSearcher(selectQuery);
string systemSid;
foreach (ManagementObject managementObject in managementObjectSearcher.Get())
{
if (1 == (byte)managementObject["SIDType"])
{
systemSid = managementObject["SID"] as string;
break;
}
}
systemSid = systemSid.Substring(0, systemSid.Value.LastIndexOf('-'));
Win32_UserAccount class documentation:
https://msdn.microsoft.com/en-us/library/aa394507(v=vs.85).aspx
UPDATE
About 100 times faster:
using Microsoft.Win32;
RegistryKey key = null;
string sid = null;
try
{
foreach (string subKeyName in Registry.Users.GetSubKeyNames())
{
if(subKeyName.StartsWith("S-1-5-21-"))
{
sid = subKeyName.Substring(0, subKeyName.LastIndexOf('-'));
break;
}
}
}
catch (Exception ex)
{
// ...
}
finally
{
if (key != null)
key.Close();
}
Within c#, I need to be able to
Connect to a remote system, specifying username/password as appropriate
List the members of a localgroup on that system
Fetch the results back to the executing computer
So for example I would connect to \SOMESYSTEM with appropriate creds, and fetch back a list of local administrators including SOMESYSTEM\Administrator, SOMESYSTEM\Bob, DOMAIN\AlanH, "DOMAIN\Domain Administrators".
I've tried this with system.directoryservices.accountmanagement but am running into problems with authentication. Sometimes I get:
Multiple connections to a server or shared resource by the same user, using more than one user name, are not allowed. Disconnect all previous connections to the server or shared resource and try again. (Exception from HRESULT: 0x800704C3)
The above is trying because there will be situations where I simply cannot unmap existing drives or UNC connections.
Other times my program gets UNKNOWN ERROR and the security log on the remote system reports an error 675, code 0x19 which is KDC_ERR_PREAUTH_REQUIRED.
I need a simpler and less error prone way to do this!
davidg was on the right track, and I am crediting him with the answer.
But the WMI query necessary was a little less than straightfoward, since I needed not just a list of users for the whole machine, but the subset of users and groups, whether local or domain, that were members of the local Administrators group. For the record, that WMI query was:
SELECT PartComponent FROM Win32_GroupUser WHERE GroupComponent = "Win32_Group.Domain='thehostname',Name='thegroupname'"
Here's the full code snippet:
public string GroupMembers(string targethost, string groupname, string targetusername, string targetpassword)
{
StringBuilder result = new StringBuilder();
try
{
ConnectionOptions Conn = new ConnectionOptions();
if (targethost != Environment.MachineName) //WMI errors if creds given for localhost
{
Conn.Username = targetusername; //can be null
Conn.Password = targetpassword; //can be null
}
Conn.Timeout = TimeSpan.FromSeconds(2);
ManagementScope scope = new ManagementScope("\\\\" + targethost + "\\root\\cimv2", Conn);
scope.Connect();
StringBuilder qs = new StringBuilder();
qs.Append("SELECT PartComponent FROM Win32_GroupUser WHERE GroupComponent = \"Win32_Group.Domain='");
qs.Append(targethost);
qs.Append("',Name='");
qs.Append(groupname);
qs.AppendLine("'\"");
ObjectQuery query = new ObjectQuery(qs.ToString());
ManagementObjectSearcher searcher = new ManagementObjectSearcher(scope, query);
ManagementObjectCollection queryCollection = searcher.Get();
foreach (ManagementObject m in queryCollection)
{
ManagementPath path = new ManagementPath(m["PartComponent"].ToString());
{
String[] names = path.RelativePath.Split(',');
result.Append(names[0].Substring(names[0].IndexOf("=") + 1).Replace("\"", " ").Trim() + "\\");
result.AppendLine(names[1].Substring(names[1].IndexOf("=") + 1).Replace("\"", " ").Trim());
}
}
return result.ToString();
}
catch (Exception e)
{
Console.WriteLine("Error. Message: " + e.Message);
return "fail";
}
}
So, if I invoke Groupmembers("Server1", "Administrators", "myusername", "mypassword"); I get a single string returned with:
SERVER1\Administrator
MYDOMAIN\Domain Admins
The actual WMI return is more like this:
\\SERVER1\root\cimv2:Win32_UserAccount.Domain="SERVER1",Name="Administrator"
... so as you can see, I had to do a little string manipulation to pretty it up.
This should be easy to do using WMI. Here you have a pointer to some docs:
WMI Documentation for Win32_UserAccount
Even if you have no previous experience with WMI, it should be quite easy to turn that VB Script code at the bottom of the page into some .NET code.
Hope this helped!
I would recommend using the Win32 API function NetLocalGroupGetMembers. It is much more straight forward than trying to figure out the crazy LDAP syntax, which is necessary for some of the other solutions recommended here. As long as you impersonate the user you want to run the check as by calling "LoginUser", you should not run into any security issues.
You can find sample code for doing the impersonation here.
If you need help figuring out how to call "NetLocalGroupGetMembers" from C#, I reccomend that you checkout Jared Parson's PInvoke assistant, which you can download from codeplex.
If you are running the code in an ASP.NET app running in IIS, and want to impersonate the user accessing the website in order to make the call, then you may need to grant "Trusted for Delegation" permission to the production web server.
If you are running on the desktop, then using the active user's security credentials should not be a problem.
It is possible that you network admin could have revoked access to the "Securable Object" for the particular machine you are trying to access. Unfortunately that access is necessary for all of the network management api functions to work. If that is the case, then you will need to grant access to the "Securable Object" for whatever users you want to execute as. With the default windows security settings all authenticated users should have access, however.
I hope this helps.
-Scott
You should be able to do this with System.DirectoryServices.DirectoryEntry. If you are having trouble running it remotely, maybe you could install something on the remote machines to give you your data via some sort of RPC, like remoting or a web service. But I think what you're trying should be possible remotely without getting too fancy.
If Windows won't let you connect through it's login mechanism, I think your only option is to run something on the remote machine with an open port (either directly or through remoting or a web service, as mentioned).