Chr☆s Kwok 的技术笔记

.NET, C#, WPF, WCF, WF, .NetCore & LINQ ... I know how it works because I know why it works ...

博客园 首页 新随笔 订阅 管理

【目标】定时检测特定应用是否运行,如没运行则启动它:

方法1:创建Windows任务计划程序(外部程序实例已运行时则忽略):


最后一步就是指定运行程序(起始路径设置为外部程序的所在路径很重要,否则可能无法因找不到程序无法启动):


方法2:创建Windows服务来启动外部程序(外部程序编写需要有自己判断实例是否已存在,存在则退出,保持单实例):

但是通常应用程序的带UI界面的,遗憾的是Windows服务是不支持运行UI界面程序的,譬如下面方式运行带UI界面的应用则发现打不开

//打开程序的地址
var fileName = path;
//判断文件是否存在,如果不存在,返回 false
Console.WriteLine(File.Exists(fileName));

ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.FileName = fileName;
Process process = new Process();
process.StartInfo = startInfo;
process.Start();

这里就需要用到“绕过UAC以最高权限”来启动:

自Vista操作系统之后,微软考虑到安全因素,在系统管理员账户和标准用户之间创出了UAC(用户账户控制)。当标准用户启动需管理员权限的操作时要弹框让用户确认,这样可防止恶意软件或间谍软件随意修改系统造成破坏。

但对于必须要通过最高权限运行交互进程来说就造成问题,微软API接口提供CreateProcessAsUser函数用于在后台服务程序中启动前台进程,但启动时要请求UAC权限(由于后台服务是最高权限启动,其创建的子进程也继承最高权限),这时后台UAC窗口无法显示在前台界面上,造成程序永远等待无法启动。

vista之后,微软会为每个登录用户分配一个会话,后台服务在系统启动时最先启动,分配的会话ID为0,其后每登录一个用户会话ID加1:

问题来了,由于有会话隔离,我们无法在一个会话程序中直接启动另一会话的程序。但微软有一个特殊的进程,对于每个会话会有一个对应的进程,这个进程就是winlogin.exe:

winlogin进程的作用

Winlogon.exe进程是微软公司为其Windows操作系统定义的一个非常重要的系统核心进程,被称为“Windows登陆应用程序”,它会随着系统启动而启动并一直运行。通常情况下此进程应该是安全的,并且只占用很少的CPU及内存资源,如果资源占用过多则很有可能被病毒“劫持”。

请不要尝试将本进程终止(也无法在任务管理器将其终止)或将此进程从系统中删除,这会导致你的系统无法正常运行。因为Winlogon.exe进程为系统提供了有以下4项重要的功能:
  1、在登录系统时加载的用户配置文件,以及执行注销用户与锁定计算机;
  2、负责处理Ctrl+Alt+Del快捷键(SAS)的功能;
  3、监控键盘和鼠标使用情况来决定什么时候启动屏幕保护程序;
  4、检验Windows操作系统激活密钥是否为合法许可;

可以发现winlogin进程是后台服务进程,但所属登录用户会话,那是不是可以通过这个进程来达到我们绕过UAC的限制启动前台交互程序呢?没错!!!

有了winlogin进程,我们可以在后台服务中先查询到winlogin进程信息,获取其访问令牌,最后通过CreateProcessAsUser将进程启动到活动登录用户当前活动会话。由于和前台界面所属同一会话,启动后的程序便可以进行交互。
下面为有心人写好的代码(ApplicationLoader.cs):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Security;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace CheckProcService
{
    /// <summary>
    /// Class that allows running applications with full admin rights. In
    /// addition the application launched will bypass the Vista UAC prompt.
    /// </summary>
    public class ApplicationLoader
    {
        #region Structures

        [StructLayout(LayoutKind.Sequential)]
        public struct SECURITY_ATTRIBUTES
        {
            public int Length;
            public IntPtr lpSecurityDescriptor;
            public bool bInheritHandle;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct STARTUPINFO
        {
            public int cb;
            public String lpReserved;
            public String lpDesktop;
            public String lpTitle;
            public uint dwX;
            public uint dwY;
            public uint dwXSize;
            public uint dwYSize;
            public uint dwXCountChars;
            public uint dwYCountChars;
            public uint dwFillAttribute;
            public uint dwFlags;
            public short wShowWindow;
            public short cbReserved2;
            public IntPtr lpReserved2;
            public IntPtr hStdInput;
            public IntPtr hStdOutput;
            public IntPtr hStdError;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct PROCESS_INFORMATION
        {
            public IntPtr hProcess;
            public IntPtr hThread;
            public uint dwProcessId;
            public uint dwThreadId;
        }

        #endregion

        #region Enumerations

        enum TOKEN_TYPE : int
        {
            TokenPrimary = 1,
            TokenImpersonation = 2
        }

        enum SECURITY_IMPERSONATION_LEVEL : int
        {
            SecurityAnonymous = 0,
            SecurityIdentification = 1,
            SecurityImpersonation = 2,
            SecurityDelegation = 3,
        }

        #endregion

        #region Constants

        public const int TOKEN_DUPLICATE = 0x0002;
        public const uint MAXIMUM_ALLOWED = 0x2000000;
        public const int CREATE_NEW_CONSOLE = 0x00000010;

        public const int IDLE_PRIORITY_CLASS = 0x40;
        public const int NORMAL_PRIORITY_CLASS = 0x20;
        public const int HIGH_PRIORITY_CLASS = 0x80;
        public const int REALTIME_PRIORITY_CLASS = 0x100;

        #endregion

        #region Win32 API Imports

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool CloseHandle(IntPtr hSnapshot);

        [DllImport("kernel32.dll")]
        static extern uint WTSGetActiveConsoleSessionId();

        [DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUser", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
        public extern static bool CreateProcessAsUser(IntPtr hToken, String lpApplicationName, String lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes,
            ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandle, int dwCreationFlags, IntPtr lpEnvironment,
            String lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);

        [DllImport("kernel32.dll")]
        static extern bool ProcessIdToSessionId(uint dwProcessId, ref uint pSessionId);

        [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")]
        public extern static bool DuplicateTokenEx(IntPtr ExistingTokenHandle, uint dwDesiredAccess,
            ref SECURITY_ATTRIBUTES lpThreadAttributes, int TokenType,
            int ImpersonationLevel, ref IntPtr DuplicateTokenHandle);

        [DllImport("kernel32.dll")]
        static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);

        [DllImport("advapi32", SetLastError = true), SuppressUnmanagedCodeSecurityAttribute]
        static extern bool OpenProcessToken(IntPtr ProcessHandle, int DesiredAccess, ref IntPtr TokenHandle);

        #endregion

        /// <summary>
        /// Launches the given application with full admin rights, and in addition bypasses the Vista UAC prompt
        /// </summary>
        /// <param name="applicationName">The name of the application to launch</param>
        /// <param name="procInfo">Process information regarding the launched application that gets returned to the caller</param>
        /// <returns></returns>
        public static bool StartProcessAndBypassUAC(string applicationName, string currentDirectory, out PROCESS_INFORMATION procInfo)
        {
            uint winlogonPid = 0;
            IntPtr hUserTokenDup = IntPtr.Zero, hPToken = IntPtr.Zero, hProcess = IntPtr.Zero;
            procInfo = new PROCESS_INFORMATION();

            // obtain the currently active session id; every logged on user in the system has a unique session id
            uint dwSessionId = WTSGetActiveConsoleSessionId();

            // obtain the process id of the winlogon process that is running within the currently active session
            Process[] processes = Process.GetProcessesByName("winlogon");
            foreach (Process p in processes)
            {
                if ((uint)p.SessionId == dwSessionId)
                {
                    winlogonPid = (uint)p.Id;
                }
            }

            // obtain a handle to the winlogon process
            hProcess = OpenProcess(MAXIMUM_ALLOWED, false, winlogonPid);

            // obtain a handle to the access token of the winlogon process
            if (!OpenProcessToken(hProcess, TOKEN_DUPLICATE, ref hPToken))
            {
                CloseHandle(hProcess);
                return false;
            }

            // Security attibute structure used in DuplicateTokenEx and CreateProcessAsUser
            // I would prefer to not have to use a security attribute variable and to just 
            // simply pass null and inherit (by default) the security attributes
            // of the existing token. However, in C# structures are value types and therefore
            // cannot be assigned the null value.
            SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
            sa.Length = Marshal.SizeOf(sa);

            // copy the access token of the winlogon process; the newly created token will be a primary token
            if (!DuplicateTokenEx(hPToken, MAXIMUM_ALLOWED, ref sa, (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, (int)TOKEN_TYPE.TokenPrimary, ref hUserTokenDup))
            {
                CloseHandle(hProcess);
                CloseHandle(hPToken);
                return false;
            }

            // By default CreateProcessAsUser creates a process on a non-interactive window station, meaning
            // the window station has a desktop that is invisible and the process is incapable of receiving
            // user input. To remedy this we set the lpDesktop parameter to indicate we want to enable user 
            // interaction with the new process.
            STARTUPINFO si = new STARTUPINFO();
            si.cb = (int)Marshal.SizeOf(si);
            si.lpDesktop = @"winsta0\default"; // interactive window station parameter; basically this indicates that the process created can display a GUI on the desktop

            // flags that specify the priority and creation method of the process
            int dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE;

            // create a new process in the current user's logon session
            bool result = CreateProcessAsUser(hUserTokenDup,        // client's access token
                                            null,                   // file to execute
                                            applicationName,        // command line
                                            ref sa,                 // pointer to process SECURITY_ATTRIBUTES
                                            ref sa,                 // pointer to thread SECURITY_ATTRIBUTES
                                            false,                  // handles are not inheritable
                                            dwCreationFlags,        // creation flags
                                            IntPtr.Zero,            // pointer to new environment block 
                                            currentDirectory,       // name of current directory 
                                            ref si,                 // pointer to STARTUPINFO structure
                                            out procInfo            // receives information about new process
                                            );

            // invalidate the handles
            CloseHandle(hProcess);
            CloseHandle(hPToken);
            CloseHandle(hUserTokenDup);

            return result; // return the result
        }

    }
}

然后创建一个服务类(CheckProcRunningService.cs):

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace CheckProcService
{
    public partial class CheckProcRunningService : ServiceBase
    {
        public CheckProcRunningService()
        {
            InitializeComponent();
        }

        private const int _MIN_INTERVAL = 60000;
        protected override void OnStart(string[] args)
        {
            CommandLineArguments arguments = new CommandLineArguments(args);
            ConfigHelper.Arguments = arguments;

            System.Threading.ThreadStart job = new System.Threading.ThreadStart(StartJob);
            System.Threading.Thread thread = new System.Threading.Thread(job);
            thread.Start();
            EventLog.WriteEntry(this.ServiceName, string.Format("CheckProcRunningService -period:{0} -procfilename:{1}", this.Period.ToString(), this.ProcFileName), EventLogEntryType.Information);
            return;
        }

        private static int? _period;
        public int Period
        {
            get
            {
                if (!_period.HasValue)
                {
                    int period = _MIN_INTERVAL;
                    //EventLog.WriteEntry(this.ServiceName, string.Format("-period:{0}", ConfigHelper.Arguments.Parameters.ContainsKey("Period")), EventLogEntryType.Warning);
                    if (ConfigHelper.Arguments.Parameters.ContainsKey("Period"))
                        int.TryParse(ConfigHelper.Arguments.Parameters["Period"], out period);
                    _period = period;
                }
                return _period.Value;
            }
        }

        private static string _procfilename;
        public string ProcFileName
        {
            get
            {
                if (string.IsNullOrWhiteSpace(_procfilename))
                {
                    string fn = Path.Combine(Path.GetDirectoryName(typeof(Program).Assembly.Location), "trayicondemo.exe");
                    if (ConfigHelper.Arguments.Parameters.ContainsKey("ProcFileName"))
                        fn = ConfigHelper.Arguments.Parameters["ProcFileName"];
                    _procfilename = fn;
                }
                return _procfilename;
            }
        }

        protected override void OnStop()
        {
            if (_timer != null) _timer.Dispose();
        }

        private Timer _timer;
        private void StartJob()
        {
            try
            {
                EventLog.WriteEntry(this.ServiceName, string.Format("Starting on {0} Paramters[Period: {1} ProcFileName:{2}]", DateTime.Now, this.Period.ToString(), this.ProcFileName), EventLogEntryType.Information);

                _timer = new Timer(new TimerCallback(ExecCheckRunJob));
                _timer.Change(0, this.Period);
            }
            catch (Exception ex)
            {
                EventLog.WriteEntry(this.ServiceName, ex.Message, EventLogEntryType.Warning);
            }
        }

        private void ExecCheckRunJob(object state)
        {
            //string errMsg = string.Empty;
            //ProcessHelper.ExecBatch(fn, false, false, false, "", ref errMsg);

            // the name of the application to launch;
            // to launch an application using the full command path simply escape
            // the path with quotes, for example to launch firefox.exe:
            //      String applicationName = "\"C:\\Program Files (x86)\\Mozilla Firefox\\firefox.exe\"";
            //string fn = "cmd.exe";

            // launch the application
            ApplicationLoader.PROCESS_INFORMATION procInfo;
            string currentDirectory = Path.GetDirectoryName(this.ProcFileName);
            ApplicationLoader.StartProcessAndBypassUAC(this.ProcFileName, currentDirectory, out procInfo);
        }
    }
}

覆写方法OnStart提供了命令行参数来指定检测频率(间隔多少毫秒,应用程序全路径),命令行参数辅助类(CommandLineUtility.cs)代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Specialized;
using System.Text.RegularExpressions;

namespace CheckProcService
{
    /// <summary>
    /// 命令行参数类
    /// 测试参数如下:-param0 -param1 value1 --param2 value2 /param3 value3 =param4 value4 :param5 value5 -param6=param6 -param7:param7 -param8.1 "1234" -param8.2 "1 2 3 4" -param9.1 '1234' -param9.2='1 2 3 4'
    /// 各参数项测试内容如下:
    /// -param0,只有参数项
    /// -param1 value1,有参数项,有参数值
    /// --param2 value2,用--标记参数项开头
    /// /param value3,用/标记参数项开头
    /// =param4 value4,用=标记参数项开头
    /// :param5 value5,用:标记参数项开头
    /// -param6=param6,用=标记参数项与参数值的关系
    /// -param7:param7,用:标记参数项与参数值的关系
    /// -param8.1 "1234",用""指定参数
    /// -param8.2 "1 2 3 4",用""指定参数(含空格)
    /// -param9.1 '1234',用''指定参数
    /// -param9.2='1 2 3 4',用''指定参数(含空格)
    /// </summary>
    public class CommandLineArguments
    {
        // Variables
        private StringDictionary _parameters;

        // Constructor
        public CommandLineArguments(string[] Args)
        {
            Parameters = new StringDictionary();
            Regex spliter = new Regex(@"^-{1,2}|^/|=|:",
                RegexOptions.IgnoreCase | RegexOptions.Compiled);

            Regex remover = new Regex(@"^['""]?(.*?)['""]?$",
                RegexOptions.IgnoreCase | RegexOptions.Compiled);

            string parameter = null;
            string[] parts;

            // Valid parameters forms:
            // {-,/,--}param{ ,=,:}((",')value(",'))
            // Examples: 
            // -param1 value1 --param2 /param3:"Test-:-work" 
            //   /param4=happy -param5 '--=nice=--'
            foreach (string txt in Args)
            {
                // Look for new parameters (-,/ or --) and a
                // possible enclosed value (=,:)
                parts = spliter.Split(txt, 3);

                switch (parts.Length)
                {
                    // Found a value (for the last parameter 
                    // found (space separator))
                    case 1:
                        if (parameter != null)
                        {
                            if (!Parameters.ContainsKey(parameter))
                            {
                                parts[0] =
                                    remover.Replace(parts[0], "$1");

                                Parameters.Add(parameter, parts[0]);
                            }
                            parameter = null;
                        }
                        // else Error: no parameter waiting for a value (skipped)
                        break;

                    // Found just a parameter
                    case 2:
                        // The last parameter is still waiting. 
                        // With no value, set it to true.
                        if (parameter != null)
                        {
                            if (!Parameters.ContainsKey(parameter))
                                Parameters.Add(parameter, "true");
                        }
                        parameter = parts[1];
                        break;

                    // Parameter with enclosed value
                    case 3:
                        // The last parameter is still waiting. 
                        // With no value, set it to true.
                        if (parameter != null)
                        {
                            if (!Parameters.ContainsKey(parameter))
                                Parameters.Add(parameter, "true");
                        }

                        parameter = parts[1];

                        // Remove possible enclosing characters (",')
                        if (!Parameters.ContainsKey(parameter))
                        {
                            parts[2] = remover.Replace(parts[2], "$1");
                            Parameters.Add(parameter, parts[2]);
                        }

                        parameter = null;
                        break;
                }
            }
            // In case a parameter is still waiting
            if (parameter != null)
            {
                if (!Parameters.ContainsKey(parameter))
                    Parameters.Add(parameter, "true");
            }
        }

        // Retrieve a parameter value if it exists 
        // (overriding C# indexer property)
        public string this[string param]
        {
            get
            {
                return (Parameters[param]);
            }
        }

        public StringDictionary Parameters
        {
            get { return _parameters; }
            set { _parameters = value; }
        }
    }

    public class ConfigHelper
    {
        public static CommandLineArguments Arguments { get; set; }
    }
}

服务安装类(Installer.cs)如下:

using Microsoft.Win32;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration.Install;
using System.Linq;
using System.Threading.Tasks;

namespace CheckProcService
{
    [RunInstaller(true)]
    public partial class ServiceInstaller : Installer
    {
        public ServiceInstaller()
        {
            InitializeComponent();

            this.serviceProcessInstaller1.Account = System.ServiceProcess.ServiceAccount.LocalSystem;
            this.serviceProcessInstaller1.Password = null;
            this.serviceProcessInstaller1.Username = null;
            //this.serviceProcessInstaller1.Account = System.ServiceProcess.ServiceAccount.User;
            //this.serviceProcessInstaller1.Password = "Bronzepen1o3$";
            //this.serviceProcessInstaller1.Username = "guoshaoyue@hissoft.com";

            this.serviceInstaller1.ServiceName = "procsvcl";
            this.serviceInstaller1.DisplayName = "Check Compiler Manager Running";
            this.serviceInstaller1.Description = "Check compiler process running, otherwise run it immediately.";
            this.serviceInstaller1.StartType = System.ServiceProcess.ServiceStartMode.Automatic;
        }

        protected override void OnAfterInstall(IDictionary savedState)
        {
            base.OnAfterInstall(savedState);

            //设置允许服务与桌面交互
            RegistryKey rk = Registry.LocalMachine;
            string key = @"SYSTEM\CurrentControlSet\Services\" + this.serviceInstaller1.ServiceName;
            RegistryKey sub = rk.OpenSubKey(key, true);
            int value = (int)sub.GetValue("Type");
            sub.SetValue("Type", value | 256);
        }
    }
}

主程序如下(Program.cs):

using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Threading.Tasks;

namespace CheckProcService
{
    static class Program
    {
        /// <summary>
        /// 应用程序的主入口点。
        /// </summary>
        static void Main()
        {
            ServiceBase[] ServicesToRun;
            ServicesToRun = new ServiceBase[]
            {
                new CheckProcRunningService()
            };
            ServiceBase.Run(ServicesToRun);
        }
    }
}

部署服务代码如下:

net stop procsvcl

C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe /u "F:\GitHub\PycharmProjects\PyQtTest\dist\CheckProcService.exe"
pause

C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe "F:\GitHub\PycharmProjects\PyQtTest\dist\CheckProcService.exe"
pause

sc start procsvcl /period:300000 /procfilename:"F:\GitHub\PycharmProjects\PyQtTest\dist\trayicondemo.exe"

通过测试我们把命令行参数传递进去了:

posted on 2022-09-02 15:16  Chr☆s  阅读(2359)  评论(0编辑  收藏  举报