4

In a test framework process A has to start process B under different user credentials (say, _limited_user) using CreateProcessWithLogonW API. lpStartupInfo->lpDesktop is NULL, so process B is supposed to run in the same desktop and window station as process A.

Everything works fine when process A is started manually (as _glagolig). But when process A is started by the test framework service (running under designated test framework’s user account _test_framework) that does not work. CreateProcessWithLogonW returns success but process B is unable to do any work. It terminates right away apparently because its conhost.exe fails to initialize user32.dll and returns 0xC0000142 (I got that from SysInternals’ procmon.exe logs). So it looks like the problem is with desktop/window station access.

I would like to understand the root cause. It is not clear what makes test framework service’s desktop/window station objects different from those of a user that logged in manually.

Also I would like to find a workaround while keeping the overall scheme the same (test framework’s service under account _test_framework has to start process B under _limited_user).

glagolig
  • 1,100
  • 1
  • 12
  • 28
  • 1
    A user who logs in manually runs in an interactive desktop. A services does not run in an interactive session/desktop and should not be creating interactive processes within its session, since the user will never be able to see it. If process B is an interactive process (which is likely if it needs `user32.dll`), then you should be specifying an interactive desktop in `lpStartupInfo->szDesktop` for it to run on so the user can see it. – Remy Lebeau Nov 28 '13 at 02:58
  • @Remy Lebeau Process B would run in the context of service just fine if the service did not introduce a separate user. Process B does not need user32.dll but conhost.exe does. Process B is not allowed to run because conhost.exe fails. – glagolig Nov 28 '13 at 04:37
  • @Harry It is STATUS_DLL_INIT_FAILED. http://stackoverflow.com/questions/13425106/createprocess-succeeds-but-getexitcodeprocess-returns-c0000142 – glagolig Nov 28 '13 at 06:53
  • The puzzle, come to think of it, is why it works in the interactive case. Windows might be doing some behind-the-scenes magic to give the new logon session access to the interactive desktop. However, this probably isn't relevant to solving your actual problem; you need to either create a new workstation and desktop, or change the permissions on the existing ones, or modify process B so that it does not generate a console. – Harry Johnston Nov 28 '13 at 06:58
  • @glagolig: my mistake; comment deleted. – Harry Johnston Nov 28 '13 at 06:58
  • Note: it only works in the interactive case if you explicitly set `lpStartupInfo->lpDesktop` to `NULL` rather than the default of `WinSta0\Default`. If you do not set `lpDesktop` to NULL, the subprocess will fail to launch with error 0xC0000142. – Harry Johnston Nov 28 '13 at 23:50
  • Note: making process B a non-console process won't help. As per the linked article, any process that links to user32.dll requires access to the window station and desktop. It may be possible to write a process that will work without this access, but it does not appear to be supported. – Harry Johnston Nov 29 '13 at 02:23
  • It seems to boil down to the (apparently undocumented) fact that `CreateProcessWithLogonW` will sometimes include the parent process Logon SID in the access token for the new process. At least, that's what makes it work in the interactive case when `lpDesktop` is `NULL`. – Harry Johnston Nov 29 '13 at 02:25
  • http://blogs.msdn.com/b/ntdebugging/archive/2007/01/04/desktop-heap-overview.aspx – Hans Passant Dec 02 '13 at 23:56

2 Answers2

3

Addendum: according to the documentation, it should be possible to use CreateProcessAsUser without going through these steps, provided you don't want the new process to interact with the user. I haven't tested this yet, but assuming it is true, that would be a much simpler solution for many scenarios.

It turns out that Microsoft has already provided sample code to manipulate window station and desktop access rights, under the title Starting an Interactive Client Process in C++. As of Windows Vista, starting a subprocess in the default window station is no longer enough to allow the subprocess to interact with the user, but it does allow the subprocess to run with alternate user credentials.

I should note that Microsoft's code uses LogonUser and CreateProcessAsUser rather than CreateProcessWithLogonW. This does mean that the service will need SE_INCREASE_QUOTA_NAME privilege and possibly SE_ASSIGNPRIMARYTOKEN_NAME. It may be preferable to substitute CreateProcessWithTokenW which only requires SE_IMPERSONATE_NAME. I don't recommend the use of CreateProcessWithLogonW in this context because it doesn't allow you to access the logon SID prior to launching the subprocess.

I wrote a minimal service to demonstrate using Microsoft's sample code:

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

#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();
}

This must be linked with Microsoft's sample code. You can then install the service using the sc command:

sc create demosvc-createprocesswithlogonw binPath= c:\path\demosvc.exe DisplayName= "Demo service for CreateProcessWithLogonW"
Harry Johnston
  • 35,639
  • 6
  • 68
  • 158
  • I have seen that MSDN sample before. I could not make it work on my target machines. (I suspect it does not work when no one is logged on interactively so winsta0 does not exist. I log on through remote desktop. Not sure if my suspicions are right though.) – glagolig Dec 09 '13 at 04:21
  • `winsta0` always exists in each session whether anyone is logged on or not. Perhaps the account the service was running in didn't have the necessary privileges to call `CreateProcessAsUser`? What error code did you get? – Harry Johnston Dec 09 '13 at 10:16
  • Took me some time to try it again... CreateProcessAsUser fails, the error is 1314 (0x522) ERROR_PRIVILEGE_NOT_HELD. I will have to play with priviledges of the caller. But even if I make this work it would be interesting to know why I could not just run it in the window station of the service. (Since my workaround with a separate service works the interactive desktop is not required.) – glagolig Dec 20 '13 at 00:23
  • There's no mystery. The ACL on the service window station doesn't grant access to _limited_user. You could use the `AddAceToWindowStation` and `AddAceToDesktop` functions from the sample code to make the necessary changes, just pass the current window station and desktop and the SID for _limited_user, but keep in mind that doing this may expose your service to attack from _limited_user. The solution you're using now is safer. – Harry Johnston Dec 21 '13 at 03:16
1

I ended up with the following workaround. I configured a different service running as _limited_user and started on demand. Then the test framework can start and stop the limited user service. And the limited user service can run the processes required for my tests.

The workaround works. Hence my processes do not require interactive desktops (even though they load user32.dll). Apparently user32.dll can be loaded in non-interactive context. But there is some unknown subtlety that does not allow the process to run when started directly from test framework service using CreateProcessWithLogonW.

glagolig
  • 1,100
  • 1
  • 12
  • 28
  • 1
    Windows creates dummy display surfaces as necessary, so you don't need to be in an interactive context to use GUI functions. But you do need the correct permissions to the window station and desktop, whether you're going to be using GUI functions or not. – Harry Johnston Dec 09 '13 at 10:18