Windows下自删除的艺术
2023-11-16 15:20:23 Author: xz.aliyun.com(查看原文) 阅读量:18 收藏

通常来说,在windows程序不可能在运行的时候实现删除自己,微软设计之初为了保证程序的安全性,当一个可执行程序运行的时候会处于一种被占用的状态,如果尝试删除程序,会显示程序被占用,一般需要结束掉程序后才能删掉,而自删除利用了NTFS文件特性达到的程序运行时解除文件锁定,最终删除自身的效果,本篇文章是对此项技术的总结,这项技术已经出现很多年了,互联网上最早的消息来自2021年,于jonasLy在推特公开了这项技术

NTFS 的 Alternate Data Stream

NTFS(New Technology File System)文件系统包括对备用数据流的支持。这是一个相对不太为人熟知的功能,主要是为了与Macintosh文件系统中的文件兼容性而被引入的。备用数据流允许文件包含多个数据流,而每个文件至少包含一个数据流。在Windows操作系统中,每个文件的默认数据流称为:$DATA

尽管Windows资源管理器(Windows Explorer)没有提供一种直观的方式来查看文件中的备用数据流,也没有提供一种在不删除文件的情况下删除这些数据流的方法,但实际上可以相对容易地创建和访问它们。备用数据流中的可执行文件可以从命令行中运行,但不会在Windows资源管理器(或控制台)中显示

创建方法

notepad hello.txt:test

文件格式:

<filename>:<stream name>:<stream type>

查看方法:

dir /r

这是我创建的内容:

核心原理

  1. 微软虽然阻止直接删除自己,但是并不阻止重命名自己,我们可以轻松的按下F2修改我们运行的程序名称;
  2. 文件锁和重命名挂钩,文件锁和着你重命名的文件走,假设我们重命名为Alternate Data Stream类型的文件,文件锁便会锁定我们重命名的文件;
  3. 要知道所有的Alternate Data Stream都有主的文件,主文件现在没有被使用意味着主文件是可以被删除的;虽然无法直接在系统上进行重命名带有:的文件名,但是微软提供的Windows API却不影响我们对本身进行重命名;
  4. 通过删除自己的主文件,根据微软的系统特性,备选的数据流也会被系统自动删除,即便是有文件锁也会被强制释放;
  5. 程序走到Main方法,PE文件已经加载到内存里面了,因此删除自己不影响程序的正常运行。

编程实现

核心Windows API

SetFileInformationByHandle是用来重新设置文件名,同时也可以用来设置删除位:

BOOL SetFileInformationByHandle(
  [in] HANDLE                    hFile,     //要更改信息的文件的句柄。
  [in] FILE_INFO_BY_HANDLE_CLASS FileInformationClass,  //指定要更改的信息类型
  [in] LPVOID                    lpFileInformation,  //指向包含要更改的指定文件信息类的信息的缓冲区的指针
  [in] DWORD                     dwBufferSize  //lpFileInformation 的大小,以字节为单位
);

实现:

if(SetFileInformationByHandle(hFile, FileInformationClass, &FileInformation, sizeof(FileInformation)))
{
   std::cout << "delete self success" << std::endl;
}

查看FileInformationClass的枚举类型:

typedef enum _FILE_INFO_BY_HANDLE_CLASS {
    FileBasicInfo,                     // 文件的基本信息
    FileStandardInfo,                  // 文件的标准信息
    FileNameInfo,                      // 文件的名称信息
    FileRenameInfo,                    // 文件的重命名信息
    FileDispositionInfo,               // 文件的处置信息
    FileAllocationInfo,                // 文件的分配信息
    FileEndOfFileInfo,                 // 文件的结束信息
    FileStreamInfo,                    // 文件流的信息
    FileCompressionInfo,               // 文件的压缩信息
    FileAttributeTagInfo,              // 文件属性标签信息
    FileIdBothDirectoryInfo,           // 文件和目录信息(同时包括文件和目录的标识信息)
    FileIdBothDirectoryRestartInfo,    // 文件和目录信息(同时包括文件和目录的标识信息)的重启信息
    FileIoPriorityHintInfo,            // 文件的IO优先级提示信息
    FileRemoteProtocolInfo,            // 文件的远程协议信息
    FileFullDirectoryInfo,             // 完整的目录信息
    FileFullDirectoryRestartInfo,      // 完整的目录信息的重启信息
    FileStorageInfo,                   // 文件的存储信息
    FileAlignmentInfo,                 // 文件的对齐信息
    FileIdInfo,                        // 文件的标识信息
    FileIdExtdDirectoryInfo,           // 文件的扩展目录信息
    FileIdExtdDirectoryRestartInfo,    // 文件的扩展目录信息的重启信息
    FileDispositionInfoEx,             // 文件的扩展处置信息
    FileRenameInfoEx,                  // 文件的扩展重命名信息
    FileCaseSensitiveInfo,             // 文件的大小写敏感信息
    FileNormalizedNameInfo,            // 文件的规范化名称信息
    MaximumFileInfoByHandleClass       // 用于计数的枚举最大值
} FILE_INFO_BY_HANDLE_CLASS, *PFILE_INFO_BY_HANDLE_CLASS;

微软搜索找到FileRenameInfo的具体属性:

typedef struct _FILE_RENAME_INFO {
  union {
    BOOLEAN ReplaceIfExists;
    DWORD   Flags;
  } DUMMYUNIONNAME;
  BOOLEAN ReplaceIfExists;
  HANDLE  RootDirectory;
  DWORD   FileNameLength;
  WCHAR   FileName[1];
} FILE_RENAME_INFO, *PFILE_RENAME_INFO;

进一步查询文档翻阅:

一个以NUL结尾的宽字符字符串,包含文件的新路径。该值可以是以下之一:

  • 绝对路径(驱动器、目录和文件名)。
  • 相对于进程当前目录的路径。
  • NTFS文件流的新名称,以冒号:开头。

新的文件流NTFS应该要用 : 开始

HeapAlloc堆上分配,HeapAlloc可以帮助我们动态的分配内存空间

/**
 * 分配指定大小的内存块,并返回指向分配内存的指针。
 *
 * @param hHeap (输入): 用于分配内存的堆句柄。
 * @param dwFlags (输入): 分配标志,如 HEAP_ZERO_MEMORY(分配后初始化为零)等。
 * @param dwBytes (输入): 要分配的内存块的大小(以字节为单位)。
 *
 * @return 返回指向分配内存的指针。如果分配失败,返回 NULL。
 *
 * @remarks 通过调用HeapFree函数释放分配的内存。
 */
DECLSPEC_ALLOCATOR LPVOID HeapAlloc(
  [in] HANDLE hHeap,
  [in] DWORD  dwFlags,
  [in] SIZE_T dwBytes
);

部分代码实现:

const wchar_t* NewStream = L":endlessparadox";
PFILE_RENAME_INFO pRename = nullptr; //空指针指向结构体

hFile = CreateFileW(szPath, DELETE | SYNCHRONIZE, FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL);
pRename = (PFILE_RENAME_INFO)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwBytes);

GetModuleFileNameW:检索包含指定模块的文件的完全限定路径。该模块必须已被当前进程加载。

/**
 * 获取指定模块的文件名或当前进程的可执行文件路径。
 *
 * @param hModule (输入,可选): 要获取文件名的模块的句柄,传入 NULL 表示当前进程。
 * @param lpFilename (输出): 存储获取到的文件名的缓冲区,应为一个 WCHAR 字符数组。
 * @param nSize (输入): lpFilename 缓冲区的大小(以字符数为单位)。
 *
 * @return 返回文件名的长度(以字符数表示,不包括 null 终止符)。
 *         如果函数执行失败,返回 0,可以通过调用 GetLastError() 获取更多错误信息。
 */
DWORD GetModuleFileNameW(
  [in, optional] HMODULE hModule,
  [out]          LPWSTR  lpFilename,
  [in]           DWORD   nSize
);

CreateFileW : 创建或打开一个文件或 I/O 设备,然后返回一个文件句柄以供后续操作

/**
 * 创建或打开一个文件或 I/O 设备,然后返回一个文件句柄以供后续操作。
 *
 * @param lpFileName (输入): 要创建或打开的文件名或 I/O 设备的路径。
 * @param dwDesiredAccess (输入): 打开文件的访问权限,如读取、写入等。
 * @param dwShareMode (输入): 其他进程可以与文件共享的方式,如共享读取、共享写入等。
 * @param lpSecurityAttributes (输入,可选): 安全描述符,用于控制文件或目录的安全性。通常传入NULL。
 * @param dwCreationDisposition (输入): 文件的创建或打开方式,如创建新文件、打开已有文件等。
 * @param dwFlagsAndAttributes (输入): 文件或目录的属性标志,如普通文件、目录等,以及其他标志位。
 * @param hTemplateFile (输入,可选): 用于复制文件属性的文件句柄。通常传入NULL。
 *
 * @return 返回一个文件句柄,用于后续文件操作。如果函数执行失败,返回INVALID_HANDLE_VALUE (-1),
 *         可以通过调用 GetLastError() 获取更多错误信息。
 */
HANDLE CreateFileW(
  [in]           LPCWSTR               lpFileName,
  [in]           DWORD                 dwDesiredAccess,
  [in]           DWORD                 dwShareMode,
  [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  [in]           DWORD                 dwCreationDisposition,
  [in]           DWORD                 dwFlagsAndAttributes,
  [in, optional] HANDLE                hTemplateFile
);

RtlCopyMemory例程将源内存块的内容复制到目标内存块

/**
 * @brief 复制内存区域
 * 
 * @param Destination 目标内存区域的指针,数据将被复制到这里
 * @param Source 源内存区域的指针,数据将从这里被复制
 * @param Length 要复制的字节数
 * 
 * @note 这个函数用于将源内存区域中的数据复制到目标内存区域中,以字节为单位进行复制。
 * @note 这是一个通用的内存复制函数,允许你复制数据到任何类型的内存区域。
 * @note 源内存区域是只读的,不会被修改。
 */
void RtlCopyMemory(
   void*       Destination,
   const void* Source,
   size_t      Length
);

完整C++代码实现

#include <Windows.h>  
#include <iostream>  

BOOL Self_Delete() {  
    const wchar_t* NewStream = L":endlessparadox";  
    WCHAR szPath[MAX_PATH * 2] = { 0 };  

    // 获取当前可执行文件的路径  
    if (GetModuleFileNameW(NULL, szPath, MAX_PATH * 2) == 0) {  
        std::wcerr << L"[!] GetModuleFileNameW fail , code is  " << GetLastError() << std::endl;  
        return FALSE;  
    }  

    // 打开文件
    HANDLE hFile = CreateFileW(szPath,  
                               DELETE | SYNCHRONIZE,  
                               FILE_SHARE_READ,  
                               NULL,  
                               OPEN_EXISTING,  
                               NULL, NULL);  
    if (hFile == INVALID_HANDLE_VALUE) {  
        std::wcerr << L"[!] CreateFileW fail , code is " << GetLastError() << std::endl;  
        return FALSE;  
    }  

    // 准备重命名信息  
    SIZE_T sRename = sizeof(FILE_RENAME_INFO) + sizeof(wchar_t) * wcslen(NewStream);  
    PFILE_RENAME_INFO pRename = (PFILE_RENAME_INFO)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sRename);  
    if (!pRename) {  
        CloseHandle(hFile);  
        std::wcerr << L"[!] HeapAlloc fail , code is " << GetLastError() << std::endl;  
        return FALSE;  
    }  

    pRename->FileNameLength = wcslen(NewStream) * sizeof(wchar_t);  
    RtlCopyMemory(pRename->FileName, NewStream, pRename->FileNameLength);  
    std::wcout << L"[i] Renaming :$DATA to file data as " << NewStream << std::endl;  

    if (!SetFileInformationByHandle(hFile, FileRenameInfo, pRename, sRename)) {  
        std::wcerr << L"[!] SetFileInformationByHandle fail, code is" << GetLastError() << std::endl;  
        CloseHandle(hFile);  
        HeapFree(GetProcessHeap(), 0, pRename);  
        return FALSE;  
    }  

    std::wcout << L"[+] Completed" << std::endl;  
    CloseHandle(hFile);  

    // 打开文件以删除  
    hFile = CreateFileW(szPath,  
                        DELETE | SYNCHRONIZE,  
                        FILE_SHARE_READ,  
                        NULL,  
                        OPEN_EXISTING,  
                        NULL, NULL);  

    if (hFile == INVALID_HANDLE_VALUE && GetLastError() == 0) {  
        std::wcout << "free memory" << std::endl;  
        HeapFree(GetProcessHeap(), 0, pRename);  
        return TRUE;  
    }  

    FILE_DISPOSITION_INFO Delete = { 0 };  
    Delete.DeleteFile = TRUE;  
    std::wcout << L"[+] Deleting ....." << std::endl;  

    if (!SetFileInformationByHandle(hFile, FileDispositionInfo, &Delete, sizeof(Delete))) {  
        std::wcerr << L"[!] SetFileInformationByHandle fail, code is  " << GetLastError() << std::endl;  
        CloseHandle(hFile);  
        HeapFree(GetProcessHeap(), 0, pRename);  
        return FALSE;  
    }  

    CloseHandle(hFile);  
    HeapFree(GetProcessHeap(), 0, pRename);  
    wprintf(L"[+] Done\n");  
    return TRUE;  
}  

int main() {  

    Self_Delete();  
    std::wcout << "stop in memory" << std::endl;  
    std::string userInput; // 声明一个字符串变量用于存储用户输入  
    std::cout << "请输入一个字符串: ";  
    std::cin >> userInput ;
    std::cout << "你输入的字符串是: " << userInput << std::endl;  
    return 0;  
}

执行效果:

一些现实场景下的利用方法

配合自删除反调试

简单的判断反调试的代码,可以写一个定期判断的逻辑,当有人尝试分析调试的时候就自我删除

#include <windows.h>  
#include <iostream>  

int main() {  

    while(TRUE){  
        if (IsDebuggerPresent()) {  
            std::cout << "Debugger is attached." << std::endl;  
            std::getchar();
            self_deletio();
            exit(0);  
        } else {  
            std::cout << "Debugger is not attached." << std::endl;  
            std::getchar();  
            Sleep(500);  
        }  
    }  
}

高级的执行完任务自销毁

我们可以把自删除功能编入工具,实现执行完任务后就自我销毁,达到一种非常隐蔽的实战效果,进一步延长我们自己开发的工具的存活时间,这类方法更加优雅,对比调用cmd和使用MoveFileEx方式是需要重启电脑等更加隐蔽安全

钓鱼活动中的无样本攻击

设想一下这样的场景,实际的恶意程序托管在攻击者控制的服务器下。钓鱼邮件诱导攻击者访问此恶意程序,普通用户一般对此类程序不会进行备份上传,如果钓鱼成功,攻击者立刻销毁本地和云端上的样本,这可能会大大增加溯源和分析的难度,尽管我们可以通过网络流量还原样本,但是攻击者也可以在流量层面进一步做手脚,获取最初样本的难度就会有一定难度提升。

兼容性和稳定性测试

在系统win11、win10、win7、ws2012均通过测试

总结

jonasLy利用文件特性巧妙的转移了文件锁,使得文件锁移动在可选备份流,得以在Ring3层面下达到自删除,这项技术将会存在很久很久,直到微软把NTFS技术废弃掉或者修改掉文件的API底层,2年已经过去,目前来说微软没有打算修复这个缺陷。

参考资料:

https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/c54dec26-1551-4d3a-a0ea-4fa40f848eb3
https://learn.microsoft.com/en-us/windows/win32/api/
https://www.youtube.com/watch?v=lcJdlzKS_5o&ab_channel=crow
https://chat.openai.com/
https://twitter.com/jonasLyk/status/1350401461985955840
https://github.com/secur30nly/go-self-delete
https://github.com/LloydLabs/delete-self-poc/tree/main
https://owasp.org/www-community/attacks/Windows_alternate_data_stream


文章来源: https://xz.aliyun.com/t/13045
如有侵权请联系:admin#unsafe.sh