Logo
blank Skip to main content

Starting process in the logged session under the local system account

C++

This article contains the description of how to start a process in the current active session on session logon notification event with permissions of system process running in the current session.

Introduction

This article describes how to use CreateProcessAsUser with the token duplicated from the system process in the current logged session. The common concept is to start process when a new session appears and to terminate it when the session connection status is changed to โ€œdisconnectedโ€ or the session is closed. The process, which our sample service starts, is the same executable file that the service itself, but started with the parameter โ€œappโ€. The singularity of the process consists in the permission restriction for the users without administrator privileges.

This article is a logical continuing of another one โ€“ Monitoringof Logon/Logout in Terminal and Client Sessions.

Waiting for the system process in the session

Letโ€™s describe the situation when we get the notification about session log in event. We want to start a process at that moment as the user that is the owner of our service. It is NT AUTHORITYSYSTEM. To call the CreateProcessAsUser function we need to have the handle to the primary token that represents a user with TOKEN_QUERY, TOKEN_DUPLICATE access rights. We could duplicate the token of the current module (our service), it is running as NT AUTHORITYSYSTEM. But it is running in the console session with session ID 0.  What if we need the handle to the token of the process that is running in the same session, which we want to run a process in? There is a small nuance โ€“ in the moment when we get the session connecting notification, there can be no process in the session, from which we could duplicate the token. So there has to be a thread for waiting process run by the user that we need, and with the session ID of the process that matches the logged session ID, so that we will be able to duplicate the token of this process. As in the previous article project sample, boost::thread is used. Here is the class-wrapper for work with the thread:

C
typedef boost::shared_ptr<boost::thread> ThreadPtr;

    class ThreadHolder
    {
        boost::shared_ptr<CThreadForSession> m_pThreadParams;
        ThreadPtr m_thread;

        bool m_bStopInDestructor;

    public:
        ThreadHolder(boost::shared_ptr<CThreadForSession> pThreadParams, ThreadPtr thread)
            : m_bStopInDestructor(false)
        {
        }
        ~ThreadHolder()
        {
            if (m_bStopInDestructor)
            {
                m_pThreadParams->Stop();
                m_thread->join();
            }
        }
        void SetStopEvent (bool stop) { m_bStopInDestructor = stop; }
    };

This class contains the pointer to the thread to stop it in destructor and the pointer to the class, which implements the function executing in the thread – class CThreadForSession, to set stop event in the destructor of the ThreadHolder class. Here is the CThreadForSession class with the function running in the separate thread:

C++
class CThreadForSession
{
    int m_sid;
    sm::WinNotificationEvent m_stopEvent;
    ISObserver * m_pObserver;

public:
    CThreadForSession(int sid, ISObserver * pObserver)
        : m_sid(sid)
        , m_pObserver(pObserver)
    {}
    ~CThreadForSession()
    {}

    void Execute() ;
    void Stop() { m_stopEvent.set(); }
}; 

WinNotificationEvent class encapsulates work with events. It creates an event in the constructor calling the CreateEvent() function and implements such important methods as Wait and Set using the WaitForSingleObject() and SetEvent() WinApi functions correspondingly.

C++
class WinNotificationEvent
{
    HANDLE hEvent_;

public:
    WinNotificationEvent(BOOL bManulaReset = TRUE)
    {
        hEvent_ = ::CreateEvent(NULL, bManulaReset, FALSE, NULL);
        if(hEvent_ == NULL)
            throw std::runtime_error("can't create event.");
    }
    void set()
    {
        if(hEvent_)
         {
		if (!::SetEvent(hEvent_))
            throw std::runtime_error("can't set event.");
         }

    }
    bool wait(DWORD dwTimeInMs = INFINITE)
    {
        if(hEvent_)
        {
            DWORD dwRes = WaitForSingleObject(hEvent_,dwTimeInMs);
            if (dwRes == WAIT_TIMEOUT)
                return false;
            if (dwRes == WAIT_OBJECT_0)
                return true;
            throw std::runtime_error("can't wait event");
        }
    }
// Other methods
};

Interface ISObserver is intended to provide callback from the thread  of waiting for the system process in the session to the main thread that started it .

C++
class ISObserver
{
public:
    virtual  ~ISObserver() {}
    virtual void GotLogon(const std::wstring& userName, int sid) = 0;
    virtual void GotLogoff(int sid) = 0;
    virtual void PutProcessHandleIntoMap(CHandleGuard &hProcGuard, int sid) = 0;
};

The CSObserver class implements this interface. There is the map of the started threads and the session IDs corresponding them in the class members.

C++
class CSObserver : public ISObserver
    {
    public:
        CSObserver();
        ~CSObserver();

        // ISObserver methods
        void GotLogon(const std::wstring& userName, int sid);
        void GotLogoff(int sid);
        void PutProcessHandleIntoMap(CHandleGuard &hProcGuard, int sid);

    private:

        typedef std::map<int, HANDLE> ProcessHandlesMap;
        ProcessHandlesMap m_hProcessesPool;

        typedef std::map<int, ThreadHolder> ThreadsMap;
        ThreadsMap m_threadsPool;

        boost::mutex m_mutex;
    };

We extend the CSessionManager class and add to its members the pointer to the ISObserver interface. So in the CSessionManager::UpdateSessionsState() method, the ISObserver::GotLogon() method is called for every new session and ISObserver::GotLogoff() for every ended session. The last removes the thread by sid from the thread pool thus stoping the thread for the session if it is running at the moment, and then termninates the process in this session, in case it has not been terminated by the system yet.

The GotLogon() method illustrates the work with the threads map. It uses boost::bind for calling the function – member of the CThreadForSession class that is intended to wait for any system process in the logged session. Boost synchronization method is used in it to synchronize the access to the threads pool.

C++
void CSObserver::GotLogon(const std::wstring& userName, int sid)
{
    boost::mutex::scoped_lock lock(m_mutex);
    boost::shared_ptr<CThreadForSession> threadForSess(new CThreadForSession(sid, this));
    ThreadPtr threadPtr (new boost::thread(boost::bind
		(&CThreadForSession::Execute, threadForSess)) );
    ThreadHolder threadHolder(threadForSess, threadPtr);
    std::pair<ThreadsMap::iterator, bool> resultThread = 
		m_threadsPool.insert(std::make_pair(sid, threadHolder));
    if(!resultThread.second)
    {
        throw std::runtime_error("Cannot insert thread to the pool");
    }
    resultThread.first->second.SetStopEvent(true);
} 

Here is CThreadForSession::Execute() function implementation:

C++
void CThreadForSession::Execute()
{
    static const int delay =100;
    int res = 0;
    for(;;)
    {
        if (m_stopEvent.wait(delay))
        {
            return; // exit
        }

        CHandleGuard hTokenGuard(0);
        if (GetProcessToken(m_sid, hTokenGuard))
        {
            CHandleGuard hProcGuard(0);
            try
            {
                res = CreateProcessInSessionId
		(m_sid, hProcGuard, hTokenGuard, CreateCommandLineStr());
                if(res == 0)
                {
                    m_pObserver->PutProcessHandleIntoMap(hProcGuard, m_sid);
                }
                else
                    if(res == ERROR_PIPE_NOT_CONNECTED)
                    {
                        // write to log
                        continue;
                    }
                    else
                    {
                        // write to log
                    }
            }
            catch(const std::exception & ex)
            {
                // write to log
            }
        }
        return;
    }
}

The CreateProcessInSessionId() function returns an error code – the last error retrieved if the CreateProcessAsUser function fails. The ERROR_PIPE_NOT_CONNECTED error means that Logon complete notification has not been sent by winlogon yet. The root cause is that in certain environments, depending on the system performance, it is possible that the WTS_SESSION_LOGON notification is sent for a non-zero session before it is available for process creation. CreateProcessAsUser() fails if you pass ‘winsta0default’ as a parameter to it, because the desktop, which you are targeting at, is not created yet.

So finally, weโ€™ve got to the part of the article, where we will obtain the handle to the token.

Obtaining the handle to the user token

The GetProcessToken() function returns a handle to the user token as an out parameter. The CHandleGuard class is a class-wrapper that has a handle as its member and calls CloseHandle in destructor. Letโ€™s take a look at the token obtaining function:

C++
bool GetProcessToken(int sid, CHandleGuard &hTokenGuard)
{
    DWORD pids[1024*10], cbNeeded, cProcesses;

    if ( !EnumProcesses( pids, sizeof(pids), &cbNeeded ) )
        return false;

    // Calculate how many process identifiers were returned.
    cProcesses = cbNeeded / sizeof(DWORD);
    for(DWORD i = 0; i<cProcesses; ++i)
    {
        DWORD dwPid = pids[i];
        HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, dwPid);
        if (hProcess)
        {
            CHandleGuard guard(hProcess);
            HANDLE hToken = 0;
            if (OpenProcessToken(hProcess, TOKEN_ALL_ACCESS, &hToken))
            {
                CHandleGuard token(hToken);

                try
                {
                    std::wstring name = GetTokenUser(hToken);
                    DWORD dwSession = GetTokenSessionId(hToken);

                    const wchar_t arg[] = L"NT AUTHORITY";
                    if (_wcsnicmp(name.c_str(),arg, sizeof(arg)/sizeof(arg[0])-1)==0)
                    {
                        if(sid == dwSession)
                        {
                            hTokenGuard.reset(token.release());
                            return true;
                        }
                    }
                }
                catch(const std::exception &ex)
                {
                }
            }
        }
    }
    return false;
}

The function enumerates processes and searches for the process that runs under the “NT AUTHORITY” user and has the session ID matching with the given SID, which is the function parameter. Where the GetTokenSessionId() function is:

C++
DWORD GetTokenSessionId(HANDLE TokenHandle)
{
    DWORD id = 0, length;
    BOOL res = GetTokenInformation(TokenHandle, TokenSessionId, &id, sizeof(id), &length);
    if (res)
        return id;
    throw std::runtime_error("Invalid token");
}

And function retrieving user token is:

C++
std::wstring pml::GetTokenUser(HANDLE Token)
{
    DWORD tmp;

    std::wstring userName;

    SID_NAME_USE snUse;
    DWORD sidNameSize = 64;
    std::vector<WCHAR> sidName;
    sidName.resize(sidNameSize);

    DWORD sidDomainSize = 64;
    std::vector<WCHAR> sidDomain;
    sidDomain.resize(sidNameSize);

    DWORD userTokenSize = 1024;
    std::vector<WCHAR> tokenUserBuf;
    tokenUserBuf.resize(userTokenSize);
    TOKEN_USER *userToken = (TOKEN_USER *)&tokenUserBuf.front();

    if( GetTokenInformation( Token, TokenUser, userToken, userTokenSize, &tmp ) )
    {
        if( LookupAccountSidW( NULL, userToken->User.Sid, 
	&sidName.front(), &--sidNameSize, &apm;sidDomain.front(), 
	&--sidDomainSize, &snUse ) )
        {
            userName = &sidDomain.front();
            userName += L"";
            userName += &sidName.front();
        }
        else
        {
            WCHAR *pSidString;
            if(ConvertSidToStringSidW(userToken->User.Sid, &pSidString))
            {
                CLocalAllocGuard memGuard(pSidString);
                userName = pSidString;
            }
            else
                throw std::runtime_error(__FUNCTION__"() - ConvertSidToStringSidW fail");
        }
    }
    else
        throw std::runtime_error(__FUNCTION__"() - GetTokenInformation fail");

    return userName;
}

So the GetProcessToken() function is being called from the CThreadForSession::Execute() function in the separate thread until the handle to the token satisfying conditions will be retrieved. Now weโ€™ve got to the final part of the article โ€“ process creation.

Running process using the CreateProcessAsUser() function

The final purpose of the article is to run process with the given token. First, it needs to be duplicated with the access rights of the token TOKEN_ALL_ACCESS and the impersonation level of the new token SecurityImpersonation value. Then the SetTokenInformation() function has to be called. Then we can call the CreateProcessAsUser() WinApi function. In the STARTUPINFOW structure, we have to set lpDesktop field to the “Winsta0default” value if we want the process to have access to GDI, and leave it blank otherwise. Here is the code that starts the process:

C++
int pml::CreateProcessInSessionId (int sid, CHandleGuard &hProcessGuard, 
	CHandleGuard &hTokenGuard, const std::wstring & commandLine)
{
    HANDLE hDpToken(0);
    if(!DuplicateTokenEx(hTokenGuard.get(), TOKEN_ALL_ACCESS, 
	NULL, SecurityImpersonation, TokenPrimary, &hDpToken))
    {
        throw std::runtime_error
	("CreateRecItProcessInSessionId: DuplicateTokenEx failed");
    }
    CHandleGuard hDpTokenGuard(hDpToken);

    if(!SetTokenInformation(hDpTokenGuard.get(), TokenSessionId, &sid, sizeof(sid)))
    {
        throw std::runtime_error
	("CreateRecItProcessInSessionId: SetTokenInformation failed");
    }

    STARTUPINFOW si;
    memset( &si, 0, sizeof(si));
    si.cb = sizeof(si);
    si.lpDesktop = L"Winsta0default";
    PROCESS_INFORMATION processInfo;
    memset( &processInfo, 0, sizeof(processInfo) );

    CAccessAttributes aa;
    aa.InitAttributes();

    if(!CreateProcessAsUser(hDpTokenGuard.get(), NULL, const_cast<wchar_t>
	(commandLine.c_str()), &aa.GetAttributes(), &aa.GetAttributes(), 
	false, CREATE_NO_WINDOW, NULL, NULL, &si, &processInfo))
    {
        return GetLastError();
    }
    SetKernelObjectSecurity(processInfo.hProcess, DACL_SECURITY_INFORMATION, 
	aa.GetSecurityDescriptor());

    CloseHandle(processInfo.hThread);
    hProcessGuard.reset(processInfo.hProcess);
    return 0;
}

Conclusion

The result of this article is the service that monitors session logon/logout, connect/disconnect state and starts a process in every logged session. It will create the log file with the one entry notifying that the process is started and will be running until session is logged out.

The project supports both x86 and x64 platforms. To build the solution boost environment variable has to be set, and it has to be built for both platforms. In this article boost 1.40.0 version is used.

Download sources of the sample project (ZIP, 38 KB).

References

  1. MSDN Authorization documentation (http://msdn.microsoft.com/en-us/library/aa446608%28v=VS.85%29.aspx)
  2. GNT win32 programmer forum (http://us.generation-nt.com/answer/createprocessasuser-fails-error-pipe-not-connected-help-40987232.html#r)

Have a question?

Ask our expert!

Tell us about
your project

...And our team will:

  • Process your request within 1-2 business days.
  • Get back to you with an offer based on your project's scope and requirements.
  • Set a call to discuss your future project in detail and finalize the offer.
  • Sign a contract with you to start working on your project.

Do not have any specific task for us in mind but our skills seem interesting? Get a quick Apriorit intro to better understand our team capabilities.