目录
附注
问题背景
实验测试
解决方案
方案对比
参考文档
1、解释下问题含义,指在一个64位Windows系统上面(譬如Win7x64,Win10x64),如果一个32位的程序调用系统API打开文件对话框(譬如打开文件,保存文件)或者打开文件夹对话框,在指向系统盘下的Windows\System32目录时,会发现实际指向的是Windows\SysWOW64目录问题。
2、关于64位系统文件重定向问题,网上已经有很多文章进行了介绍,这里不再赘述(参考[1])。
3、测试验证环境:Windows10 x64 1909专业版。
4、打开/保存文件和打开文件夹,主要涉及API是GetOpenFileName和SHBrowseForFolder/SHGetPathFromIDList等(备注:打开文件夹会略微繁琐一点)。
最近有客户反馈,使用我们提供的安全软件,在一些特殊场景(譬如信任文件),无法找到C:\Windows\System32下面一个指定的文件的文件(客户是想加白这个目录下面的一个文件)。收到反馈后,我们试验了下,发现真的有问题,不过初步判断应该就是x64系统上面的文件重定向问题(无法在文件对话框看到真正的c:\windows\system32下的子文件夹或者文件的原因是,被重定向到了c:\windows\syswow64下面,看到的都是这个目录下的文件和文件夹)。然而真正去解决的时候发现,bug比想象中难修复。
先准备这样一个环境:分别通过系统explorer手动建下面4个文件夹(也可以再在里面放一些具有标识作用的特殊文件):
C:\Windows\System32\00 我是真正的System32的子目录
C:\Windows\System32\01 大家都有的文件夹
C:\Windows\SysWOW64\00 我是SysWOWO64的子目录
C:\Windows\SysWOW64\01 大家都有的文件夹
(备注:标黄的部分是文件夹,如下图所示)
1、用我们的安全软件,打开测试发现,定位到c:\Windows\System32目录发现展示果然实际上SysWOW64,此时是看不到真正的c:\Windows\System32下面的文件的。
2、测试了一些常见的IM软件和系统软件,发现IM软件基本上都有类似问题,但是对它们影响不大,因为IM软件打开保存文件主要是为了传文件,一般不会需要传一个system32下的文件,即使看不到也可以拷贝到桌面再传。至于系统软件,如果是64位的软件是没有这个问题的,但是32位的软件也一样有问题,但是基本上都不影响使用。
问题比较明确,而且很显然就是前面的提到的文件重定向(这个特性是微软为了兼容32位程序的特意实现)问题,看起来似乎也有官方的解决方案,那就是关闭文件的重定向(参考[2] [3])。
1、在打开文件对话框前,直接调用Wow64DisableWow64FsRedirection先禁用文件重定向,关闭文件对话框之后再调用Wow64RevertWow64FsRedirection开启文件重定向,嗯,官方方案,稳定可靠,1个小时解决(备注,因这两个API只在vista之后才提供,因此如果程序还需要兼容xp,请通过导出函数的方式调用)!10分钟写完代码,测试,傻眼了,问题依旧。
对着API文档再撸了一遍,想起来这两个API是线程级的,也就是只影响当前线程(Disabling file system redirection affects only operations made by the current thread.),并不影响其它线程,这其实是微软的一个绝妙的设计,如果影响整个进程,那这两个API可能基本上就没什么用了。但是在这里,经过调试和观察发现,调用系统打开文件对话框之后,系统显然使用了其它的线程去枚举文件夹和文件,因此,在调用线程去禁用文件重定向自然是无用了。
2、难道微软就没有给打开文件对话框的API(以及类似相关API)提供可以直接禁用文件重定向的标识或者参数?查了下还真没有,网上也几乎没有相关资料,不知道是没人遇到到类似问题还是这个问题一般不严重(譬如IM软件,你把文件拷贝出来即可)?
3、做了一些初步的调试,发现微软在展示文件对话框时也有文件夹和文件的枚举动作,那么我们把这些API inline Hook起来,在微软调用前,先禁用重定向,待枚举完成再打开重定向,是否可行?看起来似乎可以,但是打开重定向的时机似乎不太好选择,而且目前只调试了一个系统,不清楚其它的系统是否也是类似的调用序列。而且这个方案也是有副作用的,因为一旦Hook生效之后,是基本上没法判断线程的归属的,可能另外一条线程也在枚举,但是它可能不需要关闭重定向。想想这个方案的可实践性太差,放弃。
4、我们自己实现一个文件对话框呢?逻辑和原理上完全可控,但是工作量太大,而且如果没有一个好的界面引擎支持,实现起来也是比较麻烦的,而且功能还一定完全满足,只是想了下感觉就可以放弃了(从有想法到放弃,估计10秒吧),当然还真的有自己实现的,只不过功能比较单薄(搜索,后缀过滤这些要实现都会比较麻烦)。
5、(备注:这里即将谈到的一个方案网上有人提到过,但是没有实现,这里简单实现了一下)前面提到官方提供的API(禁用/开启文件文件重定向)是线程级别的,在调用打开文件对话框API之后,我们是否有时机可以把一些新启动的线程的文件重定向全部关闭,待关闭文件对话框之后,再取消这个动作呢?思考了了一下,还真的有,DllMain有4个事件,其中一个是DLL_THREAD_ATTACH,在DLL成功加载之后,所有的新线程启动,都会在新线程环境,以这个参数调用一次所有DLL模块的DllMain函数,说干就干,这个DLL的代码很简单,提供两个导出函数,用于控制这个DLL里面的一个全局变量,该全局变量用于在新线程启动时判断是否需要禁用重定向。
调用侧在启动文件对话框(调用相关API前)时,先加载此DLL,然后调用导出函数DisableFsRedirection即可,在关闭文件对话框之后,调用导出函数RevertFsRedirection。
实验了一下,还是能解决问题的。但是这种方案也是有副作用的,还是无法区分线程谁是谁,一旦禁用了文件重定向,哪些线程受到了影响,无法确定。还有一个问题是,禁用了文件重定向的线程,再也没有机会去打开文件重定向了。一旦出现兼容问题,基本不可解。
如果是比较独立的小型EXE,这个方案是可以一试的。
bool g_bDisFsRedirect = false;
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
{
KWow64FsRedrt oWow64FsRedrt;
if (g_bDisFsRedirect && oWow64FsRedrt.Init())
{
oWow64FsRedrt.Close(); // 备注:无机会再打开重定向
}
break;
}
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
void __stdcall DisableFsRedirection()
{
g_bDisFsRedirect = true;
return;
}
void __stdcall RevertFsRedirection()
{
g_bDisFsRedirect = false;
return;
}
6、有没有更靠谱一点,且没有副作用的方案呢?!前面实验的时候发现,纯64位程序天然是没有文件重定向问题的,从这个思路出发,似乎也是有一个解决方案的,把调用文件对话框的逻辑放到一个64位的独立进程里面,32位的程序根据不同的环境决定是否调用这个64位独立进程,当然可能需要一些机制(命令行参数?管道?)来传递必须的参数(窗口句柄,后缀过滤字符串,窗口名等等),同样需要一些机制来返回信息(譬如选中了哪个文件)给父进程。该方案的开发量适中,无副作用。
但是这个方案也不是完全无缺点的,它的缺点一个是多了一个进程,另外一个就是通用性略差(我的意思是同一个独立进程的情况),可能能适应大部分的场景,但是如果有需求需要部分改造文件对话框(微软有提供这个消息hook接口),那么就需要在这个独立进程里面写一些定制化的代码来支持这些特殊场景(也就是可能要改这个独立进程,无法通过简单的参数方式来解耦这个逻辑,可能通过插件可以解决,但是这样就把一个小问题搞得无比复杂了)。
简单通过传递子进程命令行和管道的方式实现了一下,基本上可用(当然这里有一些细节问题需要处理,但是基本上都有通用方案。譬如如何优雅的等待子进程,同时父进程又能处理消息,子进程异常后父进程窗口disable的处理,如何安全的读取子进程返回的管道数据,防止父进程ReadFile卡死等等)。
BOOL COpenFileDlg64::Open(HWND hwndOwner, LPCWSTR lpszCmdline, ATL::CAtlStringW& strPath)
{
BOOL bRet = FALSE;
STARTUPINFO si = {0};
PROCESS_INFORMATION pi = {0};
// 读写管道句柄
HANDLE hStdOutRead = NULL;
HANDLE hStdOutWrite = NULL;
do
{
if (!lpszCmdline)
{
break;
}
SECURITY_ATTRIBUTES sa = {0};
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE; // 备注:重要
sa.lpSecurityDescriptor = NULL;
if (!::CreatePipe(&hStdOutRead, &hStdOutWrite, &sa, 0))
{
break;
}
si.cb = sizeof(STARTUPINFO);
::GetStartupInfo(&si);
si.dwFlags |= (STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES | STARTF_FORCEOFFFEEDBACK);
si.hStdOutput = hStdOutWrite;
si.hStdError = hStdOutWrite;
CAtlString strCmdline(lpszCmdline);
// 备注:重要,第5个参数设置为TRUE
if (!::CreateProcessW(
NULL,
(LPTSTR)strCmdline.GetBuffer(),
NULL,
NULL,
TRUE,
0,
NULL,
NULL,
&si,
&pi))
{
strCmdline.ReleaseBuffer();
break;
}
strCmdline.ReleaseBuffer();
MSG msg = {0};
BOOL bBreak = FALSE;
DWORD dwWait = WAIT_FAILED;
do
{
dwWait = ::MsgWaitForMultipleObjectsEx(
1,
&pi.hProcess,
2000,
QS_ALLEVENTS | QS_ALLINPUT,
MWMO_INPUTAVAILABLE);
switch (dwWait)
{
case WAIT_OBJECT_0 + 1:
{
// 分发消息
if (::PeekMessage(&msg, NULL, NULL, NULL, PM_REMOVE))
{
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
break;
}
case WAIT_OBJECT_0:
{
// 子进程已经退出
DWORD dwRead = 0;
DWORD dwAvailSize = 0;
if (!::PeekNamedPipe(hStdOutRead, NULL, 0, &dwRead, &dwAvailSize, NULL) ||
0 == dwAvailSize ||
dwAvailSize > 4 * 1024)
{
// 预览一个管道中的数据,用来判断管道中是否为空
bBreak = TRUE;
break;
}
// 申请内存
ATL::CAutoVectorPtr spVecPtr;
if (!spVecPtr.Allocate(dwAvailSize + 2))
{
bBreak = TRUE;
break;
}
memset((PUCHAR)spVecPtr, 0, dwAvailSize + 2);
dwRead = 0;
if (::ReadFile(hStdOutRead, (PVOID)spVecPtr, dwAvailSize, &dwRead, NULL) &&
dwAvailSize == dwRead)
{
std::wstring strOutInfo((WCHAR*)(PVOID)spVecPtr);
Util::CCmdParser oParser(strOutInfo);
oParser.Parse(L"##", L"==");
int nErrorCode = -1;
if (oParser.Get(L"errorcode", nErrorCode) && 0 == nErrorCode)
{
std::wstring __strPath;
oParser.Get(L"path", __strPath);
strPath = __strPath.c_str();
bRet = TRUE;
}
}
break;
}
case WAIT_TIMEOUT:
{
continue;
}
default:
{
// 异常发生
bBreak = TRUE;
break;
}
} // End switch (dwWait)
if (bBreak)
{
// 结束wait循环
break;
}
} while(dwWait != WAIT_OBJECT_0);
} while (0);
//////////////////////////////////////////////////////////////////////////
// 集中释放资源
//
if (pi.hProcess)
{
::CloseHandle(pi.hProcess);
pi.hProcess = NULL;
}
if (pi.hThread)
{
::CloseHandle(pi.hThread);
pi.hThread = NULL;
}
if (hStdOutRead)
{
::CloseHandle(hStdOutRead);
hStdOutRead = NULL;
}
if (hStdOutWrite)
{
::CloseHandle(hStdOutWrite);
hStdOutWrite = NULL;
}
if (hwndOwner &&
::IsWindow(hwndOwner) &&
!::IsWindowEnabled(hwndOwner))
{
// 可能是子进程异常了
//
::EnableWindow(hwndOwner, TRUE);
}
return bRet;
}
方案 |
优点 |
缺点 |
备注 |
Hook方案 |
基本无,代码量可能不多 |
不可能因素多,且副作用较大 |
未实现过 |
UI实现对话框 |
完全可控 |
代码量极大,且特性可能不能完全满足 |
|
DLL捕获线程 |
代码量小 |
有副作用,不太可控 |
|
独立进程 |
代码量适中,基本无副作用 |
要实现特殊效果会比较麻烦,但是也有方案 |
☆推荐 |
代码下载:https://download.csdn.net/download/magictong/12337474
[1] x64系统的判断和x64下文件和注册表访问的重定向(3) https://blog.csdn.net/magictong/article/details/5895482
[2] Wow64DisableWow64FsRedirection function https://docs.microsoft.com/en-us/windows/win32/api/wow64apiset/nf-wow64apiset-wow64disablewow64fsredirection
[3] Wow64RevertWow64FsRedirection function https://docs.microsoft.com/zh-cn/windows/win32/api/wow64apiset/nf-wow64apiset-wow64revertwow64fsredirection