SetupDiChangeState throws Access Denied - c#

My user is an Administrator (I see it in the configuration panel), the below code throws a Win32Exception in which it says Access Denied, how can I change this (Win7 32 bits) ?
static Guid VideoGuid = new Guid("4d36e968-e325-11ce-bfc1-08002be10318");
[SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
static void Main(string[] args)
{
SafeDeviceHandle handle = null;
try
{
handle = NativeMethods.SetupDiGetClassDevs(ref VideoGuid, IntPtr.Zero, IntPtr.Zero, NativeMethods.DIGCF.PRESENT);
var data = new NativeMethods.SP_DEVINFO_DATA().Initialize();
var param = new NativeMethods.SP_PROPCHANGE_PARAMS().Initialize();
param.ClassInstallHeader.InstallFunction = 0x12;
param.StateChange = NativeMethods.DICS.ENABLE; // 0x01
param.Scope = NativeMethods.DICS_GLOBAL.GLOBAL; // 0x01
param.HwProfile = 0;
RunWin32Method(() => NativeMethods.SetupDiEnumDeviceInfo(handle, 0u, out data));
RunWin32Method(() => NativeMethods.SetupDiSetClassInstallParams(handle, ref data, ref param, (UInt32)Marshal.SizeOf(param)));
RunWin32Method(() => NativeMethods.SetupDiChangeState(handle, ref data));
}
catch
{
var w = new Win32Exception(Marshal.GetLastWin32Error());
}
finally
{
if (handle != null && (!handle.IsInvalid))
handle.Close();
}
}
static void RunWin32Method(Func<bool> f)
{
if (!f())
{
Debug.WriteLine(new Win32Exception(Marshal.GetLastWin32Error()).Message);
}
}
If you want more code, just ask :-)
Thanks

Recapping the comment trail, a user in the Administrator group doesn't have admin rights on Vista/Server 2008 and later unless the process runs elevated. A manifest is required to get Windows to display the UAC elevation prompt.
This cannot work for programs that are started at login by the Run registry key or the Startup folder. Windows refuses to display the elevation prompt because the user cannot accurately guess exactly what program asked for the elevation. Code-signing the program with a certificate may fix this since that permits Windows to verify and display the program owner, never actually tried that.
Workarounds for such programs are activating it as a service or a scheduled task. Neither of which requires the manifest. The theory behind this seeming oddity is that it already requires elevation to get a service or scheduled task installed.

Related

How to programatically grant a virtual user permission to a folder (or file)

I have a service that I wrote that I need to deploy to a number (about 1100) devices. All of these devices are logged in as a regular user, not an administrator.
I can push out the service with our deployment software, which does run as an admin. Our security team does not want this service to run on the Local System account (for obvious reasons). What I've come up with is that the service will install as the Local System, but will then change it's log in account to a virtual user, which then needs access to a folder in Program Files (x86).
What I've found is that if I install the service (using remote admin access) via the command line, I can install the service, but it won't start.
When I look in the event logs, I get an UnauthorizedAccessException error.
This I suspect is because the service is already running under the virtual user which doesn't have access to start the service. So how can I get around this?
In the main class for the service, I have this method, which is supposed to give the user access to the necessary folder:
private void GiveDirectoryAccess(string dir, string user)
{
try
{
DirectoryInfo directoryInfo = new DirectoryInfo(dir);
DirectorySecurity ds = directoryInfo.GetAccessControl();
ds.AddAccessRule(new FileSystemAccessRule(user, FileSystemRights.FullControl,
InheritanceFlags.ObjectInherit | InheritanceFlags.ContainerInherit, PropagationFlags.NoPropagateInherit, AccessControlType.Allow));
directoryInfo.SetAccessControl(ds);
}
catch (Exception e)
{
SimpleLog.Log(e);
throw;
}
}
This is called right after the service is initialized:
public CheckRALVersionService()
{
InitializeComponent();
// Give directory access
string alhadminPath = System.IO.Path.Combine(pathToFolder, alhadmin);
GiveDirectoryAccess(alhadminPath, serviceUser);
string exeName = System.IO.Path.GetFileName(fullExeNameAndPath);
string tmppath = System.IO.Path.Combine(localdir, tmp);
SimpleLog.SetLogFile(logDir: tmppath, prefix: "debout." + exeName + "_", extension: "log");
watcher = new DirectoryWatcher(pathToFolder, alhadmin);
}
Then, in the ProjectInstaller class, I am changing the user to the virtual user in the serviceInstaller1_Committed method:
void serviceInstaller1_Committed(object sender, InstallEventArgs e)
{
using (ManagementObject service = new ManagementObject(new ManagementPath("Win32_Service.Name='RalConfigUpdate'")))
{
object[] wmiParams = new object[11];
wmiParams[6] = #"NT Service\RalConfigUpdate";
service.InvokeMethod("Change", wmiParams);
}
}
Do I need a helper service to give the access? Can what I want to do be done all within this service?
Thanks in advance.
One option could be to grant the regular user, the permission to start & stop the service.
There is a little tool from Microsoft for that purpose: SubInAcl!
Set Windows Service Permission!
There should be the possibility to do so using group policies as well. That should be a better approach for your use case. On the other hand, the SubInAcl method is easier to test for you. I found an older description here!
To strictly respond to your question:
You can use
System.Io.File.GetAccessControl to get a FileSecurity Class tha can be used to modify the Permissions on filesystem.
The links show some good examples.
BUT that will works ONLY if the user that will run the process will have the right to CHANGE the PERMISSIONS from Windows, if not ====> UnauthorizedAccessException
After sitting on this for a bit, I found a solution. It may not be the most elegant, but it should work for my purposes. I had all of the "parts", but was just doing things in the wrong order.
Previously, I was trying to change the user during the install process, which wasn't working. What I ended up doing was allow the service to install as the LOCAL SYSTEM account, and then change to the virtual account user during the OnStart method of the actual program.
So:
protected override void OnStart(string[] args)
{
string alhadminPath = System.IO.Path.Combine(pathToFolder, alohadmin);
try
{
// Update the service state to start pending
ServiceStatus serviceStatus = new ServiceStatus
{
dwCurrentState = ServiceState.SERVICE_START_PENDING,
dwWaitHint = 100000
};
SetServiceStatus(this.ServiceHandle, ref serviceStatus);
// Update the logs
eventLog1.WriteEntry("Starting Service", EventLogEntryType.Information, eventId++);
SimpleLog.Info("RAL Config Update Service started");
serviceStatus.dwCurrentState = ServiceState.SERVICE_RUNNING;
SetServiceStatus(this.ServiceHandle, ref serviceStatus);
// Change the user to the virutal user
using (ManagementObject service = new ManagementObject(new ManagementPath("Win32_Service.Name='RalConfigUpdate'")))
{
object[] wmiParams = new object[11];
wmiParams[6] = serviceUser;
service.InvokeMethod("Change", wmiParams);
}
GiveDirectoryAccess(alhadminPath, serviceUser);
}
catch (Exception e)
{
eventLog1.WriteEntry("Service failed to start", EventLogEntryType.Error, eventId++);
SimpleLog.Log(e);
throw;
}
}
This is working the way it should, and should also satisfy the security procedures. Thanks everyone.

How can I get my Windows service to see objects in ROT?

EDIT: Though tangentially related, the tagged "duplicate" is not a true duplicate of this question, and more importantly does not answer this question (because the problem is different). I am working with a Window Service, not a standalone userspace app.
Here is what I'm working with:
I've written a Windows service (running in Windows Server 2019) in C# that interacts with other software via COM. This other software is not software that I have access to the source for, so I cannot make changes to the COM registration flags or anything like that, but it does register itself and has an API.
I'd like to use the running object table (ROT) from my service to grab an instance of the other software via moniker display name, however the ROT is always empty from the perspective of my service, even after the service itself starts an instance of the other software via Process.Start() which returns a valid PID. To be clear, this runs fine from a standalone Windows console or Windows form application, but not as a service.
The service is configured to log in as a user who is in the administrators group on this machine. Why can't it see anything in the ROT, even after starting a process itself?
Here is a sample code snippet. When run in a console app it counts many monikers, when run in my service, counter is always 0, even after it starts a process.
[DllImport("ole32.dll")]
private static extern int CreateBindCtx(uint reserved, out IBindCtx ppbc);
private static void TestRot()
{
IBindCtx context = null;
IRunningObjectTable rot = null;
IEnumMoniker monikers = null;
log.Debug("About to start moniker check");
CreateBindCtx(0, out context);
context.GetRunningObjectTable(out rot);
rot.EnumRunning(out monikers);
var moniker = new IMoniker[1];
log.Debug("Beginning moniker loop");
var counter = 0;
while (monikers.Next(1, moniker, IntPtr.Zero) == 0)
{
counter++;
var curMoniker = moniker.First();
string name = null;
if (curMoniker != null)
{
try
{
curMoniker.GetDisplayName(context, null, out name);
}
catch (UnauthorizedAccessException)
{
log.Debug($"UnauthorizedAccessException when getting display name for curMoniker");
}
}
log.Debug($"curMoniker: {name}");
}
log.Debug("Counted monikers: " + counter);
}
Thanks in advance.

Get whether separate process is elevated

In order to get whether the current process is running with administrator privileges, we use the following C# code:
public static bool IsElevated {
get {
return new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
}
}
However, I am trying to find whether another separate process is elevated or not.
How do I go about doing that programmatically?
Try this out: https://stackoverflow.com/a/4497572/3049344
var process = Process.GetProcessesByName("YouProcessName").First();
IntPtr tokenHandle;
if (!OpenProcessToken(process.Handle, TOKEN_READ, out tokenHandle))
{
throw new ApplicationException("Could not get process token. Win32 Error Code: " + Marshal.GetLastWin32Error());
}
...

CreateProcessAsUser user context

I've already been searching long time but couldn't find a working solution yet :-(
I have created a window service that launches a client on every user logged on to a machine using CreateProcessAsUser (http://www.pinvoke.net/default.aspx/advapi32/createprocessasuser.html), WTSEnumerateSessions and so on...
This works fine already. The client starts in the user's session, shows its taskbar icon, and communication with the service is working fine.
The problem I have is that I need to have that client store temporary files in the user's profile. I tried starting with a small log file so that I can keep track of any errors that my user's could eventually experience. Unfortunately I can not save to the user's temp folder because the client somehow seems to be running in LocalSystem's context although WindowsIdentity shows the correct user: System.IO.Path.GetTempPath() always returns 'C:\Windows\Temp' but my user's don't have administrative rights so they are not able to write there... furthermore, I planned to store settings in the current user's registry which is not working, too. I think this is related to the wrong temp path in some way.
I also tried CreateEnvironmentBlock (http://www.pinvoke.net/default.aspx/userenv/CreateEnvironmentBlock.html) but I could not make it work and somewhere I found an article saying that this won't work any more on Vista or higher so I stopped researching on that one.
For testing I have created a small test form just doing this:
MessageBox.Show("Temp: " + System.IO.Path.GetTempPath() + Environment.NewLine + "User: " + WindowsIdentity.GetCurrent().Name, "Before impersonation");
WindowsIdentity currentUserId = WindowsIdentity.GetCurrent();
WindowsImpersonationContext impersonatedUser = currentUserId.Impersonate();
MessageBox.Show("Temp: " + System.IO.Path.GetTempPath() + Environment.NewLine + "User: " + WindowsIdentity.GetCurrent().Name, "After impersonation");
This one always shows the same results before and after impersonation: "Temp: C:\Windows\Temp User:testdomain\testuser" :-(
If it helps here's my function to start a process (user token is delivered by WTSEnumerateSessions) - of course this only works under LocalSystem's context:
public static Process StartProcessAsUser(IntPtr UserToken, string App, string AppPath, string AppParameters)
{
Process ResultProcess = null;
IntPtr hDupedToken = IntPtr.Zero;
NativeProcessAPI.PROCESS_INFORMATION oProcessInformation = new NativeProcessAPI.PROCESS_INFORMATION();
try
{
NativeProcessAPI.SECURITY_ATTRIBUTES oSecurityAttributes = new NativeProcessAPI.SECURITY_ATTRIBUTES();
oSecurityAttributes.Length = Marshal.SizeOf(oSecurityAttributes);
bool result = NativeProcessAPI.DuplicateTokenEx(
UserToken,
NativeProcessAPI.GENERIC_ALL_ACCESS,
ref oSecurityAttributes,
(int)NativeProcessAPI.SECURITY_IMPERSONATION_LEVEL.SecurityIdentification,
(int)NativeProcessAPI.TOKEN_TYPE.TokenPrimary,
ref hDupedToken
);
if (!result)
{
return null;
}
NativeProcessAPI.STARTUPINFO oStartupInfo = new NativeProcessAPI.STARTUPINFO();
oStartupInfo.cb = Marshal.SizeOf(oStartupInfo);
oStartupInfo.lpDesktop = String.Empty;
result = NativeProcessAPI.CreateProcessAsUser(
hDupedToken,
null,
App + " " + AppParameters,
ref oSecurityAttributes, ref oSecurityAttributes,
false, 0, IntPtr.Zero,
AppPath, ref oStartupInfo, ref oProcessInformation
);
if (result)
{
try
{
int ProcessID = oProcessInformation.dwProcessID;
try
{
ResultProcess = System.Diagnostics.Process.GetProcessById(ProcessID);
}
catch
{
ResultProcess = null;
}
}
catch (Exception ex)
{
ResultProcess = null;
}
}
}
catch
{
ResultProcess = null;
}
finally
{
if (oProcessInformation.hProcess != IntPtr.Zero)
NativeProcessAPI.CloseHandle(oProcessInformation.hProcess);
if (oProcessInformation.hThread != IntPtr.Zero)
NativeProcessAPI.CloseHandle(oProcessInformation.hThread);
if (hDupedToken != IntPtr.Zero)
NativeProcessAPI.CloseHandle(hDupedToken);
}
return ResultProcess;
}
Any ideas how I could start my processes in the user's contexts and not in the context of LocalSystem?
Thanks a lot!
Leaving this here for anyone else wondering how to do this: CreateEnvironmentBlock is what you need to use.
DuplicateTokenEx(userToken, MAXIMUM_ALLOWED | TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_IMPERSONATE, IntPtr.Zero, SecurityIdentification, TokenPrimary, out dupUserToken);
CreateEnvironmentBlock(out envBlock, dupUserToken, false);
CreateProcessAsUserW(dupUserToken, null, cmdLine, IntPtr.Zero, IntPtr.Zero, false,
(uint)(CreateProcessFlags.CREATE_NEW_CONSOLE | CreateProcessFlags.CREATE_UNICODE_ENVIRONMENT),
envBlock, processDir, ref startupInfo, out procInfo);
Ok I have found a workaround: I switched to using the USERS hive instead of the CURRENT_USER hive by using the SID provided by WindowsIdentity:
Microsoft.Win32.Registry.Users.OpenSubKey(System.Security.Principal.WindowsIdentity.GetCurrent().User.ToString() + ..., true)
This works perfectly although it feels a bit uncomfortable to get environment variables from the user's "environment" and "volatile environment" registry paths instead of just using .Net's built-in functions...
But thanks a lot for your help ;-)
EDIT:
I will not mark this as an answer because it is a) my own solution and b) just a workaround

RegLoadAppKey working fine on 32-bit OS, failing on 64-bit OS, even if both processes are 32-bit

I'm using .NET 4 and the new RegistryKey.FromHandle call so I can take the hKey I get from opening a software registry file with RegLoadAppKey and operate on it with the existing managed API.
I thought at first it was just a matter of a busted DllImport and my call had an invalid type in the params or a missing MarshalAs or whatever, but looking at other registry functions and their DllImport declarations (for instance, on pinvoke.net), I don't see what else to try (I've had hKey returned as both int and IntPtr, both worked on 32-bit OS and fail on 64-bit OS)
I've got it down to as simple a repro case as I can - it just tries to create a 'random' subkey then write a value to it. It works fine on my Win7 x86 box and fails on Win7 x64 and 2008 R2 x64, even when it's still a 32-bit process, even run from a 32-bit cmd prompt. EDIT: It also fails in the same way if it's a 64-bit process.
EDIT: it works fine if the file passed in is empty - the problem case is for the existing software registry hive. I extracted 'bare' software registry hive files from 2008 r2 (x64) and WHS v1 (x86) iso's and both have the same problem.
on Win7 x86:
INFO: Running as Admin in 32-bit process on 32-bit OS
Was able to create Microsoft\Windows\CurrentVersion\RunOnceEx\a95b1bbf-7a04-4707-bcca-6aee6afbfab7 and write a value under it
on Win7 x64, as 32-bit:
INFO: Running as Admin in 32-bit process on 64-bit OS
Unhandled Exception: System.UnauthorizedAccessException: Access to the registry key '\Microsoft\Windows\CurrentVersion\RunOnceEx\ce6d5ff6-c3af-47f7-b3dc-c5a1b9a3cd22' is denied.
at Microsoft.Win32.RegistryKey.Win32Error(Int32 errorCode, String str)
at Microsoft.Win32.RegistryKey.CreateSubKeyInternal(String subkey, RegistryKeyPermissionCheck permissionCheck, Object registrySecurityObj, RegistryOptions registryOptions)
at Microsoft.Win32.RegistryKey.CreateSubKey(String subkey)
at LoadAppKeyAndModify.Program.Main(String[] args)
on Win7 x64, as 64-bit:
INFO: Running as Admin in 64-bit process on 64-bit OS
Unhandled Exception: System.UnauthorizedAccessException: Access to the registry key '\Microsoft\Windows\CurrentVersion\RunOnceEx\43bc857d-7d07-499c-8070-574d6732c130' is denied.
at Microsoft.Win32.RegistryKey.Win32Error(Int32 errorCode, String str)
at Microsoft.Win32.RegistryKey.CreateSubKeyInternal(String subkey, RegistryKeyPermissionCheck permissionCheck, Object registrySecurityObj, RegistryOptions registryOptions)
at Microsoft.Win32.RegistryKey.CreateSubKey(String subkey, RegistryKeyPermissionCheck permissionCheck)
at LoadAppKeyAndModify.Program.Main(String[] args)
source:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("INFO: Running as {0} in {1}-bit process on {2}-bit OS",
new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator) ? "Admin" : "Normal User",
Environment.Is64BitProcess ? 64 : 32,
Environment.Is64BitOperatingSystem ? 64 : 32);
if (args.Length != 1)
{
throw new ApplicationException("Need 1 argument - path to the software hive file on disk");
}
string softwareHiveFile = Path.GetFullPath(args[0]);
if (File.Exists(softwareHiveFile) == false)
{
throw new ApplicationException("Specified file does not exist: " + softwareHiveFile);
}
// pick a random subkey so it doesn't already exist
var existingKeyPath = #"Microsoft\Windows\CurrentVersion";
var keyPathToCreate = #"RunOnceEx\" + Guid.NewGuid();
var completeKeyPath = Path.Combine(existingKeyPath, keyPathToCreate);
var hKey = RegistryNativeMethods.RegLoadAppKey(softwareHiveFile);
using (var safeRegistryHandle = new SafeRegistryHandle(new IntPtr(hKey), true))
using (var appKey = RegistryKey.FromHandle(safeRegistryHandle))
using (var currentVersionKey = appKey.OpenSubKey(existingKeyPath, true))
{
if (currentVersionKey == null)
{
throw new ApplicationException("Specified file is not a well-formed software registry hive: " + softwareHiveFile);
}
using (var randomSubKey = currentVersionKey.CreateSubKey(keyPathToCreate))
{
randomSubKey.SetValue("foo", "bar");
Console.WriteLine("Was able to create {0} and write a value under it", completeKeyPath);
}
}
}
}
internal static class RegistryNativeMethods
{
[Flags]
public enum RegSAM
{
AllAccess = 0x000f003f
}
private const int REG_PROCESS_APPKEY = 0x00000001;
// approximated from pinvoke.net's RegLoadKey and RegOpenKey
// NOTE: changed return from long to int so we could do Win32Exception on it
[DllImport("advapi32.dll", SetLastError = true)]
private static extern int RegLoadAppKey(String hiveFile, out int hKey, RegSAM samDesired, int options, int reserved);
public static int RegLoadAppKey(String hiveFile)
{
int hKey;
int rc = RegLoadAppKey(hiveFile, out hKey, RegSAM.AllAccess, REG_PROCESS_APPKEY, 0);
if (rc != 0)
{
throw new Win32Exception(rc, "Failed during RegLoadAppKey of file " + hiveFile);
}
return hKey;
}
}
Ended up with opening a support case with Microsoft Support - the problem is specific to 1) the hives that ship on the install media for recent versions of Windows and 2) RegLoadAppKey as an API. Switching over to RegLoadKey/RegUnLoadKey instead worked fine for the exact same files (in the same process, even), and since the bug in RegLoadAppKey is unlikely to get fixed (let alone soon) to deal with those specific files, I just switched to RegLoadKey/RegUnLoadKey instead.
Following link should be helpful in this scenario:
http://msdn.microsoft.com/en-us/library/ms973190.aspx#64mig_topic5

Categories

Resources