Click here to Skip to main content
1,822 members
Articles / Multimedia / C++
Article

NT Services 2013

Rate me:
Please Sign up or sign in to vote.
5.00/5 (8 votes)
28 Aug 2013CPOL 23.8K   10   2
Developing NT services compatible with today's limitations

Introduction

While looking for articles about NT services I have found that most of the information online is out dated. There are many articles and code snippets which were published before 2000 ! For example, if you look for books at 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 NT Services. The changes in the way Windows handles NT Services since Windows 7 are also important for understanding the proper way to write and use NT Services and yet keep it compatible to all OS versions.

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 from 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 an created from an empty Win32API project.

Some constants 

First there are several constants which are useful: 

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:

<pre>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

Installing a 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 to the following.

Define the handles for the SCM and the service 
SC_HANDLE schSCManager; 
SC_HANDLE schService; 
Get a handle to the SCM database 
schSCManager = OpenSCManager( 
        NULL,                    // local computer
        NULL,                    // ServicesActive database 
        SC_MANAGER_ALL_ACCESS);  // full access rights 
Open SCM  
if (NULL == schSCManager)  
{ 
	DWORD Ret = GetLastError();
        printf("OpenSCManager failed (%d)\n", Ret);
        return Ret;
 }<span style="color: rgb(17, 17, 17); font-family: 'Segoe UI', Arial, sans-serif; font-size: 14px;"</span>
Get a handle to the service 
schService = OpenServiceW( 
        schSCManager,       // SCM database 
        SERVICE_NAME,       // name of service 
        DELETE);            // need delete access 
Terminate the service 
if (schService == NULL)
{
	DWORD Ret = GetLastError();
        printf("OpenService failed (%d)\n", Ret);
        CloseServiceHandle(schSCManager);
        return Ret;
} 

 Delete the service  

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

Service Isolation 

Since NT 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 - 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 - 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:

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

Then we do the following check:

// 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

 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.

Here are the steps that need to be taken 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).

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

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:

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 follow and got the same results.

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:

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.

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

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

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.

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.

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:

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.

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 hit 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: 

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

License

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


Written By
Michael Haephrati
United States United States
Michael Haephrati, born in 1964, an entrepreneur, inventor and a musician. Haephrati worked on many ventures starting from HarmonySoft, designing Rashumon, the first Graphical Multi-lingual word processor for Amiga computer.

Worked with Amdocs and managed several software projects, among them one for the Ministry of Tourism in New Zealand. During 1995-1996 he worked as a Contractor with Apple at Cupertino. After returning to Israel, worked as a Project Manager with Top Image Systems (mostly with JCC, Nicosia), and then at a research institute made the fist steps developing the credit scoring field in Israel. He founded Target Scoring and developed a credit scoring system named ThiS, based on geographical statistical data, participating VISA CAL, Isracard, Bank Leumi and Bank Discount (Target Scoring, being the VP Business Development of a large Israeli institute).
During 2000, he founded Target Eye, and developed the first remote PC surveillance and monitoring system, named Target Eye.

Other ventures included: Data Cleansing (as part of the DataTune system which was implemented in many organizations.


Also a Code Project Member since Sunday, March 16, 2003 (10 years, 5 months)
20 Sep 2013: Best C++ article of August 2013
25 Jan 2013: Code Project - Best C++ article of December 2012
31 Dec 2012: CodeProject MVP 2013

Comments and Discussions

 
GeneralMy vote of 5 Pin
John Walles29-Aug-13 2:31
John Walles29-Aug-13 2:31 
GeneralRe: My vote of 5 Pin
Michael Haephrati8-Sep-13 5:49
Michael Haephrati8-Sep-13 5:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.