Run a program as user identity but with elevated privileges - c#

Scenario:
An administrator will install the application. The application has some kernel level operations so, it has to run with privileged mode. But the user does not have administrator credentials to run the application in elevated mode.
So, what are the best possibility to solve the above scenario.
Solution one (tried):
While installing the application through administrator, we would create an admin where we know his user name and password. So, when the user tries to perform any operation, we will run the application as elevated mode using the functions processstartinfo() and process.start() in c#. But, the application runs in admin name, so the mapped drives for the user are not visible as this admin is not in the ACL list. So, this method is getting ruled out.
Can there be a solution where elevating the user it self as admin till the operation is completed using c# application?
I'm stuck and I don't find any articles to read upon on this.
Edit :
At an enterprise level, the windows doesn't ask for admin creds for using bitlocker. So, we want to achieve the same type of functionality.
Is it possible?
Can anyone please help?
Thank you.

The solution that I did was to separate the program into two parts. One part runs as a service that is run using admin privileges and the other part runs using normal privileges.
The communication between the two programs can run via Ethernet or using shared memory.

I think you might be stuck because you are dealing with two functions that have permissions features, and both are required to properly run your application.
I have noted that you have these two:
File System Path. Only the logged-on user has access to this (the admin account does not).
Application Processes. Only the admin account can run these processes (the logged-on user will be prompted with user elevation).
To illustrate this...
| File System Path | Application Process
User | OK | --
Admin | -- | OK
The solution needs to look like this...
| File System Path | Application Process
Service | OK | OK
If possible, I would say this is the most straight-forward way, whereby you create a service account that has permissions to do both.

You could try creating a parallel thread that has admin privileges in order to execute the kernel operations. This way your whole program is contained in one executable.

What you can do is use COM+ Component Services. With .NET the easiest way is use Enterprise Services's ServicedComponent which has all sorts of wrappers and utility classes to interop with COM+ Component services.
So here are steps to do it:
1) Create a .NET Framework Class Library.
2) Add it a strong name and sign it with it
3) Add it a class like this for example (I've also put some utility method to diagnose things)
[ComVisible(true)]
public class AdminClass : ServicedComponent
{
public int DoSomethingAsAdmin()
{
// test something that a normal user shouldn't see
return Directory.GetFiles(Path.Combine(Environment.SystemDirectory, "config")).Length;
}
public string WindowsIdentityCurrentName => WindowsIdentity.GetCurrent().Name;
public string CurrentProcessFilePath => Process.GetCurrentProcess().MainModule.FileName;
// depending on how you call regsvcs, you can run as a 32 or 64 bit surrogate dllhost.exe
public bool Is64BitProcess => Environment.Is64BitProcess;
}
4) Add the following to AssemblyInfo.cs
[assembly: ApplicationName("AdminApp")]
[assembly: SecurityRole("AdminAppUser")]
[assembly: ApplicationActivation(ActivationOption.Server)]
What this does is define a COM+ application named "AdminApp", add a role named "AdminAppUser" to it, and declare the app will run as a "server" which means "out-of-process".
5) Compile that and run this command as admin
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\regsvcs.exe AdminApp.dll
or this command:
C:\Windows\Microsoft.NET\Framework\v4.0.30319\regsvcs.exe AdminApp.dll
Both commands will create the the COM + application, and host the .NET library DLL in a surrogate .exe (dllhost.exe). If you choose the first, the hosted process will run as x64, and if you run the second, the hosted process will run as x86.
You can check the result of this registration if you run Component Services (from Windows/Run):
6) Right-click the app and you'll see a whole bunch of cool things you can configure. Note you can even run this as a service (in the 'Activation' tab), etc. What you must do is configure the identity which will run this process, something like this:
Here, I've used a custom admin account. You don't want to use any of the other builtin choices.
7) Now, since default security has been enabled, basically nobody can calls this component. So we just have to add a user to the role "AdminAppUser" we created earlier. You can of course do this using the UI as shown here:
but here is a piece of code that does this programmatically (we use the COM+ administration objects) :
AddUserInRole("AdminApp", "AdminAppUser", #"SMO01\simon");
....
static void AddUserInRole(string appName, string roleName, string userName)
{
dynamic catalog = Activator.CreateInstance(Type.GetTypeFromProgID("COMAdmin.COMAdminCatalog"));
// the list of collection hierarchy : https://learn.microsoft.com/en-us/windows/desktop/cossdk/com--administration-collections
var apps = catalog.GetCollection("Applications");
var app = GetCollectionItem(apps, appName);
if (app == null)
throw new Exception("Application '" + appName + "' was not found.");
var roles = apps.GetCollection("Roles", app.Key);
var role = GetCollectionItem(roles, roleName);
if (role == null)
throw new Exception("Role '" + roleName + "' was not found.");
// UsersInRole collection
// https://learn.microsoft.com/en-us/windows/desktop/cossdk/usersinrole
var users = roles.GetCollection("UsersInRole", role.Key);
var user = GetCollectionItem(users, userName);
if (user == null)
{
user = users.Add();
user.Value["User"] = userName;
users.SaveChanges();
}
}
static dynamic GetCollectionItem(dynamic collection, string name)
{
collection.Populate();
for (int i = 0; i < collection.Count; i++)
{
var item = collection.Item(i);
if (item.Name == name)
return item;
}
return null;
}
The result should be like this:
8) Now, for the client app, using the AdminApp facilities is easy. Don't reference the .DLL as a standard .NET reference, but use it as any other external COM component. You could reference the .TLB file that was created by regsvcs, or just use the magic dynamic keyword as I demonstrate here (the drawback is you don't get autocompletion):
using System;
using System.Security.Principal;
namespace UserApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Is64BitProcess " + Environment.Is64BitProcess);
Console.WriteLine("Running As " + WindowsIdentity.GetCurrent().Name);
var type = Type.GetTypeFromProgID("AdminApp.AdminClass");
dynamic trustedClass = Activator.CreateInstance(type);
Console.WriteLine("Admin App Process Path: " + trustedClass.CurrentProcessFilePath);
Console.WriteLine("Admin App Running As: " + trustedClass.WindowsIdentityCurrentName);
Console.WriteLine("Admin App Is64BitProcess: " + trustedClass.Is64BitProcess);
Console.WriteLine("Admin App DoSomethingAsAdmin: " + trustedClass.DoSomethingAsAdmin());
}
}
}
Now, when you run it for example as "simon", you should see something like this, it works:
Is64BitProcess False
Running As SMO01\simon
Admin App Process Path: C:\WINDOWS\system32\dllhost.exe
Admin App Running As: SMO01\myAdmin
Admin App Is64BitProcess: True
Admin App DoSomethingAsAdmin: 71
and when you run it for example as "bob" who's not configured in the role, you should see something like this with an access denied, this is expected:
Is64BitProcess False
Running As SMO01\bob
Unhandled Exception: System.UnauthorizedAccessException: Retrieving the COM class factory for component with CLSID {0DC1F11A-A187-3B6D-9888-17E635DB0974} failed due to the following error: 80070005 Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED)).
at System.RuntimeTypeHandle.CreateInstance(RuntimeType type, Boolean publicOnly, Boolean noCheck, Boolean& canBeCached, RuntimeMethodHandleInternal& ctor, Boolean& bNeedSecurityCheck)
at System.RuntimeType.CreateInstanceSlow(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark)
at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark)
at System.Activator.CreateInstance(Type type, Boolean nonPublic)
at System.Activator.CreateInstance(Type type)
at UserApp.Program.Main(String[] args) in C:\Users\simon\source\repos\TrustedSystem\UserApp\Program.cs:line 14
Note we've created a trusted system without setting any password anywhere. And, I've only scratched the surface of what you can do with COM+ component. For example, you can export the app as an .MSI for easy deployment, etc.

Related

Error when calling two fullTrust apps on the load of the page

I have a UWP app that does the recording and calling functionality. For this I have added two FullTrust Apps using Desktop bridge application. When I call just one fullTrust app everything works perfectly, but when I call two FullTrust apps by passing parameters (of the FullTrust apps to be started) the the first app that was started behaves incorrectly. For these two FullTrust apps I have used two different Appservice names declared in the Package.Manifest file of the Windows Packaging Project.
I have noticed that whenever I switch the position of the Fulltrust app call the last application that is called always remains active(has the priority of the Appservice connection) even if both has different app service names.
Here is the code I have added when user opens a page in UWP that starts Win32 app and background App
if (ApiInformation.IsApiContractPresent("Windows.ApplicationModel.FullTrustAppContract", 1, 0))
{
await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync("Win32");
}
if (ApiInformation.IsApiContractPresent("Windows.ApplicationModel.FullTrustAppContract", 1, 0))
{
await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync("Background");
}
In the above code, the first app that is Started calls my Win32.exe and second background.exe.
How can I call these two apps independently? whenever we want to start them and close whenever required or may be in future I would like to start two apps at the same time but also I may need to close any app whenever required. Can anyone tel me how can I handle the correct communication path when calling two fullTrust apps at the same time?
How can I call these two apps independently?
For launching multiple desktop app, we suggest to make Launcher app to manage multiple apps, then call LaunchFullTrustProcessForCurrentAppAsync(string parameterGroupId) and pass GroupId parameter. And at first we need add the group in the desktop bridge appxmanifes file.
<Extensions>
<desktop:Extension Category="windows.fullTrustProcess" Executable="Launcher\Launcher.exe">
<desktop:FullTrustProcess>
<desktop:ParameterGroup GroupId="Background" Parameters="/background" />
<desktop:ParameterGroup GroupId="Win32" Parameters="/win32" />
</desktop:FullTrustProcess>
</desktop:Extension>
</Extensions>
Then use the Launcher to start all apps with parameter
static void Main(string[] args)
{
// determine the package root, based on own location
string result = Assembly.GetExecutingAssembly().Location;
int index = result.LastIndexOf("\\");
string rootPath = $"{result.Substring(0, index)}\\..\\";
// process object to keep track of your child process
Process newProcess = null;
if (args.Length > 2)
{
// launch process based on parameter
switch (args[2])
{
case "/background":
newProcess = Process.Start(rootPath + #"FullTrust_Background\FullTrust_Background.exe");
break;
case "/win32":
newProcess = Process.Start(rootPath + #"FullTrust_Win32\FullTrust_Win32.exe");
break;
}
}
}
For more detail please refer this tutorial.

Adding network drives in C# as admin: disappear after logoff

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

Access hosts file with powershell script from C#

I have an ASP .NET WEb Forms project, and I want to execute power-shell script to update hosts file.
private void ExecutePowerShellScript(string scriptToExecute)
{
using (PowerShell powershelInstance = PowerShell.Create())
{
var authManger = powershelInstance.Runspace.RunspaceConfiguration.AuthorizationManager;
powershelInstance.AddScript(scriptToExecute);
Collection<PSObject> results = powershelInstance.Invoke();
if (powershelInstance.Streams.Error.Count > 0)
{
throw powershelInstance.Streams.Error[0].Exception;
}
foreach (var result in results)
{
}
}
}
There is the script:
$hostsPath = "$env:windir\System32\drivers\etc\hosts";
$hosts = get-content $hostsPath;
[System.Collections.ArrayList]$arr = $hosts;
$arr.Add(someValueHere);
$arr | Out-File $hostsPath -enc ascii;
# returns results;
$arr;
# end of the script";
I tried this: Invoke(Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Unrestricted);
then this paste Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Unrestricted in the beginning of the script. Use this Set-ExecutionPolicy Unrestricted - same and same error. ccess to the path C:\Windows\System32\drivers\etc\hosts' is denied.
The script works perfectly if I ran into console application.
Update: I am running Visual Studio as administrator.
Update 2: OK, now I am using the ImpersonatedUser ,but another exception occur. "Requested registry access is not allowed."
StackTrace:
at System.ThrowHelper.ThrowSecurityException(ExceptionResource resource)
at Microsoft.Win32.RegistryKey.OpenSubKey(String name, Boolean writable)
at System.Environment.GetEnvironmentVariable(String variable, EnvironmentVariableTarget target)
at System.Management.Automation.ModuleIntrinsics.SetModulePath()
at System.Management.Automation.ExecutionContext.InitializeCommon(AutomationEngine engine, PSHost hostInterface)
at System.Management.Automation.AutomationEngine..ctor(PSHost hostInterface, RunspaceConfiguration runspaceConfiguration, InitialSessionState iss)
at System.Management.Automation.Runspaces.LocalRunspace.DoOpenHelper()
at System.Management.Automation.Runspaces.RunspaceBase.CoreOpen(Boolean syncCall)
at System.Management.Automation.PowerShell.Worker.CreateRunspaceIfNeededAndDoWork(Runspace rsToUse, Boolean isSync)
at System.Management.Automation.PowerShell.CoreInvokeHelper[TInput,TOutput](PSDataCollection`1 input, PSDataCollection`1 output, PSInvocationSettings settings)
at System.Management.Automation.PowerShell.CoreInvoke[TInput,TOutput](PSDataCollection1 input, PSDataCollection1 output,
using (ImpersonatedUser impersonatedUser = new ImpersonatedUser(username, domain, password))
{
using (PowerShell powershelInstance = PowerShell.Create())
{
powershelInstance.AddScript(scriptToExecute);
//When the .Invoke() method is called, an exception with message "Requested registry access is not allowed." was thrown.
Collection<PSObject> results = powershelInstance.Invoke();
if (powershelInstance.Streams.Error.Count > 0)
{
throw powershelInstance.Streams.Error[0].Exception;
}
}
}
Your ASP.NET executes the PowerShell script with the credentials of the worker process of the application pool, which is probably not administrative account (unless you changed it).
Modifying the hosts file is restricted to administrative accounts only, and you should consider very carefuly before you change the credentials of the worker process.
If you want to make this change then follow the instructions here: https://technet.microsoft.com/en-us/library/cc771170(v=ws.10).aspx
Again, this change can affect make your application more vulnerable to security exploits (since any exploit found in your application can be used with administrative privileges).
You may also need to turn off UAC (User Account Control) if its turned on.
Another way, is by using impersonation for temporarily elevation of your privilages. You can see a sample of a class that allow you this (wrap evething up) here: https://blogs.msdn.microsoft.com/joncole/2009/09/21/impersonation-code-in-c/
Hope this helps.
I found some other solution, but maybe it's not the best. In IIS server just add a new Application pool. In the advanced settings change the identity to custom account and enter your credentials to windows. Use the new application pool to the site.

Changing how a service is configured

When installing a service, there is a helpful .NET class called ServiceProcessInstaller. This class has a property Account, which is a ServiceAccount enumeration with possible values LocalService, LocalSystem, NetworkService and User.
This is fine at install-time, but does anybody know how I can change this value for an existing service?
I assuming that I need to move away from the actual install-type classes, and have been researching hooking into the advapi32 ChangeServiceConfig method, WMI and ManagementObjects etc.
Indeed I have found code which will actually change the account under which the service runs,
ManagementObject mo = new ManagementObject("Win32_Service.Name='" + myService + "'");
object[] configParams = new object[11];
configParams[6] = userName;
configParams[7] = password;
object result = mo.InvokeMethod("Change", configParams);
(which on its own looks a bit like black magic but makes sense when viewed with the ChangeServiceConfig signature)
However when I apply this code to a service which happens to be installed as LocalSystem, it has no effect (although when I interpret result the call is reporting success). This doesn't really surprise me since I am only setting a username and password, I am not saying "rather than running as a local service, this service needs to run under a specific user account".
Now, my gut feel is that I am heading along the right lines here. The problem is that none of the parameters in ChangeServiceConfig appear to offer the opportunity to do this.
Any ideas? TIA, Pete
Error code 16 means "Service marked for deletion". Sometimes when you change service parameter, in particular when you delete / re-create a service you need to reboot your PC for operation to complete. While it's still pending, you can't manipulate service and you get error code 16.
Also, it might not be the case, that you problem has something to do with the fact that the call is inside a dll. If you put you code in a test rig dll and call it from a test rig exe (the same way you tested it in a test rig exe) and don't create / delete service in between I think it will work anyway.
The reason it does not working in your application on my opinion has to do with what you did with the service before (and this something most likely is not described in your question).
You need Impersonate an thread to run at context of user.
Try this class :
A small C# Class for impersonating a User
or this one :
Impersonate User
Return code is 21: "Invalid Parameter".
I ran into the same issue: Problem occurs when trying to apply a new user/password to a service which currently has "LocalSystem" with "Allow Service to interact with desktop" enabled.
To resolve, set the "DesktopInteract" flag in the "Change" query
var query = new ManagementPath(string.Format("Win32_Service.Name='{0}'", serviceName)); // string.Format("SELECT * FROM Win32_Service where Name='{0}'", serviceName);
using (ManagementObject service = new ManagementObject(query))
{
object[] wmiParams = new object[10];
//WMI update doesn't work if the service's user is currently set to LocalSystem
// with Interact with desktop on
wmiParams[5] = false;
wmiParams[6] = serviceUserName;
wmiParams[7] = password;
//update credentials for the service
var rtn = service.InvokeMethod("Change", wmiParams);
}

How to set "interact with desktop" in windows service installer

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.

Categories