I've developed a small C# software that interfaces with a piece of hardware that will always be connected to the pc. Since this software requires administrative privileges and I want it to start as the PC starts, I'm evaluating what's the best way to deploy it.
The ideal would be to create an installer that takes care of disabling the UAC prompt for that specific software during its installation, so that everytime that specific software starts I won't receive any UAC prompt. Would this be possible? What are the possible alternatives?
You can't create a setup that installs an application in such a way that an interactive app requires admin privilege but does not prompt for it. The install is separate from the app.
Assuming you build an MSI-based Windows Installer setup:
The install almost certainly requires admin privileges to install, and the tool you use to build the MSI will let you say that, and the install will prompt for elevation.
The way you build an app that runs elevated is to have a manifest that requests elevation when it starts.
If you literally want your code to run when the machine starts then it must be installed as a service. Practically all other methods (the Run registry key, the Startup folder in Program menu etc) are called when a user logs in (NOT when the system starts). If you need a UI to do something with the device then you'll need a Windows UI app that talks to the service (because services cannot interact with the desktop).
I find the answer for run a application with admin permission at startup.
Basically I just created a task with run level Highest and your trigger is on Logon.
I found the code in vb in this repository: https://bitbucket.org/trparky/start_program_at_startup_without_uac
Sub addTask(taskName As String, taskDescription As String, taskEXEPath As String, taskParameters As String)
taskName = taskName.Trim
taskDescription = taskDescription.Trim
taskEXEPath = taskEXEPath.Trim
taskParameters = taskParameters.Trim
If Not IO.File.Exists(taskEXEPath) Then
MsgBox("Executable path not found.", MsgBoxStyle.Critical, Me.Text)
Exit Sub
End If
Dim taskService As TaskService = New TaskService()
Dim newTask As TaskDefinition = taskService.NewTask
newTask.RegistrationInfo.Description = taskDescription
If chkEnabled.Checked Then newTask.Triggers.Add(New LogonTrigger)
Dim exeFileInfo As New FileInfo(taskEXEPath)
newTask.Actions.Add(New ExecAction(Chr(34) & taskEXEPath & Chr(34), taskParameters, exeFileInfo.DirectoryName))
newTask.Principal.RunLevel = TaskRunLevel.Highest
newTask.Settings.Compatibility = TaskCompatibility.V2_1
newTask.Settings.AllowDemandStart = True
newTask.Settings.DisallowStartIfOnBatteries = False
newTask.Settings.RunOnlyIfIdle = False
newTask.Settings.StopIfGoingOnBatteries = False
newTask.Settings.AllowHardTerminate = False
newTask.Settings.UseUnifiedSchedulingEngine = True
newTask.Settings.ExecutionTimeLimit = Nothing
newTask.Settings.Priority = ProcessPriorityClass.Normal
newTask.Principal.LogonType = TaskLogonType.InteractiveToken
taskService.RootFolder.SubFolders(strTaskFolderName).RegisterTaskDefinition(taskName, newTask)
newTask.Dispose()
taskService.Dispose()
newTask = Nothing
taskService = Nothing
End Sub
So all I did was translated this code to c# and make tests
using Microsoft.Win32.TaskScheduler;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CreateTaskTest
{
class Program
{
static void Main(string[] args)
{
addTask();
//deleteTask();
}
static void addTask()
{
// Get the service on the local machine
using (TaskService ts = new TaskService())
{
// Create a new task definition and assign properties
TaskDefinition newTask = ts.NewTask();
newTask.RegistrationInfo.Description = "Rondinelli Morais Create Task";
newTask.Triggers.Add(new LogonTrigger());
newTask.Actions.Add(new ExecAction("C:\\Windows\\regedit.exe"));
newTask.Principal.RunLevel = TaskRunLevel.Highest;
newTask.Principal.LogonType = TaskLogonType.InteractiveToken;
newTask.Settings.Compatibility = TaskCompatibility.V2_1;
newTask.Settings.AllowDemandStart = true;
newTask.Settings.DisallowStartIfOnBatteries = false;
newTask.Settings.RunOnlyIfIdle = false;
newTask.Settings.StopIfGoingOnBatteries = false;
newTask.Settings.AllowHardTerminate = false;
newTask.Settings.UseUnifiedSchedulingEngine = true;
newTask.Settings.Priority = System.Diagnostics.ProcessPriorityClass.Normal;
// Register the task in the root folder
ts.RootFolder.RegisterTaskDefinition(#"Test", newTask);
newTask.Dispose();
ts.Dispose();
}
}
static void deleteTask()
{
using (TaskService ts = new TaskService())
{
var tasks = ts.FindAllTasks(new System.Text.RegularExpressions.Regex(#"Test"));
foreach(var task in tasks){
ts.RootFolder.DeleteTask(task.Name);
}
}
}
}
}
I'm using regedit.exe on exemple because this program required admin permission for run.
Create the task, make logoff and login again and you will see the regedit open after logon.
OBS: To create or delete task you have run visual studio as administrator, or put this code in the install process of your program
Let me know if this worked for someone
Related
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.
This question explains how to create a shortcut in C#.
For example,
using System;
using IWshRuntimeLibrary;
using System.IO;
namespace TestCreateShortcut
{
class Program
{
static void Main(string[] args)
{
WshShell shell = new WshShell();
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
string source = "C:\\foo\\hello.exe";
string dest = desktop + "\\hello.lnk";
IWshRuntimeLibrary.IWshShortcut shortcut =
(IWshRuntimeLibrary.IWshShortcut)shell.CreateShortcut(dest);
shortcut.TargetPath = source;
shortcut.WorkingDirectory = new FileInfo(source).Directory.Name;
shortcut.Save();
}
}
}
The problem is that, as an Administrator user, I want to create this shortcut on another user's Desktop. I could change the desktop string to be their Desktop path instead of mine, but the catch is this user will have been created right before I want to call the code to make a Desktop shortcut, so there is no C:\Users\TheUser folder yet!
What are some ways to alleviate this situation or make it possible to put a shortcut on a newly-created user's desktop?
I preferably want to do this before the users actually log in for the first time. Thank you.
My program uses Inno Setup to install/uninstall it. In my application code I create a Global mutex using the CreateMutex Windows API function. Then in my Inno Setup program I have the following code:
AppMutex=Global\MyProgramMutex.2A23834B-2919-4007-8C0A-3C7EDCA7186E
function InitializeSetup(): Boolean;
begin
Result := True;
if (CreateMutex(0, False, '{#SetupSetting('AppId')}') <> 0) and (DLLGetLastError = ERROR_ALREADY_EXISTS) then
begin
Result := False;
MsgBox('Another instance of the Setup program is already running. Please close it and try again', mbCriticalError, MB_OK);
end;
if CheckForMutexes('{#SetupSetting('AppMutex')}') then
begin
Result := False;
MsgBox('{#SetupSetting('AppName')} ' + 'appears to be running. Please close all instances of the program before continuing.', mbCriticalError, MB_OK);
end;
end;
This works great, as expected, for the user running the Inno Setup program. The question/problem I have is: If I "Switch User" and start the application as a different user, and then switch back to the original user, the Setup program does not detect that the application is running under a different user.
I'm not knowledgeable all round enough to know, if the Setup program can detect the running application.
As documented, in Inno Setup KB Detect instances running in any user session with AppMutex:
To detect mutexes created in other sessions, your application must create two mutexes: one with a Global\ prefix and the other without.
Mutexes with the Global\ prefix are accessible from any user session. A like-named mutex must also be created in the session namespace (i.e. without the Global\ prefix) in case the creation of the Global mutex failed due to security restrictions or lack of operating system support (versions of Windows NT prior to 4.0 Terminal Server Edition don't support the Global\ prefix).
Additionally, a special security descriptor must be passed in each of the CreateMutex() calls to ensure the mutex is accessible by different users.
To make a mutex accessible by all users in C#, see:
What is a good pattern for using a Global Mutex in C#?
In sum, the code in your C# application should be like:
const string mutexId = "MyProg";
MutexAccessRule allowEveryoneRule =
new MutexAccessRule(
new SecurityIdentifier(WellKnownSidType.WorldSid, null),
MutexRights.FullControl, AccessControlType.Allow);
MutexSecurity securitySettings = new MutexSecurity();
securitySettings.AddAccessRule(allowEveryoneRule);
Mutex globalMutex = null;
try
{
bool createdNew;
globalMutex =
new Mutex(false, "Global\\" + mutexId, out createdNew, securitySettings);
}
catch (UnauthorizedAccessException)
{
// Ignore
}
Mutex localMutex = new Mutex(false, mutexId);
try
{
// Run your program here
}
finally
{
// These have to be called only after the application (its windows) closes.
// You can also remove these calls and let the system release the mutexes.
if (globalMutex != null)
{
globalMutex.Dispose();
}
localMutex.Dispose();
}
On Inno Setup side, all you need is to list both mutexes in the AppMutex directive:
[Setup]
AppMutex=MyProg,Global\MyProg
You do not need your CreateMutex and CheckForMutexes calls in the InitializeSetup funcion.
Great idea Martin. Here is my full solution for using mutex objects in WPF that an Inno Setup program will detect, even when other logged-in users are running the WPF app. BTW. I used Visual Studio.
Assume that both the WPF app and project are called 'MyWPFApp'
Open the project properties for MyWPFApp; on the 'Application' tab ensure that the startup object is 'MyWPFApp.App'.
Change the Build Action of App.xaml from ApplicationDefinition to Page.
Remove the StartupUri property from App.xaml, if used.
If an Application.Startup event is used, remove any code that instantiates and displays the MainWindow.
Add the following, or similar, code to App.xaml.cs as part of the App class.
public partial class App : Application
{
private static readonly string _MutexID = "MyWPFApp"; // or whatever
[STAThread]
public static void Main()
{
var application = new App();
application.InitializeComponent();
MutexAccessRule allowEveryoneRule = new MutexAccessRule(
new SecurityIdentifier(WellKnownSidType.WorldSid, null),
MutexRights.FullControl,
AccessControlType.Allow);
MutexSecurity securitySettings = new MutexSecurity();
securitySettings.AddAccessRule(allowEveryone);
Mutex globalMutex = null;
try
{
bool createdNew;
globalMutex = new Mutex(false, "Global\\" + _MutexID, out createdNew, securitySettings);
}
catch (UnauthorizedAccessException)
{
// ignore
}
Mutex localMutex = new Mutex(false, _MutexID);
try
{
MainWindow mainWin = new MainWindow();
application.Run(mainWin);
}
finally
{
if (globalMutex != null)
{
globalMutex.Dispose();
}
localMutex.Dispose();
}
}
}
The final step is to include the following line in the Inno Setup script:
[Setup]
AppMutex=MyWPFApp,Global\MyWPFApp
I tried to architect the code using C# using statements for both mutexes, but I got brain freeze.
Alternatively, one can also create a separate class with a Main method in it, and place the above code there. This requires steps 4 and 5 above, and in step 2 change the Startup object to the new class containing the Main method.
Thanks Martin.
Bob
I've been trying for a couple weeks now to run a non-elevated web browser from an elevated process, I have tried various things, duplicating the explorer token, using the WinSafer Apis mentioned here and various other techniques that all failed. Finally I decided to use Microsoft's suggestion of using the Task Scheduler to run the application.
I used the Task Scheduler Managed Wrapper, at first I tried running explorer.exe and passing the url as a command but that did not work so I created a dummy executable that'll launch the site using Process.Start.
Here is how I create the task:
public static void LaunchWin8BrowserThroughTaskScheduler(string sURL)
{
String RunAsUserExecPath = AppDomain.CurrentDomain.BaseDirectory + "URLLaunch.exe";
String Command = string.Format("-w \"{0}\"", sURL);
using (TaskService ts = new TaskService())
{
TaskDefinition td = ts.NewTask();
td.RegistrationInfo.Description = "URL Launch";
td.Principal.LogonType = TaskLogonType.InteractiveToken;
TimeTrigger trigger = new TimeTrigger(DateTime.Now.AddSeconds(2))
{
Enabled = true,
EndBoundary = DateTime.Now.AddSeconds(10)
};
td.Triggers.Add(trigger);
td.Actions.Add(new ExecAction( RunAsUserExecPath, Command, null));
td.Settings.StartWhenAvailable = true;
//Delete the task after 30 secs
td.Settings.DeleteExpiredTaskAfter = new TimeSpan(0,0,0,30);
ts.RootFolder.RegisterTaskDefinition("URL Launch", td, TaskCreation.CreateOrUpdate, null, null, TaskLogonType.InteractiveToken);
}
}
and this is the code to my dummy executable:
static void Main(string[] args)
{
if(args.Length<=1)
return;
string sCmd = args[0];
string sArg = args[1];
if(string.IsNullOrEmpty(sCmd)||string.IsNullOrEmpty(sArg))
return;
switch (sCmd)
{
case "-w":
{
Process prs = Process.Start(sArg);
}
break;
}
}
This method is working, and the browser is indeed launched non-elevated and I was able to confirm that by checking the Elevated column in Windows 8's task manager.
The only nuance here is that the browser is not launched as the top most window, it is running in the background and I think its got to do with the fact that its being run through task scheduler.
This is causing me problems especially with Modern UI browsers because Windows does not switch to them when a page is launched. I can see that the page has been successfully launched in Chrome, for example, while running in Windows 8 mode, but the fact that it does not switch to the browser just defies the whole purpose of this workaround.
I thought about using SetForegroundWindow but sadly running a URL like the example above or through explorer.exe, Process.Start returns null.
I was wondering if someone can help me fix this and be able to run the browser in foreground.
Regards
I've been able to solve the issue with a very simplistic method.
Just write a shortcut file to somewhere like the TempFolder and execute it through explorer.exe likes so:
public static void GoURL(string url)
{
string sysPath = Environment.GetFolderPath(Environment.SpecialFolder.System);
string ExplorerPath = Path.Combine(Directory.GetParent(sysPath).FullName,
"explorer.exe");
string TempDir = Environment.GetFolderPath(Environment.SpecialFolder.InternetCache);
string shortcutPath = Path.Combine(TempDir, "Mylink.url");
urlShortcutToTemp(url, shortcutPath);
System.Diagnostics.Process.Start(ExplorerPath, shortcutPath);
}
private static void urlShortcutToTemp(string linkUrl, string shortcutPath)
{
using (StreamWriter writer = new StreamWriter(shortcutPath))
{
writer.WriteLine("[InternetShortcut]");
writer.WriteLine("URL=" + linkUrl);
writer.Flush();
}
}
The same solution can be applied to executables with lnk shortcuts.
I have an application, that needs to get the last shutdown time. I have used EventLog class to get the shutdown time. I have separate class file that is designed to read/write event log. ReadPowerOffEvent function is intended to get the power off event.
public void ReadPowerOffEvent()
{
EventLog eventLog = new EventLog();
eventLog.Log = logName;
eventLog.MachineName = machineName;
if (eventLog.Entries.Count > 0)
{
for (int i = eventLog.Entries.Count - 1; i >= 0; i--)
{
EventLogEntry currentEntry = eventLog.Entries[i];
if (currentEntry.InstanceId == 1074 && currentEntry.Source=="USER32")
{
this.timeGenerated = currentEntry.TimeGenerated;
this.message = currentEntry.Message;
}
}
}
}
But whenever it tries to get the event entry count, it throws an IOException saying "The Network Path Not found". I tried to resolve, but I failed. Please help me out...
I think you sent wrong Log name, this worked for me
EventLog myLog = new EventLog();
myLog.Log = "System";
myLog.Source = "User32";
var lastEntry = myLog;
EventLogEntry sw;
for (var i = myLog.Entries.Count -1 ; i >=0; i--)
{
if (lastEntry.Entries[i].InstanceId == 1074)
sw = lastEntry.Entries[i];
break;
}
}
You have to have the "Remote Registry" service running on your machine (or the machine you want to run this app on). I suspect that this service in set to manual start on your machine. You may have to change the setting on this service to automatic.
If this app is going to be running on other machines, you may want to put some logic into your app to check to make sure this service is running first. If it isn't then you will need to start it up through your app.
Note:
The "Remote Registry" service enables remote users to modify registry setting on your computer. By default, the "Startup type" setting for the "Remote Registry" service may be set to "Automatic" or "Manual" which is a security risk for a single user (or) notebook PC user.
So, to make sure that only users on your computer can modify the system registry disable this "Remote Registry" service.