假如我们需要实现一个驱动程序去收集一些操作系统的统计信息,并将这些信息发送给用户层的管理程序去处理,这就需要驱动程序与管理程序交互,如何实现这种功能呢?最简单的办法就是在管理程序里创建一个计时器,这个计时器每隔一定的间隔时间(例如,每秒一次)就触发一次,通过使用DeviceIoControl函数向驱动程序发送I/O控制码来收集统计信息(RegMon和FileMon就是这样做的)。但是如果我们的驱动程序要收集的是一些不常发生的事件信息,比如我们要收集进程创建、销毁的信息,由于系统并非每时每刻都有进程创建或者销毁,所以定时执行DeviceIoControl往往都是白忙一场,获取不到任何信息。此时我们可以考虑增加触发定时器的间隔时间(例如,把间隔时间增加到10秒),但是如果间隔时间过长又可能会出现驱动程序已经收集到信息但是由于管理程序的定时器尚未被触发而导致未能及时获取统计信息的情况发生。显然,比较合理的方案应该是由驱动程序来决定管理程序什么时候获取统计信息,因为只有驱动程序才知道具体的事件是什么时候发生的,当事件发生后,驱动程序以某种方式通知管理程序,有事件发生,你可以获取数据了,此时管理程序再用DeviceIoControl向驱动程序发送控制码以获取最新的统计信息数据。
事实上,Windows给我们提供了两种比较好的方法可以让我们达到目的。一种方法是使用异步的DeviceIoControl,这种方法要求用户在使用CreateFile创建对象时必须要设置FILE_FLAG_OVERLAPPED标志,同时填充一个OVERLAPPED结构并将其地址做为CreateFile的最后一个参数传递。异步DeviceIoControl调用可能返回的结果有三种:返回TRUE,这说明驱动程序的分派子程序可以立即完成请求;返回FALSE并且调用GetLastError取得的错误码是ERROR_IO_PENDING,这表明驱动程序的分派子程序返回STATUS_PENDING并会在稍后完成控制操作。这里要注意的是,ERROR_IO_PENDING并不代表真的出错了,它仅仅是一种系统表明任何事件正在被正常处理的途径而已;如果返回FALSE并且用GetLastError取得的错误码也不是STATUS_PENDING,那就是真的出错了。这里我们不打算详细讨论异步DeviceIoControl方法,有兴趣的话,您可以参考DDK或者MSDN。另一种方法就是同步调用了,这种方法相对要容易些,这种方法要求在驱动程序和用户程序间共享一个事件,驱动程序通过这个事件对象向客户程序发送信号,客户程序收到信号后,就可以从驱动程序接收一些自己感兴趣的信息。
如何才能共享事件呢?你可以使用一个命名对象,然后通过名字访问这个对象。我们之前在SharedSection的例子中就使用过这种方法。这种方法的基本技巧就是让该命名对象一直存在于内存中,所以缺点也就显而易见了,这个命名对象对所有进程都是可见的,但是即便如些,它也比使用无名对象要些。当然,我们还有更好的方法,就是先在客户程序中用CreateEvent创建一个对象,然后调用DeviceIoControl把事件句柄传递给驱动程序。在本章中我们将使用这种方法创建一个简单的进程监视器,这个监视器可以监视进程的创建与销毁。下面是ProcessMon管理程序运行时的截图:
12.1 全局数据
我们先来看一下common.pas中定义的一些全局数据。
const IOCTL_SET_NOTIFY = (FILE_DEVICE_UNKNOWN shl 16) or (FILE_WRITE_ACCESS shl 14) or ($800 shl 2) or METHOD_BUFFERED; IOCTL_REMOVE_NOTIFY = (FILE_DEVICE_UNKNOWN shl 16) or (0 shl 14) or ($801 shl 2) or 0; IOCTL_GET_PROCESS_DATA = (FILE_DEVICE_UNKNOWN shl 16) or (FILE_READ_ACCESS shl 14) or ($802 shl 2) or METHOD_BUFFERED; IMAGE_FILE_PATH_LEN = 512; type {$RTTI EXPLICIT METHODS([]) PROPERTIES([]) FIELDS([])} PROCESS_DATA = packed record bCreate: DWORD; dwProcessId: DWORD; { full process's image file path } szProcessName: array[0..IMAGE_FILE_PATH_LEN - 1] of AnsiChar; end;
12.2 ProcessMon的用户管理程序源码:
unit main; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, ComCtrls, WinSvc, common; type TForm1 = class(TForm) lvProcessInfo: TListView; procedure FormActivate(Sender: TObject); procedure FormCreate(Sender: TObject); procedure FormClose(Sender: TObject; var Action: TCloseAction); private { Private declarations } public { Public declarations } end; var Form1: TForm1; g_hEvent: THandle; g_fbExitNow: Boolean; g_hDevice: THandle; implementation uses GetData; var g_hSCManager: THandle; g_hService: THandle; tgd: TGetData; {$R *.dfm} procedure TForm1.FormActivate(Sender: TObject); var acModulePath: string; lpTemp: PChar; dwBytesReturned: DWORD; begin g_hSCManager := OpenSCManager(nil, nil, SC_MANAGER_ALL_ACCESS); if g_hSCManager <> 0 then begin acModulePath := GetCurrentDir + '\' + ExtractFileName('ProcessMon.sys'); g_hService := CreateService(g_hSCManager, 'ProcessMon', 'Process creation/destruction monitor', SERVICE_START or SERVICE_STOP or _DELETE, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, PChar(acModulePath), nil, nil, nil, nil, nil); if g_hService <> 0 then begin if StartService(g_hService, 0, lpTemp) then begin g_hDevice := CreateFile('\\.\ProcessMon', GENERIC_READ or GENERIC_WRITE, 0, nil, OPEN_EXISTING, 0, 0); if g_hDevice <> INVALID_HANDLE_VALUE then begin { No need it to be registered anymore } DeleteService(g_hService); { Create unnamed auto-reset event to be signalled when there is data to read. } g_hEvent := CreateEvent(nil, False, false, nil); { Create thread to wait event signalled. } tgd := TGetData.Create(False); if not DeviceIoControl(g_hDevice, IOCTL_SET_NOTIFY, @g_hEvent, SizeOf(g_hEvent), nil, 0, dwBytesReturned, nil) then begin ShowMessage('无法设置通知!'); end; end else begin ShowMessage('无法打开设备!'); end; end else begin ShowMessage('无法启动驱动!'); end; end else begin ShowMessage('无法注册驱动!'); end; end else begin ShowMessage('无法连接到SCM!'); end; end; procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); var dwBytesReturned: DWORD; _ss: SERVICE_STATUS; begin DeviceIoControl(g_hDevice, IOCTL_REMOVE_NOTIFY, nil, 0, nil, 0, dwBytesReturned, nil); g_fbExitNow := true; { If exception has occured not in loop thread it should exit now. } SetEvent(g_hEvent); Sleep(100); CloseHandle(g_hEvent); CloseHandle(g_hDevice); ControlService(g_hService, SERVICE_CONTROL_STOP, _ss); DeleteService(g_hService); CloseServiceHandle(g_hService); CloseServiceHandle(g_hSCManager); end; procedure TForm1.FormCreate(Sender: TObject); begin if(MessageDlg('由于条件所限,本驱动仅在Windows XP sp3上做过测试,是否继续?', mtWarning, [mbYes, mbNo], 0, mbYes) = mrNo) then begin Application.Terminate; end; end; end.
客户端程序比较简单,在Form的OnActivate事件处理函数中,我们加载驱动并启动它。如果启动成功则调用CreateEvent创建一个非信号态、自动重置的无名信号对象,并将其句柄保存在全局变量g_hEvent中,接下来,创建一个线程用于与内核驱动交互。这里,我使用了Delphi的TThread对象。
if not DeviceIoControl(g_hDevice, IOCTL_SET_NOTIFY, @g_hEvent, SizeOf(g_hEvent), nil, 0, dwBytesReturned, nil) then
OnClose事件处理函数完成退出前的清理工作,先调用DeviceIoControl函数向驱动程序发送IOCTL_REMOVE_NOTIFY控制码,让驱动程序停止跟踪,然后停止并卸载驱动。下面来看下线程对象。
12.4 线程对象
unit GetData; interface uses Windows, WinSvc, Classes, common, ComCtrls, SysUtils, Dialogs; type TGetData = class(TThread) private { Private declarations } ProcessData: PROCESS_DATA; procedure FillProcessInfo; protected constructor Create(CreateSuspended: Boolean); procedure Execute; override; end; implementation { Important: Methods and properties of objects in visual components can only be used in a method called using Synchronize, for example, Synchronize(UpdateCaption); and UpdateCaption could look like, procedure TGetData.UpdateCaption; begin Form1.Caption := 'Updated in a thread'; end; or Synchronize( procedure begin Form1.Caption := 'Updated in thread via an anonymous method' end ) ); where an anonymous method is passed. Similarly, the developer can call the Queue method with similar parameters as above, instead passing another TThread class as the first parameter, putting the calling thread in a queue with the other thread. } { TGetData } uses main; constructor TGetData.Create(CreateSuspended: Boolean); begin inherited Create(CreateSuspended); Priority := tpHighest; end; procedure TGetData.FillProcessInfo; var buffer: array[0..1023] of AnsiChar; rtnVal: DWORD; pTmp: PAnsiChar; tlItems: TListItem; iItemCnt: Integer; begin { The path can be it the short form. Convert it to long. } { If no long path is found or path is in long form, GetLongPathName } { simply returns the specified path. } FillChar(buffer, SizeOf(buffer), 0); rtnVal := GetLongPathName(@processData.szProcessName, @buffer, SizeOf(buffer)); if (rtnVal = 0) or (rtnVal >= SizeOf(buffer)) then begin { 1024 bytes was not enough. Just display whatever we've got from the driver. } { I want to keep the things simple. But you'd better to allocate more memory } { and call GetLongPathName again and again until the buffer size will } { satisfy the need. } pTmp := @processData.szProcessName; end else pTmp := @buffer; tlItems := Form1.lvProcessInfo.Items.Add; tlItems.Caption := string(pTmp); tlItems.SubItems.Add(Format('%8.8X', [processData.dwProcessId])); if ProcessData.bCreate <> 0 then tlItems.SubItems.Add('Created') else tlItems.SubItems.Add('Destroyed'); iItemCnt := Form1.lvProcessInfo.Items.Count; Form1.lvProcessInfo.Items[iItemCnt - 1].MakeVisible(True); end; procedure TGetData.Execute; var hThread: THandle; dwBytesReturned: DWORD; begin { Place thread code here } while True do begin if WaitForSingleObject(g_hEvent, INFINITE) <> WAIT_FAILED then begin if g_fbExitNow then Break; if DeviceIoControl(g_hDevice, IOCTL_GET_PROCESS_DATA, nil, 0, @ProcessData, SizeOf(ProcessData), dwBytesReturned, nil) then begin Synchronize(FillProcessInfo); end; end else begin ShowMessage('Wait for event failed. Thread now exits. Restart application.'); Break; end; Sleep(1); end; end; end.
constructor TGetData.Create(CreateSuspended: Boolean); begin inherited Create(CreateSuspended); Priority := tpHighest; end;
while True do begin if WaitForSingleObject(g_hEvent, INFINITE) <> WAIT_FAILED then begin if g_fbExitNow then Break; if DeviceIoControl(g_hDevice, IOCTL_GET_PROCESS_DATA, nil, 0, @ProcessData, SizeOf(ProcessData), dwBytesReturned, nil) then begin Synchronize(FillProcessInfo); end;
12.5 FillProcessInfo函数
有些时候,驱动程序返回的是短路径名(比如:C: \ PROGRA ~ 1 \ WinZip \ WinZip32.EXE),我也不明白为什么会这样。解决这个问题的方法也很简单,我们只需要调用windows提供的GetLongPathName函数。GetLongPathName的原型如下:
function GetLongPathName(lpszShortPath: PWideChar; lpszLongPath: PWideChar; cchBuffer: DWORD): DWORD; stdcall; ◎ lpszShortPath是欲转换的短路径名字符串缓冲区 ◎ lpszLongPath是转换成长路径名的接收缓冲区 ◎ cchBuffer则是接收缓冲区的长度
Var buffer: array[0..1023] of AnsiChar; … … FillChar(buffer, SizeOf(buffer), 0); rtnVal := GetLongPathName(@processData.szProcessName, @buffer, SizeOf(buffer)); if (rtnVal = 0) or (rtnVal >= SizeOf(buffer)) then begin { 1024 bytes was not enough. Just display whatever we've got from the driver. } { I want to keep the things simple. But you'd better to allocate more memory } { and call GetLongPathName again and again until the buffer size will } { satisfy the need. } pTmp := @processData.szProcessName; end else pTmp := @buffer;
unit ProcessMon; {$POINTERMATH ON} interface uses nt_status, common; function _DriverEntry(p_DriverObject: PDRIVER_OBJECT; pusRegistryPath: PUNICODE_STRING): NTSTATUS; stdcall; implementation uses ntoskrnl, fcall, macros, ProcPath; var g_usDeviceName, g_usSymbolicLinkName: UNICODE_STRING; g_pkEventObject: PKEVENT; g_fbNotifyRoutineSet: Boolean; g_ProcessData: PROCESS_DATA; g_dwImageFileNameOffset: DWORD; function DispatchCreateClose(p_DeviceObject:PDEVICE_OBJECT; p_Irp:PIRP): NTSTATUS; stdcall; begin p_Irp^.IoStatus.Status := STATUS_SUCCESS; p_Irp^.IoStatus.Information := 0; IofCompleteRequest(p_Irp, IO_NO_INCREMENT); result := STATUS_SUCCESS; end; procedure DriverUnload(pDriverObject:PDRIVER_OBJECT); stdcall; begin IoDeleteSymbolicLink(@g_usSymbolicLinkName); IoDeleteDevice(pDriverObject^.DeviceObject); end; procedure ProcessNotifyRoutine(dwParentId:HANDLE; dwProcessId:HANDLE; bCreate: DWORD); stdcall; var peProcess: PVOID; { PEPROCESS } fbDereference: Boolean; us: UNICODE_STRING; _as: ANSI_STRING; begin { reserve DWORD on stack } if PsLookupProcessByProcessId(dwProcessId, peProcess) = STATUS_SUCCESS then begin //pop peProcess ; -> EPROCESS fbDereference := True; { PsLookupProcessByProcessId references process object } end else begin { PsLookupProcessByProcessId fails (on w2k only) with STATUS_INVALID_PARAMETER } { if called in the very same process context. } { So if we are here it maight mean (on w2k) we are in process context being terminated. } peProcess := IoGetCurrentProcess; fbDereference := False; {IoGetCurrentProcess doesn't references process object } end; g_ProcessData.dwProcessId := dwProcessId; g_ProcessData.bCreate := bCreate; memset(@g_ProcessData.szProcessName, 0, SizeOf(IMAGE_FILE_PATH_LEN)); if GetImageFilePath(peProcess, @us) = STATUS_SUCCESS then begin //lea eax, g_ProcessData.szProcessName _as.Buffer := @g_ProcessData.szProcessName; _as.MaximumLength := IMAGE_FILE_PATH_LEN; _as._Length := 0; RtlUnicodeStringToAnsiString(@_as, @us, False); { Free memory allocated by GetImageFilePath } ExFreePool(us.Buffer); end else begin { If we fail to get process's image file path } { just use only process name from EPROCESS. } if g_dwImageFileNameOffset <> 0 then begin memcpy(@g_ProcessData.szProcessName, PAnsiChar(DWORD(peProcess) + g_dwImageFileNameOffset), 16); end; end; if fbDereference then begin ObfDereferenceObject(peProcess); end; { Notify user-mode client. } KeSetEvent(g_pkEventObject, 0, False); end; function DispatchControl(p_DeviceObject:PDEVICE_OBJECT; p_Irp:PIRP): NTSTATUS; stdcall; var liDelayTime: LARGE_INTEGER; pIoStkLoc: PIO_STACK_LOCATION; UserHandle: Handle; lpExEventObjectType: PPointer; pObjectType: Pointer; rtnCode: NTSTATUS; begin { Initialize to failure. } p_Irp^.IoStatus.Status := STATUS_UNSUCCESSFUL; p_Irp^.IoStatus.Information := 0; pIoStkLoc := IoGetCurrentIrpStackLocation(p_Irp); if pIoStkLoc^.Parameters.DeviceIoControl.IoControlCode = IOCTL_SET_NOTIFY then begin if pIoStkLoc^.Parameters.DeviceIoControl.InputBufferLength >= SizeOf(HANDLE) then begin if not g_fbNotifyRoutineSet then { For sure } begin UserHandle := Handle(p_Irp^.AssociatedIrp.SystemBuffer^); lpExEventObjectType := GetImportFunAddr(@ExEventObjectType); pObjectType := PVOID(lpExEventObjectType^); rtnCode := ObReferenceObjectByHandle(UserHandle, EVENT_MODIFY_STATE, pObjectType, UserMode, @g_pkEventObject, nil); if rtnCode = STATUS_SUCCESS then begin { If passed event handle is valid add a driver-supplied callback routine } { to a list of routines to be called whenever a process is created or deleted. } rtnCode := PsSetCreateProcessNotifyRoutine(@ProcessNotifyRoutine, False); p_Irp^.IoStatus.Status := rtnCode; if rtnCode = STATUS_SUCCESS then begin g_fbNotifyRoutineSet := True; DbgPrint('ProcessMon: Notification was set'#13#10); { Make driver nonunloadable } p_DeviceObject^.DriverObject^.DriverUnload := nil; end else begin DbgPrint('ProcessMon: Couldn''t set notification'#13#10); end; end else begin p_Irp^.IoStatus.Status := rtnCode; DbgPrint('ProcessMon: Couldn''t reference user event object. Status: %08X'#13#10, rtnCode); end; end; end else begin p_Irp^.IoStatus.Status := STATUS_BUFFER_TOO_SMALL; end; end else if pIoStkLoc^.Parameters.DeviceIoControl.IoControlCode = IOCTL_REMOVE_NOTIFY then begin { Remove a driver-supplied callback routine from a list of routines } { to be called whenever a process is created or deleted. } if g_fbNotifyRoutineSet then begin rtnCode := PsSetCreateProcessNotifyRoutine(@ProcessNotifyRoutine, True); p_Irp^.IoStatus.Status := rtnCode; if rtnCode = STATUS_SUCCESS then begin g_fbNotifyRoutineSet := False; DbgPrint('ProcessMon: Notification was removed'#13#10); { Just for sure. It's theoreticaly possible our ProcessNotifyRoutine is now being executed. } { So we wait for some small amount of time (~50 ms). } liDelayTime.HighPart := liDelayTime.HighPart or -1; liDelayTime.LowPart := ULONG(-1000000); KeDelayExecutionThread(KernelMode, False, @liDelayTime); { Make driver unloadable } p_DeviceObject^.DriverObject^.DriverUnload := @DriverUnload; if g_pkEventObject <> nil then begin ObfDereferenceObject(g_pkEventObject); g_pkEventObject := nil; end; end else begin DbgPrint('ProcessMon: Couldn''t remove notification'#13#10); end; end; end else if pIoStkLoc^.Parameters.DeviceIoControl.IoControlCode = IOCTL_GET_PROCESS_DATA then begin if pIoStkLoc^.Parameters.DeviceIoControl.OutputBufferLength >= SizeOf(PROCESS_DATA) then begin //mov eax, [esi].AssociatedIrp.SystemBuffer memcpy(p_Irp^.AssociatedIrp.SystemBuffer, @g_ProcessData, SizeOf(g_ProcessData)); p_Irp^.IoStatus.Status := STATUS_SUCCESS; p_Irp^.IoStatus.Information := SizeOf(g_ProcessData); end else begin p_Irp^.IoStatus.Status := STATUS_BUFFER_TOO_SMALL; end; end else begin p_Irp^.IoStatus.Status := STATUS_INVALID_DEVICE_REQUEST; end; { After IoCompleteRequest returns, the IRP pointer } { is no longer valid and cannot safely be dereferenced. } IofCompleteRequest(p_Irp, IO_NO_INCREMENT); Result := p_Irp^.IoStatus.Status; end; function GetImageFileNameOffset: DWORD; var iCnt: Integer; iRtnVal: Integer; pTmp: PAnsiChar; begin { Finds EPROCESS.ImageFileName field offset } { W2K EPROCESS.ImageFileName = 01FCh } { WXP EPROCESS.ImageFileName = 0174h } { WNET EPROCESS.ImageFileName = 0154h } { Instead of hardcoding above offsets we just scan } { the EPROCESS structure of System process one page down. } { It's well-known trick. } pTmp := PAnsiChar(IoGetCurrentProcess); iCnt := 0; iRtnVal := 0; { one page more than enough. } while iCnt < $1000 do begin { Case insensitive compare. } iRtnVal := _strnicmp(PAnsiChar(pTmp + iCnt), PAnsiChar('system'), 6); if iRtnVal = 0 then Break; Inc(iCnt) end; if iRtnVal = 0 then begin { Found. } Result := iCnt; end else begin { Not found. } Result := 0; end; end; function _DriverEntry(p_DriverObject: PDRIVER_OBJECT; pusRegistryPath: PUNICODE_STRING): NTSTATUS; stdcall; var status: NTSTATUS; pDeviceObject: PDEVICE_OBJECT; begin status := STATUS_DEVICE_CONFIGURATION_ERROR; RtlInitUnicodeString(@g_usDeviceName, '\Device\ProcessMon'); RtlInitUnicodeString(@g_usSymbolicLinkName, '\DosDevices\ProcessMon'); if IoCreateDevice(p_DriverObject, 0, @g_usDeviceName, FILE_DEVICE_UNKNOWN, 0, True, @pDeviceObject) = STATUS_SUCCESS then begin if IoCreateSymbolicLink(@g_usSymbolicLinkName, @g_usDeviceName) = STATUS_SUCCESS then begin p_DriverObject^.MajorFunction[IRP_MJ_CREATE] := @DispatchCreateClose; p_DriverObject^.MajorFunction[IRP_MJ_CLOSE] := @DispatchCreateClose; p_DriverObject^.MajorFunction[IRP_MJ_DEVICE_CONTROL] := @DispatchControl; p_DriverObject^.DriverUnload := @DriverUnload; g_fbNotifyRoutineSet := False; memset(@g_ProcessData, 0, SizeOf(g_ProcessData)); { it can be not found and equal to 0, btw } g_dwImageFileNameOffset := GetImageFileNameOffset; status := STATUS_SUCCESS; end else begin IoDeleteDevice(pDeviceObject); end; end; result := status; end; end.
12.7 DriverEntry
在DriverEntry中,我们做了一项额外的工作从EPROCESS结构中取得ImageFileName字段的偏移。在EPROCESS结构中,ImageFileName字段是这样定义的:
ImageFileName: array [0..15] of AnsiChar;
这个字段不是空结束字符串,所以ImageFileName字段最多只能保存文件名的前16个字符。我们会把这个字段的内容复制到自定义结构PROCESS_DATA中,但也仅在使用GetImageFilePath无法取得映像文件名的时候才会用到它。对于不同版本的Windows系统中,ImageFileName字段在EPROCESS结构中的偏移也是不同的(win2k、winxp、win2k3下,这个偏移依次是$1FC、$174和$154),不过有个很简单的众所周知的方法可以定位这个偏移,你只需要逐字节向后搜索EPROCESS结构查找“system”这个字符串,它的位置就是ImageFileName字段的偏移地址。当然,由于这是个众所周知的方法,所以如果某些有心人修改了那个字符串的内容,我们的方法就失效了。不过至少我没有修改我自己的系统,所以这个方法在我的系统上是有效的,当运行DriverEntry时我们是处于系统上下文中。
g_dwImageFileNameOffset := GetImageFileNameOffset;
调用GetImageFileNameOffset函数将返回ImageFileName字段在EPROCESS结构中的偏移或者返回零(如果没找到)。GetImageFileNameOffset函数很简单,我们来看下它是如何实现的。
pTmp := PAnsiChar(IoGetCurrentProcess); iCnt := 0; iRtnVal := 0; { one page more than enough. } while iCnt < $1000 do begin { Case insensitive compare. } iRtnVal := _strnicmp(PAnsiChar(pTmp + iCnt), PAnsiChar('system'), 6); if iRtnVal = 0 then Break; Inc(iCnt) end;
if iRtnVal = 0 then begin { Found. } Result := iCnt; end else begin { Not found. } Result := 0; end;
12.8 对IOCTL_SET_NOTIFY控制码的处理
当驱动程序接收到用户管理程序发送过来的IOCTL_SET_NOTIFY控制码后,驱动程序就要开始跟踪进程的创建与销毁了。
在驱动中定义了一个全局变量g_fbNotifyRoutineSet,如果驱动程序已经在跟踪进程的创建与销毁,此变量的值为True,否则为False。所以我们在在收到IOCTL_SET_NOTIFY控制码时先要检查g_fbNotifyRoutineSet的值,因为有可能此时驱动已经在跟踪进程的创建与销毁了。
UserHandle := Handle(p_Irp^.AssociatedIrp.SystemBuffer^);
UserHandle就是用户程序传送过来的Event对象句柄。在以后的操作中,我们需要使用指向对象有指针而不是对象描述符,因此我们必须要对用户传递过来的数据进行有效性检查,然后取得用户程序的Event对象的指针,这一点在驱动程序中是非常重要的。ObReferenceObjectByHandle函数可以替我们完成这项工作,它的原型是:
function ObReferenceObjectByHandle( _Handle: HANDLE; DesiredAccess: ACCESS_MASK; ObjectType: PVOID; AccessMode: KPROCESSOR_MODE; _Object: PVOID; HandleInformation: POBJECT_HANDLE_INFORMATION): NTSTATUS; stdcall; ◎ _Handle指定打开的对象句柄; ◎ DesiredAccess指定对象的访问请求类型,需要指出,这里的访问类型依赖于具体的对象类型,请不要使用常用的访问权限; ◎ ObjectType是一个指向对象类型的指针,在这里,对象类型可以是PExEventObjectType、 PExSemaphoreObjectType、PIoFileObjectType、PPsProcessType、PPsThreadType、PSeTokenObjectType、PTmEnlistmentObjectType、PTmResourceManagerObjectType、PTmTransactionManagerObjectType、PTmTransactionObjectType,如果此参数为nil,则由操作系统负责根据所传递的对象句柄去检查并匹配出对应的对象类型; ◎ AccessMode指定访问检查的方式,有UserMode和KernelMode两种,一般较低级的驱动都使用KernelMode; ◎ _Object用于接收返回的指向与对象句柄相关联的对象类型的指针; ◎ HandleInformation,这个参数在驱动程序中都设为nil。
本节将简要介绍一下Windows内核里提供的对象,Windows 2000有27种内核对象,Windows XP和Windows 2003则更多。我机器的操作系统是Windows XP SP3,有31种内核对象,以下是我机器上内核对象类型的截图:
以上对象都是真实存在于你的系统中的,点击它们中的任何一个都能取得与该对象相关的一些附加信息,每个对象都有一个OBJECT_TYPE结构与之对应。
当操作系统被请求去创建一个新的内核对象时,操作系统会根据所要求的对象匹配正确的OBJECT_TYPE结构。内核导出了部分对象类型结构(在ntoskrnl.exe中),比如用于描述WindowStation内核对象的OBJECT_TYPE结构通过ExWindowStationObjectType变量导出,本例中使用的Event内核对象则通过ExEventObjectType变量导出。
lpExEventObjectType := GetImportFunAddr(@ExEventObjectType); pObjectType := PVOID(lpExEventObjectType^); DbgPrint('ProcessMon: ExEventObjectType - %08X'#13#10, MmGetPhysicalAddress(pObjectType));
红框框住的是对象的名称,再看$0AC46090开始处的四个地址,这四个地址分别是:TotalNumberOfObjects(系统中现有Event对象的总数量)、TotalNumberOfHandles(系统中已打开的Event对象句柄数)、HighWaterNumberOfObjects(系统中最多可同时存在的Event对象数)、HighWaterNumberOfHandles(系统中最多可同时存在的Event句柄数),它们的值分别是:$157D(5501)、$1658(5720)、$2DBA(11706)、$2E91(11921),这个数值与我们用相关的工具软件查看Event属性时得到的值是一致的,见下图:
以上这些值是随时都会变化的,数值不同也是正常的,我只是截取的时机掌握的比较好,所以看着它们都是相同的^_^。
12.10 继续分析IOCTL_SET_NOTIFY
UserHandle := Handle(p_Irp^.AssociatedIrp.SystemBuffer^); lpExEventObjectType := GetImportFunAddr(@ExEventObjectType); pObjectType := PVOID(lpExEventObjectType^); DbgPrint('ProcessMon: ExEventObjectType - %08X'#13#10, MmGetPhysicalAddress(pObjectType)); rtnCode := ObReferenceObjectByHandle(UserHandle, EVENT_MODIFY_STATE, pObjectType, UserMode, @g_pkEventObject, nil);
rtnCode := PsSetCreateProcessNotifyRoutine(@ProcessNotifyRoutine, False); p_Irp^.IoStatus.Status := rtnCode;
Function PsSetCreateProcessNotifyRoutine( NotifyRoutine: PCREATE_PROCESS_NOTIFY_ROUTINE; Remove: Boolean): NTSTATUS; stdcall; ◎ NotifyRoutine是一个函数指针,指向我们要安装的回调函数的地址; ◎ Remove参数指定是安装回调函数还是删除回调函数,如果为TRUE,则是删除回调函数,如果为False则是安装回调函数。
TCREATE_PROCESS_NOTIFY_ROUTINE = procedure( ParentId: HANDLE; ProcessId: HANDLE; _Create: Boolean); stdcall;
可安装的回调函数的数量是有限制的,最多可以安装8个回调函数。DDK的文档中特别强调如果你安装了回调函数,那么请记住在中止驱动前要删除它。类似的函数还有PsSetCreateThreadNotifyRoutne、PsSetLoadImageNotifyRoutine,不过这两个函数与PsSetCreateProcessNotifyRoutine相比,少了Remove参数。
if rtnCode = STATUS_SUCCESS then begin g_fbNotifyRoutineSet := True; DbgPrint('ProcessMon: Notification was set'#13#10);
p_DeviceObject^.DriverObject^.DriverUnload := nil;
到了这里,一切都按我们的要求顺利实现了,现在要做的事情就是等待,等待系统中任何进程发生改变,一旦有这样的情况发生,我们的回调函数就将被调用。
12.11 ProcessNotifyRoutine回调函数
if PsLookupProcessByProcessId(dwProcessId, peProcess) = STATUS_SUCCESS then begin fbDereference := True; { PsLookupProcessByProcessId references process object } end else begin peProcess := IoGetCurrentProcess; fbDereference := False; {IoGetCurrentProcess doesn't references process object } end;
g_ProcessData.dwProcessId := dwProcessId; g_ProcessData.bCreate := bCreate;
memset(@g_ProcessData.szProcessName, 0, SizeOf(IMAGE_FILE_PATH_LEN)); if GetImageFilePath(peProcess, @us) = STATUS_SUCCESS then begin _as.Buffer := @g_ProcessData.szProcessName; _as.MaximumLength := IMAGE_FILE_PATH_LEN; _as._Length := 0; RtlUnicodeStringToAnsiString(@_as, @us, False); { Free memory allocated by GetImageFilePath } ExFreePool(us.Buffer);
end else begin { If we fail to get process's image file path } { just use only process name from EPROCESS. } if g_dwImageFileNameOffset <> 0 then begin memcpy(@g_ProcessData.szProcessName, PAnsiChar(DWORD(peProcess) + g_dwImageFileNameOffset), 16); end; end;
KeSetEvent(g_pkEventObject, 0, False);
未完待续