作者: 一块三毛钱
邮件: zhongts@163.com
日期: 2004.12.29
远程线程技术被大量的使用在木马、蠕虫等软件当中,通过在别的进程中插入线程的方式运行代码,具有相当高的隐蔽性。比如常见的 Explorer.exe 进程中有十几个线程同时运行,在其中插入一个线程后,谁也分辨不出来哪个就是插入的远程线程。本文提供了一种方法可以监视远程线程的创建活动,记录下来远程线程的 ID 等重要数据,这样就可以方便大家查出哪个进程往哪个进程中插入了远程线程。
由于本文需要编写驱动程序,所以不熟悉驱动程序编写的读者可以找一些驱动方面的书籍先看看,这里推荐大家到罗云彬的网站上去下载翻译的 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
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
/*有时候我们希望能够动态监视系统中任意进程/线程的创建与销毁。为了达
到此目的我翻阅了 DDK 手册,发现其提供的 PsSetCreateProcessNotifyRoutine(),
PsSetCreateThreadNotifyRoutine(),等函数可以实现此功能。这两个函数可以
通过向系统注册一个 CALLBALCK 函数来监视进程/线程等操作。函数原形如下:
NTSTATUS
PsSetCreateProcessNotifyRoutine(
IN PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine,
IN BOOLEAN Remove
);
VOID
(*PCREATE_PROCESS_NOTIFY_ROUTINE) (
IN HANDLE ParentId,
IN HANDLE ProcessId,
IN BOOLEAN Create
);
NTSTATUS
PsSetCreateThreadNotifyRoutine(
IN PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
);
VOID
(*PCREATE_THREAD_NOTIFY_ROUTINE) (
IN HANDLE ProcessId,
IN HANDLE ThreadId,
IN BOOLEAN Create
);
通过原形可以看出,其 CALLBACK 函数只提供了进程ID/线程ID。并没有提供
进程名。那么我们要进一步通过进程ID来获取进程名。这需要用到一个未公开
的函数 PsLookupProcessByProcessId()。函数原形如下:
NTSTATUS PsLookupProcessByProcessId(
IN ULONG ulProcId,
OUT PEPROCESS * pEProcess
);
函数输出的 EPROCESS 结构也是未公开的内核进程结构,很多人称其为 KPEB。
EPROCESS 结构中的偏移 0x1FC 指向当前进程名的偏移。(这个结构虽然可以在
驱动程序中直接使用。但没有公布其结构,网上有不少高手已将其结构给出。有
兴趣可以自行搜索,或去 IFS DDK 中获取,这里因为结构太长,就不贴出来了)
有了这个结构我们就可以从中得到进程名。NT系统还提供了一个函数可以动态监
视进程装载映像。此函数可以得到进程加栽时所调用的 DLL 名称与全路径,还有
一些映像信息。为我们获得更详细的进程装载信息提供了更好的帮助。
函数原形如下:
NTSTATUS
PsSetLoadImageNotifyRoutine(
IN PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine
);
VOID
(*PLOAD_IMAGE_NOTIFY_ROUTINE) (
IN PUNICODE_STRING FullImageName,
IN HANDLE ProcessId, // where image is mapped
IN PIMAGE_INFO ImageInfo
);
typedef struct _IMAGE_INFO {
union {
ULONG Properties;
struct {
ULONG ImageAddressingMode : 8; //code addressing mode
ULONG SystemModeImage : 1; //system mode image
ULONG ImageMappedToAllPids : 1; //mapped in all processes
ULONG Reserved : 22;
};
};
PVOID ImageBase;
ULONG ImageSelector;
ULONG ImageSize;
ULONG ImageSectionNumber;
} IMAGE_INFO, *PIMAGE_INFO;
利用以上提供的函数与结构,我们便能实现一个进程/线程监视器。下面这段
代码演示了如何实现此功能。
*/
/*****************************************************************
文件名 : WssProcMon.c
描述 : 进程/线程监视器
作者 : sinister
最后修改日期 : 2002-11-02
*****************************************************************/
#include "ntddk.h"
#include "windef.h"
#include "string.h"
#define SYSNAME "System"
//#define ProcessNameOffset 0x1fc
ULONG GetProcessNameOffset();
VOID Unload(IN PDRIVER_OBJECT DriverObject);
static NTSTATUS MydrvDispatch (IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp);
NTSTATUS PsLookupProcessByProcessId(IN ULONG ulProcId, OUT PEPROCESS * pEProcess);
VOID ProcessCreateMon ( IN HANDLE hParentId, IN HANDLE PId,IN BOOLEAN bCreate);
VOID ThreadCreateMon (IN HANDLE PId, IN HANDLE TId, IN BOOLEAN bCreate);
VOID ImageCreateMon (IN PUNICODE_STRING FullImageName, IN HANDLE ProcessId, IN PIMAGE_INFO ImageInfo );
// 驱动入口
ULONG ProcessNameOffset =0;
NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath )
{
UNICODE_STRING nameString, linkString;
PDEVICE_OBJECT deviceObject;
NTSTATUS status;
int i;
//建立设备
RtlInitUnicodeString( &nameString, L"//Device//WssProcMon" );
status = IoCreateDevice( DriverObject,
0,
&nameString,
FILE_DEVICE_UNKNOWN,
0,
TRUE,
&deviceObject
);
if (!NT_SUCCESS( status ))
return status;
RtlInitUnicodeString( &linkString, L"//DosDevices//WssProcMon" );
status = IoCreateSymbolicLink(&linkString, &nameString);
if (!NT_SUCCESS( status ))
{
IoDeleteDevice(DriverObject->DeviceObject);
return status;
}
ProcessNameOffset = GetProcessNameOffset();
/*status = PsSetLoadImageNotifyRoutine(ImageCreateMon);
if (!NT_SUCCESS( status ))
{
DbgPrint("PsSetLoadImageNotifyRoutine()/n");
return status;
}*/
status = PsSetCreateThreadNotifyRoutine(ThreadCreateMon);
if (!NT_SUCCESS( status ))
{
DbgPrint("PsSetCreateThreadNotifyRoutine()/n");
return status;
}
status = PsSetCreateProcessNotifyRoutine(ProcessCreateMon, FALSE);
if (!NT_SUCCESS( status ))
{
DbgPrint("PsSetCreateProcessNotifyRoutine()/n");
return status;
}
for ( i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
{
DriverObject->MajorFunction[i] = MydrvDispatch;
}
DriverObject->DriverUnload = Unload;
return STATUS_SUCCESS;
}
VOID Unload(IN PDRIVER_OBJECT DriverObject)
{
UNICODE_STRING linkString;
PsRemoveCreateThreadNotifyRoutine(ThreadCreateMon);
PsSetCreateProcessNotifyRoutine(ProcessCreateMon, TRUE);
RtlInitUnicodeString( &linkString, L"//DosDevices//WssProcMon" );
IoDeleteSymbolicLink( &linkString );
IoDeleteDevice( DriverObject->DeviceObject );
}
//处理设备对象操作
static NTSTATUS MydrvDispatch (IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0L;
IoCompleteRequest( Irp, 0 );
return Irp->IoStatus.Status;
}
HANDLE g_dwProcessId;
BOOL g_bMainThread;
VOID ProcessCreateMon ( IN HANDLE hParentId, IN HANDLE PId,IN BOOLEAN bCreate )
{
PEPROCESS EProcess;
ULONG ulCurrentProcessId;
LPTSTR lpCurProc;
NTSTATUS status;
#ifdef _AMD64_
ULONG ProcessId = HandleToUlong(PId);
status = PsLookupProcessByProcessId( ProcessId, &EProcess);
#else
HANDLE ProcessId =PId;
status = PsLookupProcessByProcessId( (ULONG)PId, &EProcess);
#endif
if (!NT_SUCCESS( status ))
{
DbgPrint("PsLookupProcessByProcessId()/n");
return ;
}
if ( bCreate )
{
//g_dwProcessId = ProcessId;
g_bMainThread = TRUE;
lpCurProc = (LPTSTR)EProcess;
lpCurProc = lpCurProc + ProcessNameOffset;
DbgPrint( "CREATE PROCESS = PROCESS NAME: %s , PROCESS PARENTID: %d, PROCESS ID: %d, PROCESS ADDRESS %x:/n",
lpCurProc,
hParentId,
PId,
EProcess );
}
else
{
DbgPrint( "TERMINATED == PROCESS ID: %d/n", PId);
}
}
VOID ThreadCreateMon (IN HANDLE PId, IN HANDLE TId, IN BOOLEAN bCreate)
{
PEPROCESS EProcess,ParentEProcess;
LPTSTR lpCurProc,lpParnentProc;
NTSTATUS status;
#ifdef _AMD64_
ULONG System=4;
ULONG dwParentPID=HandleToUlong(PsGetCurrentProcessId());//创建该线程的进程
ULONG ProcessId = HandleToUlong(PId);
status = PsLookupProcessByProcessId( ProcessId, &EProcess);
status = PsLookupProcessByProcessId( dwParentPID, &ParentEProcess);
#else
HANDLE System=(HANDLE)4;//under xp, in win2k is 8
HANDLE dwParentPID=PsGetCurrentProcessId();//创建该线程的进程
HANDLE ProcessId =PId;//ProcessId 是进程号,这里的进程号是指向包括该线程的进程,而不是创建该线程的进程
status = PsLookupProcessByProcessId( (ULONG)ProcessId, &EProcess);
status = PsLookupProcessByProcessId( (ULONG)dwParentPID, &ParentEProcess);
#endif
if (!NT_SUCCESS( status ))
{
DbgPrint("PsLookupProcessByProcessId()/n");
return ;
}
if ( bCreate )
{
if((g_bMainThread==TRUE)&&(ProcessId!=System)&&(ProcessId!=dwParentPID))
{
HANDLE dwParentTID=PsGetCurrentThreadId();
lpCurProc = (LPTSTR)EProcess;
lpParnentProc= (LPTSTR)ParentEProcess;
lpCurProc += ProcessNameOffset;
lpParnentProc +=ProcessNameOffset;
DbgPrint("caller: Name=%s PID=%d TID=%d/t/tcalled: Name=%s PID=%d TID=%d/n", /
lpParnentProc, dwParentPID, dwParentTID, lpCurProc, ProcessId, TId);
g_bMainThread=FALSE;
}
lpCurProc = (LPTSTR)EProcess;
lpCurProc = lpCurProc + ProcessNameOffset;
DbgPrint( "CREATE THREAD = PROCESS NAME: %s PROCESS ID: %d, THREAD ID: %d/n", lpCurProc, PId, TId );
}
else
{
DbgPrint( "TERMINATED == THREAD ID: %d/n", TId);
}
}
//VOID ImageCreateMon (IN PUNICODE_STRING FullImageName, IN HANDLE ProcessId, IN PIMAGE_INFO ImageInfo )
//
//{
// DbgPrint("FullImageName: %S,Process ID: %d/n",FullImageName->Buffer,ProcessId);
// DbgPrint("ImageBase: %x,ImageSize: %d/n",ImageInfo->ImageBase,ImageInfo->ImageSize);
//}
ULONG GetProcessNameOffset()
{
PEPROCESS curproc;
int i;
curproc = PsGetCurrentProcess();
//
// Scan for 12KB, hopping the KPEB never grows that big!
//
for( i = 0; i < 3*PAGE_SIZE; i++ ) {
if( !strncmp( SYSNAME, (PCHAR) curproc + i, strlen(SYSNAME) )) {
return i;
}
}
//
// Name not found - oh, well
//
return 0;
}
根据你的改为C代码,并支持WINXP -64.......