对于程序的自删除,现在网上比较流行的做法一般是创建一个BAT文件来实现,这种方法虽然可行,但毕竟是基于脚本的技术。本文要讨论的是另外一种做法,即在其他进程创建代码以实现删除自身程序。

API说明
我的思路大致是这样的:打开一个目标进程->在目标进程内分配合适大小的内存->构建我们的机器代码->将代码写到目标进程->调用远程线程执行这段代码。涉及到的相关系统API如下。
.DLL命令 OpenProcess,整数型,"Kernel32.dll","OpenProcess",打开进程
        .参数 dwDesiredAccess,整数型,需要的进程权限
        .参数 bInheritHandle,逻辑型,是否可以继承
.参数 dwProcessId,整数型,目标进程的PID
.DLL命令 VirtualAllocEx,整数型,"Kernel32.dll","VirtualAllocEx",分配空间
   .参数 hProcess,整数型,进程句柄
   .参数 lpAddress,整数型,地址指针
   .参数 dwSize,整数型,大小
   .参数 flAllocationType,整数型
   .参数 flProtect,整数型,页面保护属性
.DLL命令WriteProcessMemory,整数型,"Kernel32.dll","WriteProcessMemory",写内存
   .参数 hProcess,整数型,进程句柄
   .参数 lpBaseAddress,整数型,地址
   .参数 lpBuffer,字节集,要写入的内容,这里用字节集
   .参数 nSize,整数型,大小
   .参数 lpNumberOfBytesWritten,整数型,传址,实际写入的大小
.DLL命令 CreateRemoteThread,整数型,"Kernel32.dll","CreateRemoteThread",公开
   .参数 hProcess,整数型,进程句柄
   .参数 lpThreadAttributes,整数型,NULL,线程安全属性
   .参数 dwStackSize,整数型,NULL,堆栈大小
   .参数 lpStartAddress,整数型,Pointer,过程地址
   .参数 lpParameter,整数型,NULL,参数
   .参数 dwCreationFlags,整数型,NULL,建立标志
   .参数 lpThreadId,整数型,传址,Thread identifier,线程表示符



小提示:易语言里面是没有NULL这个数据类型的,这一点和C++等其他高级语言不一样,比如 LPSECURITY_ATTRIBUTES lpThreadAttributes,本来的数据类型应该是SECURITY_ATTRIBUTES的结构,但是我们这里并不需要使用,在C++里面我们可以直接传送NULL,这个NULL就是空的意思。那么实质上就是0指针,也就是整数型的0。另外SIZE_T* lpNumberOfBytesWritten,也就是写内存的最后一个参数实际上是一个输出参数,也就是说函数返回以后,这个参数将返回一些信息,而不是只是调用参数。


代码分析

GetWindowThreadProcessId (GetShellWindow (), PID)
‘获取explorer的进程ID
.如果真 (PID = 0)
信息框 (“无法删除自身!!!”, 0, )
返回 ()
进程句柄 = OpenProcess (#_PROCESS_ALL_ACCESS, 假, PID)
‘打开目标进程
BaseAddress =VirtualAllocEx (进程句柄, 0, 1000, #_MEM_COMMIT, #_PAGE_EXECUTE_READWRITE)
‘在目标进程空间分配内存
.如果真 (BaseAddress = 0)
信息框 (“无法删除自身!!! Error on BaseAddress ”, 0, )
返回 ()
.如果真结束
写入数据 =构建汇编码 (BaseAddress)
WriteProcessMemory (进程句柄, BaseAddress, 写入数据, 取字节集长度 (写入数据), WrittenBytes)
.如果真 (取字节集长度 (写入数据) ≠ WrittenBytes)
信息框 (“警告:长度与原先的不符!程序可能出现异常”, 0, )
返回 ()
.如果真结束
.如果真 (CreateRemoteThread (进程句柄, 0, 0, BaseAddress, GetCurrentProcessId (), 0, ThreadId) < 0)
信息框 (“无法删除自身!!!”, 0, )
返回 ()
.如果真结束
结束 ()

这段代码涉及到了#_PROCESS_ALL_ACCESS和#_PAGE_EXECUTE_READWRITE,易语言本身是没有提供的,那么是怎么得来的呢?我们打开这个函数的MSDN,在其Requirements部分可以看到DLL这一栏的说明:Requires Kernel32.dll,表示这个函数在动态链接库Kernel32.dll中。我们用相关的编辑器\Microsoft Platform SDK\Include\WInnt.h搜索对应的常量(这些头文件可以从Microsoft的网站上下载SDK得来,或者安装C++),我搜索到的结果为“#define PAGE_EXECUTE_READWRITE 0x40”,用计算器将0x40换算成十进制64写到我们的易语言IDE中即可,如图1所示。

图1
下面介绍我们的重点了:构建汇编码子程序!也是本文的核心所在。(需要读者有一定的汇编基础,如果没有,看看我的注释也应该知道大体在干什么了。)
删除文件的API是DeleteFile,在目标进程内执行结束后我们要释放内存,并结束线程。因此要用到VirtualFree和ExitThread。

DeleteFile = GetAddress (“Kernel32.dll”, “DeleteFileA”)
VirtualFree = GetAddress (“Kernel32.dll”, “VirtualFree”)
ExitThread = GetAddress (“Kernel32.dll”, “ExitThread”)
‘GetAddress函数是获取指定函数在指定动态链接库内的地址,这里有一个技巧,Microsoft出于效率考虑,在所有的应用程序中,核心动态链接库(如Kernel32.dll、User32.dll等)的地址在每一个进程中是一样的,所以我们可以在本进程内获取对应的函数地址。

汇编码 = 汇编码 + { 191 } + 到字节集 (DeleteFile)
‘mov edi,kernel32.DeleteFileA
汇编码 = 汇编码 + { 104, 51, 51, 51, 51 }
‘push XXXXXXXX暂时用51来表示,下面来替换成对应的代码
汇编码 = 汇编码 + { 255, 215, 131, 248, 0, 116, 244}
‘call edi、cmp eax,0、je short V2200611.7120CB27,调用目标API(考虑到系统延迟)并循环直到删除成功。

汇编码=汇编码+{191}+到字节集(VirtualFree)+{184}+到字节集(ExitThread)
‘mov edi,kernel32.VirtualFree为了避免计算相对地址(Call address的机器码要计算相对地址),采取通过寄存器来Call API的间接办法来实现。
‘mov eax,kernel32.ExitThread,把这两个API的地址写到指定的寄存器
汇编码=汇编码+{104,0, 64,0,0,104,0,16,0,0}
‘push 4000参数四:页面属性常量,push 1000参数三:分配的大小
汇编码=汇编码+{104,34,34,34,34}
‘要 Free的地址,这里后面计算后替换写入
汇编码=汇编码+{80,255,231}
‘push eax;jmp edi
汇编码 = 汇编码 + { 195 }
‘retn 这一句应该是多余的,因为永远也无法执行到,线程已经结束了。

注意上面的代码,我们为什么要“jmp edi”而不是“call edi”呢?为什么我们压的是四个参数,而不是三个呢?首先,edi在标准的WINAPI调用后的值不会变,我们不需要重新赋值。Call的本质是压入返回地址并jmp到目标地址,这里我们是模拟call!注意eax的地址,是ExitThread!也就是说当执行完 VirtualFree后,不返回到原来的地址中继续执行(也无法执行,因为已经Free掉了这段内存,如果返回将造成内存访问错误,从而引发异常!),而是返回到了kernel32.ExitThread,直接退出这个线程!
大家注意看了,下面的代码是计算字符串偏移,是嵌入机器码成功的关键之处。

偏移=取字节集长度(汇编码)
文件地址=取空白文本(255)
GetModuleFileName(0,文件地址,255)
‘获取当前的执行文件全名
汇编码=汇编码+到字节集(文件地址)+{0}
‘把我们的文件名的字符串(注意不是指针)全部写到机器码后面。
位置=寻找字节集(汇编码,{34,34,34,34},)
‘寻找我们刚才做的标记“要Free的地址,这里后面计算后替换写入”
汇编码=字节集替换(汇编码,位置,4,到字节集(到整数(BaseAddress)))
‘把我们随便写的那个字节集替换成正确的代码,即这段内存的地址!
位置=寻找字节集(汇编码,{51,51,51,51},)
‘同样寻找替换
汇编码=字节集替换(汇编码,位置,4,到字节集(到整数(BaseAddress+偏移)))
‘替换为我们计算好偏移后的字符串所对应的指针,也就是要删除的文件名地址写到这里面

我们计算好的这段汇编码在目标进程中应该是这样的,如图2所示,不同的机器可能得到的地址会不一样。

图2
最后我们再CreateRemoteThread(进程句柄,0,0,BaseAddress,GetCurrentProcessId(),0,ThreadId)就可以在EXPLORER内执行我们的自删除代码了!

总结
我们巧妙地利用CreateRemoteThread这个函数在其他的进程中完成了删除自身,巧妙的在别的进程构建自己的代码,从而利用一些特定的环境执行一些特定的操作。当然了,这个函数还可以用于很多操作,比如DLL注入等,不只是利用于一些木马技术。希望本文能够起到这个抛砖引玉的作用,同时也希望大家思路能够更开阔一些,不要只局限于用BAT删除自身。同时我再给大家留一个作业:本程序只实现了对本进程的 EXE删除,如果有多个文件的情况下又该如何构建机器码呢?构造机器码在EXPLORER内执行的时候,如果程序退出,也就是循环删除成功所需要的时间过长,会导致CPU时间上升,如果解决?(提示:可以嵌入Sleep()函数来实现)。