Case with TerminateProcess

So what’s the case you might ask? Well as I wrote my article about how to write a Winlogon Notification Packages, I often noticed in the C examples I found on the net, that those examples made use of SafeTerminateProcess so I wondered what this call stands for and why they used it.

After some research I found an article which was from Dr Dobbs written somewhere around 1999 which explains the difficulties around TerminateProcess()

Just to mention a few of them:

  • Any application that does not have a console control handler or a message loop will not be able to react to the WM_CLOSE and will be killed via TerminateProcess().
  • Deadlock in the message processing thread and using too small a timeout value in the call to WaitForSingleObject(), can cause TerminateProcess() to be used as well.
  • It is generally a bad idea to call TerminateProcess() because the system does not shut down the process in an orderly fashion.
  • Any DLLs used by the process will not receive the DLL_PROCESS_DETACH event, disk buffers are not properly flushed, and memory shared with other processes can be left in an inconsistent state.

So what we can do now? Simple make a call to SafeTerminateProcess instead!

safetp.h — Declaration for SafeTerminateProcess()

/* Header file for SafeTerminateProcess */

#ifndef _SAFETP_H__ #define _SAFETP_H__

BOOL SafeTerminateProcess(HANDLE hProcess, UINT uExitCode);

#endif

/* End of File */ 


SafeTerminateProcess() takes advantage of the fact that Win32′s ExitProcess() has a function signature compatible with that of a thread entry point. By “compatible,” I mean that the parameters of both function prototypes are the same type, but they have different return types. This lets me launch ExitProcess() in any process, using Win32′s CreateRemoteThread(), causing that process to perform an orderly shutdown.


The fact that ExitProcess() has a void return type while a thread function is expected to return a DWORD is not a problem. This just means that the exit code of the remote thread will be whatever happened to be in the return value register (EAX on 80×86 processors) when ExitProcess() finishes.


SafeTerminateProcess() has the same signature as TerminateProcess() and starts out by using DuplicateHandle() to ensure that it will have rights to create a thread in the remote process. Handles returned from CreateProcess() always have full privileges. 

SafeTerminateProcess() checks to make sure that the process is still running (no point in shooting a dead horse), and sets an appropriate GetLastError() value if it is not. If it is alive, I call CreateRemoteThread() with ExitProcess() as the entry point and pass SafeTerminateProcess()’s uExitCode parameter as the thread parameter. If the call fails for some reason, SafeTerminateProcess() saves the GetLastError() value so it can use it before returning.

/*
    Safely terminate a process by creating a remote thread
    in the process that calls ExitProcess
*/

#define STRICT
#include

BOOL SafeTerminateProcess(HANDLE hProcess, UINT uExitCode)
{
    DWORD dwTID, dwCode, dwErr = 0;
    HANDLE hProcessDup = INVALID_HANDLE_VALUE;
    HANDLE hRT = NULL;
    HINSTANCE hKernel = GetModuleHandle("Kernel32");
    BOOL bSuccess = FALSE;

    BOOL bDup = DuplicateHandle(GetCurrentProcess(),
                                hProcess,
                                GetCurrentProcess(),
                                &hProcessDup,
                                PROCESS_ALL_ACCESS,
                                FALSE,
                                0);

    // Detect the special case where the process is
    // already dead...
    if ( GetExitCodeProcess((bDup) ? hProcessDup : hProcess, &dwCode) &&
         (dwCode == STILL_ACTIVE) )
    {
        FARPROC pfnExitProc;

        pfnExitProc = GetProcAddress(hKernel, "ExitProcess");

        hRT = CreateRemoteThread((bDup) ? hProcessDup : hProcess,
                                 NULL,
                                 0,
                                 (LPTHREAD_START_ROUTINE)pfnExitProc,
                                 (PVOID)uExitCode, 0, &dwTID);

        if ( hRT == NULL )
            dwErr = GetLastError();
    }
    else
    {
        dwErr = ERROR_PROCESS_ABORTED;
    }

    if ( hRT )
    {
        // Must wait process to terminate to
        // guarantee that it has exited...
        WaitForSingleObject((bDup) ? hProcessDup : hProcess,
                            INFINITE);

        CloseHandle(hRT);
        bSuccess = TRUE;
    }

    if ( bDup )
        CloseHandle(hProcessDup);

    if ( !bSuccess )
        SetLastError(dwErr);

    return bSuccess;
}
/* End of File */

So simple lets translate the above code to Delphi and make a small program to check if our translation works.

unit Main;

interface

uses
  Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TMainForm = class(TForm)
    edtProcHandle: TEdit;
    edtExitResult: TEdit;
    btnTerminate:  TButton;
    btnRunProcess: TButton;
    procedure btnTerminateClick(Sender: TObject);
    procedure btnRunProcessClick(Sender: TObject);
  private
    { Private-Deklarationen }
  public
    { Public-Deklarationen }
    RemoteProcHandle: cardinal;
    function SafeTerminateProcess(const hProcess: cardinal;
      uExitCode: cardinal): boolean;
    procedure StartApp(const App, Parameters, CurDir: string);
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

uses
  JwaWindows,
  JWsclToken,
  JwsclSid,
  JwsclKnownSid,
  JwsclTypes,
  JwsclAcl,
  JwsclConstants,
  JwsclLsa,
  JwsclExceptions,
  JwsclStrings;

// Get remote process handle from process id
function GetProcessHandleFromID(ID: DWORD): THandle;
begin
  Result := OpenProcess(PROCESS_CREATE_THREAD or PROCESS_QUERY_INFORMATION or
    PROCESS_VM_OPERATION or PROCESS_VM_WRITE or PROCESS_VM_READ, False, ID);
end;

// creates a process and waits for input idle
procedure TMainForm.StartApp(const App, Parameters, CurDir: string);
var
  StartupInfo: TStartupInfo;
  ProcInfo: TProcessInformation;
  pEnv: Pointer;
  pCurDir, pCmdLine: PChar;
begin
  ZeroMemory(@StartupInfo, sizeof(StartupInfo));
  StartupInfo.cb := SizeOf(StartupInfo);
  StartupInfo.lpDesktop := 'winsta0\default';

  CreateEnvironmentBlock(@pEnv, 0, True);

  try
    if Length(Parameters) > 0 then
      pCmdLine := PChar('"' + App + '" ' + Parameters)
    else
      pCmdLine := PChar('"' + App + '" ');

    pCurDir := nil;
    if Length(CurDir) > 0 then
      pCurDir := PChar(CurDir);

    if not CreateProcess(PChar(App), pCmdLine, nil, nil, True,
      CREATE_NEW_CONSOLE or CREATE_UNICODE_ENVIRONMENT, pEnv, pCurDir,
      StartupInfo, ProcInfo) then
      raiseLastOsError;
  finally
    DestroyEnvironmentBlock(pEnv);
  end;

  // remote process handle
  RemoteProcHandle := GetProcessHandleFromID(ProcInfo.dwProcessId);

  //wait for 60 secs
  //WaitForSingleObject(ProcInfo.hProcess, 60 * 1000);

  //wait until process can receive user input
  WaitForInputIdle(ProcInfo.hProcess, 60 * 1000);

  CloseHandle(ProcInfo.hProcess);
  CloseHandle(ProcInfo.hThread);
end;

// SafeTerminateProcess implementation
function TMainForm.SafeTerminateProcess(const hProcess: cardinal;
  uExitCode: cardinal): boolean;
var
  dwTID, dwCode, dwErr: DWORD;
  hProcessDup: cardinal;
  bDup: BOOL;
  hrt:  cardinal;
  hKernel: HMODULE;
  bSuccess: BOOL;
  FARPROC: Pointer;
begin
  dwTID := 0;
  dwCode := 0;
  dwErr := 0;
  hProcessDup := INVALID_HANDLE_VALUE;
  hrt := NULL;
  bSuccess := False;

  if (Win32Platform = VER_PLATFORM_WIN32_NT) then
  begin
    bDup := DuplicateHandle(GetCurrentProcess(), hProcess,
      GetCurrentProcess(), @hProcessDup, PROCESS_ALL_ACCESS, False, 0);

    // Detect the special case where the process is
    // already dead...
    if (GetExitCodeProcess(hProcessDup, dwCode)) then
    begin
      hKernel := GetModuleHandle('Kernel32');
      FARPROC := GetProcAddress(hKernel, 'ExitProcess');
      hRT := CreateRemoteThread(hProcessDup, nil, 0, Pointer(FARPROC),
        @uExitCode, 0, @dwTID);

      if (hRT = NULL) then
        dwErr := GetLastError()
      else
        dwErr := ERROR_PROCESS_ABORTED;

      if (hrt <> Null) then
        WaitForSingleObject(hProcessDup,
          INFINITE);
      CloseHandle(hRT);
      bSuccess := True;

      if (bDup) then
        CloseHandle(hProcessDup);

      if not (bSuccess) then
        SetLastError(dwErr);

      Result := bSuccess;
    end;
  end;
end;

procedure TMainForm.btnRunProcessClick(Sender: TObject);
begin
  StartApp('c:\windows\system32\calc.exe', '', '');
  edtProcHandle.Text := IntToStr(RemoteProcHandle);
end;

procedure TMainForm.btnTerminateClick(Sender: TObject);
begin
  JwEnablePrivilege(SE_DEBUG_NAME, pst_Enable);

  case SafeTerminateProcess(RemoteProcHandle, 0) of
    True: edtExitResult.Text  := 'Safely terminated!';
    False: edtExitResult.Text := 'Something went wrong';
  end;

end;

end.

 


Even though it seems redundant to use GetProcAddress() to retrieve a pointer to ExitProcess(), it is necessary. The reason is that a Win32 executable links to DLL functions via an indirect pointer called a thunk. The thunks are located in different areas of memory for each process, and the operating system loader fills them in with real addresses when the code initially starts up and before the process starts running.


If you were to pass the address of ExitProcess() directly like this: you would be passing the address of the thunk in the calling process, which would be meaningless to CreateRemoteThread()’s target process and most likely cause an access violation. By explicitly grabbing the function’s location with GetProcAddress() I am getting the actual location in memory instead of the address of the thunk entry. Since kernel32.dll (the module that exports ExitProcess()) is at the same location in every process, it’s perfectly fine to directly use the function’s address. If the call to CreateRemoteThread() succeeded, SafeTerminateProcess() then uses WaitForSingleObject() to pause until the thread exits, ensuring that the remote process has now perished.


The function then closes the handles it created. If the function is returning FALSE, I also restore the GetLastError() value I saved so that the caller can examine it to find out what went wrong. Another interesting note is that the CreateRemoteThread() call caused a DLL_THREAD_ATTACH event but no corresponding DLL_THREAD_DETACH event. This is the expected and documented behavior. The only time the DLL_THREAD_DETACH event occurs is when a thread exits while the process is still running — threads terminated in normal process shutdown sequence don’t fire this event.


Also, SafeTerminateProcess() isn’t perfect. Its problems, however, stem from its lack of portability and a special case situation rather than the corruption issues TerminateProcess() suffers from. SafeTerminateProcess() will work only on NT since neither Win95, Win98, or WinCE support CreateRemoteThread(). WinCE suffers from the additional problem of not supporting the ExitProcess() API function.

For applications that are deadlocked in this way, using the less attractive TerminateProcess() is the only option on NT and Win9x. Under CE, however, there’s no way to kill a process that’s deadlocked in DllMain(). It’ll just remain as a ghost in the background until you reboot the device.

Download

[download id="6" Format="4"]

References

[1] Microsoft Knowledge Base Article #Q178893, HOWTO: Terminate an Application “Cleanly” in Win32,

KB Q178893

[2] A Safer Alternative to TerminateProcess()

Dr Dobbs

 

Comments are closed.