Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

Windows Services

4.87/5 (36 votes)
10 Apr 2020CPOL6 min read 49.7K  
An up to date article about NT Services
Since most of the information I found online about Windows services was outdated, this article is my attempt to give more up to date information about the topic.

Introduction

While looking for articles about Windows services, I found that most of the information online is outdated. There are many articles and code snippets which were published before 2000 ! For example, if you look for books on Amazon, you will find: Win32 System Service or Professional NT Service, both published before or near 2000, So I decided to write my own research about Windows Services. The changes in the way Windows handles Windows Services since Windows 7 are also important for understanding the proper way to write and use Windows Services and yet keep it compatible to all OS versions. I invite you to read also a more advanced article about Windows Services. 

Background

NT Service (also known as Windows Service) is the term given to special process which is loaded by the Service Control Manager of the NT kernel and runs in the background right after Windows starts (before users log on). Services are needed mostly to perform core and low level OS tasks, such as Web serving, event logging, file serving, help and support, printing, cryptography, and error reporting. That being said, and unlike what many people think, there isn't any limitation preventing anyone to add any other functionality to a service including user interface and anything else that normal applications do.

The Basics

Before we go ahead to complex issues, let's start with the basics. A typical service based project will be created from an empty Win32API project.

Some Constants

First, there are several constants which are useful:

C++
SERVICE_STATUS        g_ServiceStatus = {0};
SERVICE_STATUS_HANDLE g_StatusHandle = NULL;
HANDLE                g_ServiceStopEvent = INVALID_HANDLE_VALUE; 

SERVICE_STATUS will be used to obtain the current status of the service.

SERVICE_STATUS_HANDLE will be used to hold the current status.

See:

C++
BOOL WINAPI SetServiceStatus(
  _In_  SERVICE_STATUS_HANDLE hServiceStatus,
  _In_  LPSERVICE_STATUS lpServiceStatus 
);

Giving a Name to Our Service

C++
#define SERVICE_NAME L"CodeProjectDemo" 

Installing Our Service

Here is a typical code for installation:

C++
DWORD InstallTheService()
{
	SC_HANDLE schSCManager;
    	SC_HANDLE schService;
    	TCHAR szPath[MAX_PATH];
    	if(!GetModuleFileName(NULL, szPath, MAX_PATH))
    	{
		DWORD Ret = GetLastError();
        	printf("Cannot install service (%d)\n", Ret);
        	return Ret;
    	}
	// Get a handle to the SCM database.  
    	schSCManager = OpenSCManager( 
        NULL,                    // local computer
        NULL,                    // ServicesActive database 
        SC_MANAGER_ALL_ACCESS);  // full access rights 
 
    	if (NULL == schSCManager) 
    	{
		DWORD Ret = GetLastError();
        	printf("OpenSCManager failed (%d)\n", Ret);
        	return Ret;
    	}
    	// Create the service.
    	schService = CreateServiceW( 
        schSCManager,              // SCM database 
        SERVICE_NAME,              // name of service 
        SERVICE_NAME,              // service name to display 
        SERVICE_ALL_ACCESS,        // desired access 
        SERVICE_WIN32_OWN_PROCESS, // service type 
        SERVICE_AUTO_START,		   // start type 
        SERVICE_ERROR_NORMAL,      // error control type 
        szPath,                    // path to service's binary 
        NULL,                      // no load ordering group 
        NULL,                      // no tag identifier 
        NULL,                      // no dependencies 
        NULL,                      // LocalSystem account 
        NULL);                     // no password 
 
    	if (schService == NULL) 
    	{
		DWORD Ret = GetLastError();
		if (Ret != 1073)
		{
			printf("CreateService failed (%d)\n", Ret);
			CloseServiceHandle(schSCManager);
			return Ret;
		}
		else
		{
			printf("The service is exists\n");
		}
    	}
    	else
		printf("Service installed successfully\n");
    	CloseServiceHandle(schService); 
    	CloseServiceHandle(schSCManager);
	return 0;
}  

Uninstalling a Service

When you wish to uninstall a service, you do the following:

Define the Handles for the SCM and the Service

C++
SC_HANDLE schSCManager; 
SC_HANDLE schService; 

Get a Handle to the SCM Database

C++
schSCManager = OpenSCManager( 
        NULL,                    // local computer
        NULL,                    // ServicesActive database 
        SC_MANAGER_ALL_ACCESS);  // full access rights 

Open SCM

C++
if (NULL == schSCManager)  
{ 
	DWORD Ret = GetLastError();
        printf("OpenSCManager failed (%d)\n", Ret);
        return Ret;
 }

Get a Handle to the Service

C++
schService = OpenServiceW( 
        schSCManager,       // SCM database 
        SERVICE_NAME,       // name of service 
        DELETE);            // need delete access 

Terminate the Service

C++
if (schService == NULL)
{
	DWORD Ret = GetLastError();
        printf("OpenService failed (%d)\n", Ret);
        CloseServiceHandle(schSCManager);
        return Ret;
} 

Delete the Service

C++
if (!DeleteService(schService) ) 
{
	DWORD Ret = GetLastError();
        printf("DeleteService failed (%d)\n", Ret);
}
else printf("Service deleted successfully\n");  

Cleanup

C++
CloseServiceHandle(schService);  
CloseServiceHandle(schSCManager);  

Service Isolation

Since Windows Services operate under the SYSTEM user account as opposed to any other user account exists, services can become powerful and can be a potential security risk. Because of that, Microsoft introduced isolation of services. Before that change, all services ran in Session 0 along with applications.

Image 1

Image 1 - Before isolation

Since Windows Vista, Windows Server 2008, and later versions of Windows, the operating system isolates services in Session 0 and runs applications in other sessions, a session per logged user, so services are protected from attacks that originate in application code.

Image 2

Image 2 - After isolation

As a result, if an NT Service tries to access the Clipboard, take a snapshot of the active screen or displays a dialog box, since it can't access any space used by any of the logged users, captured Clipboard data will contain nothing, captured screen will be in fact an empty desktop like image with nothing shown on it and when a dialog box is displayed, since the user is not running in Session 0, he will not see the UI and therefore will not be able to provide the input that the service is looking for. In order to respond, the user will need to switch to a different view to see it.

To know whether we need to assume the service will be isolated, based on the OS version, we can create the following argument:

C++
BOOL api_bSessionIsolated = FALSE; //   are we isolated or not?

Then, we do the following check:

C++
// Checking the current Windows version
OSVERSIONINFO osVersionInfo = {0};
osVersionInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
GetVersionEx(&osVersionInfo);
if (osVersionInfo.dwMajorVersion >= 6)
    api_bSessionIsolated = TRUE;
else
    api_bSessionIsolated = FALSE;

Displaying a Message Box by an Isolated Service

Call WTSSendMessage.

C++
 BOOL WTSSendMessage(
  _In_   HANDLE hServer,
  _In_   DWORD SessionId,
  _In_   LPTSTR pTitle,
  _In_   DWORD TitleLength,
  _In_   LPTSTR pMessage,
  _In_   DWORD MessageLength,
  _In_   DWORD Style,
  _In_   DWORD Timeout,
  _Out_  DWORD *pResponse,
  _In_   BOOL bWait
);

Communicating with Other Sessions

In order to interact and communicate with other sessions, the service should use the CreateProcessAsUser API in order to create an Agent which will run all user related tasks under the user's session and will interact with the service, while it runs under session 0.

Given below are the steps that need to be followed to implement that properly.

Step 1: Obtaining the Current Active Windows Session

That is done by calling WTSGetActiveConsoleSessionId which returns the ID of the current active Windows session at the console (i.e., the machine keyboard and display, as opposed to WTS sessions).

C++
DWORD WTSGetActiveConsoleSessionId(void);  

I have read about failure or errors when calling WTSGetActiveConsoleSessionId (for example, cases in which it always returns 0 when invoked by an NT Service) so I will introduce another option which would be enumerating all sessions and finding the one that is in WTSConnected state.

To understand that method, we first need to understand the possible states of each session which are defined in the WTS_CONNECTIONSTATE_CLASS which can be found the Windows SDK header files.

See c:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Include\WtsApi32.h.

C++
typedef enum _WTS_CONNECTSTATE_CLASS {
    WTSActive,              // User logged on to WinStation
    WTSConnected,           // WinStation connected to client
    WTSConnectQuery,        // In the process of connecting to client
    WTSShadow,              // Shadowing another WinStation
    WTSDisconnected,        // WinStation logged on without client
    WTSIdle,                // Waiting for client to connect
    WTSListen,              // WinStation is listening for connection
    WTSReset,               // WinStation is being reset
    WTSDown,                // WinStation is down due to error
    WTSInit,                // WinStation in initialization
} WTS_CONNECTSTATE_CLASS;    

So our function will look like this:

C++
DWORD WINAPI GetActiveSessionId()
{
    PWTS_SESSION_INFO pSessionInfo = 0;
        DWORD dwCount = 0;
        WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, &pSessionInfo, &dwCount);
       DWORD dwActive;
        for (DWORD i = 0; i < dwCount; ++i)
        {
            WTS_SESSION_INFO si = pSessionInfo[i];
            if (WTSActive == si.State)
            { 
                    dwActive = si.SessionId;
            WriteToLog(L"Session ID = %d",dwActive);
                    break;
            }
        }     
    WTSFreeMemory(pSessionInfo);
    return dwActive;
} 

Note: I add WriteToLog to anything I code, which is great to trace anything into one continuous log file.

In most cases, while you are logged in and assuming only one user is logged in, the session ID will be "1".

But do we need to enumerate all sessions or can we just use WTSGetActiveConsoleSessionId?

My conclusion is YES. I changed my function as follows and got the same results.

C++
DWORD WINAPI GetActiveSessionId()
{
   DWORD dwActive;
   dwActive = WTSGetActiveConsoleSessionId();
   WriteToLog(L"Session ID according to WTSGetActiveConsoleSessionId is %d",dwActive);
 
    PWTS_SESSION_INFO pSessionInfo = 0;
    DWORD dwCount = 0;
    WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, &pSessionInfo, &dwCount);
    for (DWORD i = 0; i < dwCount; ++i)
    {
        WTS_SESSION_INFO si = pSessionInfo[i];
        if (WTSActive == si.State)
        { 
            dwActive = si.SessionId;
            WriteToLog(L"Session ID = %d",dwActive);
            break;
        }
    }     
    WTSFreeMemory(pSessionInfo);
    return dwActive;
}

which means that it can just look like this:

C++
DWORD WINAPI GetActiveSessionId()
{ 
       DWORD dwActive;
       dwActive = WTSGetActiveConsoleSessionId(); 
    return dwActive; 
}

Step 2: Querying the Token of the Current Session

Next, we call WTSQueryUserToken to get the token for that session.

C++
BOOL WTSQueryUserToken(
  _In_   ULONG SessionId,
  _Out_  PHANDLE phToken 
);    

We call WTSQueryUserToken passing to it the session ID from the last call:

C++
WTSQueryUserToken (GetActiveSessionId(), &hToken)    

Alternative Method

Please note that WTSQueryUserToken can only be called from services running under LocalSystem account. An alternative would be calling OpenProcessToken.

C++
BOOL WINAPI OpenProcessToken(
  _In_   HANDLE ProcessHandle,
  _In_   DWORD DesiredAccess,
  _Out_  PHANDLE TokenHandle
);

The process handle can be the current process or a process that (almost) always runs, explorer.exe.

Step 3: Duplicating the Token

Next, we call DuplicateTokenEx to duplicate the token.

C++
DuplicateTokenEx(hToken,MAXIMUM_ALLOWED,NULL,SecurityIdentification,
                 TokenPrimary, &hTokenDup); 

Step 4: Creating the Environment

Next, we create the environment for the new process to be created by calling CreateEnvironmentBlock.

Here is how that is done:

C++
BOOL WINAPI CreateEnvironmentBlock(
  _Out_     LPVOID *lpEnvironment,
  _In_opt_  HANDLE hToken,
  _In_      BOOL bInherit 
); 

Invoking the New Process

Now we are ready to create the new process, invoking it from the service and yet creating it under the active user's account. We do that by calling CreateProcessAsUser.

C++
BOOL WINAPI CreateProcessAsUser(
  _In_opt_     HANDLE hToken,
  _In_opt_     LPCTSTR lpApplicationName,
  _Inout_opt_  LPTSTR lpCommandLine,
  _In_opt_     LPSECURITY_ATTRIBUTES lpProcessAttributes,
  _In_opt_     LPSECURITY_ATTRIBUTES lpThreadAttributes,
  _In_         BOOL bInheritHandles,
  _In_         DWORD dwCreationFlags,
  _In_opt_     LPVOID lpEnvironment,
  _In_opt_     LPCTSTR lpCurrentDirectory,
  _In_         LPSTARTUPINFO lpStartupInfo,
  _Out_        LPPROCESS_INFORMATION lpProcessInformation
);

Note: It is advised to use the W version (UNICODE) and not the A version, as it has some bugs.

Cleaning Up

Before termination of our application, the cleanup includes calling CloseHandle and DestroyEnvironmentBlock.

Handling Various Scenarios

After building a skeleton of a service which invokes a process to run in the user's session, the big challenge is to keep track and address a wide range of scenarios such as:

  • Switching users - any case in which a new user logs in or the current user logs out and then logs in under a different credentials, etc.
  • Log off / on - testing how the service operates between a log out and a log in, i.e., what is done when the Windows log in screen appears. (for example, could backup solutions, which contain a Service part, continue to send files to the server at that time). After logging off, the system destroys the session associated with that user.WTSQueryUserToken
  • Restart (hard and soft) - what happens when the user's session process is running and the end, user presses the "Restart" menu or performs a hard restart.
  • Turn PC off and on (hard and soft) - what happens when the user presses the "Turn off" menu, or just hits the On/Off button and performs a hard turn off.
  • Windows updates - we need to add to that cases where restart includes installing Windows update, which takes place before restart actually starts and after Windows starts again, just before log in.

My WriteToLog Routine

Finally, I wanted to share with you my WriteToLog routine:

C++
void WriteToLog(LPCTSTR lpText, ...)
{
	FILE* file;
	CTime time = CTime::ApiGetCurrentLocalTime();
	CString strMsg;
	va_list ptr;
	va_start(ptr, lpText);
	strMsg.VFormat(lpText, ptr);
	CString strDate = time.FormatDate(_T("d/MM/yyyy"));
	CString strTime = time.FormatTime(_T("hh:mm:ss tt"));
	
	CString strTrace;
	strTrace.Format(_T("%s %s: %s"), (LPCTSTR)strTime, 
                   (LPCTSTR)strDate, (LPCTSTR)strMsg);
	
	file = _tfopen(LOG_FILENAME, L"a");
	if (file)
	{
		_ftprintf(file, _T("\n%s\n"), (LPCTSTR)strTrace);
		fclose(file);
	}
}

History

  • 23rd August, 2013: Initial version

License

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