65.9K
CodeProject 正在变化。 阅读更多。
Home

自删除可执行文件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (44投票s)

2007年1月7日

CPOL

11分钟阅读

viewsIcon

218562

downloadIcon

1935

如何编写一个可以自我删除的程序

目录

引言

我前几天读到了一篇有趣的文章,讨论了Win32应用程序在执行完成后从磁盘删除自身的各种机制。基本问题当然是,当模块正在执行时,操作系统会锁定该文件。所以,像这样的代码是行不通的

TCHAR szModule[MAX_PATH];
GetModuleFileName( NULL, szModule, MAX_PATH );
DeleteFile( szModule );

在可用的各种选项中,上述文章的作者建议以下方法作为最终方法,因为它具有在所有Microsoft Windows版本(从'95开始)上都能正常工作的优点。

现在是时候去看看这篇文章了(在那里,请务必看看其他一些文章 - 相当不错的网站)。这是链接

简而言之,这是该方法

  • 当需要删除自身时,我们首先启动一个保证在所有Windows计算机上都存在的外部进程(例如explorer.exe),并使其处于挂起状态。我们通过调用CreateProcess来实现这一点,并将CREATE_SUSPENDED作为dwCreationFlags参数传递。请注意,当一个进程以这种方式启动时,真的无法确定主线程何时会被挂起。但是,它似乎在入口点被调用之前很久就被挂起了,事实上,它甚至发生在进程的Win32环境完全初始化之前。
  • 在此之后,我们通过GetThreadContext获取挂起的远程进程主线程的CONTEXT数据(基本上是CPU寄存器状态)。
  • 然后,我们操纵堆栈指针(ESP)在远程堆栈上分配一些空间,用于存储我们的数据(例如要删除的可执行文件的路径)。之后,我们通过调用WriteProcessMemory将我们编写的用于删除文件的本地例程的二进制代码(及其所需数据)放入远程进程。
  • 接下来,我们修改指令指针(EIP),使其指向我们已复制到远程进程中的二进制代码,并通过SetThreadContext更新挂起线程的上下文。
  • 最后,我们通过ResumeThread恢复远程进程的执行。由于远程线程中的EIP现在指向我们的代码,它将执行它;这当然会愉快地删除原始可执行文件。就这样!

就这么简单,嗯?嗯,是的,但是...

虽然这种方法确实能完成任务,但我们的删除代码在Windows有机会完全初始化远程进程之前就在其中执行,这对我们可以调用的API类型施加了一些限制。事实证明,像DeleteFileExitProcess这样的API在此半成品状态下确实可以工作。我琢磨着,我将修改一下方法,使其允许我们从注入的代码中调用任何API。这是我所做的

  • 和以前一样,我们以挂起状态启动外部进程。但是,我们没有将代码放在ESP暂停时指向的位置,而是将其放在可执行文件的入口点例程之上,即,我们用我们注入的代码替换了远程进程的入口点。当入口点代码执行时,我们可以肯定Win32环境已完全初始化并准备就绪!
  • 要确定模块的入口点在哪里,我们需要解析PE文件格式结构。例如,在您自己的程序中,以下代码将为您提供进程可执行映像中入口点例程的指针
  • #pragma pack( push, 1 )
    
    struct coff_header
    {
        unsigned short machine;
        unsigned short sections;
        unsigned int timestamp;
        unsigned int symboltable;
        unsigned int symbols;
        unsigned short size_of_opt_header;
        unsigned short characteristics;
    };
    
    struct optional_header
    {
        unsigned short magic;
        char linker_version_major;
        char linker_version_minor;
        unsigned int code_size;
        unsigned int idata_size;
        unsigned int udata_size;
        unsigned int entry_point;
        unsigned int code_base;
    };
    
    #pragma pack( pop )
    
    //
    // get the module address
    //
    
    char *module = (char *)GetModuleHandle( NULL );
    
    //
    // get the sig
    //
    
    int *offset = (int*)( module + 0x3c );
    char *sig = module + *offset;
    
    //
    // get the coff header
    //
    
    coff_header *coff = (coff_header *)( sig + 4 );
    
    //
    // get the optional header
    //
    
    optional_header *opt = (optional_header *)( (char *)coff + sizeof( coff_header ) );
    
    //
    // get the entry point
    //
    
    char *entry_point = (char *)module + opt->entry_point;
  • 顺便说一下,您定义的入口点,即mainWinMain,并不是实际的入口点例程。编译器会插入自己的入口点,该入口点又会调用我们的函数。这个入口点通常会执行CRT初始化和清理等操作。例如,在ANSI控制台应用程序中,实际的入口点例程叫做mainCRTStartup

你在哪里,哦!远程进程的伟大入口点?

  • 因此,很自然地,我们应该能够以类似的方式找到远程进程中的入口点例程,使用ReadProcessMemory。虽然这是真的,但寻找上面代码中与module变量相对应的远程进程变量比预期的要棘手。问题在于,没有方便的GetModuleHandle例程可以用于远程进程。
  • 事实证明,GetModuleHandle返回的虚拟地址仅在该进程的地址空间内有效。然而,ReadProcessMemory需要实际地址才能工作。那么问题来了,我们如何才能知道远程进程在内存中的基地址呢?事实证明,解决方案需要我们深入研究OS的内部!这个解决方案的功劳归于Ashkbiz Danehkar,他的文章《Injective Code Inside Import Table》概述了一种查找此信息的方法。
  • 简而言之,操作系统为系统中的每个线程维护一个用户模式数据结构,称为线程环境块(TEB),它描述了您想了解的关于线程的一切信息,包括指向另一个称为进程环境块(PEB)的数据结构的指针,正如您可能注意到的,它描述了进程,包括,对我们来说很幸运的是,指向进程在内存中的映像基地址的指针!然而,这些结构没有被(Microsoft)记录。但是一些非常非常聪明的人 在这里已经独立地弄清楚了这些结构的布局!
  • 所以,我们只需要
    • 找出主线程的TEB在远程进程中的位置;此信息存储在线程的FS寄存器中,可通过GetThreadSelectorEntry API访问。
    • 通过线程TEB中的指针使用ReadProcessMemory读取PEB。
    • 使用PEB中映像基地址的指针,并解析PE结构,直到我们得到一个指向远程进程入口点例程的引用。
    • 呼!

    这是实现这一目标的代码

    //
    // Gets the address of the entry point routine given a
    // handle to a process and its primary thread.
    //
    
    DWORD GetProcessEntryPointAddress( HANDLE hProcess, HANDLE hThread )
    {
        CONTEXT             context;
        LDT_ENTRY           entry;
        TEB                 teb;
        PEB                 peb;
        DWORD               read;
        DWORD               dwFSBase;
        DWORD               dwImageBase, dwOffset;
        DWORD               dwOptHeaderOffset;
        optional_header     opt;
    
        //
        // get the current thread context
        //
    
        context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
        GetThreadContext( hThread, &context );
    
        //
        // use the segment register value to get a pointer to
        // the TEB
        //
    
        GetThreadSelectorEntry( hThread, context.SegFs, &entry );
        dwFSBase = ( entry.HighWord.Bits.BaseHi << 24 ) |
                         ( entry.HighWord.Bits.BaseMid << 16 ) |
                         ( entry.BaseLow );
    
        //
        // read the teb
        //
    
        ReadProcessMemory( hProcess, (LPCVOID)dwFSBase,
                           &teb, sizeof( TEB ), &read );
    
        //
        // read the peb from the location pointed at by the teb
        //
    
        ReadProcessMemory( hProcess, (LPCVOID)teb.Peb,
                           &peb, sizeof( PEB ), &read );
    
        //
        // figure out where the entry point is located;
        //
    
        dwImageBase = (DWORD)peb.ImageBaseAddress;
        ReadProcessMemory( hProcess, (LPCVOID)( dwImageBase + 0x3c ),
                           &dwOffset, sizeof( DWORD ), &read );
    
        dwOptHeaderOffset = ( dwImageBase + dwOffset + 4 + sizeof( coff_header ) );
        ReadProcessMemory( hProcess, (LPCVOID)dwOptHeaderOffset,
                           &opt, sizeof( optional_header ), &read );
    
        return ( dwImageBase + opt.entry_point );
    }
  • 如果您想知道初始化dwFSBase的奇怪代码是什么意思,我只能将您引向MSDN中LDT_ENTRY数据结构的文档。这类结构部分是系统程序员容易过早秃顶的原因!

向远程进程传递数据

  • 现在我们知道了入口点在远程进程中的位置,这应该很简单,对吧?不对!仍然存在一个棘手的小问题,那就是如何将数据传递给远程进程!
  • 删除我们可执行文件的例程如下所示

    #pragma pack(push, 1)
    
    //
    //  Structure to inject into remote process. Contains
    //  function pointers and code to execute.
    //
    
    typedef struct _SELFDEL
    {
        HANDLE  hParent;                // parent process handle
    
    
        FARPROC fnWaitForSingleObject;
        FARPROC fnCloseHandle;
        FARPROC fnDeleteFile;
        FARPROC fnSleep;
        FARPROC fnExitProcess;
        FARPROC fnRemoveDirectory;
        FARPROC fnGetLastError;
        FARPROC fnLoadLibrary;
        FARPROC fnGetProcAddress;
        BOOL    fRemDir;
        TCHAR   szFileName[MAX_PATH];   // file to delete
    
    } SELFDEL;
    
    #pragma pack(pop)
    
    //
    //  Routine to execute in remote process.
    //
    
    void remote_thread(SELFDEL *remote)
    {
        // wait for parent process to terminate
    
        remote->fnWaitForSingleObject(remote->hParent, INFINITE);
        remote->fnCloseHandle(remote->hParent);
    
        // try to delete the executable file
    
        while(!remote->fnDeleteFile(remote->szFileName))
        {
            // failed - try again in one second's time
    
            remote->fnSleep(1000);
        }
    
        // finished! exit so that we don't execute garbage code
    
        remote->fnExitProcess(0);
    }
  • 您可能已经注意到,函数remote_thread通过函数指针进行所有系统调用,而不是直接调用它们。这是因为,在正常情况下,编译器会在程序调用动态加载的DLL中的例程时生成微小的存根。这个存根会跳转到由操作系统加载程序在运行时初始化的表中的函数指针。由于我们不希望为要注入到远程进程中的代码生成这些花哨的存根,因此我们仅处理函数指针。

    幸运的是,系统API(在kernel32user32等中)始终在所有进程中的同一虚拟地址加载。所以,我们只需要用我们想从远程进程中进行的所有系统调用的指针初始化一个数据结构,并将该结构也传递过去。当然,使用我们的入口点覆盖策略,我们该如何做到这一点?长话短说,我选择了以下方法。

  • 首先,我修改了remote_thread,使其看起来像这样
  • //
    //  Routine to execute in remote process.
    //
    
    void remote_thread()
    {
        //
        // this will get replaced with a
        // real pointer to the data when it
        // gets injected into the remote
        // process
        //
    
        SELFDEL *remote = (SELFDEL *)0xFFFFFFFF;
    
        //
        // wait for parent process to terminate
        //
    
        remote->fnWaitForSingleObject(remote->hParent, INFINITE);
        remote->fnCloseHandle(remote->hParent);
    
        //
        // try to delete the executable file
        //
    
        while(!remote->fnDeleteFile(remote->szFileName))
        {
            //
            // failed - try again in one second's time
            //
    
            remote->fnSleep(1000);
        }
    
        //
        // finished! exit so that we don't execute garbage code
        //
    
        remote->fnExitProcess(0);
    }
  • 然后我将其转换为shellcode。如果您以前从未听说过shellcode这个词,那么这里有一个快速入门。如果您已经知道它是什么,那么可以随意跳过下一节。

Shellcode?

shellcode是(在安全领域)用于利用中的有效载荷的二进制机器码的专业术语。这里有一个快速而粗糙的方法,可以从编译源文件时生成的obj文件生成shellcode。在我们的例子中,我们有兴趣为remote_thread例程生成shellcode。您需要做的是

  • 首先,使用/c命令行选项编译源文件(在我们的例子中是selfdel.c)。这会导致编译器跳过链接步骤。
  • cl /nologo /c selfdel.c
  • 现在,使用实用程序dumpbin来反汇编您的obj文件,如下所示
  • dumpbin /disasm:bytes selfdel.obj > s.asm

    这会生成一个名为s.asm的文件,看起来像这样

    Microsoft (R) COFF/PE Dumper Version 8.00.50727.42
    Copyright (C) Microsoft Corporation. All rights reserved.
    
    
    Dump of file selfdel.obj
    
    File Type: COFF OBJECT
    
    _remote_thread:
      00000000: 55                 push        ebp
      00000001: 8B EC              mov         ebp,esp
      00000003: 83 EC 10           sub         esp,10h
      00000006: 53                 push        ebx
      00000007: C7 45 F0 FF FF FF  mov         dword ptr [ebp-10h],0FFFFFFFFh
                FF
      0000000E: 8B 45 F0           mov         eax,dword ptr [ebp-10h]
    
    ...... more stuff like this ......
    
      000000D2: C3                 ret ; <-- this marks the end of the
                                             assembly dump for _remote_thread
    
    ...... even more stuff like this ......
  • _remote_thread:标记了remote_thread例程的汇编转储的开始,而包含ret语句的行表示例程的结束。在VS.NET 2002/2003/2005中打开s.asm,并删除除_remote_thread:ret之间的内容之外的所有内容。删除包含_remote_thread:的行,这样您将得到类似这样的内容
  •   00000000: 55                 push        ebp
      00000001: 8B EC              mov         ebp,esp
      00000003: 83 EC 10           sub         esp,10h
      00000006: 53                 push        ebx
      00000007: C7 45 F0 FF FF FF  mov         dword ptr [ebp-10h],0FFFFFFFFh
                FF
      0000000E: 8B 45 F0           mov         eax,dword ptr [ebp-10h]
    
    ...... more stuff like this ......
    
      000000D2: C3                 ret
  • 您剩下的是一个可以分为三个逻辑列的文件。
    • 直到冒号(包括冒号)的所有内容都是该指令的字节偏移量。因此,当您看到数字'00000003'时,它表示该指令距离例程开头有3个字节。
    • 接下来是该指令的机器码,后面有一个或多个空格。
    • 机器码之后的所有内容都是汇编指令。
  • 我们感兴趣的是第二列,我们使用Visual Studio的查找/替换对话框中的一些精妙的正则表达式来提取它。在Visual Studio中打开查找/替换对话框,并按给定顺序运行以下表达式
  • 序号。 查找 替换 描述
    1 [0-9A-F]+\::b+{[0-9A-F:b]+}.* \1 从文件中删除第一列和第三列。
    2 ^:b+ 删除前导空格。确保“替换为”文本框中绝对没有任何内容。
    3 :b+$ 删除尾随空格。同样,确保“替换为”文本框中绝对没有任何内容。
    4 \n 空格 删除所有换行符,以便文件中只有一行。在“替换为”文本框中输入一个空格字符。
    5 空格 ', '\\x 将所有空格替换为文字', '\x
  • 现在,在此行的开头键入
  • char shellcode[] = { '\x

    最后,在此行的末尾键入

    ' };

    完成所有这些之后,您应该得到类似这样的内容

    char shellcode[] = {
        '\x55', '\x8B', '\xEC', '\x83', '\xEC',
        '\x10', '\x53', '\xC7', '\x45', '\xF0',
        '\xFF', '\xFF', '\xFF', '\xFF', '\x8B',
    
        // ...... more stuff like this ......
    
    
        '\x5B', '\x8B', '\xE5', '\x5D', '\xC3'
    };

    呼!

我们该如何处理这个Shellcode?

  • remote_thread转换为shellcode后,我们得到类似这样的内容(这只是一个代表性的shellcode,而不是上面显示的例程生成的)
    char shellcode[] = {
        '\x55', '\x8B', '\xEC', '\x83', '\xEC',
        '\x10', '\x53', '\xC7', '\x45', '\xF0',
        '\xFF', '\xFF', '\xFF', '\xFF',   // replace these 4 bytes
    
                                          // with actual address
    
    
        '\x8B', '\x45', '\xF0', '\x8B', '\x48',
        '\x20', '\x89', '\x4D', '\xF4', '\x8B',
        '\x55', '\xF0', '\x8B', '\x42', '\x24',
        '\x89', '\x45', '\xFC', '\x6A', '\xFF', ... more shell code here

    事实证明,在我们的例子中,我们在remote_thread中将指针remote初始化为0xFFFFFFFF的值,在shellcode中也显示为相同的方式。由于我们知道入口点在远程进程中的位置,因此我们需要做的第一件事是在覆盖入口点之前,将shellcode中的0xFFFFFFFF替换为数据的实际指针。看起来是这样的

    STARTUPINFO             si = { sizeof(si) };
    PROCESS_INFORMATION     pi;
    SELFDEL                 local;
    DWORD                   data;
    TCHAR                   szExe[MAX_PATH] = _T( "explorer.exe" );
    DWORD                   process_entry;
    
    //
    // this shellcode self-deletes and then shows a messagebox
    //
    
    char shellcode[] = {
        '\x55', '\x8B', '\xEC', '\x83',
        '\xEC', '\x10', '\x53', '\xC7',
    
        '\xFF', '\xFF', '\xFF', '\xFF',   // replace these 4 bytes
    
                                          // with actual address
    
        '\x8B', '\x45', '\xF0', '\x8B',
        '\x48', '\x20', '\x89', '\x4D',
    
    
        ... snipped lots of meaningless shellcode here! ...
    
        '\xFF', '\xD0', '\x5B', '\x8B',
        '\xE5', '\x5D', '\xC3'
    
    };
    
    //
    // initialize the SELFDEL object
    //
    
    local.fnWaitForSingleObject     = (FARPROC)WaitForSingleObject;
    local.fnCloseHandle             = (FARPROC)CloseHandle;
    local.fnDeleteFile              = (FARPROC)DeleteFile;
    local.fnSleep                   = (FARPROC)Sleep;
    local.fnExitProcess             = (FARPROC)ExitProcess;
    local.fnRemoveDirectory         = (FARPROC)RemoveDirectory;
    local.fnGetLastError            = (FARPROC)GetLastError;
    local.fnLoadLibrary             = (FARPROC)LoadLibrary;
    local.fnGetProcAddress          = (FARPROC)GetProcAddress;
    
    //
    // Give remote process a copy of our own process handle
    //
    
    DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(),
        pi.hProcess, &local.hParent, 0, FALSE, 0);
    GetModuleFileName(0, local.szFileName, MAX_PATH);
    
    //
    // get the process's entry point address
    //
    
    process_entry = GetProcessEntryPointAddress( pi.hProcess, pi.hThread );
    
    //
    // replace the address of the data inside the
    // shellcode (bytes 10 to 13)
    //
    
    data = process_entry + sizeof( shellcode );
    shellcode[13] = (char)( data >> 24 );
    shellcode[12] = (char)( ( data >> 16 ) & 0xFF );
    shellcode[11] = (char)( ( data >> 8 ) & 0xFF );
    shellcode[10] = (char)( data & 0xFF );
    
    //
    // copy our code+data at the exe's entry-point
    //
    
    VirtualProtectEx( pi.hProcess,
                      (PVOID)process_entry,
                      sizeof( local ) + sizeof( shellcode ),
                      PAGE_EXECUTE_READWRITE,
                      &oldProt );
    WriteProcessMemory( pi.hProcess,
                        (PVOID)process_entry,
                        shellcode,
                        sizeof( shellcode ), 0);
    WriteProcessMemory( pi.hProcess,
                        (PVOID)data,
                        &local,
                        sizeof( local ), 0);
    
    //
    // Let the process continue
    //
    
    ResumeThread(pi.hThread);

    好了,就这样。

您可以在此处找到一个自删除可执行文件的代码(该代码除了其他功能外,还从远程进程被劫持的入口点显示一个消息框)。

结论

您在职业生涯中可能永远不需要编写删除自己的程序,但其中有一些巧妙但有些晦涩的技巧,嗯?!一个执行此类操作的商业程序是您在使用Copilot服务时下载的帮助程序。但是,我怀疑该程序使用了另一种技术,可能更直接,也更有趣一万倍;)。简而言之,Copilot 功能规范文档中的一个实现说明指出,自删除是一个简单的过程:

  • 将一个小的EXE作为资源嵌入到您的主程序中
  • 将该EXE提取到临时位置,将FILE_FLAG_DELETE_ON_CLOSE传递给CreateFile
  • 并让小的EXE在主EXE退出后删除它

FILE_FLAG_DELETE_ON_CLOSE标志应该会导致OS在关闭对它的所有打开句柄时删除小的EXE。这确实很巧妙,但嘿,那有什么乐趣呢?!

修订历史

  • 2007年1月7日:文章首次发布
  • 2010年1月27日:修复了生成shellcode的搜索/替换字符串中的拼写错误
© . All rights reserved.