Click here to Skip to main content
15,881,898 members
Please Sign up or sign in to vote.
5.00/5 (2 votes)
Since Windows 7, security of Windows Services has been strengthened. Services now run isolated in Session 0, so there can be no interaction between the user (typically session 1, 2, 3, etc.) with Windows Services. See: this article[^]
I read an old article which yet addresses this new change this[^] and I am trying to find out why my when I create a process as a user, from a Windows Service, I see no UI when I run it on Windows Server 2012, while it works perfectly on Windows 10.

The article says:
"In Windows Server 2012, when bringing online the same Cluster Resource, the UI application does not become visible. This happens, because the Cluster Service is also a Windows Service, and the Cluster Resources launched by the Cluster Service are run in Session 0, which does not have user interaction. The workaround is to create a Windows Service that launches the UI application, and make this Windows Service a Cluster Resource. "


The following function I wrote is supposed to be invoked by a Windows Service in order to start a child process (as a user), so the child process can interact with the user, i.e. have UI. It works perfectly on Windows desktop but fails to work properly on Windows Server 2012. The UI isn't shown (even not a Console window) and a hot key we register for uninstalling the service, doesn't work as well.


Can someone guide me how to make this workaround?

What I have tried:

See my code here[^].

I followed the guidelines of this article.[^] Invoke my process as a user from a Windows Service, as Windows Services are isolated in session 0. So since Services can't have their own UI, nor any interactions with the user, this child process which is a Win32 program, does such interaction and is invoked by the Service using:
C++
bRes = CreateProcessAsUserW(hToken, NULL, &commandLine[0], NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS | CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | CREATE_DEFAULT_ERROR_MODE, pEnv, NULL, &startupInfo, &processInfo);
The following code:
C++
void WINAPI Run(DWORD dwTargetSessionId, int desktop, LPTSTR lpszCmdLine)
{
    wprintf(_T("Run client start"));

    if (hPrevAppProcess != NULL)
    {
        TerminateProcess(hPrevAppProcess, 0);
        WaitForSingleObject(hPrevAppProcess, INFINITE);
    }

    HANDLE hToken = 0;
    WTS_SESSION_INFO *si;
    DWORD cnt = 0;
    WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, &si, &cnt);
    for (int i = 0; i < (int)cnt; i++)
    {
        if (si[i].SessionId == 0)continue;
        wprintf(_T("Trying session id %i (%s) user admin token"), si[i].SessionId, si[i].pWinStationName);
        HANDLE userToken;
        if (WTSQueryUserToken(si[i].SessionId, &userToken))
        {
            wprintf(_T("WTSQueryUserToken succeced"));
            TOKEN_LINKED_TOKEN admin;
            DWORD len;
            if (GetTokenInformation(userToken, TokenLinkedToken, &admin, sizeof(TOKEN_LINKED_TOKEN), &len))
            {
                wprintf(_T("Success using user admin token"));
                hToken = admin.LinkedToken;
                break;
            }
            else
                wprintf(L"GetTokenInformation() failed");
            CloseHandle(userToken);
        }
        else
            wprintf(L"WTSQueryUserToken() failed");

    }
    if (hToken == 0)
    {
        HANDLE systemToken;
        OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &systemToken);
        DuplicateTokenEx(systemToken, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &hToken);
        CloseHandle(systemToken);
        int i;
        for (i = 0; i < (int)cnt; i++)
        {
            if (si[i].SessionId == 0)continue;
            if (SetTokenInformation(hToken, TokenSessionId, &si[i].SessionId, sizeof(DWORD)))
            {
                wprintf(_T("Success using system token with set user session id %i"), si[i].SessionId);
                break;
            }
        }
        if (i == cnt)
            wprintf(_T("No success to get user admin token nor system token with set user session id"));
    }
    WTSFreeMemory(si);

    STARTUPINFO startupInfo = {};
    startupInfo.cb = sizeof(STARTUPINFO);

    startupInfo.lpDesktop = _T("winsta0\\default");

    LPVOID pEnv = NULL;
    CreateEnvironmentBlock(&pEnv, hToken, TRUE);

    PROCESS_INFORMATION processInfo = {};
    PROCESS_INFORMATION processInfo32 = {};

    TCHAR szCurModule[MAX_PATH] = { 0 };
    GetModuleFileName(NULL, szCurModule, MAX_PATH);

    BOOL bRes = FALSE;

    std::wstring commandLine;
    commandLine.reserve(1024);

    commandLine += L"\"";
    commandLine += szCurModule;
    commandLine += L"\" \"";
    commandLine += SERVICE_COMMAND_LUNCHER;
    commandLine += L"\"";

    wprintf(_T("launch SG_WinService with CreateProcessAsUser ...  %s"), commandLine.c_str());

    bRes = CreateProcessAsUserW(hToken, NULL, &commandLine[0], NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS |
        CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | CREATE_DEFAULT_ERROR_MODE, pEnv,
        NULL, &startupInfo, &processInfo);

    if (bRes == FALSE) 
    {
            DWORD   dwLastError = ::GetLastError();
            TCHAR   lpBuffer[256] = _T("?");
            if (dwLastError != 0)    // Don't want to see a "operation done successfully" error ;-)
                ::FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM,                 // It´s a system error
                    NULL,                                      // No string to be formatted needed
                    dwLastError,                               // Hey Windows: Please explain this error!
                    MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),  // Do it in the standard language
                    lpBuffer,              // Put the message here
                    255,                     // Number of bytes to store the message
                    NULL);
            (_T("CreateProcessAsUser(SG_WinService) failed - Error : %s (%i)"), lpBuffer, dwLastError);
    }
    else
    {
        wprintf(_T("CreateProcessAsUser(SG_WinService) success. New process ID: %i"), processInfo.dwProcessId);
    }
}
returns the following output:

Quote:
05.06.2019 04:25 5856: Trying session id 1 (Console) user admin token

05.06.2019 04:25 5856: Trying session id 2 (RDP-Tcp#27) user admin token

05.06.2019 04:25 5856: Trying session id 65536 (RDP-Tcp) user admin token

05.06.2019 04:25 5856: Success using system token with set user session id 1

05.06.2019 04:25 5856: launch SG_WinService with CreateProcessAsUser ... "C:\myservice\SG_WinService.exe" "ServiceIsLuncher"

05.06.2019 04:25 5856: CreateProcessAsUser(SG_WinService) success. New process ID: 5580

So instead of running the child process on Session 1, it runs as "SYSTEM".
Posted
Updated 11-Jun-19 8:54am
v7
Comments
johannesnestler 6-Jun-19 10:24am    
Great, but Maybe don't do a workarround, make it like it should be. Maybe read Dave's comment again and implement like he told you, shouldn't be that hard… In .NET I use WCF "AdministrationService" hosted inside my windowsservice for IPC - so my Desktop app can connect to the service and vice versa. I do this for all my windowsservice by Default, if nothing else I can monitor the WindowsService and "look inside".
Michael Haephrati 7-Jun-19 6:48am    
This is a technical question. Not a request for consultancy. We prefer to avoid using .NET in Desktop and core Server applications and use pure Win32 API. Dave's answer is not an answer but more like a comment. I addressed this comment. To be more specific, when you run any executable on Windows Server 2012 and this executable launches another executable, no UI will be shown. Further, following Microsoft's guidelines, using CreateProcessAsUser, for some reason the only token found, is of the special "SYSTEM" user which causes this problem.
[no name] 10-Jun-19 0:30am    
Why doesn’t it work from Remote Desktop?

I managed to find the reason for this problem and solve it.
The source code was OK as it is. I did everything right, as I looked over all sessions to use a token from the first user logged on and running the child process with the UI on this session.

For some reason, it didn't work because I connected to the Windows Server 2012 via Remote Desktop, but when I ran it physically on the same server, it worked: UI was shown and hot key worked!

I received help after posing the same question at Stackoverflow[^] and received help from Harry Johnston[^] so thanks!

He advised me to call GetLastError() which I did. It seems that when connecting via Remote Desktop (as Administrator), the Administrator user isn't necessarily logged in which is why the only session possible for launching the child process was "SYSTEM" which was wrong, since this "user" isn't meant to be used for other than System services with no UI.

My updated code that works when physically installed on Windows Server 2012 is:

void WINAPI Run(DWORD dwTargetSessionId, int desktop, LPTSTR lpszCmdLine)
{
    wprintf(_T("Run client start"));

    if (hPrevAppProcess != NULL)
    {
        TerminateProcess(hPrevAppProcess, 0);
        WaitForSingleObject(hPrevAppProcess, INFINITE);
    }


    HANDLE hToken = 0;
    WTS_SESSION_INFO *si;
    DWORD cnt = 0;
    WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, &si, &cnt);
    for (int i = 0; i < (int)cnt; i++)
    {
        if (si[i].SessionId == 0)continue;
        wprintf(_T("Trying session id %i (%s) user admin token"), si[i].SessionId, si[i].pWinStationName);
        HANDLE userToken;
        if (WTSQueryUserToken(si[i].SessionId, &userToken))
        {
            wprintf(_T("WTSQueryUserToken succeced"));
            TOKEN_LINKED_TOKEN admin;
            DWORD len;
            if (GetTokenInformation(userToken, TokenLinkedToken, &admin, sizeof(TOKEN_LINKED_TOKEN), &len))
            {
                wprintf(_T("Success using user admin token"));
                hToken = admin.LinkedToken;
                break;
            }
            else
                wprintf(L"GetTokenInformation() failed");

            CloseHandle(userToken);
        }
        else
        {
            DWORD error = GetLastError();
            if (error)
            {
                LPVOID lpMsgBuf;
                DWORD bufLen = FormatMessage(
                    FORMAT_MESSAGE_ALLOCATE_BUFFER |
                    FORMAT_MESSAGE_FROM_SYSTEM |
                    FORMAT_MESSAGE_IGNORE_INSERTS,
                    NULL,
                    error,
                    MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                    (LPTSTR)&lpMsgBuf,
                    0, NULL);
                utils::WriteLogFile(L"Error '%s'\n", lpMsgBuf);
            }
        }

    }
    if (hToken == 0)
    {
        HANDLE systemToken;
        OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &systemToken);
        DuplicateTokenEx(systemToken, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &hToken);
        CloseHandle(systemToken);
        int i;
        for (i = 0; i < (int)cnt; i++)
        {
            if (si[i].SessionId == 0)continue;
            if (SetTokenInformation(hToken, TokenSessionId, &si[i].SessionId, sizeof(DWORD)))
            {
                wprintf(_T("Success using system token with set user session id %i"), si[i].SessionId);
                break;
            }
        }
        if (i == cnt)
            wprintf(_T("No success to get user admin token nor system token with set user session id"));
    }
    WTSFreeMemory(si);


    STARTUPINFO startupInfo = {};
    startupInfo.cb = sizeof(STARTUPINFO);

    startupInfo.lpDesktop = _T("winsta0\\default");

    LPVOID pEnv = NULL;
    CreateEnvironmentBlock(&pEnv, hToken, TRUE);

    PROCESS_INFORMATION processInfo = {};
    PROCESS_INFORMATION processInfo32 = {};

    TCHAR szCurModule[MAX_PATH] = { 0 };
    GetModuleFileName(NULL, szCurModule, MAX_PATH);

    BOOL bRes = FALSE;

    std::wstring commandLine;
    commandLine.reserve(1024);

    commandLine += L"\"";
    commandLine += szCurModule;
    commandLine += L"\" \"";
    commandLine += SERVICE_COMMAND_LUNCHER;
    commandLine += L"\"";

    wprintf(_T("launch SG_WinService with CreateProcessAsUser ...  %s"), commandLine.c_str());

    bRes = CreateProcessAsUserW(hToken, NULL, &commandLine[0], NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS |
        CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE | CREATE_DEFAULT_ERROR_MODE, pEnv,
        NULL, &startupInfo, &processInfo);

    if (bRes == FALSE)
    {
        DWORD   dwLastError = ::GetLastError();
        TCHAR   lpBuffer[256] = _T("?");
        if (dwLastError != 0)    // Don't want to see a "operation done successfully" error ;-)
            ::FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM,                 // It´s a system error
                NULL,                                      // No string to be formatted needed
                dwLastError,                               // Hey Windows: Please explain this error!
                MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),  // Do it in the standard language
                lpBuffer,              // Put the message here
                255,                     // Number of bytes to store the message
                NULL);
        (_T("CreateProcessAsUser(SG_WinService) failed - Error : %s (%i)"), lpBuffer, dwLastError);
    }
    else
    {
        wprintf(_T("CreateProcessAsUser(SG_WinService) success. New process ID: %i"), processInfo.dwProcessId);
    }
}
 
Share this answer
 
That was way back in 2006. Launching a process as a user from a service is frowned upon now. Just don't do it as security has been tightened up since then, not allowing services to launch processes on the user desktop.

Think about it. What would happen if there wasn't a user logged on at the time?

The proper way to do it would be you have a small user app that runs when the user logs in and it talks to the service via an IPC channel, like named pipes. When the service has to launch an app, it just tells the small application to do it for the service. Launching an app from there is super easy and will always be done as the user that's logged in.

Your service doesn't even have to worry about a user not being logged in. If there's no app listening on the other end of the IPC channel, there's no app to launch your process.
 
Share this answer
 
Comments
Michael Haephrati 4-Jun-19 7:56am    
My question isn't about finding a workaround for Services, as they are now isolated, but about a phenomenon with Windows Server 2012 that EVEN the child Win32 program, doesn't show any UI.

I am aware of the changes but my question comes after these changes were made. The change you are referring to is this: https://www.msigeek.com/1521/appcompat-series-session-0-isolation-and-secure-desktop.
Since this change, Windows services can't have any interaction with the user, and therefor need to invoke a client process per each logged in user (that answers your question about a user not logged in), and my question is about that. It works perfectly on Windows Desktop (i.e. 10), but not on Windows Server 2012.

You wrote: "The proper way to do it would be you have a small user app that runs when the user logs in and it talks to the service via an IPC channel, like named pipes. When the service has to launch an app, it just tells the small application to do it for the service. Launching an app from there is super easy and will always be done as the user that's logged in.", well my user app works perfectly alongside with the Service on Windows 10, but not on Windows Server 2012.
Windows services are designed for no user interaction, so using an UI is always a design flaw. Write into the system log some message and harden your code that it can deal with the problems.

My experience is to close the complete service in the top loop and check every roundtrip whether the service as to restart its inner functionality. Maybe you can show an icon or better send some e-mail. Sometimes an extra delay after errors makes sense.

Always remember: a server runs normally without an logged in user to lift up security.
 
Share this answer
 
v2
Comments
Michael Haephrati 7-Jun-19 6:50am    
This isn't a correct answer. No one claimed Windows services have or should have any UI. That would be impossible as services run on session 0, which is also known as "user SYSTEM". The question is about the recommended method of invoking a Desktop process by calling CreateProcessAsUser() which fails only on Server 2012 (please don't mix between "service" and "server").
Michael Haephrati 7-Jun-19 6:54am    
I would be happy to access your answer if you go over the question again. Look at the source code from WTSEnumerateSessions() and down. The question is about not being able to find the first logged in user and instead finding only the "SYSTEM" token, which leads to launching a desktop client (with UI) as "SYSTEM", which is bad, as you rightly say, but the purpose is to launch the desktop client application to the logged in user, and when that is properly done, the UI is shown. I have managed to do so on Windows 10.

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900