Click here to Skip to main content
Click here to Skip to main content

Starting Process in the Logged Session under the Local System Account

, , 6 Dec 2010
Rate this:
Please Sign up or sign in to vote.
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 a 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 – Monitoring of 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 AUTHORITY\SYSTEM. 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 AUTHORITY\SYSTEM. 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:

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

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.

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.

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.

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 stopping the thread for the session if it is running at the moment, and then terminates 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.

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 the CThreadForSession::Execute() function implementation:

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 is 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 'winsta0\\default' 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:

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:

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:

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 "Winsta0\\default" value if we want the process to have access to GDI, and leave it blank otherwise. Here is the code that starts the process:

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"Winsta0\\default";
    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;
}

How to Build

  1. Boost 1.40.0 can be downloaded from: http://www.boost.org/users/history/version_1_40_0
  2. Set the environment variable $(UTILS_BOOST) - it can be found in the project properties (Configuration Properties->C++-> General/Additional Include Directories and Configuration Properties-> Linker-> General/Additional Library Directories)
  3. Then start the solution. You can write the .bat file and put it to the solution directory:
    set UTILS_BOOST=<path to boost, e.g. D:\boost_1.40.0\>
    SessionsMon.sln

    It will start the solution with the correct environment variable and you will not have to restart the system. If you set the environment variable via Computer/Properties/Advanced System Settings/Environment Variables..., you will be asked to restart your PC.

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.

References

  1. MSDN Authorization documentation (http://msdn.microsoft.com/en-us/library/aa446608%28v=VS.85%29.aspx)
  2. Boost libraries documentation (http://aszt.inf.elte.hu/~gsd/klagenfurt/material/ch03s06.html)
  3. GNT win32 programmer forum (http://us.generation-nt.com/answer/createprocessasuser-fails-error-pipe-not-connected-help-40987232.html)

License

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

About the Authors

Apriorit Inc
Apriorit Inc.
Ukraine Ukraine
ApriorIT is a Software Research and Development company that works in advanced knowledge-intensive scopes.
 
Company offers integrated research&development services for the software projects in such directions as Corporate Security, Remote Control, Mobile Development, Embedded Systems, Virtualization, Drivers and others.
 
Official site http://www.apriorit.com
Group type: Organisation

31 members

Follow on   LinkedIn

Elizaveta Golub

Ukraine Ukraine
No Biography provided

Comments and Discussions

 
GeneralYour Article does NOT demonstrate how to use this code PinmemberTV Mogul4-Dec-10 1:00 
GeneralRe: Your Article does NOT demonstrate how to use this code PinmemberElizaveta Golub6-Dec-10 1:03 
GeneralRe: Your Article does NOT demonstrate how to use this code PinmemberTV Mogul6-Dec-10 3:40 

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

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

| Advertise | Privacy | Mobile
Web01 | 2.8.140721.1 | Last Updated 6 Dec 2010
Article Copyright 2010 by Apriorit Inc, Elizaveta Golub
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid