欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-03-11 17:37:25
...

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

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

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

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

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.

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

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.

再看看这些接口能给我们一些什么信息。

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

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

Parameters

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。

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