win7及以上系统C++实现Hook explorer文件级监控复制、剪切和删除操做

最近项目组须要实现一个对windows用户文件操做进行监控的功能,(也就是使用explorer资源管理器的操做),因而乎我就想到了使用Hook的方法进行拦截,查找一番资料后发现XP调用的是最简单的CopyFileEx, MoveFileWithProgressW, ReplaceFileW之类的API,因此XP是最好解决的,可是到了Vista及之后的系统中,微软采用了一种新的方法——com组件里的IID_IFileOperation接口类。这样的话就有点麻烦了,可是资料中也有人实现了对com组件的HOOK,是经过修改从com组件的虚表从而实现挂钩。所以,我也尝试了一下这种方法,就是Hook IFileOperation中的CopyItems/MoveItems/DeleteItem等接口从而实现对文件操做的监控。具体方法就下面给出。使用这种方法的过程当中,我发现了一些问题,以致于我没法彻底套用这种方法。问题以下:html

  1. 经过Hook以上接口没法达到文件级的监控,就是说若是我选择了一个文件夹,而后对它进行复制或者剪切操做,则获取到的文件操做信息只有这个文件夹的路径和目的路径,至于文件夹里有什么,是否有套层,无从得知。PS:这个接口里是有个单数形式的CopyItem/MoveItem/DeleteItem的系列接口API,可是不管我怎么Hook他们,都不会被触发,也许是我使用的方法不对把,或者这几个API根本没有被调用。
  2. 经过以上接口我只能获得用户有该操做的意向,若是用户在过程当中点击了取消,或者操做根本没有成功,我无从得知详情。
  3. 当遇到有文件名重复后获得新文件名时,我没法获取到准确的新文件名,由于接口所给的只有源路径和目的目录的路径(新文件路径还得我本身拚...),并无更加详细的信息能够供我使用。

因为以上几种缘由,我没法经过上述方法实现我想要的功能,因此我开始另辟蹊径,我认为windows应该还有更详细更底层一点的接口能够供我使用,通过多天的查找和尝试,我终于找到了一个符合我需求的接口IFileOperationProgressSink。windows

对于这个接口的描述我给出微软的说明:api

Exposes methods that provide a rich notification system used by callers of IFileOperation to monitor the details of the operations they are performing through that interface.app

意思应该是说这个接口是用来响应IFileOperation的各类操做和消息的,还能获得更为详细的操做信息。而在这个接口类中,有着许多我寻找了许久的API,好比PreCopyItem/PreMoveItem/PreDeleteItem以及PostCopyItem/PostMoveItem等等系列API,而且根据微软官方的说明,Pre前缀系列的API是指在操做进行前的消息处理,Post前缀的系列API是指在操做完成后所调用的处理函数。下面以CopyItem为例,给出微软的说明。ide

IFileOperationProgressSink::PreCopyItem method

Performs caller-implemented actions before the copy process for each item begins.函数

IFileOperationProgressSink::PostCopyItem method

Performs caller-implemented actions after the copy process for each item is complete.post

再看看这些接口能给咱们一些什么信息。测试

如下是接口的定义和参数说明。this

HRESULT PostCopyItem(
  DWORD      dwFlags,
  IShellItem *psiItem,
  IShellItem *psiDestinationFolder,
  LPCWSTR    pszNewName,
  HRESULT    hrCopy,
  IShellItem *psiNewlyCreated
);

Parameters.net

dwFlags

bitwise value that contains flags that were used during the copy operation. Some values can be set or changed during the copy operation. See TRANSFER_SOURCE_FLAGS for flag descriptions.

psiItem

Pointer to an IShellItem that specifies the source item.

psiDestinationFolder

Pointer to an IShellItem that specifies the destination folder to which the item was copied.

pszNewName

Pointer to the new name that was given to the item after it was copied. This is a null-terminated Unicode string. Note that this might not be the name that you asked for, given collisions and other naming rules.

hrCopy

The return value of the copy operation. Note that this is not the HRESULT returned by CopyItem, which simply queues the copy operation. Instead, this is the result of the actual copy.

psiNewlyCreated

Pointer to an IShellItem that represents the new copy of the item.

从这里咱们就能看到,以上的问题都可以解决了。从API的命名能够看出这个接口是文件级的(由于是单数。。已测试),从参数能够直接获取到操做后的新文件名,甚至直接获得新文件的路径,都不用你去拼接,还有操做的结果也有,你可根据是否操做成功来进行操做。这样的话,你是想监控(Post)仍是想拦截(Pre)都也能够了。

找到这里我觉得我已经完成任务了,这么完美的接口,直接Hook不就完事了吗?然而事情并无这么简单。

我尝试使用跟上面同样的方法去HOOK这个接口,结果卡住了,由于Hook com组件的时候须要调用CoCreateInstance API来获取对应com组件的接口地址,从而Hook接口中对应的API,而调用这个API的最重要的两个参数(第一个和第四个),分别是这个com组件的方法类ID(CLSID),以及对应的接口类ID(IID),而我翻遍了全部的相关头文件和微软的相关资料都只有我须要的接口类IID_IFileOperationProgressSink{04b0f1a7-9490-44bc-96e1-4296a31252e2},没法找到对应的CLSID,我甚至去暴力遍历了对应注册表全部的CLSID挨个试,都没能找到。。。挣扎了2,3的天的我终于意识到微软压根就没给这个类的CLSID。因而我放弃了经过正道去Hook这个接口,而后在IFileOperation接口里找到了一个神奇的API——Advise。

PS1:在通过对不一样系统的测试时,我发现了使用Advise函数进行挂钩的行为不是很稳定,有时容易致使explorer崩溃,因此我在项目中放弃了这个方法,改为了使用下面这个方法。

PS2:在以后的尝试中,我不放弃的又去轮了一遍注册表,结果发现当时漏掉了很多注册表项,而后我就在win7 64位上找到了好几个(通常是3个)CLSID可以与上面的IID进行匹配,这三个CLSID分别对应不一样的操做行为,想要研究一下的人能够试试这个方法。

我所遍历到的CLSID以下:

//B能挂复制,删除,剪切,重命名,Win7有效
static const IID CLSID_IFileOperationProgressSinkB = {0x7abbbcca, 0xe01b, 0x4560, {0x82, 0x28, 0x92, 0xe1, 0xee, 0xdb, 0xc9, 0x08}};
//C能挂新建
static const IID CLSID_IFileOperationProgressSinkC = {0xD969A300, 0xE7FF, 0x11d0, {0xA9, 0x3B, 0x00, 0xA0, 0xC9, 0x0F, 0x27, 0x19}};
//Win10的复制,删除,剪切,重命名,功能与B相同
static const IID CLSID_IFileOperationProgressSinkD = {0xD105A4D4, 0x344C, 0x48EB, {0x98, 0x66, 0xEE, 0x37, 0x8D, 0x90, 0x65, 0x8B}};

IFileOperation::Advise method

Enables a handler to provide status and error information for all operations.

HRESULT Advise(
  IFileOperationProgressSink *pfops,
  DWORD                      *pdwCookie
);

Type: IFileOperationProgressSink*

Pointer to an IFileOperationProgressSink object to be used for progress status and error notifications.

我发现这个函数的第一个参数就是我要的IFileOperationProgressSink的接口指针,而后我再看这个函数的说明,他说在全部的操做进行时都会调用它,再看一下第一个参数的说明,说会把全部的错误和状态都发送给IFileOperationProgressSink对象去处理,那就是说这两个接口是相互依存的了,那我可不能够经过Hook这个接口来获取接口地址再去Hook IFileOperationProgressSink类的相关方法呢?那就来试试吧。

使用Hook CopyItems同样的方法来Hook 一下Advise。具体实现以下:

static HRESULT __stdcall MyAdvise(IFileOperation* pThis,IFileOperationProgressSink *pfops,DWORD *pdwCookie)
{
	OutputDebugString(_T("--------------Advise happened! ----------------"));

		//判断跳过回收站操做,挂上回收站会崩溃,回收站为0
	if (pfops != NULL && *pdwCookie != 0)
	{
		if ((PostCopyItem_old == NULL/* || gs_bCopyFlag == FALSE*/) )
		{
			PostCopyItem_old = (PPostCopyItem)HookVtbl((LPVOID)pfops, 0, PostCopyItem_Index, (size_t)MyPostCopyItem);
			if (PostCopyItem_old == NULL) OutputDebugString(_T("PostCopyItem Hook ERR!\n")); 
		}
		if ((PostMoveItem_old == NULL/* || gs_bMoveFlag == FALSE*/))
		{
			PostMoveItem_old = (PPostMoveItem)HookVtbl((LPVOID)pfops, 0, PostMoveItem_Index, (size_t)MyPostMoveItem);
			if (PostMoveItem_old == NULL) OutputDebugString(_T("PostMoveItem Hook ERR!\n")); 
		}
		if ((PostDeleteItem_old == NULL || gs_bDelFlag == FALSE) && g_nSysVerCode <= SYS_VER_7)
		{
			//若是挂错了,从新挂回去
			if (PostDeleteItem_old != NULL && gs_bDelFlag == FALSE)
				HookVtbl(pfops, 0, PostDeleteItem_Index, (size_t)PostDeleteItem_old);
			PostDeleteItem_old = (PPostDeleteItem)HookVtbl((LPVOID)pfops, 0, PostDeleteItem_Index, (size_t)MyPostDeleteItem);
			if (PostMoveItem_old == NULL) OutputDebugString(_T("PostDeleteItem Hook ERR!\n")); 
		}
		
	}

	return Advise_old(pThis, pfops, pdwCookie);
}
static HRESULT __stdcall MyPostCopyItem(IFileOperationProgressSink * This, DWORD dwFlags, IShellItem *psiItem,
							   IShellItem *psiDestinationFolder,LPCWSTR pszNewName,HRESULT hrCopy,IShellItem *psiNewlyCreated)
{
	OutputDebugString(_T("---------------PostCopyItem happened------------------\n\n"));
	
	if (pszNewName != NULL && wcslen(pszNewName) > 0 && SUCCEEDED(hrCopy))
	{
		LPWSTR lpTmp = NULL;
	    CString TmpSrc,TmpDst;

	    psiItem->GetDisplayName(SIGDN_FILESYSPATH, &lpTmp);
	    TmpSrc.Format(_T("Src: %s\n"), lpTmp);
	    OutputDebugString(TmpSrc);

	    CoTaskMemFree(lpTmp);
	
		psiNewlyCreated->GetDisplayName(SIGDN_FILESYSPATH, &lpTmp);
		TmpDst.Format(_T("Dst: %s\n"), lpTmp);
		OutputDebugString(TmpDst);
		CoTaskMemFree(lpTmp);
	}

	return PostCopyItem_old(This, dwFlags, psiItem, psiDestinationFolder, pszNewName, hrCopy, psiNewlyCreated);
}

通过测试,完美实现了文件级监控的功能。

说明几点问题:

  1. 使用com组件接口的时候要注意选择接口的类型,shobjidl.h里给出了两种接口,C和C++这两种,使用的时候要统一,这两种参数个数不同,C接口要比C++的多一个,否则会致使explorer崩溃。(就由于各类崩溃的问题我险些都放弃了。。。)
  2. 文件级的监控仅限于复制和剪切操做,删除文件夹的时候只会调用一次,好像是由于windows的回收站机制的问题。
  3. 在查找资料的时候发现好多帖子都是同样的方法,有的甚至一个字都不变直接复制粘贴的。。也是醉了。。
  4. 本人菜鸟一名,若是有说的不对的地方,还请各位大大多多谅解,多多指教。
  5. 具体的项目和代码我回头整理一下会上传到csdn上。

参考资料:

http://www.freebuf.com/column/134192.html

https://blog.csdn.net/zcl2770/article/details/52885634?locationNum=15&fps=1

https://bbs.csdn.net/topics/390692709?list=lz

https://docs.microsoft.com/zh-cn/windows/desktop/api/shobjidl_core/nf-shobjidl_core-ifileoperation-advise

https://docs.microsoft.com/zh-cn/windows/desktop/api/shobjidl_core/nf-shobjidl_core-ifileoperationprogresssink-postcopyitem