问题的提出
在VC++编写程序的过程中,有时可能会遇到可执行文件加载到内存中运行,在结束返回时需要删除该文件自身的情况。一般情况下,因为可执行文件的映像还在内存中,删除该文件会导致 “无法删除文件:拒绝访问。源文件可能正被使用”的错误提示,所以用常规方法不能实现这个功能。
一 硬编码
这段代码在PROCESS没有结束前就将启动PROCESS的EXE文件删除了.
int main(int argc, char *argv[])
{
HMODULE module = GetModuleHandle(0);
CHAR buf[MAX_PATH];
GetModuleFileName(module, buf, sizeof buf);
CloseHandle(HANDLE(4));
__asm {
lea eax, buf
push 0
push 0
push eax
push ExitProcess
push module
push DeleteFile
push UnmapViewOfFile
ret
}
return 0;
}
现在,我们先看一下堆栈中的东西
偏移 内容
24 0
20 0
16 offset buf
12 address of ExitProcess
8 module
4 address of DeleteFile
0 address of UnmapViewOfFile
调用RET返回到了UnmapViewOfFile,也就是栈里的偏移0所指的地方.当进入UnmapViewOfFile的流程时,栈里见到的是返回地址DeleteFile和HMODUL module.也就是说调用完毕后返回到了DeleteFile的入口地址.当返回到DeleteFile时,看到了ExitProcess的地址,也就是返回地址.和参数EAX,而EAX则是buffer.buffer存的是EXE的文件名.由GetModuleFileName(module, buf, sizeof buf)返回得到.执行了DeleteFile后,就返回到了ExitProcess的函数入口.并且参数为0而返回地址也是0.0是个非法地址.如果返回到地址0则会出错.而调用ExitProcess则应该不会返回.
这段代码的精妙之处在于:
1.如果有文件的HANDLE打开,文件删除就会失败,所以,CloseHandle(HANDLE(4));是十分巧妙的一手.HANDLE4是OS的硬编码,对应于EXE的IMAGE.在缺省情况下,OS假定没有任何调用会关闭IMAGE SECTION的HANDLE,而现在,该HANDLE被关闭了.删除文件就解除了文件对应的一个句柄.
2.由于UnmapViewOfFile解除了另外一个对应IMAGE的HANDLE,而且解除了IMAGE在内存的映射.所以,后面的任何代码都不可以引用IMAGE映射地址内的任何代码.否则就OS会报错.而现在的代码在UnmapViewOfFile后则刚好没有引用到任何IMAGE内的代码.
3.在ExitProcess之前,EXE文件就被删除了.也就是说,进程尚在,而主线程所在的EXE文件已经没了.(WINNT/9X都保护这些被映射到内存的WIN32 IMAGE不被删除.)
二、多进程的方法
可以通过多进程的方法解决这个问题。可执行文件在结束返回前,创建一个运行命令窗口的新进程,当然该命令窗口以隐藏方式执行,并通过传递参数执行删除功能。为了避免可执行文件的映像还在内存中就执行删除,需要把当前进程设置为实时优先级,而命令窗口进程设置为很低的IDLE优先级,这样首先可执行文件结束返回,再运行命令窗口的删除命令,就实现了该文件自身的删除功能。
SelfDelete()完成的功能是可执行文件在运行中删除本文件,实现方法是调用命令窗口来删除运行的文件。命令窗口程序是由环境变量COMSPEC定义的,Win9x/ME使用COMMAND.COM,WinNT/2K/XP使用CMD.COM。程序把命令字符串“/c del filename > nul”传递给命令窗口,其中filename是需要删除文件的全路径文件名,文件名需要转换为8.3格式;/c开关用于命令窗口退出。
命令窗口通过调用ShellExecuteEx()函数以单独的进程运行,它的窗口句柄在SHELLEXECTUEINFO结构中的成员变量hProcess定义。自删除需要解决一个特殊的问题,即主程序必须在命令窗口删除它之前退出并关闭其打开的文件句柄。为了做到这一点,我们必须同步两个独立、并行的进程:当前程序进程和命令窗口进程。这可以通过操作CPU资源优先级来临时降低命令窗口的运行优先级别。这样,主程序将分配到CPU的所有资源直到其正常退出,而阻塞其它任何命令窗口的执行直到主程序结束。设置REALTIME_PRIORITY_CLASS和THREAD_PRIORITY_TIME_CRITICAL优先级会引起窗口悬挂状态,为了避免出现该问题,自删除应用必须通过返回TRUE来彻底退出。程序员需要确定在删除文件执行前完成所有进程和线程和关闭所有句柄。SelfDelete()一定要在程序的main函数中调用。
一些限制:
1、出现错误时命令窗口不能删除文件,但程序没有错误检查,因此确认需要删除的文件没有设置为隐藏、系统和只读属性;
2、尽管出现错误时命令窗口并没有真正删除文件,但是资源浏览器还是会删除程序图标,这时可按F5键更新文件夹以显示正确的文件列表;
3、命令窗口只能运行一个命令,多个文件删除命令和以及删除其所在的目录需要使用命令描述文件。
以下代码在VC++6.0通过编译链接。
INT APIENTRY WinMain(…)
{
…
// on program exit
// close all handles etc.
if(!SelfDelete())
{
// add error messaging
}
return 0; // WinMain exit
}
#include
#include
BOOL SelfDelete()
{
SHELLEXECUTEINFO sei;
TCHAR szModule [MAX_PATH],
szComspec[MAX_PATH],
szParams [MAX_PATH];
// get file path names:
if((GetModuleFileName(0,szModule,MAX_PATH)!=0) &&
(GetShortPathName(szModule,szModule,MAX_PATH)!=0) &&
(GetEnvironmentVariable(“COMSPEC”,szComspec,MAX_PATH)!=0))
{
// set command shell parameters
lstrcpy(szParams,”/c del “);
lstrcat(szParams, szModule);
lstrcat(szParams, ” > nul”);
// set struct members
sei.cbSize = sizeof(sei);
sei.hwnd = 0;
sei.lpVerb = “Open”;
sei.lpFile = szComspec;
sei.lpParameters = szParams;
sei.lpDirectory = 0;
sei.nShow = SW_HIDE;
sei.fMask = SEE_MASK_NOCLOSEPROCESS;
// invoke command shell
if(ShellExecuteEx(&sei))
{
// suppress command shell process until program exits
SetPriorityClass(sei.hProcess,IDLE_PRIORITY_CLASS);
SetPriorityClass(GetCurrentProcess(),
REALTIME_PRIORITY_CLASS);
SetThreadPriority(GetCurrentThread(),
THREAD_PRIORITY_TIME_CRITICAL);
// notify explorer shell of deletion
SHChangeNotify(SHCNE_DELETE,SHCNF_PATH,szModule,0);
return TRUE;
}
}
return FALSE;
}