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

Posted

技术标签:

【中文标题】如何从 Windows 服务启动进程到当前登录的用户会话【英文标题】:How to start a process from windows service into currently logged in user's session 【发布时间】:2021-10-26 05:06:18 【问题描述】:

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

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

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

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

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

更新: 首先,.NET 与这里无关,它实际上是纯 Win32 的东西。 这就是我正在做的事情。以下代码在我的 Windows 服务中(通过 P/Inkove 使用 win32 函数的 C#,我跳过了导入签名,它们都在这里 - 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);

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

【问题讨论】:

【参考方案1】:

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

https://github.com/murrayju/CreateProcessAsUser

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

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

【讨论】:

如果同时有两个活动的 RDP 会话怎么办?还是 RDP 会话和本地会话? @zespri 代码假定只有一个活动会话,因此它有效地随机选择一个。我怀疑WTSEnumerateSessions api 是否对排序做出任何保证,但代码将选择列表中的最后一个活动会话。我认为您无法知道哪个会话是“正确的”会话,但如果有,您可以修改 for 循环。我假设这是在服务器操作系统上? 优秀。正是我需要和正在寻找的。工作一种享受。 好东西!非常感谢! 请注意,win 10 中 Metro 应用程序的进程 ID 与 procInfo.dwProcessId 报告的不同【参考方案2】:

A blog on MSDN describes a solution

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

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

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

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

一旦你有了这个令牌调用 CreateProcessAsUser 令牌。此令牌已具有正确的会话 ID。

【讨论】:

链接已损坏。你能证明你做了什么吗?我正在开发一个类似的 WatchDog 应用程序,但无法解决这个问题。谢谢!【参考方案3】:

这样做是个坏主意。尽管可能并非完全不可能,但 Microsoft 尽一切努力使这变得尽可能困难,因为它启用了所谓的 Shatter Attacks。看看拉里奥斯特曼wrote about it back in 2005:

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

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

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

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

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

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

Developing for Windows: Session 0 Isolation

【讨论】:

我不希望我的服务是交互式的(显示任何 UI),我只希望它能够启动另一个进程(在用户会话中)。这有点不同,不是吗? @Shrike:如果您不想显示 UI,那么为什么在会话 0 中启动该进程还不够?我认为您需要添加更多详细信息,说明为什么该过程需要交互式会话以及为什么建议的解决方法不适合您。 有两个参与者:windows-service 和 UI-program。服务器本身不显示 UI,它只是启动程序(作为一个新进程)。该程序具有 UI(并且需要会话 1)。我的服务是一种看门狗,定期“ping”程序并在出现问题时启动/重新启动它。我在原始帖子中添加了一些说明 感谢您提供非常有趣的帖子的链接。但这并没有帮助,因为它只是指“使用 CreateProcessAsUser”。我知道这一点,请参阅我的代码示例。创建的进程进入会话 #0 而不是交互的,我不知道如何修复它。【参考方案4】:

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

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

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

【讨论】:

我认为这是公认的解决方案,但经过彻底的解释。好多了。 虽然此过程适用于 Windows 10,但似乎不适用于 Windows Server 2016【参考方案5】:

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

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

它需要 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;

【讨论】:

【参考方案6】:

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

【讨论】:

我用的是 CreateProcessWithLogonW (msdn.microsoft.com/en-us/library/ms682431%28VS.85%29.aspx),几乎一样【参考方案7】:

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

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

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

【讨论】:

您是否打算将此作为更新的答案?如果是这样,只需添加相关详细信息并照此发布。对我来说,这似乎不仅仅是一条评论,但我不想花时间弄清楚整个问题。【参考方案8】:

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

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

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

【讨论】:

以上是关于如何从 Windows 服务启动进程到当前登录的用户会话的主要内容,如果未能解决你的问题,请参考以下文章

以 Windows 登录用户身份从服务启动进程

如何在启动时启动 Docker 守护进程(Windows 服务)而无需登录?

如何从 Windows 服务启动具有 UI 的进程?

如何从Windows服务启动非后台进程?

windows进程全解

从主机 Windows 上无法远程访问 Linux 的 Tomcat 服务器解决方法