Ok, first of all: my task is, to map a network drive programmatically from a C# program that runs as administrator.
I managed to map a drive as my normal admin user without elevated privileges and it was still visible after logoff/reboot (even though it didn't reconnect, but that's a different story). I did this with the WNetAddConnection2 WinApi function and also with the net use command in cmd, just to check.
Sadly, both didn't work with elevated privileges. In this case the drive is added as it schould, but after a reboot it is completely gone.
Is it even possible to achieve this with elevated privileges or is there some Windows account magical stuff I don't know about that prevents that?
Here is the Code I used, setting all flags that should usually make the drive be remembered and also reconnected:
uint flags = (uint)(Flags.CONNECT_CMD_SAVECRED |
Flags.CONNECT_INTERACTIVE |
Flags.CONNECT_COMMANDLINE |
Flags.CONNECT_UPDATE_PROFILE);
NETRESOURCE NetworkResource = new NETRESOURCE();
oNetworkResource.dwType = ResourceType.RESOURCETYPE_DISK;
oNetworkResource.lpLocalName = Console.ReadLine() + ":";
oNetworkResource.lpRemoteName = #"\\[Server]\foo";
oNetworkResource.lpProvider = null;
Console.WriteLine(WNetAddConnection2(NetworkResource, "[Password]", #"[Domain]\[Username]", flags));
it was still visible after logoff/reboot
this is because, when CONNECT_UPDATE_PROFILE flag used - called exported, but undocumented function I_MprSaveConn (from mpr.dll) which save in registry, under HKEY_CURRENT_USER\Network\<lpLocalName> information which you pass to WNetAddConnection2. but I_MprSaveConn at very begin call function bool IsElevatedCaller(PLUID ) and if function return true - it just exit, without saving in registry. so you absolute correct - when you call WNetAddConnection2 from elevated process (without impersonation) - this connection not persist (info not saved in registry)
solution: you need got not elevated token (say from explorer) - open/duplicate (for TokenImpersonation type) and call SetThreadToken. in this case IsElevatedCaller (can) return false (it first try open thread token (only if it not exist - process token) ) and query opened token for TokenElevationType (and return true if TokenElevationTypeFull )
so this of course not documented, but current (i test) if you impersonate self thread with not elevated token (how you got it separate question) flag CONNECT_UPDATE_PROFILE will be worked well
Related
I have a .NET application that creates a new local user like so:
var principalContext = new PrincipalContext(ContextType.Machine);
var userPrincipal = new UserPrincipal(principalContext);
userPrincipal.Name = StandardUserName.Text;
userPrincipal.Description = "New local user";
userPrincipal.UserCannotChangePassword = true;
userPrincipal.PasswordNeverExpires = true;
userPrincipal.Save();
// Add user to the users group
var usersGroupPrincipal = GroupPrincipal.FindByIdentity(principalContext, UserGroupName.Text);
usersGroupPrincipal.Members.Add(userPrincipal);
usersGroupPrincipal.Save();
Next, I want to set some registry values for that user. For that, I need the user's SID:
private string GetSidForStandardUser()
{
var principalContext = new PrincipalContext(ContextType.Machine);
var standardUser = UserPrincipal.FindByIdentity(principalContext, StandardUserName.Text);
return standardUser.Sid.ToString();
}
And create a new subkey:
var key = string.Format("{0}{1}", GetSidForStandardUser(), keyString);
var subKey = Registry.Users.CreateSubKey(key, RegistryKeyPermissionCheck.ReadWriteSubTree);
However, I get an IOException on the call to CreateSubKey that tells me the Parameter is invalid. This happens because the subkey for that user does not exist yet until the user logs in for the first time. If I check regedit (under admin privileges) before logging in as the new user I can see that the SID does not exist under HKEY_Users. If I log in as the new user, then log out and back in as my original user and refresh regedit, the new SID exists.
My question is: is there a way to add subkeys for users that haven't logged in yet? I'd like to avoid having to log in as the new user and then back out halfway during the process.
I've since found a solution to the problem, but it's not pretty and it raises all sorts of new problems you have to deal with. Still, it works. I'm posting the solution here for my own reference and for others who may have need for it in the future.
The problem is that a user's registry hive is in their user profile folder (e.g. c:\users\username) in a file called NTUSER.DAT. However, a user's user profile folder isn't created until they log in, so when you create a new user there's no user profile yet and no NTUSER.DAT file containing their registry hive, so you can't edit any of their registry settings.
There's a trick, though: the user profile does get created when you run something under that user's credentials. There's an executable called runas.exe that lets you run a second executable under a specified user's credentials. If you create a new user and make it run, say, cmd.exe, like so:
runas /user:newuser cmd.exe
...it'll open a Cmd instance, but more importantly, create newuser's profile in the \users folder, including NTUSER.DAT.
Now, Cmd.exe leaves a command window open, which you can close manually but it's kind of clunky. https://superuser.com/a/389288 pointed me to rundll32.exe which, when run without any parameters, does nothing and exits immediately. Also, it's available on every Windows installation.
So, by calling runas and telling it to run rundll32.exe as the new user, we can create the user's profile without any further interaction:
Process.Start("runas", string.Format("/user:{0} rundll32", "newuser"));
Well... almost with no interaction. Runas opens a console window that requires you to enter the user's password, even if no password is set (it wants you to just press enter). This is annoying, but can be solved with some clever use of Pinvoke and optionally System.Windows.Forms to bring the window to the foreground and send it some keypresses:
var createProfileProcess = Process.Start("runas",
string.Format("/user:{0} rundll32",
"newuser"));
IntPtr hWnd;
do
{
createProfileProcess.Refresh();
hWnd = createProfileProcess.MainWindowHandle;
} while (hWnd.ToInt32() == 0);
SetForegroundWindow(hWnd);
System.Windows.Forms.SendKeys.SendWait("{ENTER}");
This creates the profile, waits until the window has a handle, and then calls the Win32 function SetForegroundWindow() to bring it to the foreground. Then, it uses SendKeys.SendWait to send an enter key to that window. If you don't want to use a WinForms DLL, there are Win32 functions you can PInvoke for this, but for this particular scenario I found the winforms way quicker and easier.
This works, but reveals yet another problem: runas won't let you run stuff under an account that has no password. Superuser to the rescue again; https://superuser.com/a/470539 points out that there's a Local Policy called Limit local account use of blank passwords to console logon only that can be disabled to allow this exact scenario. I didn't want users to have to manually disable this policy, so I used the corresponding registry value LimitBlankPasswordUse in HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa.
I now disable the policy by setting the registry value to 0, run the runas command to create the profile, then re-enable the policy by setting the value to 1 afterwards.(It would probably be cleaner to check the value first and only re-enable it if it was set in the first place, but for demonstration purposes this will do:
const string keyName = "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Lsa";
Registry.SetValue(keyName, "LimitBlankPasswordUse", 0);
var createProfileProcess = Process.Start("runas",
string.Format("/user:{0} rundll32",
"newuser"));
IntPtr hWnd;
do
{
createProfileProcess.Refresh();
hWnd = createProfileProcess.MainWindowHandle;
} while (hWnd.ToInt32() == 0);
SetForegroundWindow(hWnd);
System.Windows.Forms.SendKeys.SendWait("{ENTER}");
Registry.SetValue(keyName, "LimitBlankPasswordUse ", "1");
This works! However, the user's registry hive isn't loaded yet, so you still won't be able to read or write to it. For that, the process needs a couple of privileges, which you can again provide using some Win32 methods:
OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
out _myToken);
LookupPrivilegeValue(null, SE_RESTORE_NAME, out _restoreLuid);
LookupPrivilegeValue(null, SE_BACKUP_NAME, out _backupLuid);
_tokenPrivileges.Attr = SE_PRIVILEGE_ENABLED;
_tokenPrivileges.Luid = _restoreLuid;
_tokenPrivileges.Count = 1;
_tokenPrivileges2.Attr = SE_PRIVILEGE_ENABLED;
_tokenPrivileges2.Luid = _backupLuid;
_tokenPrivileges2.Count = 1;
AdjustTokenPrivileges(_myToken,
false,
ref _tokenPrivileges,
0,
IntPtr.Zero,
IntPtr.Zero);
AdjustTokenPrivileges(_myToken,
false,
ref _tokenPrivileges2,
0,
IntPtr.Zero,
IntPtr.Zero);
And finally load the hive using the new user's SID:
// Load the hive
var principalContext = new PrincipalContext(ContextType.Machine);
var standardUser = UserPrincipal.FindByIdentity(principalContext, "newuser");
var sid = standardUser.Sid.ToString();
StringBuilder path = new StringBuilder(4 * 1024);
int size = path.Capacity;
GetProfilesDirectory(path, ref size);
var filename = Path.Combine(path.ToString(), "newuser", "NTUSER.DAT");
Thread.Sleep(2000);
int retVal = RegLoadKey(HKEY_USERS, sid, filename);
I found most of this code in Load registry hive from C# fails.
RegLoadKey should return 0 on success. I noted that occasionally, it would fail to load the hive for no apparent reason. Reasoning that perhaps the necessary files in the user profile had not yet been created, I added a Thread.Sleep(2000) before loading the hive to give Windows time to create all the necessary files. There's probably a neater way to do this, but for now this'll work.
Now, you can load and set registry values for newuser using the newuser's SID, for instance:
var subKeyString = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced";
var keyString = string.Format("{0}{1}", sid, subKeyString);
var subKey = Registry.Users.CreateSubKey(keyString,
RegistryKeyPermissionCheck.ReadWriteSubTree);
subKey.SetValue("EnableBalloonTips", 0, RegistryValueKind.DWord);
Just to be sure, I also unloaded the registry hive when I was done. I'm not sure if it's required, but it seems like the neat thing to do:
var retVal = RegUnLoadKey(HKEY_USERS, GetSidForStandardUser());
I need to find a way to guarantee that people using my library have set the requestedExecutionLevel to a minimum of highestAvailable in the application manifest, as follows:
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
</requestedPrivileges>
When they haven't set up the manifest correctly, I want to throw an exception so the developer is aware about this need, without having to read the documentation.
The main question: Is it possible to access this information, if so how?
Instead of checking this setting I already thought about checking the result of this setting. When set to highestAvailable I would expect any user part of the administrators group, to be running as administrator.
This would be possible using:
WindowsPrinciple.IsInRole() to check the currently used role.
Seemingly more difficult to find, a method to check whether a user is in a given group, or more particularly the administrator group. The IsUserInAdminGroup() method listed on the UAC code samples might do the trick.
Your approach is flawed, the dev that uses your library is going to dislike it greatly. The issue is that the manifest isn't the only way to get a process to run elevated. And in fact is almost never the way it is done when you debug code, the Visual Studio Hosting Process is the EXE you run and has the active manifest, without "highestAvailable". The dev will instead elevate Visual Studio itself, the program he starts inherits the security token and will run elevated as well.
What you really want to know is whether the process is elevated when it uses your library and that the user account belongs to the Administrators group. That's rather hard to come by, if the process does not run elevated then the security token is one of a normal user.
You need the code available here. Throw the exception if IsUserInAdminGroup is true and IsProcessElevated is false.
As Hans Passant points out in his answer, the real underlying question - why in this scenario you would want to check the manifest - is:
How to check whether the current user can run the process with elevated privileges?
As suggested in the question, the following would work:
var myPrincipal = new WindowsPrincipal( WindowsIdentity.GetCurrent() );
if ( !myPrincipal.IsInRole( WindowsBuiltInRole.Administrator ) &&
IsUserInAdminGroup() )
{
throw new NotSupportedException( "Some useful comments ..." );
}
The main question is thus, how do you write IsUserInAdminGroup()? The code listed in the UAC self-elevation sample albeit useful, doesn't explain what is going on, and why it is needed.
Hans Passant replied in a comment "Windows emulates a non-elevated process too well, no way to tell from .NET that the user account is in fact an admin account. Falling back to interrogating the token with pinvoke is the workaround used in the linked code.".
In short, you'll need to rely on P/Invoke in order to implement IsUserInAdminGroup(), of which the code can be found in the UAC sample.
More interesting perhaps, is why?
In order to find out I refactored the sample code and incorporated the function into my library. The result in my opinion is a bit more clear. Below you can find the outline, the comments are probably more relevant than the code since it depends on other classes etc ...
Starting from Windows Vista, you have different token types as expressed by TOKEN_ELEVATION_TYPE. Although you can access WindowsIdentity.Token through .NET, this isn't the token we need to check whether someone is administrator. This is a limited token. It has a linked elevated token attached to it, but this isn't exposed in .NET.
Pretty much all the (semi-pseudo) code below does is look up whether there is such an elevated token attached to the original token, and use that to check IsInRole() instead.
// Default token's received aren't impersonation tokens,
// we are looking for an impersonation token.
bool isImpersonationToken = false;
// Open the access token of the current process.
SafeTokenHandle processToken;
if ( !AdvApi32.OpenProcessToken( ..., out processToken ) )
{
MarshalHelper.ThrowLastWin32ErrorException();
}
// Starting from Vista linked tokens are supported which need to be checked.
if ( EnvironmentHelper.VistaOrHigher )
{
// Determine token type: limited, elevated, or default.
SafeUnmanagedMemoryHandle elevationTypeHandle = ...;
if ( !AdvApi32.GetTokenInformation( ... elevationTypeHandle ) )
{
MarshalHelper.ThrowLastWin32ErrorException();
}
var tokenType = (AdvApi32.TokenElevationType)Marshal.ReadInt32(
elevationTypeHandle.DangerousGetHandle() );
// If limited, get the linked elevated token for further check.
if ( tokenType == AdvApi32.TokenElevationType.TokenElevationTypeLimited )
{
// Get the linked token.
SafeUnmanagedMemoryHandle linkedTokenHandle = ...;
if ( !AdvApi32.GetTokenInformation( ... linkedTokenHandle ) )
{
MarshalHelper.ThrowLastWin32ErrorException();
}
processToken = new SafeTokenHandle(
Marshal.ReadIntPtr( linkedTokenHandle.DangerousGetHandle() ) );
// Linked tokens are already impersonation tokens.
isImpersonationToken = true;
}
}
// We need an impersonation token in order
// to check whether it contains admin SID.
if ( !isImpersonationToken )
{
SafeTokenHandle impersonatedToken;
if ( !AdvApi32.DuplicateToken( ..., out impersonatedToken ) )
{
MarshalHelper.ThrowLastWin32ErrorException();
}
processToken = impersonatedToken;
}
// Check if the token to be checked contains admin SID.
var identity= new WindowsIdentity( processToken.DangerousGetHandle() );
var principal = new WindowsPrincipal( identity );
return principal.IsInRole( WindowsBuiltInRole.Administrator );
The action I need help about, is to execute a EXE file on own servers disk from a intranet-webpage, which IIS are on same server-installation. The webpage use a business layer to execute a ProcessStart together with given parameters.
When I perform the execution from web, the taskmanager show me that the application are starting up with the IIS AppPool of webpage as user. Few seconds later it's killed. In my database logs, I can see;
The Microsoft Jet database engine cannot open the file '\\computer\pathfile.ext'. It is already opened exclusively by another user, or you need permission to view its data.
That's correct. The EXE tool are, in turn, loading files from other computers. This is a special behavior which are well studied and well working while using the tool from desktop.
My goal/question,
I want this web-function-call behave with desktop rights. Is it possible at all?
The IIS AppPool have a regular setup with account ApplicationPoolIdentity. I appeared to be "lucky unwise", without knowledge about how much IIS 7.5 and Windows Server 2008 R2 raised the security model since <=IIS6.
I tried to change the app-pool user to NetworkService, Administrator.
I tried to set the application with app-pool as exec/read right
I even tried to let webapp to run a batch-file with a call to application inside..
Then I was begin to change the ProcessStart-behavior. And here, I
don't know much of what to do. I tried to add VERB runas. Force a
password prompt is not a solution here. I tried to simulate a
username/password. No luck there. I also tried to add runas /user:
blabla as parameters with ProcessStart, after used /savecred in a
desktop command window once. No luck there.
Maybe this should work but I just don't understand the correct setup of properties. I add the ProcessStart code snippet below, also added some commented code to let you see what I tried.
public string RunProcess(ApplicationType type, int param)
{
currentSelection = GetApplicationType(type);
ProcessStartInfo info = new ProcessStartInfo(currentSelection.Path);
info.CreateNoWindow = false;
info.UseShellExecute = true;
//info.UseShellExecute = false;
//info.ErrorDialog = false;
//info.UserName = "dummyUsEr";
//info.Password = this.SecurePwd("DummyPWd");
info.WindowStyle = ProcessWindowStyle.Normal;
info.Arguments = string.Format(" {0}", param.ToString());
using (Process exec = Process.Start(info))
{
try
{
exec.WaitForExit();
}
catch
{
}
}
return output;
}
EDIT
Just to be clear, and perhaps help some another guy/girl browsing to this question, I attach the snippet of Password-generation,
protected System.Security.SecureString SecurePwd(string pwd)
{
SecureString securePwd = new SecureString();
foreach (char ch in pwd.ToCharArray())
securePwd.AppendChar(ch);
return securePwd;
}
I see that you've tried putting in a specific username and password for the process start impersonation, but you say that the process accesses files on another computer and I don't see any mention of specifying a domain name which presumably you would need to access remote files?
So like this:
info.Domain = "domainname";
info.UserName = "dummyUsEr";
info.Password = "DummyPWd";
Also, what does this.SecurePwd() do and have you tried it with just the straight password string that you're passing into it?
I have a Problem, which is... i start a programm with right click -> run as administrator.
Which means the programm is running in an administrative context.
WindowsIdentity.GetCurrent().Name;
if i try to get the user name that way i will get the user that started the programm as admin.. for example "administrator", but what i need is the name of the current logged in user which is for example: bob
Can anybody help me out? :)
You could try using WMI (System.Management.dll) to get the owner of the explorer.exe process.
string GetExplorerUser()
{
var query = new ObjectQuery(
"SELECT * FROM Win32_Process WHERE Name = 'explorer.exe'");
var explorerProcesses = new ManagementObjectSearcher(query).Get();
foreach (ManagementObject mo in explorerProcesses)
{
string[] ownerInfo = new string[2];
mo.InvokeMethod("GetOwner", (object[])ownerInfo);
return String.Concat(ownerInfo[1], #"\", ownerInfo[0]);
}
return string.Empty;
}
This relies on the fact that the explorer process is single instance an so you don't end up with the possibility of having several explorer processes running with different user credentials.
You will probably need to use win32 API for that. Read about Window Station and Desktop functions here: http://msdn.microsoft.com/en-us/library/ms687107%28v=vs.85%29.aspx
Also see this question:
Get the logged in Windows user name associated with a desktop
Maybe you could start as normal user, save user name, then programmatically request elevation :
Windows 7 and Vista UAC - Programmatically requesting elevation in C#
All .NET libraries will give you the user from the current context ('Administrator' in your case).
If you are trying to secure your code, you might consider reading about: Security in the .NET framework
1) Cassia should be able to give you a list of currently logged in users including RDC.
foreach (ITerminalServicesSession sess in new TerminalServicesManager().GetSessions())
{
// sess.SessionId
// sess.UserName
}
2) WMI (SO answer)
Select * from Win32_LogonSession
3) PInvoke to WTSEnumerateSessions
4) Enumerate all instances of "explorer.exe" and get the owner using PInvoke (OpenProcessHandle).
Process[] processes = Process.GetProcessesByName("explorer");
This is a bit hacky. WMI can also be used for this.
It might be a good idea to set winmgmt as a dependency for your service if you decided to go with solution that uses WMI.
I have a windows service which runs under system account and executes some programs from time to time (yeah,yeah, I know that's a bad practice, but that's not my decision). I need to set the "interact with desktop" check, to see the gui of that executed programs, after the service is installed. I've tried several ways, putting the code below in AfterInstall or OnCommited event handlers of my service installer:
ConnectionOptions coOptions = new ConnectionOptions();
coOptions.Impersonation = ImpersonationLevel.Impersonate;
ManagementScope mgmtScope = new System.Management.ManagementScope(#"root\CIMV2", coOptions);
mgmtScope.Connect();
ManagementObject wmiService = new ManagementObject("Win32_Service.Name='" + ServiceMonitorInstaller.ServiceName + "'");
ManagementBaseObject InParam = wmiService.GetMethodParameters("Change");
InParam["DesktopInteract"] = true;
ManagementBaseObject OutParam = wmiService.InvokeMethod("Change", InParam, null);
or
RegistryKey ckey = Registry.LocalMachine.OpenSubKey(
#"SYSTEM\CurrentControlSet\Services\WindowsService1", true);
if(ckey != null)
{
if(ckey.GetValue("Type") != null)
{
ckey.SetValue("Type", ((int)ckey.GetValue("Type") | 256));
}
}
both of these methods "work". They set the check, but after I start the service it launches the exe - and gui isn't shown! So, if I stop the service, recheck and start it again - bingo! everything starts and is shown. The second way to achieve the result is to reboot - after it the gui is also shown.
So the question is: Is there a correct way to set "interact with desktop" check, so it'll start working without rechecks and reboots?
OS: Windows XP (haven't tried Vista and 7 yet...)
private static void SetInterActWithDeskTop()
{
var service = new System.Management.ManagementObject(
String.Format("WIN32_Service.Name='{0}'", "YourServiceName"));
try
{
var paramList = new object[11];
paramList[5] = true;
service.InvokeMethod("Change", paramList);
}
finally
{
service.Dispose();
}
}
And finally after searching the internet for a week - I've found a great working solution:
http://asprosys.blogspot.com/2009/03/allow-service-to-interact-with-desktop.html
Find the desktop to launch into. This
may seem facetious but it isn't as
simple as it seems. With Terminal
Services and Fast User Switching there
can be multiple interactive users
logged on to the computer at the same
time. If you want the user that is
currently sitting at the physical
console then you're in luck, the
Terminal Services API call
WTSGetActiveConsoleSessionId will get
you the session ID you need. If your
needs are more complex (i.e. you need
to interact with a specific user on a
TS server or you need the name of the
window station in a non-interactive
session) you'll need to enumerate the
Terminal Server sessions with
WTSEnumerateSessions and check the
session for the information you need
with WTSGetSessionInformation.
Now you know what session you need to
interact with and you have its ID.
This is the key to the whole process,
using WTSQueryUserToken and the
session ID you can now retrieve the
token of the user logged on to the
target session. This completely
mitigates the security problem of the
'interact with the desktop' setting,
the launched process will not be
running with the LOCAL SYSTEM
credentials but with the same
credentials as the user that is
already logged on to that session! No
privilege elevation.
Using CreateProcessAsUser and the
token we have retrieved we can launch
the process in the normal way and it
will run in the target session with
the target user's credentials. There
are a couple of caveats, both
lpCurrentDirectory and lpEnvironment
must point to valid values - the
normal default resolution methods for
these parameters don't work for
cross-session launching. You can use
CreateEnvironmentBlock to create a
default environment block for the
target user.
There is source code of the working project attached.
Same as Heisa but with WMI. (code is Powershell, but can be easily ported to C#)
if ($svc = gwmi win32_service|?{$_.name -eq $svcname})
{
try {
$null = $svc.change($svc.displayname,$svc.pathname,16,1,`
"Manual",$false,$svc.startname,$null,$null,$null,$null)
write-host "Change made"
catch { throw "Error: $_" }
} else
{ throw "Service $svcname not installed" }
See MSDN: Service Change() method for param description.