記錄外殼活動有很多好處,比如當需要監控用戶的行為,回溯系統崩潰前的過程。實現這一功能的關鍵工具相當簡單,它就是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(
注冊ShellExecuteHook
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;
要想使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函數顯示屬性對話框。總之一定要謹慎使用這項技術,確保它確實符合工作的需求。