场景描述:
1.父进程A使用函数fopen打开(创建)一个磁盘文件file.exe.tmp
2.父进程进行长时间的边下载边写入
3.下载写入完成后,使用fclose关闭文件句柄
4.重命名file.exe.tmp为file.exe
以上为理想情况下的代码执行流程。
问题:
在第四步,重命名文件有可能失败,使用 GetLastError 函数发现失败原因为 此文件被另外一个进程占用。
解决:
1.使用微软官方开发的handle.exe来查找是哪个进程占用了这个文件
(handle.exe的下载地址 https://technet.microsoft.com/en-us/sysinternals/bb896655.aspx )
使用方法:在文件下载完成,执行改名函数之前,执行 Handle.exe file.exe.tmp /accepteula 命令(使用管道获取直接结果并打印)
2.在打印结果中发现,文件是被父进程A的子进程 B 占用了。
在file.exe.tmp的下载过程中,父进程A使用 createprocess 函数创建了子进程B,看一下函数原型:
BOOL WINAPI CreateProcess(
_In_opt_ LPCTSTR lpApplicationName,
_Inout_opt_ LPTSTR lpCommandLine,
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ BOOL bInheritHandles,
_In_ DWORD dwCreationFlags,
_In_opt_ LPVOID lpEnvironment,
_In_opt_ LPCTSTR lpCurrentDirectory,
_In_ LPSTARTUPINFO lpStartupInfo,
_Out_ LPPROCESS_INFORMATION lpProcessInformation
);
其中 bInheritHandles 参数的描述为:
If this parameter TRUE, each inheritable handle in the calling process is inherited by the new process. If the parameter is FALSE, the handles are not inherited. Note that inherited handles have the same value and access rights as the original handles.
简单的说就是允许子进程继承父进程的句柄。
3.一种简单的解决方法:bInheritHandles参数设置为flase,那么子进程B就不会占用这个文件了。
但是我这里无法采用该方法,因为这个子进程B是第三方程序,他必须继承某些句柄才能玩,那么我换一种方法解决。
4.看fopen函数,实现代码在CRT中,CRT代码是开源的,路径在:Microsoft Visual Studio 10.0\VC\crt\src
一层层往下跟,发现fopen 实际是 __topenfile函数,__topenfile函数中有详细的 文件打开mode处理流程。
mode处理流程中有一段代码为:
case _T(‘N‘):
modeflag |= _O_NOINHERIT;
break;
_O_NOINHERIT的定义为:
/* Open handle inherit bit */
#define _O_NOINHERIT 0x0080 /* child process doesn‘t inherit file */
这里就是控制该句柄是否允许被子进程占用了。
那么只需要在fopen的Mode设置为 "ab+N" 就可以了。
沿着源码继续往下看,发现在CRT这一层最终的调用函数为 _tsopen_nolock (文件\Microsoft Visual Studio 10.0\VC\crt\src\open.c)
里面对flag进行了大量的处理,最终调用 CreateFile 函数创建、打开文件,并且有不少的CreateFile函数异常处理代码。
CreateFile函数是系统API,不开源,要继续跟踪就得逆向,就不继续往下了。
5.所以得出第3种解决方案:直接使用系统API CreateFile 创建、打开文件,
6.第四种解决方案:参考unlock的方法:先openprocess,然后通过 DuplicateHandle 复制句柄,再结束句柄,从而解决子进程B的占用。这个方法网上可以很容易搜到源码,我就不废话了。
7.总结
以上一共提出了4种方法解决 父进程句柄被子进程占用的问题,以下对比4种方案:
1.CreateProcess设置参数:很方便,但是如果子进程有特殊的继承句柄需求,则不适用
2.fopen设置参数:很方便,可跨平台兼容,修改代码只需增加一个字节即可解决问题
3.使用 CreateFile 创建、打开文件,替代fopen:CreateFile功能强大,但是要在商业软件中玩好这个函数不容易,需要处理大量的异常(可参考_tsopen_nolock函数的实现源码)
4.DuplicateHandle后closehandle:最强大的关闭其他进程中句柄的方式,主要运用于ring3中解除简单木马病毒的文件占用技术(无法适用于文件占坑技术),但是使用风险是有的,真正应用也需要写一大堆异常处理代码。