c# Windows Service 桌面上显示UI

时间:2023-12-16 15:59:08

介绍

本文的目的是说明如何从Windows Vista中的服务正确启动交互式进程,以及演示如何以完全管理员权限启动该进程。交互式过程是能够在桌面上显示UI的过程。

本文介绍如何创建一个名为LoaderService的服务,该服务充当应用程序加载器,其目的是在引导时启动以管理员身份运行的命令提示符。本文结束时将讨论如何扩展代码以实现更多实际目的。

Vista中的会话

让我们从头开始......你刚刚启动计算机并即将登录。登录时,系统会为您分配唯一的会话ID。在Windows Vista中,操作系统会为第一个登录计算机的用户分配一个会话ID为1。下一个登录用户将被分配一个会话ID为2.依此类推。您可以从任务管理器的“用户”选项卡中查看分配给每个登录用户的会话ID:

c# Windows Service 桌面上显示UI

注意,我表示名为Pero的用户可以控制控制台。在这种情况下,我的意思是物理控制台。物理控制台由显示器,键盘和鼠标组成。由于Pero控制着键盘,显示器和鼠标,因此他被认为是当前活跃的用户。但是,由于可以模拟用户,因此更适合引用当前活动的Session而不是当前活动的User。Win32 API包含一个函数WTSGetActiveConsoleSessionId(),该函数返回当前控制物理控制台的用户的会话ID。如果我们现在调用该方法,它将返回值1,因为这是User Pero的会话ID。

Vista中存在一个会话ID为0的特殊会话。通常称为Session0。所有Windows服务都在Session0中运行,而Session0是非交互式的。非交互式意味着无法启动UI应用程序; 但是,通过激活交互式服务检测服务(ISDS)可以解决这个问题。这不是一个非常优雅的解决方案,并不会在本文中介绍。有一个快速的5分钟Channel 9视频这表明感兴趣的人的ISDS。本文假设缺少ISDS。现在,因为Session0不是用户会话,所以它无法访问视频驱动程序,因此任何渲染图形的尝试都将失败。Session0隔离是Vista中添加的一项安全功能,用于将系统进程和服务与潜在的恶意用户应用程序隔离开来。

这是事情变得有趣的地方。这种隔离的原因是因为系统帐户(或系统用户)具有提升的权限,允许它不受Vista UAC限制的限制而运行。如果一切都在系统帐户下运行,Vista UAC也可能会被关闭。

现在,我知道你在想什么,“如果Windows Services在Session0中运行,而Session0无法启动具有UI的进程,那么我们的加载器服务如何产生一个新进程,该进程不仅具有UI,而且还在其中运行当前登录的用户会话?“从任务管理器的进程选项卡中查看此屏幕截图,并特别注意winlogon.exe进程:

c# Windows Service 桌面上显示UI

请注意,有两个winlogon.exe进程,拥有这两个进程的用户是系统用户。系统用户是一个高度特权的用户,不受我们之前讨论的Vista UAC的阻碍。另外,请注意会话ID,指示winlogon.exe进程正在运行的Sessions 。如果您记得之前的话,会话ID 1指的是用户Pero的会话,会话ID 2指的是用户Sienna的会话。这意味着在Pero会话中的系统帐户下运行winlogon.exe进程。这也意味着有一个winlogon.exe在Sienna会议期间在System帐户下运行的流程。这是适当的时候提及任何ID大于0的会话都能够产生一个交互式流程,这是一个能够显示UI的流程。

解决方案可能还不完全清楚,但不久之后,现在是时候讨论我们的战略了!

我们的战略

首先,我们将创建一个在System帐户下运行的Windows服务。此服务将负责在当前活动的用户会话中生成交互式进程。这个新创建的流程将显示一个用户界面,并以完全管理员权限运行。当第一个用户登录到计算机时,此服务将启动并将在Session0中运行; 但是,此服务生成的进程将在当前登录用户的桌面上运行。我们将此服务称为LoaderService

接下来,winlogon.exe进程负责管理用户登录和注销过程。我们知道登录到计算机的每个用户都将拥有唯一的会话ID和与其会话关联的相应winlogon.exe进程。现在,我们在上面提到过,LoaderService在System帐户下运行。我们还确认计算机上的每个winlogon.exe进程都在System帐户下运行。因为系统帐户是两者的所有者LoaderServiceWINLOGON.EXE过程中,我们LoaderService可以复制的访问令牌(和会话ID)的winlogon.exe进程然后调用Win32 API函数CreateProcessAsUser将进程启动到登录用户的当前活动Session。由于位于复制的winlogon.exe进程的访问令牌中的会话ID 大于0,因此我们可以使用该令牌启动交互式进程。

现在为了有趣的东西...代码!

代码

Windows服务位于一个名为LoaderService.cs的内部工具包项目。下面LoaderService是启动时调用的代码:

隐藏   复制代码
protected override void OnStart(string[] args)
{
// the name of the application to launch
String applicationName = "cmd.exe"; // launch the application
ApplicationLoader.PROCESS_INFORMATION procInfo;
ApplicationLoader.StartProcessAndBypassUAC(applicationName, out procInfo);
}

上面的代码调用StartProcessAndBypassUAC(...)函数,该函数将启动命令提示符(具有完全管理员权限)作为新创建的进程的一部分。有关新创建的进程的信息将存储到变量中procInfo

代码StartProcessAndBypassUAC(...)位于ApplicationLoader.cs文件中。让我们剖析该函数,以检查在Session0中运行的服务如何将进程加载到当前登录的用户会话中。首先,我们将获取当前登录用户的会话ID。这是通过调用Win32 API函数实现的WTSGetActiveConsoleSessionId()

隐藏   复制代码
// obtain the currently active session id; every logged on
// User in the system has a unique session id
uint dwSessionId = WTSGetActiveConsoleSessionId();

接下来,我们将获取当前活动Session 的winlogon.exe进程的进程ID(PID)。请记住,当前正在运行两个Sessions,如果我们复制了错误的访问令牌,我们最终可能会在另一个用户的桌面上启动我们的新流程。

隐藏   复制代码
// 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;
}
}

现在我们已经获得了winlogon.exe进程的PID ,我们可以使用该信息来获取其进程句柄。为此,我们对Win32 API进行调用OpenProcess(...)

隐藏   复制代码
// obtain a handle to the winlogon process
hProcess = OpenProcess(MAXIMUM_ALLOWED, false, winlogonPid);

获取进程句柄后,我们可以进行Win32 API调用OpenProcessToken(...)以获取winlogon.exe进程的访问令牌的句柄:

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

通过访问令牌的句柄,我们可以继续调用Win32 API函数DuplicateTokenEx(...),该函数将复制访问令牌:

隐藏   复制代码
// 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;
}

复制访问令牌有许多优点。在我们的案例中最值得注意的是,我们有一个主要访问令牌的新副本,其中还包含该复制令牌的关联登录会话。如果您参考上面显示两个winlogon.exe进程的任务管理器屏幕截图,您会注意到重复的会话ID将为1,这是当前登录用户Pero的会话ID。我们现在可以调用Win32 API函数CreateProcessAsUser在当前登录用户的Session中生成一个新进程; 在这种情况下,该进程将在User Pero的Session中生成。总而言之,下面的代码在Session0中运行,但将在Session 1中启动一个新进程:

隐藏   复制代码
STARTUPINFO si = new STARTUPINFO();
si.cb = (int)Marshal.SizeOf(si); // interactive window station parameter; basically this indicates
// that the process created can display a GUI on the desktop
si.lpDesktop = @"winsta0\default"; // 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
null, // name of current directory
ref si, // pointer to STARTUPINFO structure
out procInfo // receives information about new process
);

上面的代码将启动一个在系统帐户下以管理员身份运行的命令提示符。我想对参数发表评论@"winsta0\default"。这是一个硬编码String,微软任意选择向操作系统表明我们即将产生的进程CreateProcessAsUser应该具有对交互式windowstation和桌面的完全访问权限,这基本上意味着它允许在桌面上显示UI元素。

这就是代码的全部内容。现在,让我们讨论如何使用MSI部署此服务,以及如何将其配置为在计算机启动时自动启动!

部署代码

部署代码的最有效方法是为其创建MSI安装程序。但是,我们必须首先执行几项任务来准备我们的安装服务。首先,我们需要为LoaderService添加一个安装程序。要添加安装程序,请打开LoaderService.cs设计器。然后,右键单击,然后选择“添加安装程序”:

c# Windows Service 桌面上显示UI

上面的操作为名为的项目添加了一个新类ProjectInstaller。该类继承自Installer该类。还有对设计师可见两种成分ProjectInstaller.cs,我已改名为清晰度loaderServiceProcessInstallerloaderServiceInstaller。该loaderServiceProcessInstaller控件允许我们指定运行LoaderService的帐户。此帐户已设置为系统

c# Windows Service 桌面上显示UI

现在,我们准备添加一个安装项目。Setup项目的主要输出设置为Toolkit项目,该项目包含我们的LoaderService。这一步非常简单,我不会详细介绍添加它。但是,我想评论一下,我们需要将ProjectInstaller类连接到此MSI。如果我们不这样做,那么将部署Toolkit项目的内容,但是LoaderService不会被注册为Windows服务。要添加自定义操作,请右键单击“安装”项目,然后转到“视图”>“自定义操作”。从这里,您可以添加自定义操作。将自定义操作指定为Toolkit的主要输出足以提示它有一个自定义安装程序,在我们的例子中ProjectInstaller,需要运行。请记住,ProjectInstaller安装程序类实际上负责在Windows中注册服务:

c# Windows Service 桌面上显示UI

现在,是时候运行代码,看看我们的劳动成果!

运行代码

为了验证代码是否按预期工作,我们将构建MSI并进行安装。安装MSI时,您会注意到UAC提示您要求确认安装。我的一位好海军朋友曾告诉我,海军陆战队员有一句话,“曾经是一名海军陆战队员,永远都是海军陆战队员。” 在黑客和计算机安全方面,这将转化为“曾经是管理员,始终是管理员”。这是唯一一次安装项目的用户将看到UAC弹出窗口。由于大多数MSI需要管理员权限才能安装,因此用户不应该感到震惊。

安装完成后,您会注意到服务已经注册,自动启动(由ProjectInstaller); 但是,这只会在下次重启时发生。您也可以手动启动它。本文假设您已选择重新启动。请注意,当计算机重新启动时,将显示一个以管理员身份运行的命令提示符:

c# Windows Service 桌面上显示UI

从这里,您可以键入regeditgpedit.msc或任何您喜欢的命令,它将绕过Vista UAC提示。更重要的是,当前登录的用户甚至不需要是管理员来利用此命令提示符。原因是命令提示符在系统帐户下运行。这可以从下面屏幕截图中的任务管理器中看到。另请注意会话ID:

c# Windows Service 桌面上显示UI

但是,我们的LoaderService怎么?它在哪里,以及在哪个Session运行?让我们再看看任务管理器来解决这个问题:

c# Windows Service 桌面上显示UI

我们刚刚成功绕过Vista UAC,并说明了如何从Windows服务正确生成交互式进程。但是,我们还有更多可以做的事情!

超越基本原理

本节中讨论的主题不包含在可下载代码中。原因是为了使示例解决方案尽可能简单。以下思想旨在说明如何扩展示例代码以支持不同类型的方案。

通用解决方案

大多数Windows服务在第一个用户登录到计算机时启动,并在Session0中启动。我们的代码当前编写的方式是只有登录到计算机的第一个用户才能在其Session中启动命令提示符。这样做的原因是我们LoaderService从它的OnStart功能中产生了这个过程。该OnStart函数只执行一次,即首次启动服务时。由于第一个登录系统的用户具有在Session0中启动所有服务的净效果,因此他是在OnStart调用时将在函数中检索其会话ID的用户StartProcessAndBypassUAC。为清楚起见,下面重复OnStartLoaderService.cs中的函数:

隐藏   复制代码
protected override void OnStart(string[] args)
{
// the name of the application to launch
String applicationName = "cmd.exe"; // launch the application
ApplicationLoader.PROCESS_INFORMATION procInfo;
ApplicationLoader.StartProcessAndBypassUAC(applicationName, out procInfo);
}

那么,我们如何配置我们LoaderService在他们第一次登录时为计算机上的每个用户启动命令提示符?解决方案是在OnStart函数中:或者连接a Timer,或者Thread每隔很多秒产生一个在无限循环中运行的(Thread.Sleep(1000)可以用来控制Thread运行的频率)。我们可以使用一个List<int>对象来跟踪我们已经启动过程的所有会话ID。每次Thread执行时,我们都会检查会话ID是否已更改。可以通过调用检索当前活动的会话ID WTSGetActiveConsoleSessionId()。如果会话ID已更改,我们会检查是否已将流程启动到该会话中。如果我们没有,那么我们调用StartProcessAndBypassUAC并将会话ID添加到List<int>对象。

启动按需应用程序

可能是您不希望应用程序在用户登录计算机时立即启动。您可能只有在用户选择运行它时才能加载应用程序。此外,您可能希望系统上的每个用户都可以使用此应用程序和功能。那么问题是我们如何LoaderService在绕过Vista UAC的同时适应这一点?

在我们开始讨论之前,让我们快速讨论UAC如何应用文件和文件夹访问。Vista支持特殊文件夹的概念。Vista中有几个特殊文件夹,但我们要关注的是Documents文件夹。在.NET中,您可以通过调用查询特殊文件夹位置Environment.GetFolderPath(...)

如果您在计算机上花了足够的时间,您可能已经注意到您可以*地创建,修改和删除位于“ 文档”文件夹中的文件,而不受UAC的任何干扰。但是,如果您导航到另一个用户的文档文件夹,您将收到一个UAC提示,要求管理员允许触摸该文件夹。您可能还注意到,所有用户都共享并可访问公共文档文件夹。在Vista中,此特殊文件夹的路径为C:\ Users \ Public \ Documents。系统上的任何用户都可以*地创建,修改和删除位于此处的文件,而不受UAC的任何干扰。

现在,我们能够制定解决方案!我们可以修改我们的OnStart功能LoaderService来启动FileSystemWatcher类的实例,并将其配置为监视所有用户都可以访问的公共文档文件夹的更改。我们将不得不创建一个新的控制台应用程序来与LoaderServicevia文本文件进行通信(不要将此控制台应用程序与控制台会话混淆)。控制台应用程序的代码如下所示:

隐藏   复制代码
static void Main(string[] args)
{
string filename = @"C:\Users\Public\Documents\appToLoad.txt";
using (StreamWriter sw = new StreamWriter(filename, false))
{
sw.WriteLine("SessionID=" + WTSGetActiveConsoleSessionId());
sw.WriteLine("ApplicationToLoad=cmd.exe");
sw.Close();
}
}

在看到文件appToLoad.txt后LoaderService将解析文件并在当前活动的Session中启动命令提示符。此时,我们已成功说明了如何使用用户应用程序与服务进行通信,以及如何让它以完全管理员权限为我们启动应用程序,并在此过程中绕过Vista UAC。

ApplicationLoader.cs

using System;
using System.Security;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace Toolkit
{
/// <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, 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
null, // 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
}

}
}

如何程序是exe,但是exe需要加载其他文件的话,需要修改ApplicationLoader.cs为如下所示

using System;
using System.Security;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace Toolkit
{
/// <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 Directory, 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
Directory, // 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
}

}
}

调用方式

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.Tasks;
using System.Timers;
using System.Windows.Forms;
using Toolkit;

namespace WindowsService
{
public partial class Service1 : ServiceBase
{
public Service1()
{
InitializeComponent();
}
protected override void OnStart(string[] args)
{

try
{
System.Timers.Timer aTimer = new System.Timers.Timer();
aTimer.Elapsed += new ElapsedEventHandler(ListenDeskTop);
// 设置引发时间的时间间隔 此处设置为10秒(10000毫秒)
aTimer.Interval = 5000;
aTimer.Enabled = true;
}
catch (Exception e)
{
MessageBox.Show(e.Message);
throw;
}
}
private void ListenDeskTop(object source, ElapsedEventArgs e)
{
bool isNotePadStart = false;//标识记进程是否启动
Process[] processes = Process.GetProcesses();//获取所有进程信息
for (int i = 0; i < processes.Length; i++)
{
if (processes[i].ProcessName.ToLower() == "desktopbem")
{
isNotePadStart = true;
return;
}
// Console.WriteLine(processes[i].ProcessName.ToLower());
}
if (!isNotePadStart)
{
String applicationName = @"D:\visual studio\DesktopBmg\daemon\WindowsServiceClient\bin\Debug\DesktopBem.exe";

// launch the application
ApplicationLoader.PROCESS_INFORMATION procInfo;

                      //app path    //app Directory
ApplicationLoader.StartProcessAndBypassUAC(applicationName, @"D:\visual studio\DesktopBmg\daemon\WindowsServiceClient\bin\Debug\", out procInfo);

return;
}
}

protected override void OnStop()
{

}
}
}

参考地址

https://www.codeproject.com/Articles/35773/Subverting-Vista-UAC-in-Both-32-and-64-bit-Archite

https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-createprocessasusera