从服务启动的进程使用CreateProcessWithLogonW立即终止

4
在一个测试框架中,进程A需要使用CreateProcessWithLogonW API以不同的用户凭据(比如_limited_user)启动进程B。 lpStartupInfo->lpDesktop为NULL,因此进程B应该在与进程A相同的桌面和窗口站点运行。

当手动启动进程A(作为_glagolig)时,一切正常。但是当测试框架服务(在指定的测试框架用户帐户_test_framework下运行)启动进程A时,就会出现问题。 CreateProcessWithLogonW返回成功,但进程B无法完成任何工作。它立即终止,显然是因为其conhost.exe无法初始化user32.dll并返回0xC0000142(我从SysInternals的procmon.exe日志中获得了这个信息)。所以看起来问题在于桌面/窗口站点访问。

我想了解根本原因。目前尚不清楚是什么导致测试框架服务的桌面/窗口站对象与手动登录的用户不同。

另外,我想找到一种解决方法,同时保持总体方案不变(测试框架服务在帐户_test_framework下启动进程B,_limited_user)。


1
手动登录的用户会在交互式桌面上运行。服务不会在交互会话/桌面上运行,也不应该在其会话中创建交互式进程,因为用户将永远无法看到它。如果进程B是交互式进程(如果需要user32.dll则很可能是),则应指定一个交互式桌面在lpStartupInfo->szDesktop中运行,以便用户可以看到它。 - Remy Lebeau
@Harry 这是 STATUS_DLL_INIT_FAILED 错误。https://dev59.com/RWYr5IYBdhLWcg3wvsvR - glagolig
想一想,谜题在于为什么它在交互式情况下有效。Windows可能正在幕后进行一些魔法,以使新的登录会话可以访问交互式桌面。然而,这可能与解决您实际问题无关;您需要创建一个新的工作站和桌面,或更改现有工作站和桌面的权限,或修改进程B,使其不生成控制台。 - Harry Johnston
@glagolig:我的错误,已删除评论。 - Harry Johnston
注意:仅在交互式情况下,如果您明确将lpStartupInfo->lpDesktop设置为NULL而不是默认值WinSta0\Default,它才能正常工作。 如果您未将lpDesktop设置为NULL,则子进程将无法启动,并显示错误代码0xC0000142。 - Harry Johnston
显示剩余4条评论
2个回答

3
补充说明:根据文档,如果您不希望新进程与用户进行交互,则可以直接使用CreateProcessAsUser而无需经过这些步骤。我尚未测试过这一点,但是假设它是正确的,那么对于许多场景来说,这将是一个更简单的解决方案。

事实证明,Microsoft已经提供了样例代码以操作窗口站和桌面访问权限,标题为在C++中启动交互式客户端进程。从Windows Vista开始,在默认窗口站中启动子进程已不足以允许子进程与用户进行交互,但它确实允许子进程使用替代用户凭据运行。

我应该指出,微软的代码使用LogonUserCreateProcessAsUser而不是CreateProcessWithLogonW。这意味着服务将需要SE_INCREASE_QUOTA_NAME特权,可能还需要SE_ASSIGNPRIMARYTOKEN_NAME。更好的选择可能是替换CreateProcessWithTokenW,它只需要SE_IMPERSONATE_NAME。我不建议在此上下文中使用CreateProcessWithLogonW,因为它不允许您在启动子进程之前访问登录SID。

我编写了一个最小的服务来演示使用微软的示例代码:

/*******************************************************************/

#define _WIN32_WINNT 0x0501

#include <windows.h>

/*******************************************************************/

// See http://msdn.microsoft.com/en-us/library/windows/desktop/aa379608%28v=vs.85%29.aspx
// "Starting an Interactive Client Process in C++"

BOOL AddAceToWindowStation(HWINSTA hwinsta, PSID psid);
BOOL AddAceToDesktop(HDESK hdesk, PSID psid);
BOOL GetLogonSID (HANDLE hToken, PSID *ppsid);
VOID FreeLogonSID (PSID *ppsid);
BOOL StartInteractiveClientProcess (
    LPTSTR lpszUsername,    // client to log on
    LPTSTR lpszDomain,      // domain of client's account
    LPTSTR lpszPassword,    // client's password
    LPTSTR lpCommandLine    // command line to execute
);

/*******************************************************************/

const wchar_t displayname[] = L"Demo service for CreateProcessWithLogonW";
const wchar_t servicename[] = L"demosvc-createprocesswithlogonw";

DWORD dwWin32ExitCode = 0, dwServiceSpecificExitCode = 0;

/*******************************************************************/

#define EXCEPTION_USER 0xE0000000
#define FACILITY_USER_DEMOSVC 0x0001
#define EXCEPTION_USER_LINENUMBER (EXCEPTION_USER | (FACILITY_USER_DEMOSVC << 16))

HANDLE eventloghandle;

/*******************************************************************/

wchar_t subprocess_username[] = L"harry-test1";
wchar_t subprocess_domain[] = L"scms";
wchar_t subprocess_password[] = L"xyzzy916";
wchar_t subprocess_command[] = L"cmd.exe /c dir";

void demo(void) 
{
    if (!StartInteractiveClientProcess(subprocess_username, subprocess_domain, subprocess_password, subprocess_command))
    {
        const wchar_t * strings[] = {L"Creating subprocess failed."};
        DWORD err = GetLastError();
        ReportEventW(eventloghandle,
                    EVENTLOG_ERROR_TYPE,
                    0,
                    2,
                    NULL,
                    _countof(strings),
                    sizeof(err),
                    strings,
                    &err);
        return;
    }

    {
        const wchar_t * strings[] = {L"Creating subprocess succeeded!"};
        ReportEventW(eventloghandle,
                    EVENTLOG_INFORMATION_TYPE,
                    0,
                    1,
                    NULL,
                    _countof(strings),
                    0,
                    strings,
                    NULL);
    }

    return;
}

/*******************************************************************/

CRITICAL_SECTION service_section;

SERVICE_STATUS service_status;                     // Protected by service_section

SERVICE_STATUS_HANDLE service_handle = 0;          // Constant once set, so can be used from any thread

static DWORD WINAPI ServiceHandlerEx(DWORD control, DWORD eventtype, LPVOID lpEventData, LPVOID lpContext) 
{
    if (control == SERVICE_CONTROL_INTERROGATE)
    {
        EnterCriticalSection(&service_section);
        if (service_status.dwCurrentState != SERVICE_STOPPED)
        {
        SetServiceStatus(service_handle, &service_status);
        }
        LeaveCriticalSection(&service_section);
        return NO_ERROR;
    }

    return ERROR_CALL_NOT_IMPLEMENTED;
}

static VOID WINAPI ServiceMain(DWORD argc, LPTSTR * argv)
{
    SERVICE_STATUS status;

    EnterCriticalSection(&service_section);

    service_handle = RegisterServiceCtrlHandlerEx(argv[0], ServiceHandlerEx, NULL);
    if (!service_handle) RaiseException(EXCEPTION_USER_LINENUMBER | __LINE__, EXCEPTION_NONCONTINUABLE, 0, NULL);

    service_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
    service_status.dwCurrentState = SERVICE_RUNNING;
    service_status.dwControlsAccepted = 0;
    service_status.dwWin32ExitCode = STILL_ACTIVE;
    service_status.dwServiceSpecificExitCode = 0;
    service_status.dwCheckPoint = 0;
    service_status.dwWaitHint = 500;

    SetServiceStatus(service_handle, &service_status);

    LeaveCriticalSection(&service_section);

    /************** service main function **************/

    {
        const wchar_t * strings[] = {L"Service started!"};
        ReportEventW(eventloghandle,
                    EVENTLOG_INFORMATION_TYPE,
                    0,
                    2,
                    NULL,
                    _countof(strings),
                    0,
                    strings,
                    NULL);
    }

    demo();

    /************** service shutdown **************/

    EnterCriticalSection(&service_section);     

    status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
    status.dwCurrentState = service_status.dwCurrentState = SERVICE_STOPPED;
    status.dwControlsAccepted = 0;
    status.dwCheckPoint = 0;
    status.dwWaitHint = 500;
    status.dwWin32ExitCode = dwWin32ExitCode;
    status.dwServiceSpecificExitCode = dwServiceSpecificExitCode;

    LeaveCriticalSection(&service_section);

    SetServiceStatus(service_handle, &status);       /* NB: SetServiceStatus does not return here if successful,
                                                    so any code after this point will not normally run. */
    return;
}

int wmain(int argc, wchar_t * argv[]) 
{
    const static SERVICE_TABLE_ENTRY servicetable[2] = {
        {(wchar_t *)servicename, ServiceMain},
        {NULL, NULL}
    };

    InitializeCriticalSection(&service_section);

    eventloghandle = RegisterEventSource(NULL, displayname);
    if (!eventloghandle) return GetLastError();

    {
        const wchar_t * strings[] = {L"Executable started!"};
        ReportEventW(eventloghandle,
                    EVENTLOG_INFORMATION_TYPE,
                    0,
                    2,
                    NULL,
                    _countof(strings),
                    0,
                    strings,
                    NULL);
    }

    if (StartServiceCtrlDispatcher(servicetable)) return 0;
    return GetLastError();
}

这必须与微软的示例代码相关联。然后,您可以使用sc命令安装服务:
sc create demosvc-createprocesswithlogonw binPath= c:\path\demosvc.exe DisplayName= "Demo service for CreateProcessWithLogonW"

我以前看过MSDN的示例。但是我无法在我的目标机器上使其工作。(我怀疑当没有人交互登录时,它不起作用,因此winsta0不存在。我通过远程桌面登录。虽然我不确定我的怀疑是否正确。) - glagolig
winsta0 在每个会话中都存在,无论是否有人登录。也许运行服务的帐户没有足够的特权来调用 CreateProcessAsUser?你得到了什么错误代码? - Harry Johnston
花了一些时间才再次尝试... CreateProcessAsUser 失败,错误代码为 1314 (0x522) ERROR_PRIVILEGE_NOT_HELD。我将不得不处理调用者的权限。但即使我让它工作,了解为什么不能在服务的窗口站点中运行也很有趣。(由于我的单独服务解决方法可以工作,因此不需要交互式桌面。) - glagolig
没有什么神秘的。服务窗口站点上的ACL不授予_limited_user访问权限。您可以使用示例代码中的AddAceToWindowStationAddAceToDesktop函数进行必要的更改,只需传递当前窗口站点和桌面以及_limited_user的SID,但请记住,这样做可能会使您的服务面临来自_limited_user的攻击。您现在使用的解决方案更安全。 - Harry Johnston

1
我最终采用了以下解决方法。我配置了一个不同的服务作为_limited_user并按需启动。然后测试框架可以启动和停止有限用户服务。而有限用户服务可以运行我的测试所需的进程。
这个解决方法有效。因此,我的进程不需要交互式桌面(尽管它们加载user32.dll)。显然,user32.dll可以在非交互式上下文中加载。但是,当直接从测试框架服务使用CreateProcessWithLogonW启动进程时,存在一些未知的微妙之处,导致进程无法运行。

1
Windows会在必要时创建虚拟显示表面,因此您不需要处于交互式上下文中即可使用GUI函数。但无论您是否将使用GUI函数,都需要正确的窗口站和桌面权限。 - Harry Johnston

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