基于EasyHook实现监控explorer资源管理器文件复制、删除、剪切等操作

 1 前言

最近自己在研究一个项目,需要实现对explorer资源管理器文件操作的监控功能,网上找到一些通过C++实现Hook explorer文件操作的方法,由于本人习惯用.NET开发程序,加之C/C++基础较差,所以一直在研究如何用.NET实现,花了一周多的时间,终于基本实现了通过C# Hook资源管理器文件操作的功能,这里给出一些核心的内容,供大家参考。

2 EasyHook

2.1 简介

EasyHook是一款功能强大的挂钩引擎,开源免费,支持64位,可通过.NET语言调用(如C#、VB.NET),相关使用方法和源码下载可参考官方网站

2.2 远程注入

我们这里需要远程注入explorer程序实现Hook资源管理器的功能,所以首先要了解一下EasyHook的远程注入(Remote Hooking)功能,官网有一篇远程文件监控的教程,可以参考这篇文章

简单总结一下:远程注入功能实现包括两个步骤,需要建立两个工程,一是创建一个injection payload,也就是创建需要注入到其他进程的类库(.dll)文件;二是创建一个主程序实现将第一步生成的dll注入到其他进程及监听功能。

步骤一主要用到两个类,ServerInterface和InjectionEntryPoint(类名可自定义),其中ServerInterface实现注入目标进程后的dll与主程序之间的通信功能,需要继承MarshalByRefObject类;InjectionEntryPoint实现本地Hook安装和Hook成功后调用用户自定义逻辑的功能,需要继承EasyHook.IEntryPoint接口,包括构造函数和Run方法,构造函数的Run方法的参数必须一致,这些参数可以在主程序远程注入时传入,参数个数不限,可根据具体需求自定义。比如我这里将主程序的进程ID和句柄作为参数

//构造函数
public InjectionEntryPoint(EasyHook.RemoteHooking.IContext context, string channelName, int passPID, int passHandle)
    {
          _server = EasyHook.RemoteHooking.IpcConnectClient<ServerInterface>(channelName);
           _server.Ping();
        }

 //Run函数
public void Run( EasyHook.RemoteHooking.IContext context,string channelName, int passPID,int passHandle)
       {
//这里实现本地hook安装和hook成功后调用用户自定义函数的功能
} 

步骤二是创建主程序,主要实现远程注入和监听显示的功能,注入时可根据步骤一的定义传入相关参数,核心代码如下

 // Create the IPC server using the FileMonitorIPC.ServiceInterface class as a singleton
EasyHook.RemoteHooking.IpcCreateServer<FileMonitorHook.ServerInterface>(ref channelName,System.Runtime.Remoting.WellKnownObjectMode.Singleton);
 //FileMonitorHook.dll为步骤一生成的类库文件,该文件需要在主程序工程中引用 
string injectionLibrary = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "FileMonitorHook.dll");  
 EasyHook.RemoteHooking.Inject( targetPID,    // ID of process to inject into
                    injectionLibrary,   // 32-bit library to inject (if target is 32-bit)
                    injectionLibrary,   // 64-bit library to inject (if target is 64-bit)
                    channelName  ,       // the parameters to pass into injected library
                     passPID ,
                    passHandle     // 这两个步骤一中为自定义参数,根据需求可以再增加...
                );

3 IFileOperation接口

WIN7及以上的系统的explorer实现文件操作直接跳过了API层,而是使用了COM来代替,这个COM接口就是 IFileOperation。

关于IFileOperation接口的详细说明可参考微软官方文档 ;

网上也有大神使用C++实现了Hook IFileOperation接口,可参考这篇文章

实现文件操作的监控主要需要Hook IFileOperation接口的以下函数(C#表示)

//复制 
 void CopyItems(IntPtr punkItems,IntPtr psiDestinationFolder);

//删除  
void DeleteItems(IntPtr punkItems);

//剪切  
void MoveItems(IntPtr punkItems,IntPtr psiDestinationFolder);

经测试CopyItem、DeleteItem、MoveItem单数形式的无法实现hook,可能是explorer根本没有调用这些函数。

4 EasyHook实现COM接口的Hook

网上关于EasyHook的介绍以及官方教程都是以Hook API函数为例,对于COM接口Hook的介绍几乎没有,这也困扰了比较长的一段时间,后面在EasyHook github官方主页的提问区找到了一丝线索,加上自己的一些研究,算是基本掌握了实现COM接口hook的方法。

实现方法主要包括两个步骤:

步骤一为根据guid定义COM接口所属的类和接口本身,类ID和接口ID可通过具体名称在c++头文件中查看,名称格式如CLSID_IFileOperation、IID_IFileOperation。在C#中定义如下:

#region IFileOperation
        [ComVisible(true)]
        [Guid("3ad05575-8857-4850-9277-11b85bdb8e09")]
        public class CLSID_IFileOperation
        {

//接口所属类的内容可为空,仅方便后续通过EasyHook API确定接口位置
        }
        [ComImport]
        [Guid("947aab5f-0a5c-4c13-b4d6-4bf7836fc9f8")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        public interface IID_IFileOperation
        {
            uint Advise(IntPtr pfops, IntPtr pdwCookie);
            void Unadvise(uint dwCookie);
            void SetOperationFlags(IntPtr dwOperationFlags);
            void SetProgressMessage(
              [MarshalAs(UnmanagedType.LPWStr)] string pszMessage);
            void SetProgressDialog(
              [MarshalAs(UnmanagedType.Interface)] object popd);
            void SetProperties(
              [MarshalAs(UnmanagedType.Interface)] object pproparray);
            void SetOwnerWindow(uint hwndParent);
            void ApplyPropertiesToItem(IntPtr psiItem);
            void ApplyPropertiesToItems(
              [MarshalAs(UnmanagedType.Interface)] object punkItems);
            void RenameItem(IntPtr psiItem,
              [MarshalAs(UnmanagedType.LPWStr)] string pszNewName,
               IntPtr pfopsItem);
            void RenameItems(
             IntPtr pUnkItems,
              [MarshalAs(UnmanagedType.LPWStr)] string pszNewName);
            void MoveItem(
              IntPtr psiItem,
               IntPtr psiDestinationFolder,
              [MarshalAs(UnmanagedType.LPWStr)] string pszNewName,
              IntPtr pfopsItem);
            void MoveItems(
               IntPtr punkItems,
              IntPtr psiDestinationFolder);
            void CopyItem(
              IntPtr psiItem,
               IntPtr psiDestinationFolder,
              [MarshalAs(UnmanagedType.LPWStr)] string pszCopyName,
              IntPtr pfopsItem);
            void CopyItems(
               IntPtr punkItems,
              IntPtr psiDestinationFolder);
            void DeleteItem(
              IntPtr psiItem,
               IntPtr pfopsItem);
            void DeleteItems(
             IntPtr punkItems);
            uint NewItem(
              IntPtr psiDestinationFolder,
              IntPtr dwFileAttributes,
              [MarshalAs(UnmanagedType.LPWStr)] string pszName,
              [MarshalAs(UnmanagedType.LPWStr)] string pszTemplateName,
              IntPtr pfopsItem);
            long PerformOperations();
            [return: MarshalAs(UnmanagedType.Bool)]
            bool GetAnyOperationsAborted();
        }
        #endregion

步骤二为通过EasyHook API 确定接口位置并安装钩子,以安装复制文件监控钩子为例,主要代码如下:

             //复制文件钩子
            COMClassInfo copyItemscom = new COMClassInfo(typeof(CLSID_IFileOperation),typeof(IID_IFileOperation), "CopyItems");  
            copyItemscom.Query();
            var CopyItemsHook = EasyHook.LocalHook.Create(copyItemscom.MethodPointers[0],new CopyItems_Delegate(CopyItemsHooked), this);
            // 激活钩子
            CopyItemsHook.ThreadACL.SetExclusiveACL(new Int32[] { 0 });

其中CopyItems_Delegate为文件复制的委托函数,可定义如下:

 [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode,SetLastError = false)]
//这里第一个参数需传入接口实例
//为的是可以在替换的复制hook函数中调用原始的复制函数,达到不影响原始操作的目的。
        public delegate void CopyItems_Delegate(IID_IFileOperation self, IntPtr punkItems,
           IntPtr psiDestinationFolder);  

CopyItemsHooked为成功Hook的替换执行的复制函数,其参数与文件复制委托函数一致,这个函数是实现文件操作监控的关键,下一部分将重点介绍。

5 文件操作Hook成功后替换函数的实现和解析

文件操作Hook成功后会执行我们替换的新函数,如前面介绍的CopyItemsHooked,在函数中可通过接口实例来调用原始函数来执行原始操作,其函数结构如下:

 public void CopyItemsHooked(IID_IFileOperation self, IntPtr punkItems,IntPtr psiDestinationFolder)
        {
          self.CopyItems(punkItems, psiDestinationFolder); //执行原始操作,也可不调用这句来实现拦截文件操作的目的
        }

我们要实现文件操作监控,就必须要对这个函数传入的参数进行解析,查看微软官方文档可以发现CopyItems的原型为:HRESULT CopyItems( IUnknown *punkItems, IShellItem *psiDestinationFolder );

其中punkItems为IUnknown接口类型,可能是IShellItemArrayIDataObject,  IEnumShellItems或者IPersistIDList中的某一种,psiDestinationFolder为IShellItem接口类型。

那么我们能通过C#直接获取到其中的文件信息呢?恐怕不行,至少我不会。这里也困扰了一些时间,后面终于想到解决方案了——.NET直接实现不了解析,C++总可以吧,先用C++编写相关解析方法,然后生成类库dll,再用C#调用C++生成的类库不就可以了。

这里需要掌握使用C++编写C#可以调用的类库的知识,不了解的同学可以参考这篇文章

需要注意的是C++生成的dll区分32位和64位,可以创建两个不同平台(win32和X64)的dll文件,达到兼容的目的,如图所示,我代码里用到了Unicode编码,所以字符集配置为使用 Unicode 字符集。

c++工程设置

话不多说,上代码。

获取IShellItem接口的文件信息函数:

LPWSTR __stdcall GetDestFloderName( IShellItem* psiDestinationIntptr)

{

    IShellItem* psiDestinationFolder = (IShellItem*)(psiDestinationIntptr);
    LPWSTR lpDst = NULL;

    psiDestinationFolder->GetDisplayName(SIGDN_FILESYSPATH, &lpDst);
    return lpDst;
}

获取复制、剪切文件IUnknown接口文件信息函数(经测试复制、剪切文件的IUnknown接口为IPersistIDList):

LPWSTR  __stdcall GetSrcFloderName(IUnknown *iUnknown)

{
    IPersistIDList   *iPersistIDList = NULL;
    HRESULT hr = iUnknown->QueryInterface(IID_IPersistIDList, (void **)&iPersistIDList); //通过iUnknown接口查找IPersistIDList接口
    PIDLIST_ABSOLUTE abSourcePidl;
    hr = iPersistIDList->GetIDList(&abSourcePidl);
    TCHAR tchPath[255];
    SHGetPathFromIDList(abSourcePidl, tchPath);
    return tchPath;

}

另外删除、重命名文件的IUnknown接口也有所不同,经测试删除文件IUnknown接口为IDataObject、重命名文件IUnknown接口为IShellItemArray,实现方法都大同小异(IDataObject文件信息解析相对复杂一点,前文第三节链接教程中提供了相应解析函数),在此不一一列出。附上解析类库的头文件:

#include "stdafx.h"
#include <Shlobj.h>
#include <string>
#include <shellapi.h>
using std::string;
using std::wstring;
typedef WCHAR WPATH[MAX_PATH];
extern "C" _declspec(dllexport) LPWSTR __stdcall GetDestFloderName( IShellItem*  psiDestinationIntptr);
extern "C" _declspec(dllexport) LPWSTR __stdcall GetSrcFloderName( IUnknown* punkItems);
extern "C" _declspec(dllexport) LPWSTR  __stdcall GetDeleteFileName( IUnknown * punkItems);
extern "C" _declspec(dllexport) LPWSTR __stdcall GetRenameSrcFloderName( IUnknown * punkItems)

C++类库生成成功后,如何在C#程序中调用呢?这里用到c# DllImport调用外部dll的功能(C++中的LPWSTR可直接对应C#中的sting),部分代码如下: 

   //调用64位dll
        [DllImport("MyConvert64.dll", EntryPoint = "GetDestFloderName", CharSet = CharSet.Unicode)]
        extern static string GetDestFloderName64(IntPtr psiIntptr);
        [DllImport("MyConvert64.dll", EntryPoint = "GetSrcFloderName", CharSet = CharSet.Unicode)]
        extern static IntPtr GetSrcFloderName64(IntPtr psiIntptr);

 //调用32位dll 
        [DllImport("MyConvert32.dll", EntryPoint = "GetDestFloderName", CharSet = CharSet.Unicode)]
        extern static string GetDestFloderName32(IntPtr psiIntptr);
        [DllImport("MyConvert32.dll", EntryPoint = "GetSrcFloderName", CharSet = CharSet.Unicode)]

extern static IntPtr GetSrcFloderName32(IntPtr psiIntptr); 

这里的函数名称可自定义,只需保证EntryPoint项的函数名称与dll中函数一致即可。那么如何让程序根据系统位数自动选择不同的函数呢?可以这样实现:

  public static string GetDestFloderName(IntPtr psiIntptr)
        {
            return Environment.Is64BitOperatingSystem ? GetDestFloderName64(psiIntptr) : GetDestFloderName32(psiIntptr);
        }
 public static IntPtr GetSrcFloderName(IntPtr psiIntptr)
        {
            return Environment.Is64BitOperatingSystem ? GetSrcFloderName64(psiIntptr) : GetSrcFloderName32(psiIntptr);
        }

这样我们就可以在hook成功后的替换函数中使用C++编写的解析函数了,以复制文件hook函数为例:

public void CopyItemsHooked(IID_IFileOperation self, IntPtr punkItems,
              IntPtr psiDestinationFolder)
        {
            try
            {
                string filename = GetDestFloderName(psiDestinationFolder); //调用解析函数获取目标文件夹信息
                IntPtr srcintptr = GetSrcFloderName(punkItems); //这里获取源文件返回的是一个指针
                string ss = Marshal.PtrToStringUni(srcintptr); //可将指针转换为string信息,注意是Unicode编码
                operationtag = "复制文件-messagesplit-" + ss + "-messagesplit-" + filename;
                self.CopyItems(punkItems, psiDestinationFolder); //执行原始操作
                _server.ReportMessage(selfhandle, "开始-messagesplit-" + operationtag); //向主程序传递复制文件操作信息

            }
            catch (Exception ee)
            {
                _server.ReportMessage(selfhandle, ee.ToString()); //异常处理

            }
        }

6 文件操作Hook效果

完成Injection payload(也就是远程注入的dll)的创建后,根据上文介绍创建一个主程序来实现远程注入和监听的功能,explorer的进程ID可通过以下代码获取: Process.GetProcessesByName("explorer")[0].Id;远程注入操作见第二节或参考EasyHook官方文档。运行主程序完成远程注入后即可在实现对explorer文件操作的监控,效果如下图所示。          

效果展示

7 结语

EasyHook确实让Hook变得Easy了,给了我很大的帮助,在此感谢开发团队的无私奉献。以上代码为我实际项目中部分内容,表意为先,不一定能直接使用,写这篇文章的目的是为了让读者加深对EasyHook的理解,希望能有所帮助。

P.S:第一次写博客,转载请注明出处,谢谢

-------------------------------------2020年7月17日补充---------------------------------------

好久没有关注博客了,回看之前写的这篇文章,格式真是一言难尽,但不管怎么说,当时确实是花了很多时间才研究出来,给过去的自己点个赞。

话不多说,看到有不少同学的提问和源码需求,我这里整理了一下,把相关代码上传到腾讯工蜂上了,有兴趣的同学可以下载使用。

年代有点久远,有些细节我确实也记不太清了,请大家见谅。

工蜂git项目地址

  • 5
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 13
    评论
目前最好的EasyHook的完整Demo程序,包括了Hook.dll动态库和Inject.exe注入程序。 Hook.dll动态库封装了一套稳定的下钩子的机制,以后对函数下钩子,只需要填下数组表格就能实现了,极大的方便了今后的使用。 Inject.exe是用MFC写的界面程序,只需要在界面上输入进程ID就能正确的HOOK上相应的进程,操作起来非常的简便。 这个Demo的代码风格也非常的好,用VS2010成功稳定编译通过,非常值得下载使用。 部分代码片段摘录如下: //【Inject.exe注入程序的代码片段】 void CInjectHelperDlg::OnBnClickedButtonInjectDllProcessId() { ////////////////////////////////////////////////////////////////////////// //【得到进程ID值】 UINT nProcessID = 0; if (!GetProcessID(nProcessID)) { TRACE(_T("%s GetProcessID 失败"), __FUNCTION__); return; } ////////////////////////////////////////////////////////////////////////// //【得到DLL完整路径】 CString strPathDLL; if (!GetDllFilePath(strPathDLL)) { TRACE(_T("%s GetDllFilePath 失败"), __FUNCTION__); return; } ////////////////////////////////////////////////////////////////////////// //【注入DLL】 NTSTATUS ntStatus = RhInjectLibrary(nProcessID, 0, EASYHOOK_INJECT_DEFAULT, strPathDLL.GetBuffer(0), NULL, NULL, 0); if (!ShowStatusInfo(ntStatus)) { TRACE(_T("%s ShowStatusInfo 失败"), __FUNCTION__); return; } } //【Hook.dll动态库的代码片段】 extern "C" __declspec(dllexport) void __stdcall NativeInjectionEntryPoint(REMOTE_ENTRY_INFO* InRemoteInfo) { if (!DylibMain()) { TRACE(_T("%s DylibMain 失败"), __FUNCTION__); return; } } FUNCTIONOLDNEW_FRMOSYMBOL array_stFUNCTIONOLDNEW_FRMOSYMBOL[]= { {_T("kernel32"), "CreateFileW", (void*)CreateFileW_new}, {_T("kernel32"), "CreateFileA", (void*)CreateFileA_new}, {_T("kernel32"), "ReadFile", (void*)ReadFile_new} }; BOOL HookFunctionArrayBySymbol() { /////////////////////////////////////////////////////////////// int nPos = 0; do { /////////////////////////////// FUNCTIONOLDNEW_FRMOSYMBOL* stFunctionOldNew = &g_stFUNCTIONOLDNEW_FRMOSYMBOL[nPos]; if (NULL == stFunctionOldNew->strModuleName) { break; } /////////////////////////////// if (!HookFunctionBySymbol(stFunctionOldNew->strModuleName, stFunctionOldNew->strNameFunction, stFunctionOldNew->pFunction_New)) { TRACE(_T("%s HookFunctionBySymbol 失败"), __FUNCTION__); return FALSE; } } while(++nPos); /////////////////////////////////////////////////////////////// return TRUE; } HANDLE WINAPI CreateFileW_new( PWCHAR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile ) { TRACE(_T("CreateFileW_new. lpFileName = %s"), lpFileName); return CreateFileW( lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); }
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值