自删除可执行文件






4.67/5 (44投票s)
如何编写一个可以自我删除的程序
目录
引言
我前几天读到了一篇有趣的文章,讨论了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类型施加了一些限制。事实证明,像DeleteFile
和ExitProcess
这样的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;
main
或WinMain
,并不是实际的入口点例程。编译器会插入自己的入口点,该入口点又会调用我们的函数。这个入口点通常会执行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(在kernel32、user32等中)始终在所有进程中的同一虚拟地址加载。所以,我们只需要用我们想从远程进程中进行的所有系统调用的指针初始化一个数据结构,并将该结构也传递过去。当然,使用我们的入口点覆盖策略,我们该如何做到这一点?长话短说,我选择了以下方法。
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是(在安全领域)用于利用中的有效载荷的二进制机器码的专业术语。这里有一个快速而粗糙的方法,可以从编译源文件时生成的obj文件生成shellcode。在我们的例子中,我们有兴趣为remote_thread
例程生成shellcode。您需要做的是
- 首先,使用/c命令行选项编译源文件(在我们的例子中是selfdel.c)。这会导致编译器跳过链接步骤。
cl /nologo /c selfdel.c
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个字节。
- 接下来是该指令的机器码,后面有一个或多个空格。
- 机器码之后的所有内容都是汇编指令。
序号。 | 查找 | 替换 | 描述 |
---|---|---|---|
1 |
|
\1 |
从文件中删除第一列和第三列。 |
2 | ^:b+ |
无 | 删除前导空格。确保“替换为”文本框中绝对没有任何内容。 |
3 | :b+$ |
无 | 删除尾随空格。同样,确保“替换为”文本框中绝对没有任何内容。 |
4 | \n |
空格 | 删除所有换行符,以便文件中只有一行。在“替换为”文本框中输入一个空格字符。 |
5 | 空格 |
|
将所有空格替换为文字', '\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的搜索/替换字符串中的拼写错误