通过两种方式实现自删除,第一种方式是通过调用MoveFileEx,第二种方式是程序释放批处理脚本后,用批处理脚本删除程序自身并让批处理自我删除。
0x01 自删除技术
A) MoveFileEx方式
BOOL MoveFileEx(
LPCTSTR lpExistingFileName,
LPCTSTR lpNewFileName,
DWORD dwFlags
);
其函数原型非常简单,该函数原本用于移动文件。
第一个参数lpExistingFileName是需要被移动的文件路径
第二个参数lpNewFileName是新的位置的路径
第三个参数dwFlags是标志,这个参数比较复杂下面来着重介绍
MOVEFILE_COPY_ALLOWED: 如果文件被从C盘移动到D盘即不同卷之间移动则内部移动方式转成复制后删除
MOVEFILE_WRITE_THROUGH: 在完成移动前MoveFilEx处于阻塞状态
着重讲一下这个参数:
MOVEFILE_DELAY_UNTIL_REBOOT:
1. 这个参数只能用于管理员权限执行时。
2. 这个参数是将移动操作延迟到重新启动计算机后执行。
3. 这种移动操作执行的非常早,位于AUTOCHK执行之后,页文件创建之间。只需要知道这操作执行很早就可以。
4. 在ANSI的函数版本中该函数路径最大字符限制是MAX_PATH个即260。如果你在路径前加上\\?\前缀并且使用Unicode版本的MoveFileEx则最大字符数可以变成32,767个宽字符。特别注意的是自从Win10 v1607后可以选择不加这个前缀也可以突破MAX_PATH个字符限制。
深入探究MOVEFILE_DELAY_UNTIL_REBOOT:
这个参数实际上是对注册表进行了操作,假设程序无法访问注册表那也就无法执行删除操作,MoveFileEx通过写入
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations
来达到重启移动或者删除的目的,这也是加了这个参数需要管理员权限的原因,因为访问HKEY_LOCAL_MACHINE根键下的子键需要系统管理员权限。
并且执行重启后移动和重启后删除的格式是不一样的,可以看到PendingFileRenameOperations值的类型是REG_MULTI_SZ,意味着可以写入多个字符串。
假设是重启后删除操作,MoveFileEx会把szDstFile\0\0写入PendingFileRenameOperations
而如果是重启后移动操作,MoveFileEx会把szSrcFile\0szDstFile\0写入PendingFileRenameOperations
接下去看一下代码:
BOOL SelfDel(LPCTSTR pszFileName) {
TCHAR szTemp[MAX_PATH] = "\\\\?\\"; // 这个\\?\是有选择性的加入,如果你是Win10系统
_tcscat_s(szTemp, _countof(szTemp) * sizeof(TCHAR), pszFileName);
return(MoveFileEx(szTemp, NULL, MOVEFILE_DELAY_UNTIL_REBOOT));
}
代码非常简单但是里面内容却很多。
B) 批处理方式
批处理实现自启动的优势更大,它无需重启并且不需要管理员权限。实现原理主要是用代码创建的BAT脚本,并利用脚本的del %0可以删除自身以及删除其他文件的特性实现的。
首先来看一下这个bat脚本:
@echo off # 不显示脚本执行时产生的内容
choice /t 1 /d y /n > nul # 这条命令下面讲
del xxx.exe # 删除xxx.exe, xxx.exe代表要删除的可执行文件的名称
del %0 # 删除自身
这里主要来讲一下choice这个命令,这个命令主要是起到延时的作用以确保下面的删除操作能够完成。先看一下cmd帮助文档:
下面我用我的话来讲述一下这里需要用到的参数的作用
/t参数: 这个参数后面跟一个数字n,表示等待n秒,choice会给出一个选择框让你选一个答案,如下:
上面的/t 5就是等待5秒,在这5秒内你要选择Y, N, C中的一个输入并回车,如果你在5秒内没有输入,那系统就会默认帮你选择Y,你也可以自己设定默认值
/d参数: 这个参数后面可以跟y, n, c分别代表3个回答,d代表default,意味着默认选项,在有限时间内没有选择那就会默认选择这个参数的设置,比如我这里是/d y,所以如下:
系统默认帮我选择了Y的答案。
/c参数: 回答的总数最多有Y,N,C三种,你不可以有更多回答,但是你可以减少,/c参数就是让你选择答案数目,比如我用
/c yn的话,那结果就会变成下面那样:
问题的答案就成了Y和N两个选项了,这次我/d n意味着默认是N答案,5秒后我没有回答,系统自动帮我回答了N并且脱离了阻塞态。
我觉得这个脚本的其他应该没什么问题。接下去看看代码:
#include <windows.h>
#include <tchar.h>
#include <cstdio>
#include <shlobj.h>
#include <StrSafe.h>
// 创建批处理脚本代码
BOOL CreateChoiceBat() {
TCHAR szBat[MAX_PATH] = {0};
TCHAR szCurrentFileName[MAX_PATH] = { 0 };
HANDLE hFile = NULL;
/*
@echo off
choice /t 5 /d y /n > nul || ping 127.0.0.1 /n 5 > nul
del *.exe
del %0
*/
GetModuleFileName(NULL, szCurrentFileName, _countof(szCurrentFileName) * sizeof(TCHAR));
StringCchPrintf(szBat, _countof(szBat) * sizeof(TCHAR), "@echo off\r\nchoice /t 1 /d y /n > nul\r\ndel %s\r\ndel %%0\r\n", szCurrentFileName);
hFile = CreateFile("del.bat", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (INVALID_HANDLE_VALUE == hFile) {
_tprintf("%d", GetLastError());
return(FALSE);
}
DWORD dwWritten = 0;
if (FALSE == WriteFile(hFile, szBat, sizeof(TCHAR) * _tcslen(szBat), &dwWritten, NULL))
return(FALSE);
CloseHandle(hFile);
return(TRUE);
}
// 创建运行自删除脚本的CMD进程
BOOL SelfDelete() {
BOOL bRet = FALSE;
TCHAR szCurrentDirectory[MAX_PATH] = {0};
TCHAR szBatFileName[MAX_PATH] = {0};
TCHAR szCmd[MAX_PATH] = { 0 };
LPTSTR pStr = NULL;
GetModuleFileName(NULL, szCurrentDirectory, _countof(szCurrentDirectory) * sizeof(TCHAR));
if (NULL == (pStr = _tcsrchr(szCurrentDirectory, '\\')))
return(FALSE);
*pStr = '\0';
StringCchPrintf(szBatFileName, _countof(szBatFileName) * sizeof(TCHAR), "%s\\del.bat", szCurrentDirectory);
StringCchPrintf(szCmd, _countof(szCmd) * sizeof(TCHAR), "cmd /c call \"%s\"", szBatFileName);
if (CreateChoiceBat()) {
STARTUPINFO si = {0};
PROCESS_INFORMATION pi = {0};
si.cb = sizeof(si);
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
BOOL fOk = CreateProcess(NULL, szCmd, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
if (fOk) {
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
ExitProcess(NULL);
}
}
else
return(FALSE);
return(TRUE);
}
int _tmain() {
SelfDelete();
system("pause");
return(0);
}