在 WinXP 下通过HOOK explorer来截获用户的复制,剪切,粘贴是很容易的。只需要HOOK以下三个API:
CopyFileExW, MoveFileWithProgressW, ReplaceFileW,
至于 MoveFileA等函数,最终也是调用这三个函数,这点可以通过用IDA反kernel32.dll分析得来。
但在WIN7下却不是这个回事了,我使用上述技术做的程序在XP下能正常使用,到了WIN7下根本不行。
经调试WIN7的EXPLORER发现,对文件的移动等操作竟然似乎直接跳过了API层。
上网一搜,原来WIN7的explorer使用了COM来代替旧有的文件API,这个COM接口就是 IFileOperation。
知道了是哪个家伙,想黑它就比较容易了。
在网上找到的这个接口的定义:
Interface IUnknown
QueryInterface
AddRef
Release
EndInterface
Interface IFileOperation Extends IUnknown
Advise(pfops, pdwCookie)
Unadvise(in_dwCookie.l)
SetOperationFlags(in_dwOperationFlags.l)
SetProgressMessage(in_string_LPCWSTR_pszMessage)
SetProgressDialog(popd_in)
SetProperties(pproparray_in)
SetOwnerWindow(in_hwndParent.l)
ApplyPropertiesToItem(psiItem_in)
ApplyPropertiesToItems(punkItems_in)
RenameItem(psiItem_in, in_string_LPCWSTR_pszNewName, pfopsItem_in_unique)
RenameItems(pUnkItems_in, in_string_LPCWSTR_pszNewName)
MoveItem(psiItem_in, psiDestinationFolder_in, in_unique_string_LPCWSTR_pszNewName, pfopsItem_in_unique)
MoveItems(punkItems_in, psiDestinationFolder_in)
CopyItem(psiItem_in, psiDestinationFolder_in, in_unique_string_LPCWSTR_pszCopyName, pfopsItem_in_unique)
CopyItems(punkItems_in, psiDestinationFolder_in)
DeleteItem(psiItem_in, pfopsItem_in_unique)
DeleteItems(punkItems_in)
NewItem(psiDestinationFolder_in, in_dwFileAttributes.l, in_unique_string_LPCWSTR_pszName, in_unique_string_LPCWSTR_pszTemplateName, pfopsItem_in_unique)
PerformOperations()
GetAnyOperationsAborted(pfAnyOperationsAborted_out_BOOL)
EndInterface
根据这个定义,我们可以知道 IFileOperation的虚函数表的样子,这样就为我们HOOK虚函数表提供了可能。
再根据虚函数表的静态特性,我们可以通过新建一个对象,修改这个对象的虚函数表,然后该类的其它对象也会受到影响。
虚函数表的细节不多说,只说下具体的HOOK相关代码。
写一个函数用来HOOK对象虛函数表,如下:
int HookVtbl(void* pObject, unsigned int classIdx, unsigned int methodIdx, int newMethod)
{
int** vtbl = (int**)pObject;
DWORD oldProtect = 0;
int oldMethod = vtbl[classIdx][methodIdx];
VirtualProtect(vtbl[classIdx] + sizeof(int*) * methodIdx, sizeof(int*), PAGE_READWRITE, &oldProtect);
vtbl[classIdx][methodIdx] = newMethod;
VirtualProtect(vtbl[classIdx] + sizeof(int*) * methodIdx, sizeof(int*), oldProtect, &oldProtect);
return oldMethod;
}
如果了解对象模型,上面代码应该很好理解。参数1指对象指针,参数二指类索引,即要HOOK自己继承的第几个类的虚函数表,
如果没有父或只有一个父,则设置成0即可。参数三指成员函数的索引,参数四指要替换成的函数地址。
如想HOOK对象a的第二个父的第三个函数,则应传参 HookVtbl(a, 1, 2, xxx);
如果对虚函数表比较熟,则很容易利用上面这个函数HOOK对象。
下面我们看要HOOK的 IFileOperation 的函数索引,根据上面列出的接口定义,做出如下定义:
#define QueryInterface_Index 0
#define AddRef_Index (QueryInterface_Index + 1)
#define Release_Index (AddRef_Index + 1)
#define Advice_Index (Release_Index + 1)
#define Unadvise_Index (Advice_Index + 1)
#define SetOperationFlags_Index (Unadvise_Index + 1)
#define SetProgressMessage_Index (SetOperationFlags_Index + 1)
#define SetProgressDialog_Index (SetProgressMessage_Index + 1)
#define SetProperties_Index (SetProgressDialog_Index + 1)
#define SetOwnerWindow_Index (SetProperties_Index + 1)
#define ApplyPropertiesToItem_Index (SetOwnerWindow_Index + 1)
#define ApplyPropertiesToItems_Index (ApplyPropertiesToItem_Index + 1)
#define RenameItem_Index (ApplyPropertiesToItems_Index + 1)
#define RenameItems_Index (RenameItem_Index + 1)
#define MoveItem_Index (RenameItems_Index + 1)
#define MoveItems_Index (MoveItem_Index + 1)
#define CopyItem_Index (MoveItems_Index + 1)
#define CopyItems_Index (CopyItem_Index + 1)
#define DeleteItem_Index (CopyItems_Index + 1)
#define DeleteItems_Index (DeleteItem_Index + 1)
#define NewItem_Index (DeleteItems_Index + 1)
#define PerformOperations_Index (NewItem_Index + 1)
#define GetAnyOperationAborted_Index (PerformOperations_Index + 1)
须注意的是我们需要提供一个同样原型的函数来HOOK旧有函数,如果胡乱来,很容易造成栈失衡,崩溃去吧。
如我们想HOOK CopyItems,则应定义:
typedef HRESULT (__stdcall* PCopyItems)(IFileOperation*, IUnknown*, IShellItem*);
static PCopyItems CopyItems_old = NULL;
第一个参数是this指针,后面是常规参数。
下面那个函数指针变量是为了存放旧有的函数,然后提供一个新函数用来代替旧函数:
HRESULT __stdcall CopyItems_new(IFileOperation *pThis,
IUnknown *punkItems,
IShellItem *psiDestinationFolder)
{
HRESULT hr = CopyItems_old(pThis, punkItems, psiDestinationFolder);
OutputDebugStringA("调用了 CopyItems ");
return hr;
}
这样一来,就可以使用 CopyItems_new 来代替旧有对象的 CopyItems 函数了。
为了方便,再定义宏如下:
#define HOOK(a, b) b##_old = (P##b)HookVtbl(a, 0, b##_Index, (PBYTE)b##_new)
然后在我们的DLL加载时,我们就可以调用如下函数来完成HOOK了:
static const IID CLSID_FileOperation = {0x3ad05575, 0x8857, 0x4850, {0x92, 0x77, 0x11, 0xb8, 0x5b, 0xdb, 0x8e, 0x09}};
static const IID IID_IFileOperation = {0x947aab5f, 0x0a5c, 0x4c13, {0xb4, 0xd6, 0x4b, 0xf7, 0x83, 0x6f, 0xc9, 0xf8}};
BOOL WINAPI StartHook()
{
PVOID pInterface = NULL;
CoInitialize(NULL);
HRESULT hr = CoCreateInstance(CLSID_FileOperation, NULL, CLSCTX_SERVER, IID_IFileOperation, &pInterface);
if(FAILED(hr))
{
OutputDebugStringA("CoCreateInstance 失败");
return FALSE;
}
HOOK(pInterface, CopyItems);
return TRUE;
}
注意CoCreateInstance之前应初始化一下。这个函数就利用了虚函数表的静态特性。
至于CLSID和IID,也是查到的。HOOK其他函数也是同样的做法,或者说HOOK所有的C++对象都是类似的做法。
上面红色的代码就是关键代码了,这些代码是从我前几天刚做的一个模块中摘取的部分,
这个模块是用来完成USB设备保护的,使用户不能COPY文件到移动设备。
代码稍有改动,并未完全测试它能否正常工作。由于某些原因,我的代码不便贴上来。
但是只要理解了整个过程,则上面的代码就很清晰了。
处理文件的关键在于获取要COPY的所有文件名和COPY后的所有文件名。
COPY后所有文件名可以通过要COPY的文件名与COPY目标的目录合成得到,如要把 D:\a.txt COPY 到 C:\test\ 目录下,
则可以轻松知道COPY后文件名为 C:\test\a.txt ,当然,这是一般情况,还有其他情况如目标文件夹下已有同名文件,
需要保存为 a (1).txt 等情况,这些问题处理不是什么麻烦事,在此略过。
现在以 CopyItems 函数的HOOK为例说明获取源文件名和目标文件名的方法。
typedef WCHAR WPATH[MAX_PATH];
typedef struct _FileOperationItem
{
UINT srcCounts;
WPATH* srcList;
WCHAR destFolder[MAX_PATH];
} FileOperationItem, *PFileOperationItem;
UINT GetFilesFromDataObject(IUnknown *iUnknown, WPATH **ppPath)
{
UINT uFileCount = 0;
IDataObject *iDataObject = NULL;
HRESULT hr = iUnknown->QueryInterface(IID_IDataObject, (void **)&iDataObject);
do
{
if(!SUCCEEDED(hr))
{
break;
}
FORMATETC fmt = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM stg = { TYMED_HGLOBAL };
if(!SUCCEEDED(iDataObject->GetData(&fmt, &stg)))
{
break;
}
HDROP hDrop = (HDROP)GlobalLock(stg.hGlobal);
if(hDrop == NULL)
{
break;
}
uFileCount = DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0);
if(uFileCount <= 0)
{
break;
}
*ppPath = new WPATH[uFileCount];
if(*ppPath != NULL)
{
for(UINT uIndex = 0; uIndex < uFileCount; uIndex++)
{
DragQueryFile(hDrop, uIndex, (*ppPath)[uIndex], MAX_PATH);
}
}
else
{
uFileCount = 0;
}
GlobalUnlock(stg.hGlobal);
ReleaseStgMedium(&stg);
} while (FALSE);
return uFileCount;
}
PFileOperationItem GetOperationItem()
{
if(!TlsGetValue(g_opSlot))
{
PFileOperationItem foi = new FileOperationItem();
memset(foi->destFolder, 0, sizeof(WCHAR) * MAX_PATH);
foi->srcCounts = 0;
foi->srcList = NULL;
TlsSetValue(g_opSlot, foi);
}
return (PFileOperationItem)TlsGetValue(g_opSlot);
}
void WINAPI SetOperationState(OperationType type)
{
if(TLS_OUT_OF_INDEXES != g_tlsSlot)
{
TlsSetValue(g_tlsSlot, (void*)type);
return;
}
g_opType = type;
}
HRESULT __stdcall CopyItems_new(IFileOperation *pThis,
IUnknown *punkItems,
IShellItem *psiDestinationFolder)
{
HRESULT hr = CopyItems_old(pThis, punkItems, psiDestinationFolder);
if(SUCCEEDED(hr))
{
SetOperationState(COPY_FILE);
LPWSTR lpDst = NULL;
psiDestinationFolder->GetDisplayName(SIGDN_FILESYSPATH, &lpDst);
PFileOperationItem foi = GetOperationItem();
wcscpy(foi->destFolder, lpDst);
foi->srcCounts = GetFilesFromDataObject(punkItems, &(foi->srcList));
CoTaskMemFree(lpDst);
}
return hr;
}
其中 GetFilesFromDataObject 函数是个关键所在,这个函数从 CopyItems_new 中的 IUnknow*
参数反查要操作的文件名。COPY目标直接可以从 IShellItem* 参数,调用 GetDisplayName 来获取目标文件夹。
简单结构 FileOperationItem 的 srcCounts 表示本次操作的文件数目,srcList 表示源文件列表,destFolder 表示目标目录。
另外两个TLS 辅助函数是因为 explorer 可以有多个线程同时COPY操作。
这样,在 CopyItems_new 函数中就可以获取完整的文件列表,目标目录等信息。
MoveItems 和这个是一个思路,废话不再多说。
但是这还没有完,可以注意到 CopyItems_new 中只是记录了要操作的文件信息,并没有做别的处理。
这是因为 IFileOperation 还有一个函数: PerformOperations 。
此函数用来提交已排队的 COPY/MOVE 等操作。
比如COPY的目标已经有同名文件,则会询问是否覆盖云云,这时候才是 PerformOperations 起作用的时候。
即是说,只有调用完 PerformOperations 之后,才知道本次COPY/MOVE是否成功。
则应HOOK PerformOperations 函数,并做相应处理。只有在成功的时候,才对文件进行处理。
然后无论如何,都要清理之前文件操作申请的内存。
大致如下:
HRESULT __stdcall PerformOperations_new(IFileOperation* pThis)
{
HRESULT hr = PerformOperations_old(pThis);
PFileOperationItem foi = GetOperationItem();
do
{
if(!SUCCEEDED(hr))
{
break;
}
if(!foi)
{
break;
}
for(int i = 0; i < foi->srcCounts; ++i)
{
......
}
.....
} while (FALSE);
if(foi && foi->srcList)
{
delete[] foi->srcList;
foi->srcList = NULL;
}
if(foi)
{
foi->srcCounts = 0;
}
return hr;
}
还一个细节在于,无论移动文件,删除文件,复制文件,都会调用 PerformOperations ,
所以如果需要的话,需要自己记录一下当前文件操作的类型,当然,这个类型信息也需要是 TLS 的。
以上代码为我项目实际代码中的摘除部分,表意为先,不一定可以直接使用。