1
votes

I have read tons of SO questions on this matter, but I didn't find a real definitive guide for doing this the right way.

My goal is to enumerate [disconnected and active] user console sessions and start a process in each one of them. Every user session process requires at least these rights in its DACL :

Token access rights :

Process access rights :

But as you can read here (at the bottom) : "Windows Vista introduces protected processes to enhance support for Digital Rights Management. The system restricts access to protected processes and the threads of protected processes."
So I thought maybe only with PROCESS_QUERY_LIMITED_INFORMATION I can get some information about other processes. I tried QueryFullProcessImageName() for elevated processes starting from Vista (see Giori's answer) but it doesn't work anymore as it seems.
Solution : CreateProcessAs_LOCAL_SYSTEM using a duplicated token of the Windows service.
Problem : The spawned processes should have the respective logged on user's environment variables set to be able to locate network printers and mapped drives among other things. But if I use the service's token I inherit its PEB and I can't even translate the mapped drives to their UNC paths.

So I started looking for ways to "elevate" the process and bypassing the UAC prompt, I tried :

  • Enabling some privileges like SE_DEBUG_PRIVILEGE in the token using AdjustTokenPrivileges() (does not work if the token does not have those privileges, verification can be done first using LookUpPrivilegeValue())
  • using the token from winlogon.exe. (does not work)
  • Changing the DACL (source code) (didn't work)

The steps I'm following are :

  1. Enumerate sessions using WTSEnumerateSessions()
  2. Get the token (two choices) :
  3. Duplicate the token using DuplicateTokenEx()
  4. LookUpPrivilegeValue() / AdjustTokenPrivileges() (useless ?)
  5. CreateEnvironmentBlock()
  6. CreateProccessAsUser(), flags : NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT, Startup info's desktop : "WinSta0\Default"
  7. Change process DACL (see link above, useless ?)
  8. Dispose/Clean : destroy PEB created, close opened handles and free memory.

My question : how to grant the process created using CreateProccessAsUser() from a Windows Service running under LOCAL_SYSTEM account enough privileges/rights to get information on other processes (from other sessions; of other users and different integrity levels) without losing the user's environment variables ?

2
SeDebugPrivilege really should be the only thing you need. Does WTSQueryUserToken() not return an elevated token? It's been a while so I don't remember. If not you should be able to get it via GetTokenInformation() and TokenLinkedToken.Luke

2 Answers

2
votes

You're confused about a number of things.

Every user session process requires at least these rights in its DACL

The process DACL controls access to that process, it does not determine what access that process has. The security token for the process determines access rights.

Windows Vista introduces protected processes to enhance support for Digital Rights Management.

It seems clear that you haven't gotten far enough to worry about protected processes yet. Get it to work for ordinary processes first!

The spawned processes should have the respective logged on user's environment variables set to be able to locate network printers and mapped drives among other things.

Network printers and mapped drives have nothing to do with environment variables. I think what you're trying to do is to put the new process into the user's logon session, that's what controls network drive mappings and the like.

how to grant the process created using CreateProccessAsUser() [...] enough privileges/rights to get information on other processes (from other sessions; of other users and different integrity levels) without losing the user's environment variables ?

Don't. This would violate the integrity of the security model.

Instead, enumerate and query processes from the system service, and pass only whatever information is necessary to the user session processes, using shared memory (look up "file mapping object" in MSDN) or another suitable IPC mechanism.

1
votes

I know that this has been asked a while ago. Since I happened to have been doing the same, below is the working pseudo-code.

First, how to run a process in a user session from a service:

//IMPORTANT: All error checks are omitted for brevity!
//           Each of the lines of code below MUST be 
//           checked for possible errors!!!

//INFO: The following pseudo-code is intended to run 
//      from the Windows local service.

DWORD dwSessionID;  //Session ID to run your user process in

//Get token for the session ID
HANDLE hToken;
WTSQueryUserToken(dwSessionID, &hToken);

//Duplicate this token
HANDLE hToken2;
DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL, SecurityIdentification, TokenPrimary, &hToken2);

PSID gpSidMIL_High = NULL;

if(you_want_to_change_integrity_level_for_user_process)
{
    if(!Windows_XP)
    {
        //For example, create "high" mandaroty integrity level SID
        ::ConvertStringSidToSid(L"S-1-16-12288", &gpSidMIL_High);

        TOKEN_MANDATORY_LABEL tml = {0};
        tml.Label.Attributes = SE_GROUP_INTEGRITY;
        tml.Label.Sid = gpSidMIL_High;

        SetTokenInformation(hToken2, TokenIntegrityLevel, &tml, 
            sizeof(TOKEN_MANDATORY_LABEL) + ::GetSidLengthRequired(1));
    }
}

//Copy environment strings
LPVOID pEnvBlock = NULL;
CreateEnvironmentBlock(&pEnvBlock, hToken2, FALSE);

//Initialize the STARTUPINFO structure.
// Specify that the process runs in the interactive desktop.
STARTUPINFO si;
ZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
si.lpDesktop = _T("winsta0\\default");

PROCESS_INFORMATION pi;
ZeroMemory(&pi, sizeof(pi));

//Create non-const buffer
TCHAR pBuffCmdLine[MAX_PATH];
pBuffCmdLine[0] = 0;

//Copy process path & parameters to the non-constant buffer
StringCchCopy(pBuffCmdLine, MAX_PATH, L"\"C:\\Program Files (x86)\\Company\\Brand\\process.exe\" -parameter");

//Impersonate the user
ImpersonateLoggedOnUser(hToken2);

//Launch the process in the user session.
bResult = CreateProcessAsUser(
    hToken2,            // client's access token
    L"C:\\Program Files (x86)\\Company\\Brand\\process.exe",              // file to execute
    pBuffCmdLine[0] != 0 ? pBuffCmdLine : NULL,     // command line
    NULL,              // pointer to process SECURITY_ATTRIBUTES
    NULL,              // pointer to thread SECURITY_ATTRIBUTES
    FALSE,             // handles are not inheritable
    NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT,   // creation flags
    pEnvBlock,         // pointer to new environment block 
    NULL,              // name of current directory 
    &si,               // pointer to STARTUPINFO structure
    &pi                // receives information about new process
);

//Get last error
nOSError = GetLastError();

//Revert to self
RevertToSelf();

//At this point you may want to wait for the user process to start, etc.
//using its handle in `pi.hProcess`
...

//Otherwise, close handles
if(pi.hProcess)
    CloseHandle(pi.hProcess);
if(pi.hThread)
    CloseHandle(pi.hThread);

//Clean-up
if(pEnvBlock)
    DestroyEnvironmentBlock(pEnvBlock);

CloseHandle(hToken2);
CloseHandle(hToken);

if(gpSidMIL_High)
    ::LocalFree(gpSidMIL_High);

If you need to run your process in all sessions with a logged in interactive user, you can run the method I gave above for the sessions that you can obtain from the following enumeration:

//Enumerate all sessions
WTS_SESSION_INFO* pWSI = NULL;
DWORD nCntWSI = 0;
if(WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, NULL, 1, &pWSI, &nCntWSI))
{
    //Go through all sessions
    for(DWORD i = 0; i < nCntWSI; i++)
    {
        //To select logged in interactive user session,
        //try to get its name. If you get something, then
        //this session has a user logged in to...
        LPTSTR pUserName = NULL;
        DWORD dwcbSzUserName = 0;
        if(WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE,
            pWSI[i].SessionId, 
            WTSUserName, &pUserName, &dwcbSzUserName) &&
            pUserName &&
            dwcbSzUserName >= sizeof(TCHAR) &&
            pUserName[0] != 0)
        {
            //Use my method above to run your user process
            // in this session.
            DWORD dwSessionID = pWSI[i].SessionId;

        }

        //Free mem                          
        if(pUserName)
            WTSFreeMemory(pUserName);
    }

    //Free mem
    WTSFreeMemory(pWSI);
}