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

61
我需要从Windows服务启动一个程序。该程序是一个用户UI应用程序。此外,该应用程序应在特定用户帐户下启动。
问题在于,Window服务在会话#0中运行,但已登录的用户会话为1,2等。
因此,问题是:如何以这样的方式从窗口服务启动进程,以便它在当前登录用户的会话中运行?
我要强调的是,问题不是如何在特定帐户下启动进程(这是显而易见的- Process.Start(new ProcessStartInfo("..") { UserName=..,Password=..})). 即使我安装我的Windows以在当前用户帐户下运行,服务也将在会话#0中运行。设置“允许服务与桌面交互”也无济于事。
我的Windows服务基于.NET。
更新: 首先,.NET在这里没有任何作用,它实际上是纯Win32事物。 以下代码位于我的Windows服务中(使用win32函数通过P / InKove进行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已启动Notepad。Exitcode:" + exitCode所在的行。Exitcode为3221225794。但是没有任何新的记事本被启动。我错在哪里?
8个回答

50
Shrike的答案存在问题,它不能适用于通过RDP连接的用户。这是我的解决方案,在创建进程之前正确确定当前用户的会话。它已经测试可以在XP和7上工作。 https://github.com/murrayju/CreateProcessAsUser 一切都包含在一个单独的.NET类中,具有静态方法。
public static bool StartProcessAsCurrentUser(string appPath, string cmdLine, string workDir, bool visible)

1
@zespri 该代码假设只有一个活动会话,因此它实际上是随机选择一个。我怀疑WTSEnumerateSessions api没有关于排序的任何保证,但是该代码将选择列表中最后一个活动会话。我不认为您有任何办法知道哪个会话是“正确”的,但如果有的话,您可以修改那个for循环。我假设这是在服务器操作系统上? - murrayju
2
太好了,正是我需要的并一直在寻找的。非常有效。 - Peterp
2
好东西!非常感谢! - Vaccano
1
最好的做法是在这里至少概述代码,而不是依赖链接永远存在。 - Tommy Herbert
1
@Daniel Williams,它可以工作,但只有在使用服务用户凭据运行时才有效。只有使用服务凭据才能正确获取用户会话令牌。我刚刚在我设置的一个小虚拟服务中成功测试了它。 - Jroonk
显示剩余7条评论

35

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

这是一篇非常有用的帖子,关于如何从Windows服务中的交互会话启动新进程。

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

  • 枚举进程以获取Explorer的句柄。

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

  • 一旦您拥有令牌,就可以使用此令牌调用CreateProcessAsUser。此令牌已经具有正确的会话ID。


链接已经失效了。你能展示一下你做了什么吗?我正在开发一个类似的看门狗应用,但是无法解决这个问题。谢谢! - Ash K

10

这样做是不好的想法。虽然可能不是完全不可能的,但微软已经尽力让它变得非常困难了,因为它会启用所谓的Shatter攻击,请参见Larry Osterman在2005年写的文章

这么做之所以不好的原因是,交互式服务会启用一类被称为“Shatter”攻击的威胁(因为它们会“破坏窗口”,我想是这个原因)。

如果你搜索“shatter attack”,可以看到这些安全威胁是如何工作的。微软还发表了KB文章327618,扩展了关于交互式服务的文档,并且Michael Howard为MSDN库编写了一篇有关交互式服务的文章。最初,shatter攻击针对具有后台窗口消息泵的Windows组件(这些问题早已解决),但它们也被用来攻击弹出UI的第三方服务。

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

交互式服务不是一个好主意的第三个原因是交互式服务不能保证与Windows Vista兼容:作为Windows Vista的安全强化过程的一部分,交互式用户登录到除系统会话以外的会话中 - 第一个交互式用户在会话1中运行,而不是会话0。这实际上完全阻止了突破攻击 - 用户应用程序无法与运行在服务中的高特权窗口进行交互。

所提出的解决方法是使用用户系统托盘中的应用程序。

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

开发Windows: Session 0隔离


2
我不希望我的服务是交互式的(显示任何UI),我只希望它能够启动另一个进程(在用户会话中)。这是有点不同的情况,不是吗? - Shrike
@Shrike:如果您不想显示UI,那么在Session 0中启动该进程为什么不够?我认为您需要添加更多关于为什么该进程需要交互式会话以及为什么建议的解决方法不适合您的详细信息。 - Dirk Vollmar
1
有两个角色:Windows服务和UI程序。服务器本身不显示UI,它只启动程序(作为新进程)。该程序具有UI(并且需要会话1)。我的服务类似于看门狗,定期“ping”程序,并在出现问题时启动/重新启动它。我正在原始帖子中添加一些澄清。 - Shrike
谢谢提供这篇有趣的文章。但是它并没有帮到我,因为它只涉及到了“使用CreateProcessAsUser”。我已经知道这个了,可以看看我的代码样例。创建的进程进入了第0个会话而不是交互式会话,我不知道怎么解决。 - Shrike

7

我认为这是被接受的解决方案,但是解释得非常透彻。好多了。 - cdlvcdlv
尽管此过程适用于Windows 10,但似乎无法在Windows Server 2016上运行。 - Demodave

6
以下是我的实现方式。 它将尝试作为当前登录的用户(来自服务)启动进程。 这基于多个来源合并而成,可以正常工作。
实际上它是真正的WIN32/C++,所以可能并不能完全解决原始问题。 但我希望它能节省其他人寻找类似方法的时间。
它需要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;
}

我刚试图构建这段代码,但是出现了“\HelloWorld/helloworld.cpp:22: undefined reference to `WTSQueryUserToken' collect2.exe: error: ld returned 1 exit status Build finished with error(s).”的错误提示。可能出了什么问题?我使用的是Windows 10操作系统,g++.exe (Rev10, Built by MSYS2 project) 12.2.0版本。 - Esben von Buchwald

5

我将@murrayju的代码实现到了一个在W10上的Windows服务中。从Program Files运行可执行文件总是在VS中抛出错误-2。我认为这是由于服务的启动路径设置为System32所导致的。 在StartProcessAsCurrentUser中,在CreateProcessAsUser之前添加以下行才解决了问题:

指定workDir并没有解决问题,直到我在StartProcessAsCurrentUser中添加了以下行:

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

注意:这应该是一条评论而不是答案,但我还没有足够的声望。我花了一些时间来调试,希望能节省其他人的时间。


你是想更新回答吗?如果是的话,只需添加相关细节并发布即可。对我来说,这看起来不仅仅是一条评论,但我不想花时间去弄清整个问题。 - theMayer

2

我不知道如何在.NET中做到这一点,但通常你需要使用Win32 API的CreateProcessAsUser()函数(或其.NET等效函数),并指定所需用户的访问令牌和桌面名称。这是我在C++服务中使用的方法,它很好用。


我使用了CreateProcessWithLogonW(http://msdn.microsoft.com/en-us/library/ms682431%28VS.85%29.aspx),它几乎相同。 - Shrike

1

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

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

然后,我将该文件的位置传递给StartProcessAsCurrentUser()方法。这样就可以解决问题了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接