.net 如何从 Windows 服务启动进程到当前登录的用户会话

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/4278373/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-03 14:58:27  来源:igfitidea点击:

How to start a process from windows service into currently logged in user's session

.netwindows-services

提问by Shrike

I need to start a program from Windows Service. That program is a user UI application. Moreover that application should be started under specific user account.

我需要从 Windows 服务启动一个程序。该程序是一个用户 UI 应用程序。此外,该应用程序应在特定用户帐户下启动。

The problem is that a Window Services run in session #0, but a logged in user sessions are 1,2 etc.

问题是窗口服务在会话 #0 中运行,但登录的用户会话是 1,2 等。

So the question is: how to start a process from a window service in such a way that it run in currently logged in user's session?

所以问题是:如何从窗口服务启动进程,使其在当前登录的用户会话中运行?

I'd emphasis on that the question is not about how to start a process under specific account (it's obvious - Process.Start(new ProcessStartInfo("..") { UserName=..,Password=..})). Even if I install my windows to run under current user account the service will run in session #0 anyway. Setting "Allow service to interact with desktop" doesn't help.

我要强调的是,问题不是关于如何在特定帐户下启动进程(很明显 - Process.Start(new ProcessStartInfo("..") { UserName=..,Password=..}))。即使我安装 Windows 以在当前用户帐户下运行,该服务仍将在会话 #0 中运行。设置“允许服务与桌面交互”没有帮助。

My windows service is .net-based.

我的 Windows 服务是基于 .net 的。

UPDATE: first of all, .NET has nothing to do here, it's actually pure Win32 thing. Here's what I'm doing. The following code is in my windows service (C# using win32 function via P/Inkove, I skipped import signatures, they're all here - http://www.pinvoke.net/default.aspx/advapi32/CreateProcessWithLogonW.html):

更新:首先,.NET 在这里无关,它实际上是纯 Win32 的东西。这就是我正在做的。以下代码在我的 Windows 服务中(C# 通过 P/Inkove 使用 win32 函数,我跳过了导入签名,它们都在这里 - http://www.pinvoke.net/default.aspx/advapi32/CreateProcessWithLogonW.html):

    var startupInfo = new StartupInfo()
        {
            lpDesktop = "WinSta0\Default",
            cb = Marshal.SizeOf(typeof(StartupInfo)),
        };
    var processInfo = new ProcessInformation();
    string command = @"c:\windows\Notepad.exe";
    string user = "Administrator";
    string password = "password";
    string currentDirectory = System.IO.Directory.GetCurrentDirectory();
    try
    {
        bool bRes = CreateProcessWithLogonW(user, null, password, 0,
            command, command, 0,
            Convert.ToUInt32(0),
            currentDirectory, ref startupInfo, out processInfo);
        if (!bRes)
        {
            throw new Win32Exception(Marshal.GetLastWin32Error());
        }
    }
    catch (Exception ex)
    {
        writeToEventLog(ex);
        return;
    }
    WaitForSingleObject(processInfo.hProcess, Convert.ToUInt32(0xFFFFFFF));
    UInt32 exitCode = Convert.ToUInt32(123456);
    GetExitCodeProcess(processInfo.hProcess, ref exitCode);
    writeToEventLog("Notepad has been started by WatchdogService. Exitcode: " + exitCode);

    CloseHandle(processInfo.hProcess);
    CloseHandle(processInfo.hThread);

The code goes to the line "Notepad has been started by WatchdogService. Exitcode: " + exitCode. Exitcode is 3221225794. And there's no any new notepad started. Where am I wrong?

代码转到“记事本已由 WatchdogService 启动。退出代码:”+ 退出代码。退出代码是 3221225794。并且没有任何新的记事本启动。我哪里错了?

采纳答案by Shrike

A blog on MSDN describes a solution

MSDN 上的一篇博客描述了一个解决方案

It's terrific helpful post about starting a new process in interactive session from windows service on Vista/7.

关于从 Vista/7 上的 Windows 服务在交互式会话中启动新进程的非常有用的帖子。

For non-LocalSystem services, the basic idea is:

对于非LocalSystem服务,基本思想是:

  • Enumerate the process to get the handle of the Explorer.

  • OpenProcessToken should give you the access token. Note : The account under which your service is running must have appropriate privileges to call this API and get process token.

  • Once you have the token call CreateProcessAsUser with this token. This token already have the right session Id.

  • 枚举进程以获取资源管理器的句柄。

  • OpenProcessToken 应该为您提供访问令牌。注意:运行您的服务的帐户必须具有适当的权限才能调用此 API 并获取进程令牌。

  • 获得令牌后,使用此令牌调用 CreateProcessAsUser。此令牌已具有正确的会话 ID。

回答by murrayju

The problem with Shrike's answer is that it does not work with a user connected over RDP.
Here is my solution, which properly determines the current user's session before creating the process. It has been tested to work on XP and 7.

Shrike 的答案的问题在于它不适用于通过 RDP 连接的用户。
这是我的解决方案,它在创建进程之前正确确定当前用户的会话。它已经过测试,可以在 XP 和 7 上运行。

https://github.com/murrayju/CreateProcessAsUser

https://github.com/murrayju/CreateProcessAsUser

Everything you need is wrapped up into a single .NET class with a static method:

您需要的一切都包含在一个带有静态方法的 .NET 类中:

public static bool StartProcessAsCurrentUser(string appPath, string cmdLine, string workDir, bool visible)

回答by Dirk Vollmar

It's a bad idea to do so. Although probably not totally impossible, Microsoft did everything to make this as hard as possible, as it enables so-called Shatter Attacks. See what Larry Osterman wrote about it back in 2005:

这样做是个坏主意。尽管可能并非完全不可能,但微软尽一切努力使这一切变得尽可能困难,因为它支持所谓的粉碎攻击。看看拉里·奥斯特曼(Larry Osterman ) 在 2005 年写的关于它的内容

The primary reason for this being a bad idea is that interactive services enable a class of threats known as "Shatter" attacks (because they "shatter windows", I believe).

If you do a search for "shatter attack", you can see some details of how these security threats work. Microsoft also published KB article 327618which extends the documentation about interactive services, and Michael Howard wrote an article about interactive services for the MSDN Library. Initially the shatter attacks went after windows components that had background window message pumps (which have long been fixed), but they've also been used to attack 3rd party services that pop up UI.

The second reason it's a bad idea is that the SERVICE_INTERACTIVE_PROCESS flag simply doesn't work correctly. The service UI pops up in the system session (normally session 0). If, on the other hand, the user is running in another session, the user never sees the UI. There are two main scenarios that have a user connecting in another session - Terminal Services, and Fast User Switching. TS isn't that common, but in home scenarios where there are multiple people using a single computer, FUS is often enabled (we have 4 people logged in pretty much all the time on the computer in our kitchen, for example).

The third reason that interactive services is a bad idea is that interactive services aren't guaranteed to work with Windows Vista :) As a part of the security hardening process that went into Windows Vista, interactive users log onto sessions other than the system session - the first interactive user runs in session 1, not session 0. This has the effect of totally cutting shatter attacks off at the knees - user apps can't interact with high privilege windows running in services.

这是一个坏主意的主要原因是交互式服务启用了一类称为“粉碎”攻击的威胁(因为它们“粉碎窗户”,我相信)。

如果您搜索“粉碎攻击”,您可以看到有关这些安全威胁如何工作的一些详细信息。Microsoft 还发布了知识库文章 327618,它扩展了有关交互式服务的文档,Michael Howard 为 MSDN 库撰写了一篇有关交互式服务的文章。最初,粉碎攻击针对具有后台窗口消息泵(早已修复)的 Windows 组件,但它们也被用于攻击弹出 UI 的 3rd 方服务。

这是一个坏主意的第二个原因是 SERVICE_INTERACTIVE_PROCESS 标志根本无法正常工作。服务 UI 在系统会话(通常为会话 0)中弹出。另一方面,如果用户正在另一个会话中运行,则用户永远不会看到 UI。有两个主要场景让用户在另一个会话中连接 - 终端服务和快速用户切换。TS 并不常见,但在多人使用一台计算机的家庭场景中,通常启用 FUS(例如,我们有 4 个人几乎一直在厨房的计算机上登录)。

交互式服务是一个坏主意的第三个原因是不能保证交互式服务与 Windows Vista 一起使用 :) 作为进入 Windows Vista 的安全强化过程的一部分,交互式用户登录到系统会话以外的会话 -第一个交互式用户在会话 1 中运行,而不是在会话 0 中运行。这具有完全切断 shatter 攻击的效果 - 用户应用程序无法与在服务中运行的高权限窗口进行交互。

The proposed workaround would be to use an application in the system tray of the user.

建议的解决方法是使用用户系统托盘中的应用程序。

If you can safely ignore the issues and warnings above, you might follow the instructions given here:

如果您可以安全地忽略上述问题和警告,则可以按照此处给出的说明进行操作:

Developing for Windows: Session 0 Isolation

为 Windows 开发:会话 0 隔离

回答by Tono Nam

I found the solution in here:

我在这里找到了解决方案:

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

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

I think this is a great link.

我认为这是一个很好的链接。

回答by Tobias81

Here is how I implemented it. It will try to start a process as the currently logged-in user (from a service). This is based on several sources put together to something that works.

这是我如何实施它。它将尝试以当前登录的用户(来自服务)的身份启动一个进程。这是基于将几个来源组合在一起的有效方法。

It is actually REALLY PURE WIN32/C++, so might not be 100% helpful to the original questions. But I hope it can save other people some time looking for something similar.

它实际上是真正纯粹的 WIN32/C++,所以对原始问题可能不是 100% 有帮助。但我希望它可以为其他人节省一些时间来寻找类似的东西。

It requires Windows XP/2003 (doesn't work with Windows 2000). You must link to Wtsapi32.lib

它需要 Windows XP/2003(不适用于 Windows 2000)。您必须链接到 Wtsapi32.lib

#define WINVER 0x0501
#define _WIN32_WINNT 0x0501
#include <Windows.h>
#include <WtsApi32.h>

bool StartInteractiveProcess(LPTSTR cmd, LPCTSTR cmdDir) {
    STARTUPINFO si;
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    si.lpDesktop = TEXT("winsta0\default");  // Use the default desktop for GUIs
    PROCESS_INFORMATION pi;
    ZeroMemory(&pi, sizeof(pi));
    HANDLE token;
    DWORD sessionId = ::WTSGetActiveConsoleSessionId();
    if (sessionId==0xffffffff)  // Noone is logged-in
        return false;
    // This only works if the current user is the system-account (we are probably a Windows-Service)
    HANDLE dummy;
    if (::WTSQueryUserToken(sessionId, &dummy)) {
        if (!::DuplicateTokenEx(dummy, TOKEN_ALL_ACCESS, NULL, SecurityDelegation, TokenPrimary, &token)) {
            ::CloseHandle(dummy);
            return false;
        }
        ::CloseHandle(dummy);
        // Create process for user with desktop
        if (!::CreateProcessAsUser(token, NULL, cmd, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, cmdDir, &si, &pi)) {  // The "new console" is necessary. Otherwise the process can hang our main process
            ::CloseHandle(token);
            return false;
        }
        ::CloseHandle(token);
    }
    // Create process for current user
    else if (!::CreateProcess(NULL, cmd, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, cmdDir, &si, &pi))  // The "new console" is necessary. Otherwise the process can hang our main process
        return false;
    // The following commented lines can be used to wait for the process to exit and terminate it
    //::WaitForSingleObject(pi.hProcess, INFINITE);
    //::TerminateProcess(pi.hProcess, 0);
    ::CloseHandle(pi.hProcess);
    ::CloseHandle(pi.hThread);
    return true;
}

回答by Alex Ureche

Implemented @murrayjucode into a Windows Service on W10. Running an executable from Program Files always threw Error -2 in VS. I believe it was due to Service's start path set to System32. Specifying workDirdidn't fix the issue until I added the following line before CreateProcessAsUserin StartProcessAsCurrentUser:

在 W10上将 @murrayju代码实施到 Windows 服务中。从 Program Files 运行可执行文件总是在 VS 中抛出错误 -2。我相信这是由于服务的启动路径设置为 System32。指定workDir并没有解决问题,直到我在StartProcessAsCurrentUser 中的CreateProcessAsUser之前添加以下行:

if (workDir != null)
   Directory.SetCurrentDirectory(workDir);

PS: This should have been a comment rather than an answer, but I don't have the required reputation yet. Took me a while to debug, hope it saves someone time.

PS:这应该是评论而不是答案,但我还没有所需的声誉。花了我一段时间来调试,希望它可以节省一些时间。

回答by Remy Lebeau

I don't know how to do it in .NET, but in general you would need to use the Win32 API CreateProcessAsUser()function (or whatever its .NET equivilent is), specifying the desired user's access token and desktop name. This is what I use in my C++ services and it works fine.

我不知道如何在 .NET 中执行此操作,但通常您需要使用 Win32 APICreateProcessAsUser()函数(或任何它的 .NET 等效函数),指定所需用户的访问令牌和桌面名称。这是我在我的 C++ 服务中使用的,并且工作正常。

回答by user3480038

The accepted answer did not work in my case as the application I was starting required admin privileges. What I did is I created a batch file to start the application. It contained the following:

接受的答案在我的情况下不起作用,因为我开始的应用程序需要管理员权限。我所做的是创建了一个批处理文件来启动应用程序。它包含以下内容:

C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe  "%~dp0MySoft.exe"

Then, I passed the location of this file to StartProcessAsCurrentUser() method. Did the trick.

然后,我将此文件的位置传递给 StartProcessAsCurrentUser() 方法。做到了。