开机启动和隐藏进程的基本原理
摘要
开机启动和隐藏进程作为两种程序设计基本技术为病毒程序广泛使用,本文在开机启动和隐藏进程代码实现的基础上对实现所用到的相关技术原理进行介绍。具体涉及到的相关技术包括服务的创建,进程快照的获取,以及进程的注入。本文采用VC6.0为编译环境,通过代码和说明相结合的方式对三种技术进行阐述。
关键词: 服务 进程快照 进程注入
开机启动的方法有很多种,包括写注册表,写入启动配置文件,复制到启动目录,写成驱动程序,注册服务等,笔者的程序采用windows服务实现开机启动。服务的编写有多种方式,包括用C语言编写,用atl模板编写,以及用托管C++进行编写。为了更好地理解服务创建的基本原理,笔者采用了C语言编写服务。下面具体阐述C语言编写服务的4个步骤。
主函数的创建很简单,具体代码如下:
void main()
{
SERVICE_TABLE_ENTRY ServiceTable[2];
ServiceTable[0].lpServiceName = "MemoryStatus";
ServiceTable[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;
ServiceTable[1].lpServiceName = NULL;
ServiceTable[1].lpServiceProc = NULL;
StartServiceCtrlDispatcher(ServiceTable);
}
由于每个程序可能包含多个服务,因此需要有一个服务列表保存程序所用到得服务信息,SERVICE_TABLE_ENTRY就是这样一个列表,每一个列表项包含了服务名称(lpServiceName)和服务入口函数(lpServiceProc)两个属性。服务列表的最后一项的两个属性必须设置成NULL。在这里我们引入一个新的概念,即服务控制管理器(SCM),作为一个管理系统服务的进程。SCM等待某个进程的主线程调用StartServiceCtrlDispatcher函数,将分派表传递给ServiceCtrlDispatcher。该分派器启动一个新的线程,此线程运行每个服务所注册的服务函数(为lpServiceProc属性所指定),同时还负责监视所有服务的执行情况,将控制请求(外部对服务的启动,关闭等控制信息)传递给服务。当分派表中的所有服务执行完以后,startservicectrldispatcher调用返回,然后主线程终止。
Servicemain函数是main函数中服务列表每项所注册的服务函数,该函数是服务的入口点,在这里完成控制处理器的注册(通过RegisterServiceCtrlHandler函数实现,具体代码在笔者的程序中),RegisterServiceCtrlHandler接收两个参数:服务名和指向控制分派器调用函数的指针。控制分派器是一个函数,在创建服务的第三个步骤中给予说明。在此之后通过调用SetServiceStatus函数项SCM报告服务的状态。SetServiceStatus接收ServiceStatus结构体,此结构体给出服务的当前状态,服务处理的请求类型(如开启和关闭服务等)。
在ServiceMain函数中注册了控制处理器函数,此函数类似于Windows消息的窗口回调函数,检查SCM发送了何种请求,并针对每个不同请求采用不同的行为。如SERVICE_STOPPED请求对应于用户停止服务时产生的行为。在响应请求后需要调用SetServiceStatus向SCM报告服务状态。
进入命令行运行sc create 服务名 binpath= 服务路径,即注册服务,运行sc delete 服务名即完成了服务删除,运行 sc config 服务名start= auto/demand 即完成服务类型的选择。
本程序在main函数中实现了服务的安装,为了实现程序的开机启动,需要把服务类型设置为自动启动,此命令需要调用dos指令 sc config 服务名start=auto 执行。笔者在程序中通过调用shellexecute方法执行sc.exe进程实现了服务的安装和自动运行。不需要在cmd下对服务进行配置。
在上面的篇幅中介绍了服务的创建,创建服务并设置类型为自动启动后可以实现程序的开机启动,但是并不能完成进程功能的隐藏。那么应该如何实现进程功能隐藏呢?笔者在这里采用了进程注入的方式实现进程隐藏,此方式将程序的功能模块注入到某个必要的系统进程中,作为此系统进程的一个线程独立运行。这样人们并不能通过任务管理器找到实现此功能的源进程。
注入进程采用创建远程线程的方式实现,即获得被注入进程的句柄后,通过该句柄在被注入进程中创建一个线程。CreateRemoteThread就是完成这样一个功能的函数,该函数中有三个重要的参数,即被注入进程的句柄,注入的线程函数地址,以及注入线程所接收的参数。笔者在这里利用了动态链接库中的dllmain函数,在dllmain函数中实现注入线程所要实现的功能(调用shellexecute方法启动记事本程序),该函数在动态链接库加载时自动运行。因此我们创建的远程线程需要接收的注入的线程函数地址就变成loadlibrary函数的地址,注入线程所要接收的参数就变成了dll文件所在的路径。但是新的问题来了,每个进程都有自己独立的地址空间,loadlibrary应该隶属于被注入进程的那一块地址呢?我们知道,windows98后windows操作系统对进程访问做了很多限制,不同的进程不能互相访问彼此的地址空间。但是,loadlibrary函数在每个用户进程中都要被加载,且加载地址都相同,这样可以方便进程对系统公用dll进行共享Loadlibrary属于kernel32.dll,因此我们只需调用getprocaddress方法获得kernel32.dll中的loadlibrary地址,并将此地址作为参数传递给远程线程即可。但是这里又有了新的问题,远程线程中传入函数地址的同时要传入该函数对应的参数,如果参数是简单的数值类型可以直接传入,但是我们在这里要传入loadlibrary所对应的dll的地址,该地址类型是char*类型,即进程的逻辑地址,因此将它直接作为远程线程函数的参数是不行的。我们需要采用新的方法来解决此问题。笔者在这里首先利用被注入进程的句柄为注入进程分配一块新的内存空间,然后将dll地址拷贝到此空间中。调用virtualallocex函数可以分配新的内存空间,调用writeprocessmemory将dll的地址拷贝到此片空间,这样就完成了进程的注入功能。该线程随着被注入进程的结束而结束。至此进程的注入就完成了,但是我们前面提到过需要获得被注入进程的句柄,该句柄如何获取呢?这就涉及到了进程快照的获取问题。
进程快照的获取通过调用CreateToolhelp32Snapshot函数完成, 此函数返回进程快照的句柄。在获得快照的同时我们需要对该快照进行遍历以查找我们要注入的进程的id号,只有通过此id号才可以获得进程的句柄。遍历通过::Process32First和::Process32Next函数来完成,::Process32First返回快照的第一个进程信息,该信息中包含了进程的名称和进程的id,因此我们可以得到指定名称进程的id。得到了进程id后通过openprocess函数就可以获得进程的句柄。笔者在程序中选择了explorer.exe进程进行注入。结合上一步的进程创建远程线程注入进程的方法就完成了整个进程的注入,然后将这个进程注入代码嵌入到服务代码中的servicemain函数中就实现了开机启动和隐藏进程。但是我们在运行程序的时候会发现服务创建的时候在对应的任务管理器中也会出现服务所调用的exe程序名称。这样人们会很快通过该exe程序名称锁定服务程序地址。如何在服务启动后完成注入并关闭进程呢?关闭进程后远程线程还会起到相关作用吗?笔者在最后对这两个问题的解决方法进行说明。
首先是第一个问题,进程在关闭后远程线程是否还可以继续运行,答案是肯定的,因为远程线程被注入后就作为独立的线程被附加到被注入进程中,所以不会随着原进程结束而结束。然后就是第二个问题,如何在注入服务启动后关闭注入服务,笔者在此利用了dos命令(taskkill /im 进程名 /f)在服务启动后关闭该服务。但是直接在服务中调用shellexecute方法执行此命令系统会报出服务异常终止的错误。为了避免此情况的出现,笔者在另外一个程序中实现此功能,并在服务启动前调用此函数。值得注意的是,笔者在另外一个进程的main函数中首先让该进程睡眠10秒钟,如果不这样做系统同样会报出服务会异常关闭的错误。
到此为止,整个进程开机启动和进程隐藏的方法就介绍完毕。程序代码和该文档放在了同一个打包文件中以供参考,7个实验都附带有相关文档和注释进行说明。代码包括了服务的创建,进程快照的获取和进程注入的实现。在做此项目和7个实验的过程中。笔者的c++水平从刚刚起步步入到可以利用c++编写简单项目阶段,懂得了动态链接库的静态包含和动态加载,利用关键代码实现线程的同步,利用共享内存进行进程通信,利用socket实现文件传输,利用activatemovie控件进行视频控制,菜单的控制以及聊天程序的创建和托盘的实现等。进程开机启动和进程隐藏是对7个实验的一次综合利用,涉及到了7个实验中的文件操作,虚拟内存实现进程通信,动态链接库的动态加载等。