Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

Thank You for Your Service: Creating a Persistent Isolated Windows Service

5.00/5 (20 votes)
14 Feb 2024CPOL11 min read 24.8K   86  
Windows Services are powerful little beasts and encountering them is unavoidable when developing under Windows
Windows Services are powerful little beasts, and if you work under Windows, it’s hard not to encounter them. However, powerful as Services may be, when it comes to isolated Services, trouble is coming your way: As I was developing a new Anti-Virus, which should constantly run and restart, the isolated service failed to restart after shutting down the PC. Yeh – calling this issue a problem is an understatement. So, I decided to come up with a solution, in an effort to create a persistent Service which always restarts. I call it Weeble-Service, named after the famous Weeble-Wobble doll which never falls.

Second Prize: Best Article of October 2022

Image 1

The Service and the Beast

When programming for Windows, there’s no way around Windows Services. I have written several articles about services in the past such as this article. It seems that no matter how much I’ve worked with services, or how much I think I can handle them, I keep encountering more problems, challenges, and issues, which are undocumented or, if I’m “lucky”, they are poorly documented. Some of the issues started when the Service Isolation was introduced by Microsoft. One of the most annoying problems I’ve encountered is the inability to restart the service after shutting down the PC when Fast Startup is checked. As I could not find a solution, I decided to roll up my sleeves and create one myself, which led to the development of a persistent Service.

Thank You for Your Service…

Before I dive deeper and explain more about my solution, let’s start with the basics and explain what Services are, and why we even need to use Windows Services in the first place.

NT Service (also known as Windows Service) is the term given to a special process that is loaded by the Service Control Manager of the NT kernel and runs in the background right after Windows starts (before users log on). We use services to perform core and low-level OS tasks, such as Web serving, event logging, file serving, help and support, printing, cryptography, and error reporting.

Other than that, Services enable us to create executable applications for the long run. The reason is that a Service runs in its own Windows session environment, so it does not interfere with other components or sessions of your application. Obviously, Services are expected to start automatically once the computer boots – and we get to that in a minute.

Moving further, the obvious question is - why do we need persistent Services? The answer is pretty clear - we need the service to:

  • always run.
  • invoke itself under the logged-in user’s session.
  • be a watchdog and make sure a given application is always running.

The Windows Service needs to survive sleep, hibernate, restart, and shut down. However, as explained, there are specific and dangerous issues when “Fast Startup” is checked, and the PC is turned off and on again. In most of these cases, the service failed to restart.

Since I was developing an Anti-Virus, which is supposed to restart after reboot or shutdown, this issue created a serious problem which I was eager to solve.

Stay! Good Service…

To create the near perfect persistent Windows service, I had to solve several underlying issues first.

One of those issues was related to Service Isolation - the isolated Service can’t access any context associated with any specific user. One of our software products used to store data in c:\users\<USER NAME>\appdata\local\ but when it runs from our service, the path was invalid since the service runs from session 0. Moreover, after reboot, the Service starts before any user logs in – which led to the first piece of the solution: to be able to wait for the user to log in.

To figure out how to do this, I posted my question here.

This turned out to be a problem with no perfect solution, however, the code that accompanied this article, is used and was fully tested with no issues.

The Basics

The structure and the flow of my code may look complex, and that is for a reason. For the last 10 years, Services became isolated from other processes. Since Windows Services operate under the SYSTEM user account as opposed to any other user account exists, and run isolated.

The reason for the isolation is because 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.

Before that change, Windows Services resides alongside other programs.

Image 2

However, after the isolation, which took place since Windows Vista, things have changed.

Image 3

The idea behind my code, is to have the Windows Service launch itself as a user, by calling CreateProcessAsUserW, and that will be explained further.

My Service has several commands and when called using these command line parameters, it acts accordingly.

C++
#define SERVICE_COMMAND_INSTALL L"Install"             // The command line argument 
                                                       // for installing the service

#define SERVICE_COMMAND_LAUNCHER L"ServiceIsLauncher"  // Launcher command for 
                                                       // NT service

When SG_RevealerService is called, there are the following options:

Option 1 - called without any command line argument - nothing will happen.

Option 2 - called with the Install command line argument

In that case, the service will install itself and if a valid executable path is added after a hash (#) separator, this executable will start, and the Watch Dog will keep it running.

The Service then runs itself using CreateProcessAsUserW(), and the new process runs under the user account. It gives the Service the ability to access context that the calling instance has no access to due to Service Isolation.

Option 3 - called with the ServiceIsLauncher command line argument. The service client main application will then start. At this point, the entry function indicates that the service had started itself under a user's privileges. At this point, you can see two instances of SG_RevealerService in the Task Manager: one under SYSTEM, and the other under the currently logged-in user.

Image 4

C++
/*
RunHost
*/

BOOL RunHost(LPWSTR HostExePath,LPWSTR CommandLineArguments)
{
    WriteToLog(L"RunHost '%s'",HostExePath);

    STARTUPINFO startupInfo = {};
    startupInfo.cb = sizeof(STARTUPINFO);
    startupInfo.lpDesktop = (LPTSTR)_T("winsta0\\default");

    HANDLE hToken = 0;
    BOOL bRes = FALSE;

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

    PROCESS_INFORMATION processInfoAgent = {};
    PROCESS_INFORMATION processInfoHideProcess = {};
    PROCESS_INFORMATION processInfoHideProcess32 = {};

    if (PathFileExists(HostExePath))
    {
        std::wstring commandLine;
        commandLine.reserve(1024);

        commandLine += L"\"";
        commandLine += HostExePath;
        commandLine += L"\" \"";
        commandLine += CommandLineArguments;
        commandLine += L"\"";

        WriteToLog(L"launch host 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, &processInfoAgent);
        if (bRes == FALSE)
        {
            DWORD   dwLastError = ::GetLastError();
            TCHAR   lpBuffer[256] = _T("?");
            if (dwLastError != 0)    // Don't want to see an 
                                     // "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);
            }
            WriteToLog(L"CreateProcessAsUser failed - Command Line = %s Error : %s",
                         commandLine, lpBuffer);
        }
        else
        {
            if (!writeStringInRegistry(HKEY_LOCAL_MACHINE, 
               (PWCHAR)SERVICE_REG_KEY, (PWCHAR)SERVICE_KEY_NAME, HostExePath))
            {
                WriteToLog(L"Failed to write registry");
            }
        }
    }
    else
    {
        WriteToLog(L"RunHost failed because path '%s' does not exists", HostExePath);
    }
    hPrevAppProcess = processInfoAgent.hProcess;
    
    CloseHandle(hToken);
    WriteToLog(L"Run host end!");

    return bRes;
}

Detecting User Log On

The first challenge is to start some of the actions only when and if a user logs in.

In order to detect a user log-on, we first define a global variable.

C++
bool g_bLoggedIn = false;

It will be set to true when and if a user logs in.

Subscribing to the Logon Event

I defined the following Preprocessor Directives:

C++
#define EVENT_SUBSCRIBE_PATH    L"Security"
#define EVENT_SUBSCRIBE_QUERY    L"Event/System[EventID=4624]"

After the Service starts, we subscribe to the logon event, so the moment a user has logged in, we get an alert via the callback function we have set, and we can continue.

To implement that, we need a class to handle the creation of the subscription and waiting for the event callback.

C++
class UserLoginListner
{
    HANDLE hWait = NULL;
    HANDLE hSubscription = NULL;

public:
    ~UserLoginListner()
    {
        CloseHandle(hWait);
        EvtClose(hSubscription);
    }

    UserLoginListner()
    {
        const wchar_t* pwsPath = EVENT_SUBSCRIBE_PATH;
        const wchar_t* pwsQuery = EVENT_SUBSCRIBE_QUERY;

        hWait = CreateEvent(NULL, FALSE, FALSE, NULL);

        hSubscription = EvtSubscribe(NULL, NULL,
            pwsPath, pwsQuery,
            NULL,
            hWait,
            (EVT_SUBSCRIBE_CALLBACK)UserLoginListner::SubscriptionCallback,
            EvtSubscribeToFutureEvents);
        if (hSubscription == NULL)
        {
            DWORD status = GetLastError();

            if (ERROR_EVT_CHANNEL_NOT_FOUND == status)
                WriteToLog(L"Channel %s was not found.\n", pwsPath);
            else if (ERROR_EVT_INVALID_QUERY == status)
                WriteToLog(L"The query \"%s\" is not valid.\n", pwsQuery);
            else
                WriteToLog(L"EvtSubscribe failed with %lu.\n", status);

            CloseHandle(hWait);
        }
    }

Next, we need a function for the waiting itself:

C++
void WaitForUserToLogIn()
{
    WriteToLog(L"Waiting for a user to log in...");
    WaitForSingleObject(hWait, INFINITE);
    WriteToLog(L"Received a Logon event - a user has logged in");
}

We also need the Callback function:

C++
static DWORD WINAPI SubscriptionCallback(EVT_SUBSCRIBE_NOTIFY_ACTION action, PVOID
       pContext, EVT_HANDLE hEvent)
{
    if (action == EvtSubscribeActionDeliver)
    {
        WriteToLog(L"SubscriptionCallback invoked.");
        HANDLE Handle = (HANDLE)(LONG_PTR)pContext;
        SetEvent(Handle);
    }

    return ERROR_SUCCESS;
}

Then all we need to do is add a block of code with the following lines:

C++
WriteToLog(L"Launch client\n"); // launch client ...
{
    UserLoginListner WaitTillAUserLogins;
    WaitTillAUserLogins.WaitForUserToLogIn();
}

Once we reach the end of this block, we can be assured that a user has logged in.

Later in this article, I will explain how to find the account/user name of the logged-in user and how to use my GetLoggedInUser() function.

It's Not You, It’s Me: Impersonating a User

When we know for sure a user has logged in, we would need to impersonate that user.

The following function does the job. It doesn’t only impersonate the user, it also calls CreateProcessAsUserW() and runs itself as this user.

By doing so, we give the service access to the user’s context, including documents, desktop, etc., and allow the service to use UI, which isn’t possible for a service running from Session 0.

CreateProcessAsUserW creates a new process along with its primary thread, which will run in the context of a given user.

C++
//Function to run a process as active user from Windows service
void ImpersonateActiveUserAndRun()
{
    DWORD session_id = -1;
    DWORD session_count = 0;
    WTS_SESSION_INFOW *pSession = NULL;

    if (WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, &pSession, &session_count))
    {
        WriteToLog(L"WTSEnumerateSessions - success");
    }
    else
    {
        WriteToLog(L"WTSEnumerateSessions - failed. Error %d",GetLastError());
        return;
    }
    TCHAR szCurModule[MAX_PATH] = { 0 };

    GetModuleFileName(NULL, szCurModule, MAX_PATH);

    for (size_t i = 0; i < session_count; i++)
    {
        session_id = pSession[i].SessionId;
        WTS_CONNECTSTATE_CLASS wts_connect_state = WTSDisconnected;
        WTS_CONNECTSTATE_CLASS* ptr_wts_connect_state = NULL;
        DWORD bytes_returned = 0;
        if (::WTSQuerySessionInformation(
            WTS_CURRENT_SERVER_HANDLE,
            session_id,
            WTSConnectState,
            reinterpret_cast<LPTSTR*>(&ptr_wts_connect_state),
            &bytes_returned))
        {
            wts_connect_state = *ptr_wts_connect_state;
            ::WTSFreeMemory(ptr_wts_connect_state);
            if (wts_connect_state != WTSActive) continue;
        }
        else
        {
            continue;
        }

        HANDLE hImpersonationToken;
        if (!WTSQueryUserToken(session_id, &hImpersonationToken))
        {
            continue;
        }

        //Get the actual token from impersonation one
        DWORD neededSize1 = 0;
        HANDLE *realToken = new HANDLE;
        if (GetTokenInformation(hImpersonationToken, 
           (::TOKEN_INFORMATION_CLASS) TokenLinkedToken, 
                       realToken, sizeof(HANDLE), &neededSize1))
        {
            CloseHandle(hImpersonationToken);
            hImpersonationToken = *realToken;
        }
        else
        {
            continue;
        }
        HANDLE hUserToken;
        if (!DuplicateTokenEx(hImpersonationToken,
            TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS | MAXIMUM_ALLOWED,
            NULL,
            SecurityImpersonation,
            TokenPrimary,
            &hUserToken))
        {
            continue;
        }

        // Get user name of this process
        WCHAR* pUserName;
        DWORD user_name_len = 0;
        if (WTSQuerySessionInformationW
        (WTS_CURRENT_SERVER_HANDLE, session_id, WTSUserName, &pUserName, &user_name_len))
        {
            //Now we got the user name stored in pUserName
        }
        // Free allocated memory                         
        if (pUserName) WTSFreeMemory(pUserName);
        ImpersonateLoggedOnUser(hUserToken);
        STARTUPINFOW StartupInfo;
        GetStartupInfoW(&StartupInfo);
        StartupInfo.cb = sizeof(STARTUPINFOW);
        PROCESS_INFORMATION processInfo;
        SECURITY_ATTRIBUTES Security1;
        Security1.nLength = sizeof SECURITY_ATTRIBUTES;
        SECURITY_ATTRIBUTES Security2;
        Security2.nLength = sizeof SECURITY_ATTRIBUTES;
        void* lpEnvironment = NULL;

        // Obtain all needed necessary environment variables of the logged in user.
        // They will then be passed to the new process we create.

        BOOL resultEnv = CreateEnvironmentBlock(&lpEnvironment, hUserToken, FALSE);
        if (!resultEnv)
        {
            WriteToLog(L"CreateEnvironmentBlock - failed. Error %d",GetLastError());
            continue;
        }
        std::wstring commandLine;
        commandLine.reserve(1024);
        commandLine += L"\"";
        commandLine += szCurModule;
        commandLine += L"\" \"";
        commandLine += SERVICE_COMMAND_Launcher;
        commandLine += L"\"";
        WCHAR PP[1024]; //path and parameters
        ZeroMemory(PP, 1024 * sizeof WCHAR);
        wcscpy_s(PP, commandLine.c_str());

        // Next, we impersonate - by starting the process 
        // as if the current logged in user, has started it
        BOOL result = CreateProcessAsUserW(hUserToken,
            NULL,
            PP,
            NULL,
            NULL,
            FALSE,
            NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE,
            NULL,
            NULL,
            &StartupInfo,
            &processInfo);

        if (!result)
        {
            WriteToLog(L"CreateProcessAsUser - failed. Error %d",GetLastError());
        }
        else
        {
            WriteToLog(L"CreateProcessAsUser - success");
        }
        DestroyEnvironmentBlock(lpEnvironment);
        CloseHandle(hImpersonationToken);
        CloseHandle(hUserToken);
        CloseHandle(realToken);
        RevertToSelf();
    }
    WTSFreeMemory(pSession);
}

Finding the Logged In User

In order to find the logged in user's account name, we use the following function:

C++
std::wstring GetLoggedInUser()
{
    std::wstring user{L""};
    WTS_SESSION_INFO *SessionInfo;
    unsigned long SessionCount;
    unsigned long ActiveSessionId = -1;

    if(WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 
                            0, 1, &SessionInfo, &SessionCount))
    {
        for (size_t i = 0; i < SessionCount; i++)
        {
            if (SessionInfo[i].State == WTSActive || 
                SessionInfo[i].State == WTSConnected)
            {
                ActiveSessionId = SessionInfo[i].SessionId;
                break;
            }
        }

        wchar_t *UserName;
        if (ActiveSessionId != -1)
        {
            unsigned long BytesReturned;
            if (WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, 
                ActiveSessionId, WTSUserName, &UserName, &BytesReturned))
            {
                user = UserName;        // Now we have the logged in user name
                WTSFreeMemory(UserName);    
            }
        }
        WTSFreeMemory(SessionInfo);
    }
    return user;
}

We use this function soon after the Service kicks in, while as long as there is no user logged in, this function returns an empty string, and while it does, we know we should wait.

Watch Dog is the Service's Best Friend

Services are ideal for use along with a Watch Dog mechanism.

Such a mechanism will ensure a given application is always running, and in case it shuts down abnormally, it will restart. We always need to remember, that the user may just select the Quit menu, and in such case, we don’t want to restart the process, however, if the process is stopped via the Task Manager, or by any other means, we would want to restart it. A good example would be an anti-virus program. We would want to avoid malware to terminate the anti-virus it is supposed to detect.

To achieve that, we need the Service to provide some sort of an API to the program using it, so when the user of that program selects “Quit”, the program informs the Service that its job is done, and it can uninstall itself.

Some Building Blocks

Next, I will explain some building blocks that are required to understand the code in this article.

GetExePath

In order to obtain the path of our Service or any executable, this function will be handy.

C++
/**
 * GetExePath() - returns the full path of the current executable.
 *
 * @param values - none.
 * @return a std::wstring containing the full path of the current executable. 
 */
std::wstring GetExePath()
{
    wchar_t buffer[65536];
    GetModuleFileName(NULL, buffer, sizeof(buffer) / sizeof(*buffer));
    int pos = -1;
    int index = 0;
    while (buffer[index])
    {
        if (buffer[index] == L'\\' || buffer[index] == L'/')
        {
            pos = index;
        }
        index++;
    }
    buffer[pos + 1] = 0;
    return buffer;
}

WriteLogFile

When developing a Windows Service, (and any software, for that matter), its important to have a logging mechanism. We have a very complex logging mechanism, but for the purpose of this article, I added the minimal logging function named WriteToLog. It works like printf but everything sent to it is not just formatted but stored in a log file, which can later be checked.

The path of the log file, which is appended (the log file, not the path), would normally be the path of the Service's EXE, however, due to Service Isolation, for a short while after rebooting the PC, this path will change to c:\Windows\System32, and we don't want that, so our log functions check for the path of our EXE, and does not assume the Current Directory will remain the same throughout the lifecycle of the Service.

C++
/**
 * WriteToLog() - writes formatted text into a log file, and on screen (console)
 *
 * @param values - formatted text, such as L"The result is %d",result.
 * @return - none
 */
void WriteToLog(LPCTSTR lpText, ...)
{
    FILE *fp;
    wchar_t log_file[MAX_PATH]{L""};
    if(wcscmp(log_file,L"") == NULL) 
    {
        wcscpy(log_file,GetExePath().c_str());
        wcscat(log_file,L"log.txt");
    }
    // find gmt time, and store in buf_time
    time_t rawtime;
    struct tm* ptm;
    wchar_t buf_time[DATETIME_BUFFER_SIZE];
    time(&rawtime);
    ptm = gmtime(&rawtime);
    wcsftime(buf_time, sizeof(buf_time) / sizeof(*buf_time), L"%d.%m.%Y %H:%M", ptm);

    // store passed messsage (lpText) to buffer_in
    wchar_t buffer_in[BUFFER_SIZE];

    va_list ptr;
    va_start(ptr, lpText);

    vswprintf(buffer_in, BUFFER_SIZE, lpText, ptr);
    va_end(ptr);

    // store output message to buffer_out - enabled multiple parameters in swprintf
    wchar_t buffer_out[BUFFER_SIZE];

    swprintf(buffer_out, BUFFER_SIZE, L"%s %s\n", buf_time, buffer_in);

    _wfopen_s(&fp, log_file, L"a,ccs=UTF-8");
    if (fp)
    {
        fwprintf(fp, L"%s\n", buffer_out);
        fclose(fp);
    }
    wcscat(buffer_out,L"\n");HANDLE stdOut = GetStdHandle(STD_OUTPUT_HANDLE);
    if (stdOut != NULL && stdOut != INVALID_HANDLE_VALUE)
    {
        DWORD written = 0;
        WriteConsole(stdOut, buffer_out, wcslen(buffer_out), &written, NULL);
    }
}

More Building Blocks - Registry Stuff

Here are some functions we use to store the Watch Dog's executable's path, so when the Service retarts after a PC shutdown or reboot, it will have that path.

C++
BOOL CreateRegistryKey(HKEY hKeyParent, PWCHAR subkey)
{
    DWORD dwDisposition; //Verify new key is created or open existing key
    HKEY  hKey;
    DWORD Ret;
    Ret =
        RegCreateKeyEx(
            hKeyParent,
            subkey,
            0,
            NULL,
            REG_OPTION_NON_VOLATILE,
            KEY_ALL_ACCESS,
            NULL,
            &hKey,
            &dwDisposition);
    if (Ret != ERROR_SUCCESS)
    {
        WriteToLog(L"Error opening or creating new key\n");
        return FALSE;
    }
    RegCloseKey(hKey); //close the key
    return TRUE;
}

BOOL writeStringInRegistry(HKEY hKeyParent, PWCHAR subkey, 
                           PWCHAR valueName, PWCHAR strData)
{
    DWORD Ret;
    HKEY hKey;
    //Check if the registry exists
    Ret = RegOpenKeyEx(
        hKeyParent,
        subkey,
        0,
        KEY_WRITE,
        &hKey
    );
    if (Ret == ERROR_SUCCESS)
    {
        if (ERROR_SUCCESS !=
            RegSetValueEx(
                hKey,
                valueName,
                0,
                REG_SZ,
                (LPBYTE)(strData),
                ((((DWORD)lstrlen(strData) + 1)) * 2)))
        {
            RegCloseKey(hKey);
            return FALSE;
        }
        RegCloseKey(hKey);
        return TRUE;
    }
    return FALSE;
}

LONG GetStringRegKey(HKEY hKey, const std::wstring &strValueName, 
                     std::wstring &strValue, const std::wstring &strDefaultValue)
{
    strValue = strDefaultValue;
    TCHAR szBuffer[MAX_PATH];
    DWORD dwBufferSize = sizeof(szBuffer);
    ULONG nError;
    nError = RegQueryValueEx(hKey, strValueName.c_str(), 0, NULL, 
             (LPBYTE)szBuffer, &dwBufferSize);
    if (nError == ERROR_SUCCESS)
    {
        strValue = szBuffer;
        if (strValue.front() == _T('"') && strValue.back() == _T('"'))
        {
            strValue.erase(0, 1); // erase the first character
            strValue.erase(strValue.size() - 1); // erase the last character
        }
    }
    return nError;
}

BOOL readStringFromRegistry(HKEY hKeyParent, PWCHAR subkey, 
                            PWCHAR valueName, std::wstring& readData)
{
    HKEY hKey;
    DWORD len = 1024;
    DWORD readDataLen = len;
    PWCHAR readBuffer = (PWCHAR)malloc(sizeof(PWCHAR) * len);
    if (readBuffer == NULL)
        return FALSE;
    //Check if the registry exists
    DWORD Ret = RegOpenKeyEx(
        hKeyParent,
        subkey,
        0,
        KEY_READ,
        &hKey
    );
    if (Ret == ERROR_SUCCESS)
    {
        Ret = RegQueryValueEx(
            hKey,
            valueName,
            NULL,
            NULL,
            (BYTE*)readBuffer,
            &readDataLen
        );
        while (Ret == ERROR_MORE_DATA)
        {
            // Get a buffer that is big enough.
            len += 1024;
            readBuffer = (PWCHAR)realloc(readBuffer, len);
            readDataLen = len;
            Ret = RegQueryValueEx(
                hKey,
                valueName,
                NULL,
                NULL,
                (BYTE*)readBuffer,
                &readDataLen
            );
        }
        if (Ret != ERROR_SUCCESS)
        {
            RegCloseKey(hKey);
            return false;;
        }
        readData = readBuffer;
        RegCloseKey(hKey);
        return true;
    }
    else
    {
        return false;
    }
}

Checking If Our Host Is Running

One key ability is to guard our SampleApp (which we call "the host"), and when it’s not running, restart it, (hence the Watch Dog). In real life, we would check if the host was terminated by the user, which is OK, or terminated by some malware (which isn't OK), and in case of the later, restart it (otherwise, the user will just select Quit, but the App will “haunt”, and be executed again and again).

Here is how it's done:

We create a Timer event and every given amount of time, (shouldn't be too frequent), we check if the host's process is running, if not we start it. We use a static boolean flag (is_running) which is used to indicate that we are already in this block of code, so it won't be called while already being handled. This is something I always do in WM_TIMER code blocks, because, when a too-frequent timer is set, the code block may be called during the time the code from the previous WM_TIMER event is executed).

We also check if a user is logged in, by examining the g_bLoggedIn Boolean flag.

C++
        case WM_TIMER:
        {
            if (is_running) break;
            WriteToLog(L"Timer event");
            is_running = true;
            HANDLE hProcessSnap;
            PROCESSENTRY32 pe32;
            bool found{ false };

            WriteToLog(L"Enumerating all processess...");
            // Take a snapshot of all processes in the system.
            hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
            if (hProcessSnap == INVALID_HANDLE_VALUE)
            {
                WriteToLog(L"Failed to call CreateToolhelp32Snapshot(). 
                             Error code %d",GetLastError());
                is_running = false;
                return 1;
            }

            // Set the size of the structure before using it.
            pe32.dwSize = sizeof(PROCESSENTRY32);

            // Retrieve information about the first process,
            // and exit if unsuccessful
            if (!Process32First(hProcessSnap, &pe32))
            {
                WriteToLog(L"Failed to call Process32First(). 
                             Error code %d",GetLastError());
                CloseHandle(hProcessSnap);          // clean the snapshot object
                is_running=false;
                break;
            }

            // Now walk the snapshot of processes, and
            // display information about each process in turn
            DWORD svchost_parent_pid = 0;
            DWORD dllhost_parent_pid = 0;
            std::wstring szPath = L"";

            if (readStringFromRegistry(HKEY_LOCAL_MACHINE, 
               (PWCHAR)SERVICE_REG_KEY, (PWCHAR)SERVICE_KEY_NAME, szPath))
            {
                m_szExeToFind = szPath.substr(szPath.find_last_of(L"/\\") + 1); // The 
                                            // process name is the executable name only
                m_szExeToRun = szPath;      // The executable to run is the full path
            }
            else
            {
                WriteToLog(L"Error reading ExeToFind from the Registry");
            }

            do
            {
                if (wcsstr( m_szExeToFind.c_str(), pe32.szExeFile))
                {
                    WriteToLog(L"%s is running",m_szExeToFind.c_str());
                    found = true;
                    is_running=false;
                    break;
                }
                if (!g_bLoggedIn)
                {
                    WriteToLog(L"WatchDog isn't starting '%s' 
                               because user isn't logged in",m_szExeToFind.c_str());
                    return 1;
                }
            }
            while (Process32Next(hProcessSnap, &pe32));
            if (!found)
            {
                WriteToLog(L"'%s' is not running. Need to start it",m_szExeToFind.c_str());
                if (!m_szExeToRun.empty())    // Watch Dog start the host app
                {
                    if (!g_bLoggedIn)
                    {
                        WriteToLog(L"WatchDog isn't starting '%s' 
                                   because user isn't logged in",m_szExeToFind.c_str());
                        return 1;
                    }
                    ImpersonateActiveUserAndRun();

                    RunHost((LPWSTR)m_szExeToRun.c_str(), (LPWSTR)L"");
                }
                else
                {
                    WriteToLog(L"m_szExeToRun is empty");
                }
            }
            CloseHandle(hProcessSnap);
        }
        is_running=false;
        break;

How to Test the Service

When we wanted to test the solution, we hired 20 qualified and cooperative testers. Throughout the progress of work, more tests were successful. At some point, it worked perfectly on my own Surface Pro laptop, and luckily, one of my employees reported that on his PC, after shutting down, the service doesn't come up, or comes up but without starting itself under Ring 3. That’s good news, as in the process of development, when you suspect a bug, the worst news is not to find it and not to be able to recreate it. 10% of the testers reported a problem. So the version posted here works perfectly on my employee's PC, however, 2% of the testers still report problems from time to time. In other words, SampleApp doesn't start after shutting down the PC and turning it on.

Here are instructions for testing the service and the Watch Dog.

The SampleApp

I have included a sample application generated by the Visual Studio Wizard, as the "host" application that will be kept running by the Watch Dog. You can run it on its own, and it should look like this. This application doesn't do much. In fact, it doesn't do anything...

Here are instructions for testing the service and the Watch Dog.

Image 5

Running from CMD

Open CMD under Administrator. Change the current directory to where the Service's EXE resides and type:

C++
SG_RevealerService.exe Install#SampleApp.exe

Image 6

As you can see, we have two elements:

  • The command, which is Install and attached to it, separated by a hash (#):
  • The argument, which should be any executable you want your watchdog to watch

The Service will first start SampleApp, and from that moment, try to terminate or kill the SampleApp and the Watch Dog will restart it after a few seconds. Then try to reboot, turn the PC off and on again and see if the Service comes back and starts SampleApp. That sums up the goal and functionality of our Service.

Uninstalling

Then, to stop and uninstall, I have included the uninstall.bat which goes like this:

BAT
sc stop sg_revealerservice
sc delete sg_revealerservice
taskkill /f /im sampleapp.exe
taskkill /f /im sg_revealerservice.exe

Image 7

Thank You Note

I should thank David Delaune, from Microsoft, for a very useful advice: to subscribe to the 'logon' event instead of checking every X seconds if a user has logged in, so my code was updated accordingly.

History

  • 28th October, 2022: Initial version

License

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