0

We have a service application which spawns a process in the console session (WTSGetActiveConsoleSessionId) to allow for a desktop control style access to the machine. This works well in most situations however there are some VM's which appear to successfully create the process as far as the result of CreateProcessAsUser is concerned, however the process does not get created.

The service runs under the LocalSystem account. The process being started is NOT already running. No virus protection programs are running. We have only seen this behaviour on Windows Server 2008 R2 (but that is not to say it that is exclusive).

The code we use is as follows:

function StartProcessInSession(strProcess: String; bLocalSystem: Boolean = True; iSessionID: Integer = -1): Boolean;

  procedure SPISLog(strLog: String; bError: Boolean = False);
  begin
    Log(strLog);
    if bError then Abort;
  end;

var pi: PROCESS_INFORMATION;
  si: STARTUPINFO;
  winlogonPid, dwSessionId: DWord;
  hUserToken, hUserTokenDup, hPToken, hProcess: THANDLE;
  dwCreationFlags: DWORD;
  tp: TOKEN_PRIVILEGES;
  lpenv: Pointer;
  bError: Boolean;
  strClone: String;
begin
  if GetProcessID(strProcess, iSessionID) > 0 then
  begin
    Result := True;
    Exit;
  end;
  Result := False;
  bError := False;
  if not InitProcLibs then Exit;
  if bLocalSystem then strClone := 'winlogon.exe' else strClone := 'explorer.exe';
  winlogonPid := GetProcessID(strClone, iSessionID);
  try
    dwSessionId := WTSGetActiveConsoleSessionId();
    dwCreationFlags := NORMAL_PRIORITY_CLASS or CREATE_NEW_CONSOLE;
    ZeroMemory(@si, sizeof(STARTUPINFO));
    si.cb := sizeof(STARTUPINFO);
    si.lpDesktop := PChar('Winsta0\Default'); 
    ZeroMemory(@pi, sizeof(pi));
    hProcess := OpenProcess(MAXIMUM_ALLOWED, FALSE, winlogonPid);
    if (not OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES or TOKEN_QUERY or TOKEN_DUPLICATE or
      TOKEN_ASSIGN_PRIMARY or TOKEN_ADJUST_SESSIONID or TOKEN_READ or TOKEN_WRITE, hPToken)) then
        bError := True;
    if bError then SPISLog('SPIS - OpenProcessToken failed (' + SysErrorMessage(GetLastError) + ').', True);
    if (not LookupPrivilegeValue(nil, SE_DEBUG_NAME, tp.Privileges[0].Luid)) then bError := True;
    if bError then SPISLog('SPIS - LookupPrivilegeValue failed (' + SysErrorMessage(GetLastError) + ').', True);
    tp.PrivilegeCount := 1;
    tp.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED;
    DuplicateTokenEx(hPToken, MAXIMUM_ALLOWED, nil, SecurityIdentification, TokenPrimary, hUserTokenDup);
    SetTokenInformation(hUserTokenDup, TokenSessionId, Pointer(dwSessionId), SizeOf(DWORD));
    if (not AdjustTokenPrivileges(hUserTokenDup, FALSE, @tp, SizeOf(TOKEN_PRIVILEGES), nil, nil)) then bError := True;
    if bError then SPISLog('SPIS - AdjustTokenPrivileges failed (' + SysErrorMessage(GetLastError) + ').', True);
    if (GetLastError() = ERROR_NOT_ALL_ASSIGNED) then bError := True;
    if bError then SPISLog('SPIS - AdjustTokenPrivileges: ERROR_NOT_ALL_ASSIGNED (' + SysErrorMessage(GetLastError) + ').', True);
    lpEnv := nil;
    if (CreateEnvironmentBlock(lpEnv, hUserTokenDup, TRUE)) then
      dwCreationFlags := dwCreationFlags or CREATE_UNICODE_ENVIRONMENT
    else
      lpEnv := nil;
    if not Assigned(lpEnv) then SPISLog('SPIS - CreateEnvironmentBlock failed (' + SysErrorMessage(GetLastError) + ').', True);
    try
      UniqueString(strProcess);
      if not CreateProcessAsUser(hUserTokenDup, nil, PChar(strProcess), nil, nil, FALSE,
        dwCreationFlags, lpEnv, PChar(ExtractFilePath(strProcess)), si, pi) then bError := True;
      if bError then 
        SPISLog('SPIS - CreateProcessAsUser failed (' + SysErrorMessage(GetLastError) + ').', True)
      else
        SPISLog('Started process in ' + IntToStr(dwSessionId) + ' using token from ' + IntToStr(winlogonPid) + '.');
      try
        try CloseHandle(hProcess); except {} end;
        try CloseHandle(hUserToken); except {} end;
        try CloseHandle(hUserTokenDup); except {} end;
        try CloseHandle(hPToken); except {} end;
      except
        {}
      end;
    finally
      DestroyEnvironmentBlock(lpEnv);
    end;
  except
    on E: Exception do
    begin
      bError := True;
      if not (E is EAbort) then
        SPISLog('SPIS - ' + E.Message + ' (' + SysErrorMessage(GetLastError) + ').', True);
    end;
  end;
  Result := not bError;
end;

function GetProcessID(strProcess: String; iSessionID: Integer = -1): DWORD;
var dwSessionId, winlogonSessId: DWord;
  hsnap: THandle;
  procEntry: TProcessEntry32;
  myPID: Cardinal;
begin
  Result := 0;
  if not InitProcLibs then Exit;
  { check running processes and return ID of process in current session... }
  if iSessionID = -1 then
    dwSessionId := WTSGetActiveConsoleSessionId
  else
    dwSessionId := iSessionID;
  hSnap := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  if (hSnap = INVALID_HANDLE_VALUE) then Exit;
  strProcess := UpperCase(ExtractFileName(strProcess));
  myPID:= GetCurrentProcessId;
  procEntry.dwSize := sizeof(TProcessEntry32);
  if (not Process32First(hSnap, procEntry)) then Exit;
  repeat
    if (procEntry.th32ProcessID <> myPID) and ((UpperCase(procEntry.szExeFile) = strProcess) or
      (UpperCase(ExtractFileName(procEntry.szExeFile)) = strProcess)) then
    begin
      winlogonSessId := 0;
      if (ProcessIdToSessionId(procEntry.th32ProcessID, winlogonSessId) and (winlogonSessId = dwSessionId)) then
      begin
        Result := procEntry.th32ProcessID;
        break;
      end;
    end;
  until (not Process32Next(hSnap, procEntry));
end;

Does anyone know why it would fail, or whether there is a way of working out what is happening in the API with this call?

  • 1
    Use WTSEnumerateSessions and search for the first active session instead of WTSGetActiveConsoleSession. It doesn't work with RDP connections. – FredS Sep 23 '16 at 16:25
  • "a way of working out what is happening in the API with this call?" - need create process in suspended state, attach debugger to new created process and after this resume it – RbMm Sep 23 '16 at 17:16
  • 2
    This code is close to unreadable. The error handling is impenetrable. – David Heffernan Sep 23 '16 at 20:16
  • @FredS Thanks, will give it a try although I've been trying to inject into the console, regardless of any RDP session, I'd be happy with the physical, first session. – Ross Harvey Sep 23 '16 at 21:47
  • @RbMm This is interesting, I'll try, not sure how to create process in suspended state, or which debugger to use to attach - are you suggesting debugging the Delphi service suspended post-start? – Ross Harvey Sep 23 '16 at 21:49
  • @DavidHeffeman Admittedly it is condensed, it's trying to ensure each step is validated, I guess I was hoping anyone familiar with this kind of behaviour of CreateProcessAsUser would be able to focus on the specific element out of whack, as opposed to the routine as a whole - but point taken - the error handling is largely irrelevant to the question and adds a layer of complexity making it hard to read, I guess I was pasting the routine verbatim to illustrate I'd gone beyond the obvious. – Ross Harvey Sep 23 '16 at 21:54
  • @RossHarvey - use CREATE_SUSPENDED in flags to CreateProcessAsUser and after you attach debugger to new process - ResumeThread. Delphi or not here unrelated and you need debug not service but user process which failed to start. you wrote that "process does not get created." - but i on 100% sure that process created, but failed in LdrInitializeProcess - and this and need debug for understand issue . and possible FredS right that task in "wrong" session id – RbMm Sep 23 '16 at 22:43
  • @RossHarvey - about debugging..it hard if you not professional in this. can and not change your code but set bp just after ZwCreateUserProcess in your service. at this momment new process already created but suspended. then need attach debugger to new process and continue run service process(debug 2 process at once) and main i not sure are existing public debuggers support attach to new process at this stage (i know only one private which perfect do this) say for WinDbg need first "nop" DbgUiIssueRemoteBreakin (insert ret at begin) for success attach – RbMm Sep 23 '16 at 23:04
  • I just wrote this from the same buggy code you did, you need to set Debug privilege before you set the Session ID to the token. But if you are retrieving "winlogon.exe" or "explorer.exe" you are essentially getting that from a session that is already logged on so there is no need for the call to set/change the Session ID of the token. In my case I altered my decade old FindProcessID to include the session ID which I passed in from the function in my last comment. – FredS Sep 24 '16 at 01:30
  • Note that it is safer to use WTSQueryUserToken rather than stealing a token from an existing process. Or if you want the child to run as SYSTEM, you can just use your own token and change the session. – Harry Johnston Sep 24 '16 at 02:57
  • "set Debug privilege before you set the Session ID to the token" - 1) this not dependent on order 2) not need at all set Debug privilege in hUserTokenDup 3) not need to set TokenSessionId also because it already here 4) use WTSQueryUserToken of course better - but all this not explain why user process fail initialize (it created !). the best choice use debugging here. strange only that all public debuggers (or i mistake ?) have problems to attach to new process at this stage (just after create, before beginning execution) – RbMm Sep 24 '16 at 13:07
  • You don't need a debugger to debug. Trace debugging. – David Heffernan Sep 25 '16 at 21:33
  • @FredS I set the privilege before the session ID and this gave the same effort unfortunately, it appeared to be successful but no process was created. – Ross Harvey Sep 27 '16 at 12:34
  • @HarryJohnston I tried to go down the WTSQueryUserToken route but I get "An attempt was made to reference a token that does not exist". My usage is: WTSQueryUserToken(WTSGetActiveConsoleSessionId(), hUserToken). I'd prefer to use this method if it's more reliable, but I couldn't seem to work out why I was getting that error. – Ross Harvey Sep 27 '16 at 12:38
  • I'm unfamiliar with external process debugging, especially Trace Debugging, I really appreciate your efforts and it's beyond the scope of the question to ask for assistance in this area but if you have a quick pointer I would be very grateful. – Ross Harvey Sep 27 '16 at 12:41
  • A couple more things; this line should be SetTokenInformation(hUserTokenDup, TokenSessionId, @dwSessionId, SizeOf(DWORD)); and you can give yourself the required access rights to work on this inside an application via "Local Policy". Just don't forget to remove them afterwards. BTW, when I wrote this SetTokenInformation gave me a rights not enabled error unless seDEBUG was enabled first. – FredS Sep 27 '16 at 17:29
  • On re-reading the question, it isn't clear to me whether there is actually a user logged in at the console. Could you clarify that? Attempting to run a process in a logged-on users session is quite a different scenario to attempting to run a process on the logon screen. Also, it would be useful to know the exit code for the child process, which you can find by keeping the process handle open, waiting for the child to exit, then calling GetExitCodeProcess. – Harry Johnston Sep 27 '16 at 20:31
  • @FredS Thanks Fred, I changed Pointer(dwSessionId) to "@dwSessionId" but the effect was the same. I've also tried commenting out the entire adjust privileges part of the process - this still works on what would appear to be domain joined PC's but not workgroup, but on the workgroup machines (which is where it seems this process doesn't get created) the effect is the same. – Ross Harvey Sep 30 '16 at 08:43
  • @HarryJohnston Thanks Harry, and apologies - let me clarify: the user could be logged on, or not, but ultimately I'd want the process spawned into the logon screen so the desktop control style process (which is what I'm spawning) can be used for the users to login. I kept the handle open for a while (ten seconds afterwards) to capture the exit process - initially I get 259 (immediately after) which represents the process is STILL_ACTIVE, but half a second later I get a status of 3222601730 which I have no idea, presumably is returning something once the process has exited. – Ross Harvey Sep 30 '16 at 08:49
  • 0xC0150002. One of the "the application was unable to start correctly" errors, though not one of the more common ones. Basically something about the context in which the process is running messing up initialization, usually related to windowing. I'm not sure whether `Winsta0\Default` is valid when nobody is logged in, for example. (BTW, if you want the logon screen, WTSQueryUserToken definitely won't work. No user!) – Harry Johnston Sep 30 '16 at 09:24

1 Answers1

0

thank you all so much for your help, I finally found the problem, the process I was starting statically linked a DLL (specifically aw_sas64.dll), this worked on most machines but not others, I'm still unsure why (the DLL is in the same folder as the EXE).

I couldn't get the DLL to work by dynamically linking it (although the 32-bit version dynamically linked OK), but once I'd commented the static link and usage out the process was started OK by the above procedure.

Again, big thanks to everyone, I still have a few problems left but this solves the mystery.