监视远程线程的创建

作者: 一块三毛钱
邮件: zhongts@163.com
日期: 2004.12.29

    远程线程技术被大量的使用在木马、蠕虫等软件当中,通过在别的进程中插入线程的方式运行代码,具有相当高的隐蔽性。比如常见的 Explorer.exe 进程中有十几个线程同时运行,在其中插入一个线程后,谁也分辨不出来哪个就是插入的远程线程。本文提供了一种方法可以监视远程线程的创建活动,记录下来远程线程的 ID 等重要数据,这样就可以方便大家查出哪个进程往哪个进程中插入了远程线程。

    下面分别是 IceSword v1.06 和本文代码所记录下来的远程线程创建的情况:
    
    
    
    
    由于本文需要编写驱动程序,所以不熟悉驱动程序编写的读者可以找一些驱动方面的书籍先看看,这里推荐大家到罗云彬的网站上去下载翻译的 KmdTut 来看。同时把 KmdKit 也下载下来,因为本文代码用到了这个软件包。安装好 Masm32 和 KmdKit 之后才能编译本文提供的代码。如果编译代码时提示 error LNK2001: unresolved external symbol _PsRemoveCreateThreadNotifyRoutine@4 错误,则把本文提供的 ntoskrnl.lib 复制到 lib\w2k 文件夹中覆盖原文件即可。我也是刚学驱动编程,下面提供的只是一个很简单的例子,要想实用还有很多事情要做。

    首先是监视线程的创建问题,然后再区分哪些是远程线程。要想监视线程的创建需要用到这样的一个函数 PsSetCreateThreadNotifyRoutine。通过该函数我们注册一个回调函数,每次当系统中有新的线程创建的时候就会调用我们的回调函数。在这个回调函数中我们就可以把所有的线程的创建记录下来。如果要监视进程的创建则还有另外一个函数 PsSetCreateProcessNotifyRoutine 可以完成这个功能。监视线程创建的回调函数的函数原型如下:

VOID
(*PCREATE_THREAD_NOTIFY_ROUTINE) (
    IN HANDLE  ProcessId,
    IN HANDLE  ThreadId,
    IN BOOLEAN  Create
    );
    
    ProcessId 是进程号,这里的进程号是指向包括该线程的进程,而不是创建该线程的进程。ThreadId 是将要创建的线程的线程号。Create 用来指出是创建线程还是销毁线程。监视进程创建的回调函数的函数原型如下:

VOID
(*PCREATE_PROCESS_NOTIFY_ROUTINE) (
    IN HANDLE  ParentId,
    IN HANDLE  ProcessId,
    IN BOOLEAN  Create
    );

    ParentId 是父进程号,ProcessId 是进程号,Create 表示创建还是销毁进程。
    
    有了这两个函数我们就可以监视所有的进程和线程的创建和销毁活动了。下面来看看代码,我把主要的代码都列了出来。
    
DriverEntry proc uses esi, pDriverObject:PDRIVER_OBJECT, pusRegistryPath:PUNICODE_STRING
  LOCAL  status : NTSTATUS
  LOCAL  pDeviceObject : PDEVICE_OBJECT
  
  ......
  
  mov  g_dwProcessId, 0
  mov  g_bMainThread, FALSE
  lea  eax, _ProcessCallback
  invoke  PsSetCreateProcessNotifyRoutine, eax, FALSE
  lea  eax, _ThreadCallback
  invoke  PsSetCreateThreadNotifyRoutine, eax
  mov  status, eax
  
  ......
  
DriverEntry endp

    上面就是注册回调函数的代码部分,_ProcessCallback 和 _ThreadCallback 分别是进程和线程监视函数。在驱动程序的启动部分注册了回调函数,还需要在驱动的卸载部分移去注册的回调函数。代码如下:

_DriverUnload proc pDriverObject:PDRIVER_OBJECT

  lea  eax, _ProcessCallback
  invoke  PsSetCreateProcessNotifyRoutine, eax, TRUE
  lea  eax, _ThreadCallback
  invoke  PsRemoveCreateThreadNotifyRoutine, eax
  
  invoke  IoDeleteSymbolicLink, addr g_usSymbolicLinkName
  mov  eax, pDriverObject
  invoke  IoDeleteDevice, (DRIVER_OBJECT PTR [eax]).DeviceObject
  ret
_DriverUnload endp

    给 PsSetCreateProcessNotifyRoutine 函数的第二个参数传递 TRUE 就可以移去注册的进程回调函数。移去注册的线程回调函数需要调用 PsRemoveCreateThreadNotifyRoutine 函数,这个函数是一个未公开函数,从 Windows XP 以后提供,由于手边没有 Windows 2000 系统,不能验证,大家可以看看自己的 Windows 2000 系统中有没有这个函数。因为这个一个未公开函数,所以调用的时候不能直接调用,需要引入库才行。生成引入库的办法也很简单,利用 Masm32 软件包中自带的 inc2l 工具即可,使用办法大家可以参考 Masm32 自己生成引入库的方法。上文之所以提到要覆盖 ntoskrnl.lib 文件就是这个原因。

    本来监视远程线程只需要注册一个线程回调函数即可,因为要判断是否是远程线程,要根据创建线程的进程和包含线程的进程的不同才能判断是否是远程线程。所以,我们还需要注册一个进程回调函数。

_ProcessCallback proc uses esi,ParentId:DWORD, ProcessId:DWORD, bCreate:DWORD

  .if bCreate
    mov  eax, ProcessId
    mov  g_dwProcessId, eax
    mov  g_bMainThread, TRUE
  .endif
  ret
_ProcessCallback endp

    这个就是进程回调函数,如果新创建一个进程,则把 g_bMainThread 设置为 TRUE,把进程 ID 保存到 g_dwProcessId 中。因为一个新的进程被创建时,它的主线程不是它自己创建的,而是它的父进程创建的。这里父进程和它自己的进程肯定不是同一个进程,但这个时候创建的主线程不是远程线程。上面的代码就是记录进程的创建,那么紧接着创建的线程就不是远程线程。

_ThreadCallback proc uses ebx esi edi, ProcessId:DWORD, ThreadId:DWORD, bCreate:DWORD
  LOCAL  lpParentEProcess, lpEProcess
  LOCAL  dwParentPID, dwParentTID
  
  cmp  g_bMainThread, TRUE
  je  exit_0
  
  cmp  bCreate, 0
  je  exit_0
  
  cmp  ProcessId, 4
  je  exit_0
  
  invoke  PsGetCurrentProcessId
  mov  dwParentPID, eax
  cmp  eax, ProcessId
  je  exit_0
  
  invoke  PsGetCurrentThreadId
  mov  dwParentTID, eax
  
  invoke  PsLookupProcessByProcessId, dwParentPID, addr lpParentEProcess
  cmp  eax, STATUS_SUCCESS
  jne  exit_0
  invoke  PsLookupProcessByProcessId, ProcessId, addr lpEProcess
  cmp  eax, STATUS_SUCCESS
  jne  exit_0
  
  mov  esi, lpParentEProcess
  add  esi, g_dwOffset
  mov  edi, lpEProcess
  add  edi, g_dwOffset
  
  invoke  DbgPrint, $CTA0("调用方: Name=%s PID=%d TID=%d\t\t被调用方: Name=%s PID=%d TID=%d\n"), \
      esi, dwParentPID, dwParentTID, edi, ProcessId, ThreadId
  
exit_0:
  mov  g_bMainThread, FALSE
  ret
_ThreadCallback endp

    这段代码是线程回调函数,也是我们的核心代码。先判断是不是一个进程的主线程创建,如果不是继续判断。是不是创建线程?如果是则继续判断。进程是否是 SYSTEM 进程?如果是则忽略。这是因为每次打开文件夹、切换文件夹 Explorer.exe 都会在 SYSTEM 进程当中创建一个远程线程,所以我们忽略它。大家可以把这两句注释掉再看看程序的输出就能明白。

    因为回调函数中只有两个参数,一个是 ProcessId 表示包含线程的进程号,另外一个是 ThreadId 表示创建的线程号。所以我们还需要找出那个创建线程的进程号,才能够比较创建线程的进程和包含线程的进程是不是同一个进程,从而判断是不是远程线程。这里就要提到一个问题,当某一个进程创建线程的时候,系统是在该进程上下文中调用我们的线程回调函数,所以我们可以通过 PsGetCurrentProcessId 函数来取得该进程号。再通过 PsGetCurrentThreadId 取得线程号,注意这个线程不是要创建的线程,而是包含创建线程代码的线程。

    代码接着又调用一个未公开函数 PsLookupProcessByProcessId 来取得某个进程的 EPROCESS 结构,EPROCESS 结构在 KmdKit 所带的 w2kundoc.inc 中有详细的说明,在 EPROCESS 结构的 ImageFileName 成员中保存着进程的名字。因为系统不同 ImageFileName 成员的偏移位置也不同,所以,根据系统的不同代码中采用了一个全局变量 g_dwOffset 来保存这个偏移。下面是判断系统的代码:

invoke  PsGetVersion, NULL, addr g_dwSystemMinorVersion, NULL, NULL
.if g_dwSystemMinorVersion==0
  mov  g_dwOffset, 1FCh
.elseif g_dwSystemMinorVersion==1
  mov  g_dwOffset, 174h
.elseif g_dwSystemMinorVersion==2
  mov  g_dwOffset, 154h
.endif

所有的工作做完之后就是把收集到的信息输出来,通过 DbgPrint 函数可以达到这个目的。

    大家可以通过 KmdKit 自带的 KmdManager 工具注册/运行本文代码生成的 RemoteThreadMonitor.sys 文件,然后通过 DbgView 或者 SoftICE 工具查看代码的输出。知道了远程线程的线程号可以用 Process Explorer 等工具杀掉远程线程。

参考资料:
(1) sinister 编写进程/线程监视器
    http://www.xfocus.net/articles/200303/495.html
(2) DDK
(3) KmdKit
    http://www.freewebs.com/four-f/
(4) KmdTut 中文翻译
    http://asm.yeah.net


附件:Monitor.rar