监视远程线程的创建

监视远程线程的创建

作者: 一块三毛钱
邮件: 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

一:SSDT表的hook检测和恢复 ~!~~~ 二:IDT表的hook检测和恢复 ~~~~~~(idt多处理器的恢复没处理,自己机器是单核的,没得搞,不过多核的列举可以) 三:系统加载驱动模块的检测 通过使用一个全局hash表(以DRIVEROBJECT为对象)来使用以下的方法来存储得到的结果,最终显示出来 1.常规的ZwQuerySystemInformation来列举 2通过打开驱动对象目录来列举 3搜索内核空间匹配驱动的特征来列举(这个功能里面我自己的主机一运行就死机,别的机器都没事,手动设置热键来蓝屏都不行,没dump没法分析,哎,郁闷) 4从本驱动的Modulelist开始遍历来列举驱动 四:进程的列举和进程所加载的dll检测 采用以下方法来列举进程: 1ZwQuerySystemInformation参数SystemProcessesAndThreadsInformation来枚举 2进程EPROCESS 结构的Activelist遍历来枚举 3通过解析句柄表来枚举进程 4通过Handletablelisthead枚举进程 5进程创建时都会向csrss来注册,从这个进程里面句柄表来枚举进程 6通过自身进程的HANDLETABLE来枚举进程 7通过EPROCESS的SessionProcessLinks来枚举进程 8通过EPROCESS ---VM---WorkingSetExpansionLinks获取进程 9暴力搜索内存MmSystemRangeStart以上查找PROCESS对象 进程操作: 进程的唤醒和暂停通过获取PsSuspendProcess和PsResumeProcess来操作的 进程结束通过进程空间清0和插入apc。 采用以下方法查找DLL: 1遍历VAD来查找dll 2挂靠到对应的进程查找InLoadOrderLinks来枚举dll 3暴力搜索对应进程空间查找pe特征来枚举dll DLL的操作: Dll的卸载是通过MmUnmapViewOfSection和MmmapViewOfSection(从sdt表中相应函数搜索到的)来实现的(本来想直接清0 dll空间,有时行有时不行)(只要将这个进程的ntdll卸载了,进程就结束了,一个好的杀进程的办法撒,绿色环保无污染),注入dll使用的是插入apc实现的。(注入的dll必须是realse版的。Debug版会出现***错误,全局dll注入貌似也是)插入apc效果不是很好,要有线程有告警状态才执行。 五:线程信息的检测 遍历ThreadList来枚举线程 线程的暂停和唤醒都是通过反汇编获取PsResumeThreadPsSuspendThread直接从r3传来ETHREAD来操作的,通过插入APC来结束线程 六:shadow sdt表的hook检测与恢复 没有采用pdb来解决函数名问题,直接写入xp和03的shandow表函数名(主要是自己的网不稳定,连windbg有时都连不上微软) 七:系统所有的过滤驱动的检测 查看各device下是否挂接有驱动之类的,可直接卸载 八:系统常用回调历程的检测和清除 只检查了PsSetLoadImageNotifyRoutine PsSetCreateThreadNotifyRoutine PsSetCreateProcessNotifyRoutine CmRegisterCallback这几个,至于那个什么shutdown回调不知道是啥玩意,就没搞了,有知道的顺便告诉我下撒,谢谢 九:文件系统fat和ntfs的分发函数检测 直接反汇编fat和ntfs的DriverEntry得到对应的填充分发的偏移,然后和当前已经运行的文件系统的分发相比是否被hook,并附带恢复 十:文件查看功能 自己解析ntfs和fat的结构,来实现列举文件和直接写磁盘删除。附带有普通的删除和发生IRP来删除。不过这里面有点问题,ntfs删除有时把目录给搞坏了,大家凑合着吧, Ntfs网上删除这些操作的代码不多,就是sudami大大的利用ntfs-3g来实现的,看了下,太多了,充满了结构。然后自己对照着系统删除文件时目录的变化来自己实现的。只处理了$BITMAP对应的位清除,父目录的对应文件的索引项的覆盖,删除文件对应的filerecord清0. 另外偷懒时间都没处理,呵呵,y的,一个破时间都都搞好几个字节移来移去的。 十一:常用内核模块钩子的检测和恢复 这里只检测了主要的内核模块nkrnlpa**.exe的.win32k.sys,hal.dll,比对它们的原始文件从而查找eat inline hook,iat hook ,和inline hook。Inline是从TEXT段开始一段位置开始比较的。(有点慢貌似,等待显示扫描完成就好了) 十二:应用层进程所加载dll的钩子 应用层钩子检测和内核模块钩子检测原理一样,不过为了能读写别的进程的空间,并没有使用openprocess去打开进程,而是通过KiattachProcess挂靠到当前进程,然后通过在r0直接读写进程空间的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值