Hooking thread creation/termination

偶尔善良 提交于 2019-12-17 19:04:22

问题


Is it possible to hook into thread termination on Windows? IOW, I would like to be notified if a thread inside the process (not interested in other processes and their threads) has terminated (either normally or - more important - forcefully).

Alternatively, hooking into thread creation would also do.

Rationale: I have a library that manages some information on per-thread basis (think of it as a process-wide per-thread cache for some information). When a thread is terminated I have to remove all thread-specific information from the cache. [Cache associations are implemented using thread ID which may get reused for future threads.]

There's no problem with "normal" execution order as the library user will detach the current thread from the library which will clear the state. Problems start to appear if somebody kills the thread owning cached resource.


回答1:


You could use something like Detours to do API-level hooking of Win32 APIs like TerminateThread.

I'm not seeing why you need to do this, though. It sounds like you need to clear the thread's associated cache when the thread dies so you can re-use that slot if another thread with the same ID comes along. Is this correct?

If so, couldn't you just clear the cache association in DllMain when you get the DLL_THREAD_ATTACH event? This is essentially your new thread notification. At this point, you know you have a new thread, so isn't it safe to clear the existing associated cache?

The other alternative that might work is thread-local storage (TLS). You can use Win32 APIs like TlsAlloc/TlsSetValue to store thread-specific information. You could also define a variable with __declspec(thread) to have the compiler manage the TLS for you. This way, each thread maintains its own cache. The code remains the same for each thread, but the data accesses are relative to the thread.




回答2:


The best way is to call WaitForSingleObject with the HANDLE of the thread (call OpenThread using the thread id to get the HANDLE).




回答3:


If your program is in a dll, you can set up to handle the DllMain method. This is called when a thread or process starts/ends.

For example,

library MyDLL;

uses
   SysUtils, Windows;

procedure DllMain(reason: integer) ;
var
   dyingThreadId: Cardinal;
begin
   case reason of
     DLL_THREAD_DETACH:
     begin
          dyingThreadId := GetCurrentThreadId();
          // handle thread exit with thread id
     end;
   end;
end; 

begin
   DllProc := @DllMain;
end.

EDIT: The call is made in the context of the exiting thread, so you can call GetCurrentThreadId() to get the thread's id.




回答4:


You can use the Win32_ThreadStopTrace WMI event to detect the termination of any thread in the system.

To start monitoring this event you must write a WQLsentence like this

Select * from Win32_ThreadStopTrace Within 1 Where ProcessID=PID_Of_Your_App

check this sample

uses
 Classes;

type
   TProcWmiEventThreadeCallBack = procedure(AObject: OleVariant) of object;
   TWmiEventThread    = class(TThread)
   private
     Success      : HResult;
     FSWbemLocator: OleVariant;
     FWMIService  : OleVariant;
     FEventSource : OleVariant;
     FWbemObject  : OleVariant;
     FCallBack    : TProcWmiEventThreadeCallBack;
     FWQL         : string;
     FServer      : string;
     FUser        : string;
     FPassword    : string;
     FNameSpace   : string;
     TimeoutMs    : Integer;
     procedure RunCallBack;
   public
     Constructor Create(CallBack : TProcWmiEventThreadeCallBack;const Server,User,PassWord,NameSpace,WQL:string;iTimeoutMs : Integer); overload;
     destructor Destroy; override;
     procedure Execute; override;
   end;

implementation

uses
 SysUtils,
 ComObj,
 Variants,
 ActiveX;

constructor TWmiEventThread.Create(CallBack : TProcWmiEventThreadeCallBack;const Server,User,PassWord,NameSpace,WQL:string;iTimeoutMs : Integer);
begin
  inherited Create(False);
  FreeOnTerminate := True;
  FCallBack       := CallBack;
  FWQL            := WQL;
  FServer         := Server;
  FUser           := User;
  FPassword       := PassWord;
  FNameSpace      := NameSpace;
  TimeoutMs       := iTimeoutMs;
end;

destructor TWmiEventThread.Destroy;
begin
  FSWbemLocator:=Unassigned;
  FWMIService  :=Unassigned;
  FEventSource :=Unassigned;
  FWbemObject  :=Unassigned;
  inherited;
end;


procedure TWmiEventThread.Execute;
const
  wbemErrTimedout = $80043001;
begin
  Success := CoInitialize(nil); //CoInitializeEx(nil, COINIT_MULTITHREADED);
  try
    FSWbemLocator := CreateOleObject('WbemScripting.SWbemLocator');
    FWMIService   := FSWbemLocator.ConnectServer(FServer, FNameSpace, FUser, FPassword);
    FEventSource  := FWMIService.ExecNotificationQuery(FWQL);
    while not Terminated do
    begin
      try
       FWbemObject := FEventSource.NextEvent(TimeoutMs); //set the max time to wait (ms)
      except
       on E:EOleException do
       if EOleException(E).ErrorCode=HRESULT(wbemErrTimedout) then //Check for the timeout exception   and ignore if exist
        FWbemObject:=Null
       else
       raise;
      end;

      if FindVarData(FWbemObject)^.VType <> varNull then
        Synchronize(RunCallBack);

      FWbemObject:=Unassigned;
    end;
  finally
    case Success of
      S_OK, S_FALSE: CoUninitialize;
    end;
  end;
end;

procedure TWmiEventThread.RunCallBack;
begin
  FCallBack(FWbemObject);
end;

Now to use this thread in your app you must call it in this way

WmiThread:=TWmiEventThread.Create(
  Log,
  '.',
  '',
  '',
  'root\cimv2',
  Format('Select * from Win32_ThreadStopTrace Within 1 Where ProcessID=%d',[GetCurrentProcessId]),1);

and in the callback function

procedure TForm1.Log(AObject: OleVariant);
begin        
    { 
      The OleVariant parameter has these properties
      uint32 ProcessID;
      uint8  SECURITY_DESCRIPTOR[];
      uint32 ThreadID;
      uint64 TIME_CREATED;         
    }
    //do your stuff here
    Memo1.Lines.Add(Format('Thread %s terminated ',[AObject.ThreadID]));
end;



回答5:


program ThreadExitHook;

{$APPTYPE CONSOLE}

uses
  Windows,
  Classes,
  madCodeHook;

type
  TLdrShutdownThread = procedure; stdcall;

var
  LdrShutdownThreadNext : TLdrShutdownThread;

procedure LdrShutdownThreadCallback; stdcall;
begin
  WriteLn('Thread terminating:', GetCurrentThreadId);
  LdrShutdownThreadNext;
end;

begin
  HookAPI('ntdll.dll', 'LdrShutdownThread', @LdrShutdownThreadCallback, @LdrShutdownThreadNext);

  TThread.CreateAnonymousThread(procedure begin
    WriteLn('Hello from Thread');
    Sleep(1000);
  end).Start;

  ReadLn;

  UnhookAPI(@LdrShutdownThreadNext);
end.

Here is a version that does not depend on any external library:

program Project7;

{$APPTYPE CONSOLE}

uses
  Windows,
  Classes;

{==============================================================================}
function IsWin9x: Boolean;
asm
  MOV     EAX, FS:[030H]
  TEST    EAX, EAX
  SETS    AL
end;
{------------------------------------------------------------------------------}
function CalcJump(Src, Dest: DWORD): DWORD;
begin
  if (Dest < Src) then begin
    Result := Src - Dest;
    Result := $FFFFFFFF - Result;
    Result := Result - 4;
  end else begin
    Result := Dest - Src;
    Result := Result - 5;
  end;
end;
{------------------------------------------------------------------------------}
function OpCodeLength(Address: DWORD): DWORD; cdecl; assembler;
const
  O_UNIQUE = 0;
  O_PREFIX = 1;
  O_IMM8 = 2;
  O_IMM16 = 3;
  O_IMM24 = 4;
  O_IMM32 = 5;
  O_IMM48 = 6;
  O_MODRM = 7;
  O_MODRM8 = 8;
  O_MODRM32 = 9;
  O_EXTENDED = 10;
  O_WEIRD = 11;
  O_ERROR = 12;
  asm
    pushad
    cld
    xor edx, edx
    mov esi, Address
    mov ebp, esp
    push    1097F71Ch
    push    0F71C6780h
    push    17389718h
    push    101CB718h
    push    17302C17h
    push    18173017h
    push    0F715F547h
    push    4C103748h
    push    272CE7F7h
    push    0F7AC6087h
    push    1C121C52h
    push    7C10871Ch
    push    201C701Ch
    push    4767602Bh
    push    20211011h
    push    40121625h
    push    82872022h
    push    47201220h
    push    13101419h
    push    18271013h
    push    28858260h
    push    15124045h
    push    5016A0C7h
    push    28191812h
    push    0F2401812h
    push    19154127h
    push    50F0F011h
    mov ecx, 15124710h
    push    ecx
    push    11151247h
    push    10111512h
    push    47101115h
    mov eax, 12472015h
    push    eax
    push    eax
    push    12471A10h
    add cl, 10h
    push    ecx
    sub cl, 20h
    push    ecx
    xor ecx, ecx
    dec ecx
  @@ps:
    inc  ecx
    mov  edi, esp
  @@go:
    lodsb
    mov  bh, al
  @@ft:
    mov  ah, [edi]
    inc  edi
    shr  ah, 4
    sub  al, ah
    jnc  @@ft
    mov al, [edi-1]
    and al, 0Fh
    cmp  al, O_ERROR
    jnz  @@i7
    pop edx
    not edx
  @@i7:
    inc edx
    cmp al, O_UNIQUE
    jz  @@t_exit
    cmp al, O_PREFIX
    jz  @@ps
    add  edi, 51h
    cmp  al, O_EXTENDED
    jz   @@go
    mov edi, [ebp+((1+8)*4)+4]
  @@i6:
    inc  edx
    cmp  al, O_IMM8
    jz   @@t_exit
    cmp  al, O_MODRM
    jz   @@t_modrm
    cmp  al, O_WEIRD
    jz   @@t_weird
  @@i5:
    inc  edx
    cmp  al, O_IMM16
    jz   @@t_exit
    cmp  al, O_MODRM8
    jz   @@t_modrm
  @@i4:
    inc  edx
    cmp  al, O_IMM24
    jz   @@t_exit
  @@i3:
    inc  edx
  @@i2:
    inc  edx
    pushad
    mov  al, 66h
    repnz scasb
    popad
    jnz  @@c32
  @@d2:
    dec  edx
    dec  edx
  @@c32:
    cmp  al, O_MODRM32
    jz   @@t_modrm
    sub  al, O_IMM32
    jz   @@t_imm32
  @@i1:
    inc  edx
  @@t_exit:
    jmp @@ASMEnded
  @@t_modrm:
    lodsb
    mov  ah, al
    shr  al, 7
    jb   @@prmk
    jz   @@prm
    add  dl, 4
    pushad
    mov  al, 67h
    repnz scasb
    popad
    jnz  @@prm
  @@d3:  sub  dl, 3
    dec  al
  @@prmk:jnz  @@t_exit
    inc  edx
    inc  eax
  @@prm:
    and  ah, 00000111b
    pushad
    mov  al, 67h
    repnz scasb
    popad
    jz   @@prm67chk
    cmp  ah, 04h
    jz   @@prmsib
    cmp  ah, 05h
    jnz  @@t_exit
  @@prm5chk:
    dec  al
    jz   @@t_exit
  @@i42: add  dl, 4
    jmp  @@t_exit
  @@prm67chk:
    cmp  ax, 0600h
    jnz  @@t_exit
    inc  edx
    jmp  @@i1
  @@prmsib:
    cmp  al, 00h
    jnz  @@i1
    lodsb
    and  al, 00000111b
    sub  al, 05h
    jnz  @@i1
    inc  edx
    jmp  @@i42
  @@t_weird:
    test byte ptr [esi], 00111000b
    jnz  @@t_modrm
    mov  al, O_MODRM8
    shr  bh, 1
    adc  al, 0
    jmp  @@i5
  @@t_imm32:
    sub  bh, 0A0h
    cmp  bh, 04h
    jae  @@d2
    pushad
    mov  al, 67h
    repnz scasb
    popad
    jnz  @@chk66t
  @@d4:  dec  edx
    dec  edx
  @@chk66t:
    pushad
    mov  al, 66h
    repnz scasb
    popad
    jz   @@i1
    jnz  @@d2
  @@ASMEnded:
    mov esp, ebp
    mov [result+(9*4)], edx
    popad
end;
{------------------------------------------------------------------------------}
function ApiHook(ModName, ApiName: PChar; FuncAddr, HookedApi: Pointer; var MainApi: Pointer): Boolean;
var
  dwCount, Cnt, i, jmp: DWORD;
  P: Pointer;
  hMod, OldP, TMP: Cardinal;
begin
  Result := False;
  if IsWin9x then
    Exit;
  P := FuncAddr;
  if P = nil then begin
    hMod := GetModuleHandle(ModName);
    if hMod = 0 then
      hMod := LoadLibrary(ModName);
    P := GetProcAddress(hMod, ApiName);
  end;
  if (P = nil) or (HookedApi = nil) then
    Exit;
  if not VirtualProtect(P, $40, PAGE_EXECUTE_READWRITE, @OldP) then
    Exit;
  if ((Byte(P^) = $68) and (DWORD(Pointer(DWORD(P) + 1)^) = DWORD(HookedApi))) then
    Exit;
  MainApi := VirtualAlloc(nil, $1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
  if MainApi = nil then
    Exit;
  Cnt := 0;
  for dwCount := 0 to $3F do begin
    Inc(Cnt, OpCodeLength(DWORD(P) + Cnt));
    for i := 0 to Cnt - 1 do
      PByte(MainApi)[i] := PByte(P)[i];
    if Cnt > 5 then
      Break;
  end;
  PByte(MainApi)[Cnt] := $68;
  DWORD(Pointer(DWORD(MainApi) + Cnt + 1)^) := DWORD(P) + Cnt;
  PByte(MainApi)[Cnt + 5] := $C3;
  PByte(MainApi)[Cnt + 6] := $99;
  if (OpCodeLength(DWORD(MainApi)) = 5) and
    ((Byte(MainApi^) = $E8) or (Byte(MainApi^) = $E9)) then
  begin
    jmp := DWORD(P) + DWORD(Pointer(DWORD(MainApi) + 1)^) + 5;
    DWORD(Pointer(DWORD(MainApi) + 1)^) := CalcJump(DWORD(MainApi), jmp);
  end;
  PByte(P)[0] := $68;
  DWORD(Pointer(DWORD(P) + 1)^) := DWORD(HookedApi);
  PByte(P)[5] := $C3;
  VirtualProtect(P, $40, OldP, @TMP);
  Result := True;
end;
{------------------------------------------------------------------------------}
function ApiUnHook(ModName, ApiName: PChar; FuncAddr, HookedApi: Pointer; var MainApi: Pointer): Boolean;
var
  dwCount, Cnt, i, jmp: DWORD;
  P: Pointer;
  hMod, OldP, TMP: Cardinal;
begin
  Result := False;
  if IsWin9x then
    Exit;
  P := FuncAddr;
  if P = nil then begin
    hMod := GetModuleHandle(ModName);
    P := GetProcAddress(hMod, ApiName);
  end;
  if (P = nil) or (MainApi = nil) or (HookedApi = nil) then
    Exit;
  if not VirtualProtect(P, $40, PAGE_EXECUTE_READWRITE, @OldP) then
    Exit;
  if ((Byte(P^) <> $68) or (DWORD(Pointer(DWORD(P) + 1)^) <> DWORD(HookedApi))) then
    Exit;
  Cnt := 0;
  for dwCount := 0 to $3F do begin
    Inc(Cnt, OpCodeLength(DWORD(MainApi) + Cnt));
    if (Byte(Pointer(DWORD(MainApi) + Cnt)^) = $C3) and
      (Byte(Pointer(DWORD(MainApi) + Cnt + 1)^) = $99) then
      Break;
    for i := 0 to Cnt - 1 do
      PByte(P)[i] := PByte(MainApi)[i];
  end;
  if (OpCodeLength(DWORD(P)) = 5) and ((Byte(P^) = $E8) or (byte(P^) = $E9)) then begin
    jmp := DWORD(MainApi) + DWORD(Pointer(DWORD(MainApi) + 1)^) + 5;
    DWORD(Pointer(DWORD(P) + 1)^) := CalcJump(DWORD(P), jmp);
  end;
  VirtualProtect(P, $40, OldP, @TMP);
  VirtualFree(MainApi, 0, MEM_RELEASE);
  Result := True;
end;
{==============================================================================}

type
  TLdrShutdownThread = procedure; stdcall;

var
  LdrShutdownThreadNext : TLdrShutdownThread;

procedure LdrShutdownThreadCallback; stdcall;
begin
  WriteLn('Thread terminating:', GetCurrentThreadId);
  LdrShutdownThreadNext;
end;

begin
  ApiHook('ntdll.dll', 'LdrShutdownThread', nil, @LdrShutdownThreadCallback, @LdrShutdownThreadNext);

  TThread.CreateAnonymousThread(procedure begin
    WriteLn('Hello from Thread');
    Sleep(1000);
    WriteLn('Waking up');
  end).Start;

  ReadLn;

  ApiUnHook('ntdll.dll', 'LdrShutdownThread', nil, @LdrShutdownThreadCallback, @LdrShutdownThreadNext);

  TThread.CreateAnonymousThread(procedure begin
    WriteLn('Hello from Thread');
    Sleep(1000);
    WriteLn('Waking up');
  end).Start;

  ReadLn;
end.



回答6:


Chris' mention of DLL_THREAD_ATTACH gave me an idea ...

Basically, associating cache with thread ID is a bad thing. I have to rework my library so that a thread will initially establish some kind of handle and then manage associations using this handle.




回答7:


I guess if you really want to badly enough, you could use the debugging API (e.g., WaitForDebugEvent, ContinueDebugEvent), . You'll get an EXIT_THREAD_DEBUG_EVENT when a thread exits.

I can't say that's exactly a straightforward or clean way to do it, but if you can't come up with anything else, it's probably better than nothing.




回答8:


Boost provides boost::this_thread::at_thread_exit() which allows you to provide arbitrary code to run when the current thread exits. If you call this on each thread then when it exits normally the code will be run. If a thread is terminated forcibly with TerminateThread then no more code is run on that thread, so the at_thread_exit functions are not called. The only way to handle such cases would be to hook TerminateThread, though this won't necessarily handle the case that another process terminates your threads.




回答9:


The only way to reliably do this is in a DLL that hooks DLL_THREAD_ATTACH and DLL_THREAD_DETACH. See previous discussion here.



来源:https://stackoverflow.com/questions/3802244/hooking-thread-creation-termination

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!