自删除程序的研究及实现

时间:2022-12-20 11:45:24

这是一个非常有意思的话题, 一个EXE程序如何在运行结束以后从电脑中删除. 网上一搜就会找到各种各样的介绍. 看完这篇文章或许可以帮助你不必继续Google了, 因为我相信读完这篇文章你对这个话题已经有了足够的认识. 而且据我经验, 网上太多的拷贝/粘贴都没有实践过, 甚至有误导的嫌疑. 或者这篇文章对初学者有一定的难度, 但是我还是建议你多花点时间理解一下, 结合代码能让你有更深刻的体会. 而且有一些经验,教训可以避免你犯同样的错误. 更何况我也是一个初学者...如有不当之处敬请谅解...

 

 

声明一下, 本文中所有的方法都非原创, 我所作的只是把文字的东西变成代码, 同时我会对每一种方法的利弊进行分析.

本文一共阐述了7种方法, 其中5种能成功工作. 另外一种颇为经典的方法只能在NT下运行, 另外一种也是网上广为流传的方法未能成功. 原因不解. 

 

接下来正式开始我们的介绍. 如果你是第一次碰到这个话题, 下面的代码可能是最先出现在你脑中的:

当然, 这是不可行的. 要不然也没有讨论的必要. 因为这个时候的文件是被锁住的. 至于为什么这里不便深究. 有兴趣的读者可以研究一些文件映射对象及程序的加载过程. 当然这是一个更加复杂的话题. 其实接下来有一个方法会利用到这个知识, 所以还是会提到一点. 当然也只是一点而已, 因为我也就知道这么一点...

 

 

下面的链接是一篇自删除问题的介绍, 其中列举了一些方法. 你可以看也可以不看. 我放在这里的原因只是想告诉你, 有一些比较'搓'的办法我就不研究了. 我们需要完成的任务是在程序运行结束之后立刻删除, 注意是立刻. 而我所谓'搓'的办法是要在重启计算机后才会删除. 这显然不是我们满意的. MoveFileEx和WININIT.INI就是两种我提到的比较'搓'的办法. 如果你明白了我的意图, 那你可以直接跳过这个链接继续往下了.

http://www.catch22.net/tuts/selfdel

 

 

另外有必要提一下的是我的测试环境, 我使用的机器是win7 x86和win7 x64. 而在win7 64下又分两种情况, 程序以x86及x64方式编译/运行. 每一种环境下都可能导致不同的测试结果. 在具体介绍每一种方法的时候我还会单独提到这个问题.

  

1. Batch File

第一种我介绍的是batch file. batch file运行的时候是不会被lock的, 这就是意味着我们可以临时产生一个batch file, 由这个batch file负责EXE文件及自身的删除:

代码很简单, 不多解释. 代码中有个Sleep只是让你知道在你启动了BAT文件以后原程序不必立刻退出, 因为在BAT文件中我们会重复的进行删除直到成功为止. 但是总是不是那么的舒服, 毕竟有个BAT文件一直在那边等会让你觉得浪费CPU. 这个代码应该可以运行在所有支持BAT文件的windows系统下(本人只在win7 x86和x64下测试通过).

   

2. ComSpec

这是一种接近Batch File的方法, 也很简单:

代码也再简单不过, 没有解释的必要. 跟BAT的区别只是这里我们需要程序立刻返回.

       

3. NT_ONLY

这个我都不知道用什么名字形容比较好, 因为它实在太经典了. 唯一的不足之处是只能在windows NT下运行, 所以我们暂且就称这种方法为: NT_ONLY. 还记得之前我们提过EXE不能删除是因为被锁住了. 这个锁住是因为这个EXE文件被当做一个文件映射, 而有一个函数叫做UnmapViewOfFile可以解除这个映射. 你是不是又在动脑筋了?于是一气呵成:

很可惜, 不但EXE没有删除, 程序还崩溃了(NT, 未验证). UnmapViewOfFile意味着取消文件映射, 具体来说, 是取消用户空间某一段内存到文件的映射, 或者说, 是把该用户空间的内存区域标识为没有映射, 直接导致的后果是把这个内存区域标识为无效. 而UnmapViewOfFile之后的代码正是在这个区域中的, 这下可好, 当运行到GetMouduleFileName的时候内存访问无效, 程序崩溃.

 

接下来让我们整理一下, 有没有办法可以避免这个问题. 在调用完UnmapViewOfFile之后, DeleteFile也是我们必须调用的. 但是这样的话存在两个问题: 1. DeleteFile的调用者不能是我们自己的代码. 2. 既然UnmapViewOfFile之后我们的代码已经无效了, 那程序如何返回?对于第二个问题, ExitProcess可以帮助我们退出程序. 但是ExitProcess又有谁去调呢?另外这些函数的参数我们怎么传递给它们?带着这些疑问, 我们来欣赏一下<<windows NT Native API Reference>>的作者Gary Nebbett的天才之作:

在研究这段汇编代码之前我们注意到有个CloseHandle((HANDLE)4)的调用. 这是因为在NT/2000中保存了一个用户空间的对这个文件映射对象的引用的句柄, 这个句柄是个硬编码:4. 在UnmapViewOfFile之前我们需要先关闭这个句柄. 接下来的一段汇编是整个程序的核心, 在研究这个代码之前请确保你已经充分理解了retn这个返回指令的意义, 如果你对下面的问题没有搞明白, 那我保证你是不会理解这个代码的.

 

调用B(0)的前后堆栈发生了什么变化? 特别是在B函数返回retn 4这条指令做了什么?你可以这么理解这个指令: EIP=[esp], esp+=8. 当然这个是一气呵成的, 这么写只是便于你理解. 有了这个基础, 在结合下面的堆栈变化图, 我相信理解这个代码对你来说已经不在话下了:

自删除程序的研究及实现

 

 

4. Using a DLL

第三种方法虽然精妙但是却只能在NT/2000下工作, 在此基础上有人想出了更加完美的解决方案. 一个动态链接库总是以LoadLibrary, FreeLibrary的方式加载/卸载. 卸载?不就是从内存中撤掉一个DLL的映射么?那我们是不是把UnmapViewOfFile替换成FreeLibrary实现对一个DLL的自我删除呢?事实证明是完全可行的. 那么谁来加载这个DLL, 如果由我们的EXE加载肯定不行, 因为EXE的生命周期肯定长于DLL, EXE还是没办法删除. 微软提供了我们一个工具rundll32.exe允许我们以命令行的形式加载一个DLL并且调用DLL内部的函数, 当然这个函数的原型有一定的规范. 使用方法如下:rundll32.exe XXX.dll,_Func@16 AnyStringYouWantToPassIntoFunc

接下来让我们整理一下, 如何用DLL实现这个目标:

1. 首先我们肯定需要两个文件: EXE+DLL.

2. EXE负责启动rundll32进程, 同时以参数的形式将EXE的路径名传给rundll32, 确切的说是传给DLL中的函数.

3. EXE退出, rundll32开始运行. DLL中的函数被调用, 首先删除EXE文件, 再通过FreeLibrary的手法完成自己的删除.

 

一切都很完美, 但是我们可以做的更完美:

1. 我们只需要提供一个EXE文件, DLL以二进制资源的形式嵌入EXE资源段. EXE运行的时候动态生成这个DLL.

2. EXE必须在启动rundll32之后立刻退出么?答案是否定的, 我们可以在DLL中WaitForSingleObject等待EXE退出. 那么如何在DLL中获得EXE的进程句柄呢?方法当然有很多, 但是有个最直截了当的: 我们还是以参数的形式与EXE路径一起传到DLL的函数中.

 

有了以上的基础, 下面的代码就不难理解了. EXE:

 

DLL:

代码虽然都已给出, 但是要运行起来还是不够的. 因为在我们会把DLL已资源的形式嵌入到EXE中, 所以我们还需要一个resource.h和一个rc文件来完成这个事情, 另外还需要设置一下项目依赖关系, 因为编译EXE的时候需要保证DLL以后生成. 这些工作就交给读者自己了.

读到这里, 你有没有一个疑问: 这个代码在x64下编译运行么?答案是100%否定的. 因为x64根本不支持嵌入式汇编. 那么你可能会继续问: 如果单独用一个asm文件呢?其实我也有这个问题, 甚至还打算动手写一个x64版本的DLL. 刚动手不久, 猛然发现这个技术在x64下面是不可行的, 原因在于x64下函数的前四个参数是用寄存器(rcx,rdx,r8,r9)传递的, 并非堆栈. 如果你还不能理解这一点, 那说明你对之前的那段汇编代码理解不够.

 

 

5. CreateRemoteThread

这也是一个惯用的手法, 编写代码期间也遇到了不少问题. 之前我有一篇文章就是比较详细得记录了用这个方法时遇到的一些问题或现象. 现在你只需关心下面的内容, 在适当的时候我会提醒你可以翻翻那一篇文章. 用CreateRemoteThread的基本想法也很简单: 找到一个比较common的进程, 然后在其中创建一个远程线程, 这个线程就是用来删除我们的EXE的. 当然, 如果不考虑通用性的话, 你可以简简单单的打开QQ, 然后再任务管理器找到进程ID, 注入完事. 估计代码不超过100行. 但是由于我当初的想法就是写一个'比较'通用的程序, 而不是说不打开QQ就不能运行. 那么下面的问题接踵而至:

1. 如何选择进程保证能在不同机器上运行. 当然如果lsass/csrss这样的系统进程能注入的话最好不过, 因为基本上现在的windows平台都会有这些系统进程. 很可惜, 这样进程是不允许你注入的. 这个时候你不妨看看我之前的一篇文章, 其中列举了win 7下CreateRemoteThread所有的情况.

2. 既然系统进程不能使用, 那么我们只能用非系统进程, 但是每个用户的非系统进程都不一样, 怎么样?遍历吧. 问题又来了, 遍历下来以后如何删选, 可能是系统进程可能是用户进程, 在win7x64还可能是x64也可能是x86的. 什么样的进程符合我们的需求?也许你想说, 找到一个进程就注入, 总有一个会成功. 但是事实证明这是一个馊主意, 特别是用x64注入x86进程的时候会导致目标进程崩溃. 所以我们还是老老实实挑选一个我们认为OK的吧. 这个时候如果你还没有读过我之前一篇文章<<远程线程注入时遇到的问题>>, 我还是建议你回去读一下. 通过测试结果, 你可以很清楚地看出什么样的目标是我们需要的: 用户进程/相同位宽(都是x86或者x64).

 

为了实现这个目标, 有不少的工作需要我们完成, 代码量也迅速膨胀:

还是老样子, 代码我不打算过多的解释. 通过之前的介绍加上代码注释我相信不存在什么困难了. 重点还是介绍一下期间遇到的问题已经应该注意的地方:

1. 首先需要提醒的地方是有些代码可能不是必须的, 在这个例子中EnableDebugPrivilege和GetModuleFileNameEx就是如此. 有些函数是我在不断尝试过程中加入的, 为了方便以后查阅而没有删除(当然是无害的), 一般我会加上注释说明.

2. 为了使CreateRemoteThread这个方法正常工作, 光有代码是不顶用的. 还需要一些配置, 具体做法在代码上方已经给出. 至于为什么要这样设置是另外一个话题, 请参考我之前的另外一篇文章<<运行时和编译时的安全性检查>>:

http://blog.csdn.net/panda1987/archive/2010/10/19/5952262.aspx

3. ntdll.h这个头文件不是公开的, 需要自己网上下载.  很多数据结构比如PROCESS_BASIC_INFORMATION, PEB_LDR_DATA, PEB, LDR_MODULE都是在这个头文件中定义的. 碰到了一个不解的问题是有一个winternl.h的头文件也有这些数据结构的定义, 但是有些定义(比如PEB_LDR_DATA)却与ntdll.h不同, 这个头文件是不能用的. 困惑中...

4. 有一部分API会在这样的环境中失败: OS: win7x64. Self.exe:x86. Target.exe:x64. 比如GetModuleFileNameEx就是一个例子. 简单分析一下原因: 在使用GetModuleFileNameEx的时候系统会访问进程的PEB, PEB的指针存放在一个叫PROCESS_BASIC_INFORMATION的结构体中. 那么当我们用x86的进程读取一个x64进程的PEB时会遇到什么问题? 按照正常的思维, x86进程无法装下64位指针会发生截断. 但是我还不清楚WOW64下windows做了什么处理, 更深入的说WOW64是如何实现的. 这个希望以后有足够的知识去研究. 这里只是提出有这么一个问题, 确实有部分API是在WOW64下不能正常工作的.

5. 在判断程序是否是32位还是64的问题上遇到了一点问题, 我能想到的唯一的办法就是找到可执行映像的基址, 然后逐步找到IMAGE_OPTIONAL_HEADER中Magic这个字段. 理解这个代码需要一定的PE基础, 网上有很多相关资料, 这里就不多解释了. 我遇到的问题是这样的, 在x64+x64+x64(OS:x64/本进程:x64/目标进程:x64)环境下IMAGE_DOS_HEADER都能正常得到, 但是大部分目标进程的IMAGE_NT_HEADERS却不能获得. ReadProcessMemory在读取x64进程的IMAGE_NT_HEADERS的时候出现三种情况:

1) 如果目标进程的基址<0x0000 0001 0000 0000. ReadProcessMemory失败. GetLastError()==0x000003E6. 错误信息: Invalid access to memory location.

2) 如果目标进程的基址>0x0000 0001 0000 0000. ReadProcessMemory失败. GetLastError()==0x0000012B. 错误信息: Only part of a Read ProcessMemory or WriteProcessMemory request was completed.

3) 如果目标进程的基址==0x0000 0000 0040 0000. ReadProcessMemory成功. 可惜的是这样的64位程序很少. cmd就是一个例子.

至于为什么, 我也很不解. 如果有知晓的朋友务必指教. 感激不尽!

 

6. CreateProcess

这个方法跟之前的远程线程相当类似. 比起CreateRemoteThread有一个优点就是进程的选择更加灵活, 我们可以选择explorer这样的进程作为我们的目标进程, 不需要遍历等等操作. 基本的想法是这样的:

1. 用CreateProcess创建一个进程, 比如explorer. 创建的时候挂起.

2. 找到该进程的基址, 注入代码.

3. 恢复进程的执行.

 

我们可以通过与CreateRemoteThread的比较更具体的了解这个方法, 我们知道CreateRemoteThread提供两个参数, 一个是远程函数地址, 一个是远程数据地址. 这样我们可以通过两次VirtualAllocEx得到的指针分别作为这两个参数. 这样远程函数就可以方便的访问这些数据. 可是CreateProcess就不同了, 我们是把映像的基址所在的代码替换了, 而基址通常是由CRT调用的. 这样的话数据怎么办? 最简单的例子就是EXE的路径, 放在哪里? 如何在目标代码中正确的访问? 当然方法有很多, 我们这里提供一个比较简单的:

1. 代码还是正好放在基址位置. 要不然我们还要计算、更新基址.

2. 数据紧跟着代码放. 这里的数据无非是一个RMTDATA结构.

那么问题又来了, 在代码中如何快速的定位到这个数据结构的起点? 如果我们能找到映像的基址当然一切不成问题, 只要基址+函数长度就是数据的起点. 虽然我们在代码中已经得到了映像的基址, 但是你别忘了这是在将要被删除的EXE内完成的. 如今代码已经注入到explorer, 如果要获得映像的基址, 那还是要大动干戈重新来一边. 我们有更快捷的方法, 首先请读者试想一下, 如何在程序中获得当前EIP的值, 也就是当前运行代码的地址. 我提供两种办法:

如果你对这个代码还不理解, 建议你补补汇编知识. 这里获得的是当前指令的地址, 显然不是基址. 但是我们可以肯定的是这个地址肯定大于基址, 而且相距不远. 这样的话, 我们可以利用一些标志字节来搜索数据结构的起始地址.

 

接下来的代码对你来说肯定小菜一碟了:)

这个程序可以以x86的形式在x86和x64下运行, 但是却不能以x64的形式运行. 因为explorer是32位的, 而注入函数却是以x64编译的, 不出问题才怪. 解决的方法应该不难, 可以用shellcode实现, 直接将x86下编译的二进制代码写入目标进程. 这里我就不试了, 有兴趣的读者不妨自己一试.

 

这次为止, 已经介绍了6种自删除程序的实现方法, 除第三种只能在NT/2000下运行以外, 其他程序都能在win7x86,win7x64下面运行. 第四种方法需要读者自己添加一个resource.h和rc文件, 第五第六种方法需要读者自己下载一个ntdll.h文件. 另外要对vs进行一定的设置. 在结束之前, 还有一种方法也是网上广为流传的, 有兴趣的读者不妨搜索一下'DeleteMe.cpp'. 可惜的是99%都是copy/paste. 我按照同样的方法重新写了一份代码, 经测试这个方法不能正常工作.

 

 

7. FILE_FLAG_DELETE_ON_CLOSE(Not Working)

我不能确定这个代码为什么广为流传, 或许它能再win9x上工作. 只可惜过时了...

 

 

THE END...Thank you for reading:)