记录外壳活动有很多好处,比如当需要监控用户的行为,回溯系统崩溃前的过程。实现这一功能的关键工具相当简单,它就是COM接口IShellExecuteHook。编写一个实现了这一接口的COM对象后,再在系统中注册,就可以容易地控制并影响Windows外壳的运行。Windows 98和Windows 2000都支持IShellExecuteHook外壳扩展,而在Windows 95和Windows NT 4.0上则必须安装活动桌面扩展后才支持(也就是说必须安装IE 4.01)。 一个实现了IShellExecuteHook接口的COM对象可以截获所有对ShellExecute和ShellExecuteEx函数的调用。ShellExecute和ShellExecuteEx函数主要用于执行应用程序,它们可以接收一个文件名并能自动获得同文件名相关的可执行文件名。此外,它们还支持系统安全认证。如果在NT上设定了用户的可执行权限,ShellExecute和ShellExecuteEx函数将会在创建新的进程前检查权限(CreateProcess和WinExec函数则没有这项功能)。函数调用的流程如下: (1)获得将要运行的可执行文件名。 (2)根据程序名检查用户执行权限。 (3)激活全部已注册的IshellExecuteHook扩展。 (4)当所有扩展和权限都同意执行,创建新的进程并返回。 Windows外壳大量调用ShellExecute和ShellExecuteEx函数来执行几乎是所有的资源管理器的操作,比如双击目录、浏览文件夹内容、打印编辑文档、查看文件属性、选择文档的上下文相关菜单等等。此外,开始菜单的运行对话框和DOS方式下的Start.exe也使用ShellExecuteEx函数来执行程序。简单地说几乎用户的所有外壳操作都可以被扩展截获,包括其他应用程序对ShellExecute和ShellExecteEx的调用。 编写外壳活动记录器 首先需要创建一个进程内COM对象,选菜单命令New | ActiveX Library,然后点击菜单New|Com Object,创建COM对象框架,按图2.14填充对话框的内容,然后点击OK按钮。Delphi就会自动生成框架文件,并保存生成的文件。 IShellExecuteHook的接口定义在shlobj.pas单元中,添加shlobj到单元uses部分,然后添加IShellExecuteHooko方法原型到COM对象声明部分,声明部分代码如下: unit ShellExecuteHookObj; interface uses Windows, ActiveX, ComObj, ShlObj, ShellAPI; type TTShellExecuteHook = class (TComObject, IShellExecuteHook) protected function Execute(var ShellExecuteInfo: TShellExecuteInfo): HResult; stdcall; end; const Class_TShellExecuteHook: TGUID = '{935FA400-243D-11D3-B06E-857B2AE2BE64}'; 下面就是用来截获并记录外壳操作的实现部分,一旦外壳扩展被注册后,每次ShellExecute 和ShellExecuteEx函数运行时都会调用COM对象的Execute函数。我们的核心代码就是通过Execute方法实现的。方法定义如下: function TTShellExecuteHook.Execute( var ShellExecuteInfo: TShellExecuteInfo): HResult; Execute方法会从外壳获得一个类型为TshellExecuteInfo的参数,参数定义如下: _SHELLEXECUTEINFOA = record cbSize: DWORD; fMask: ULONG; Wnd: HWND; lpVerb: PAnsiChar; lpFile: PAnsiChar; lpParameters: PAnsiChar; lpDirectory: PAnsiChar; nShow: Integer; hInstApp: HINST; { Optional fields } lpIDList: Pointer; lpClass: PAnsiChar; hkeyClass: HKEY; dwHotKey: DWORD; hIcon: THandle; hProcess: THandle; end; 这个记录结构中的lpFile包含了要运行的文件名,而lpVerb则表明执行的动作,动作由一些标准的字符串代表,比如,open(打开)、print(打印)、edit(编辑)、explore(浏览)、properties(属性)、find(查找)和其他上下文菜单的命令名。 有时,lpFile并不包含可执行文件名,这是因为ShellExecute接到的运行参数是一个文档名。比如当我们在资源管理器中双击文本文件时,Windows用文本文件名作为参数调用ShellExecute函数,而ShellExecute函数则获得同文本文件相关联的可执行文件名,然后执行。 TShellExecuteInfo结构中还记录了要运行程序的很多信息,然而这里我们只能在Execute方法中修改nCmdShow参数,nCmdShow参数定义了窗口在运行后的显示状态,包括最大化、最小化、正常等选项,对于其他参数的修改都会被外壳忽略。除此之外,在Execute方法中可以根据情况允许外壳继续缺省的任务或通知外壳取消执行,这可以通过Execute函数的返回值来实现。 如果Execute的返回值为S_FALSE,外壳就继续缺省的任务,如果返回S_OK,则外壳认为扩展已经成功,就不再继续执行了。另外如果返回一个错误代码或系统无法识别的值,则外壳会弹出错误信息。这给了我们一个控制程序运行的机会,比如可以限制任何对记事本的调用,代码如下: function TTShellExecuteHook.Execute(var ShellExecuteInfo: TShellExecuteInfo): HResult; var FileName: String; begin Result := S_FALSE; with ShellExecuteInfo do begin FileName := UpperCase(ExtractFileName(lpFile)); if Pos('NOTEPAD', FileName) = 1 then begin Result := S_OK; hInstApp := 32; MessageBox(Wnd, '不允许记事本运行!', '错误', MB_OK or MB_ICONERROR); end; end; end; 进一步,我们甚至可以利用这点实现一个自定义的安全认证机制,根据用户要求限制运行的程序。有兴趣的朋友可以试验一下,一定很有意思。 有一点要注意的是,在Execute方法下不能调用ShellExecute和ShellExecuteEx函数外部程序,如果是这样的话,我们的Execute方法又会被新的ShellExecute调用,这样系统就会进入死循环。如果我们确实想在Execute方法中调用外部程序的话,可以使用CreateProcess或WinExec函数来替代。这两个函数不会被ShellExecuteHook截获。 对于外壳动作记录器来说,只要在Execute方法中记录程序信息到日志文件中就可以了,代码非常简单,因为所有需要的信息都在TShellExecuteInfo记录中包含了,这里只记录运行的动作、文件名和时间,需要记录其他信息的话,大家可自行修改,代码示意如下: function TTShellExecuteHook.Execute( var ShellExecuteInfo: TShellExecuteInfo): HResult; var FileStream: TFileStream; a:TStringList; S:string; begin Result := S_FALSE; with ShellExecuteInfo do begin FileStream:=TFileStream.Create('c:\shellexecutehook.txt',fmopenwrite); S:=string(lpVerb)+':'+string(lpFile)+DateTimeToStr(Now)+#13#10; FileStream.Seek(FileStream.Size,soFromBeginning); FileStream.Write(PChar(S)^,Length(S)); FileStream.Free; end; end; 注册ShellExecuteHook 要想使COM对象被外壳加载,需要在注册表中注册一些信息。在下面这个子键中添加COM类的GUID及描述字符串后就可以了(描述字符串可以不赋值,但不妨给一个以便于识别)。 HKEY_LOCAL_MACHINE SOFTWARE Microsoft Windows CurrentVersion Explorer ShellExecuteHooks {CLSID}= '描述字符串' 修改注册表可以通过重载COM的类工厂的UpdateRegistry方法来实现。代码示意如下: implementation uses ComServ, SysUtils; resourcestring sCreateRegKeyError = '创建注册表项失败'; type TShellExComObjectFactory = class(TComObjectFactory) public procedure UpdateRegistry(Register: Boolean); override; end; { TShellExComObjectFactory } procedure TShellExComObjectFactory.UpdateRegistry(Register: Boolean); const hellExecuteHooksKey='SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ShellExecuteHooks'; var Handle: HKey; Status, Disposition: Integer; ClassID: String; begin ClassID := GUIDToString(Class_TShellExecuteHook); if Register then begin Status := RegCreateKeyEx(HKEY_LOCAL_MACHINE, PChar( ShellExecuteHooksKey), 0, '',REG_OPTION_NON_VOLATILE, KEY_READ or KEY_WRITE, nil, Handle, @Disposition); if Status = 0 then begin Status := RegSetValueEx(Handle, PChar(ClassID), 0, REG_SZ, PChar(Description), Length(Description) + 1); RegCloseKey(Handle); end; end else begin Status := RegOpenKeyEx(HKEY_LOCAL_MACHINE, PChar(ShellExecuteHooksKey), 0, KEY_READ or KEY_WRITE, Handle); if Status = 0 then begin Status := RegDeleteValue(Handle, PChar(ClassID)); RegCloseKey(Handle); end; end; if Status <> 0 then raise EOleError.Create(sCreateRegKeyError); inherited UpdateRegistry(Register); end; initialization TShellExComObjectFactory.Create( ComServer, TTShellExecuteHook, Class_TShellExecuteHook,'TShellExecuteHook', 'ShellExecute hook sample', ciMultiInstance, tmApartment); end. 如果系统中有多个ShellExecuteHook的话,外壳会按照ShellExecuteHook的安装顺序进行调用,如果要想使某个外壳扩展优先运行,可先删除其他扩展然后添加优先扩展,原来的扩展依次放在后面,不过这样做也可能意义不大,因为别人也会这么干。最后,程序运行的结果。 记住ShellExecuteHook并不是一个完善的用于监视系统运行的扩展。它只能监视ShellExecute和ShellExecuteEx的运行,它不能保证记录系统所有的行为。特别是很多情况下外壳并不使用ShellExecute来进行一些常用的操作,比如我们在资源管理器中选择一个文件,然后调用右键菜单的属性命令后,记录器没有记录这个动作,但如果直接调用ShellExecute(如下示)的话,ShellExecuteHook却会正确执行。 ShellExecute(nil, 'properties', 'foo.txt',nil,nil,SW_SHOW); 这说明外壳并不使用ShellExecute函数显示属性对话框。总之一定要谨慎使用这项技术,确保它确实符合工作的需求。 |
|