本系列教程版权归“i春秋”所有,转载请标明出处。
本文配套视频教程,请访问“i春秋”(www.ichunqiu.com)。
如果说我们的计算机中安装有杀毒软件,那么当我们有意或无意地下载了一个恶意程序后,杀软一般都会弹出一个对话框提示我们,下载的程序很可能是恶意程序,建议删除之类的,或者杀软就不提示,直接删除了;或者当我们运行了某一个程序,包含有可疑操作,比如创建开机启动项,那么杀软一般也会对此进行提醒;或者当我们在计算机中插入U盘,杀软往往也会第一时间对U盘进行扫描,确认没有问题后,再打开U盘……上述这些,其实都属于杀软的“主动防御”功能。
杀毒软件通常集成监控识别、病毒扫描和清除、自动升级病毒库、主动防御等功能,有的杀毒软件还带有数据恢复等功能,是计算机防御系统的重要组成部分。关于其中的“病毒扫描和清除”,我们之前在专杀工具的编写以及特征码查杀的课程中,已经讲解过基本的原理。而杀毒软件发展至今,各大安全厂商也越来越将目光放在“主动防御”技术的研发上。之所以会如此重视这项技术,是因为优秀的“主动防御”系统即便不升级病毒库,依旧可以预防各种未知木马和新病毒。因为任何恶意程序,它如果想要实现各种各样的功能,最终都是需要调用各种API函数的。那么我们就可以对敏感API函数进行监控,在API函数执行之前,先解析API函数各个参数的内容,从而判定目标程序究竟是想实现什么样的功能,如果是恶意操作,就进行拦截,让该API函数无法执行。如果是正常操作,则直接放行。
比如恶意程序都喜欢将自己添加进系统的启动项中,那么我们就可以开启对注册表的相关函数的监控,如果检测到目标程序会添加注册表启动项,那么在添加之前,先进行拦截,询问用户是否同意该程序加入启动项,如果用户同意,则取消拦截,将程序加入启动项。如果用户不同意,或者“主动防御”系统就认定目标程序是恶意程序,那么就会阻止恶意程序的这一功能,以保证我们计算机的安全。通过我们之前的课程对于病毒的分析可以知道,病毒程序往往会有多种行为,那么也就需要我们对于计算机系统的方方面面进行监控,甚至还需要一定的算法来分析各个API函数调用的相互关系,从而对目标程序进行判定。正是因为这样,“主动防御”系统才不是那么倚重病毒库的支持。因此很多高手的计算机中往往不安装任何杀毒软件,但是却一定要安装“主动防御”系统。
成熟的杀毒软件或者专门的“主动防御”系统,它们的编程实现是基于 Ring0层的,因为也只有在内核级别,才能够实现良好的监控效果。而我们的课程为了简单起见,也便于大家理解,讨论的是最简单的基于Ring3层的“主动防御”系统的实现。虽说是R3级的,但是也涉及了不少的知识,希望大家一定要先将这次所讲的理论弄清楚,这样在接下来的编程实现中,才会游刃有余。
我们需要的知识有以下几点:
1、Inline Hook的基本原理
2、DLL注入的基本方法
3、对系统进行全局监控
#include<stdio.h> #include<windows.h> int main() { char szCommandLine[] = "notepad.exe"; // 所要启动的程序 STARTUPINFO si = {sizeof(si)}; PROCESS_INFORMATION pi; si.dwFlags = STARTF_USESHOWWINDOW; // 指定wShowWindow成员有效 si.wShowWindow = TRUE; // 此成员设为TRUE的话则显示新建进程的主窗口 BOOL bRet = CreateProcess( NULL, // 不在此指定可执行文件的文件名 szCommandLine, // 命令行参数 NULL, // 默认进程安全性 NULL, // 默认进程安全性 FALSE, // 指定当前进程内句柄不可以被子进程继承 CREATE_NEW_CONSOLE,// 为新进程创建一个新的控制台窗口 NULL, // 使用本进程的环境变量 NULL, // 使用本进程的驱动器和目录 &si, &pi ); if(bRet) { // 关掉不使用的句柄 CloseHandle(pi.hThread); CloseHandle(pi.hProcess); } getchar(); return 0; }
上述程序运行后,会打开“记事本”程序。因为我们整个“主动防御”程序的设计就是围绕着CreateProcess()这个函数展开的,所以我们的例子也是以这个函数来讲解的。我们可以使用OD载入这个程序来看一下函数调用位置处的语句:
图1
可以看到,当一系列的push让参数入栈后,程序调用了call语句来调用kernel32.dll中的CreateProcess()函数。其实call语句要实现两个功能,首先是向栈中压入当前指令在内存中的位置,即利用push语句保存返回地址;之后是利用jmp语句跳转到所调用函数的入口处。可以先单步执行到上图中的call语句处,然后按下F7步入这个call:
图2
此时程序跳到了0x7C80236B的位置,也就是CreateProcess()真正实现的代码位置。通过观察当前内存中的加载状态,可知该地址位于kernel32.dll中:
图3
可以总结一下,当我们所编写的exe文件需要调用CreateProcess()函数的时候,会将kernel32.dll载入内存(其实不管运行什么程序,kernel32.dll一般都会自动载入内存),然后调用该动态链接库中的CreateProcess()函数(其实真正调用的是CreateProcessA或CreateProcessW),因为真正的CreateProcess()函数的实现在kernel32.dll模块中。这里所说的调用,其实可以理解为直接跳到该函数的地址去执行。基于这些知识,我们完全可以修改API函数在内存中的映像,从而实现对API函数的钩取。具体来说就是直接使用汇编指令jmp来改变代码的执行流程,先不让它跳到0x7C80236B的位置,而是跳到我们自己编写的代码处执行,在执行完我们自己的代码后,再决定是否让它再跳到0x7C80236B继续执行。
那么现在的问题是应该如何构造jmp语句。其实关于这个问题,我在《缓冲区溢出分析》系列的课程中,曾经讲过一个“jmp back”的方法,这里的方式与它是一样的。不妨先来看一下,我们的程序中的jmp语句的特点。进入main函数后,正好就有一个jmp:
图4
程序通过这个jmp,就来到的main函数的真正位置。这里分析一下位于0x00401005处的反汇编代码,是“E9 06000000”。其中的“E9”就是jmp对应的机器码,后面的“06000000”其实是一个跳转偏移,偏移的计算公式如下:
jmp后的偏移值 = 目的地址 – 当前地址 - 5
公式中减去5,是因为jmp指令进行跳转,需要五个字节实现。那么针对于我们当前的这个程序,结合上图,目的地址为0x00401010,当前地址为0x00401005,即:
jmp后的偏移值 = 0x00401010 - 0x00401005 – 5 = 6
也就是“E9”后面的“06”的由来。经过上述分析可知,我们只要在欲钩取的函数位置处,修改前五个字节为我们的jmp语句,使其跳向我们自己的函数位置就可以了。
由于这种方法是在程序的流程中直接进行嵌入jmp指令来改变流程的,所以就把它叫做Inline Hook。
我们希望被钩取的函数所在的程序,在每次调用CreateProcess()的时候,都能够主动执行我们的jmp语句,这就需要我们对目标程序的功能进行扩展,这可以通过让目标程序加载我们编写的DLL来完成。那么为了让目标程序能够加载DLL,就需要利用DLL注入的方法来实现。
具体到我们所要编写的主动防御程序,其实函数钩取,也就是Hook CreateProcess()功能,是通过一个DLL程序来实现的,而我们的主函数的作用,就是将DLL注入到相应的进程中,当停止监控时,再将DLL卸载。DLL的注入与卸载是一系列严格的流程,注入的流程如下:
1、OpenProcess获得要注入进程的句柄
2、VirtualAllocEx在远程进程中开辟出一段内存,长度为strlen(dllname)+1;
3、WriteProcessMemory将Dll的名字写入第二步开辟出的内存中。
4、CreateRemoteThread将LoadLibraryA作为线程函数,参数为Dll的名称,创建新线程
5、CloseHandle关闭线程句柄
卸载的流程如下:
1、CreateRemoteThread将GetModuleHandle注入到远程进程中,参数为被注入的Dll名
2、GetExitCodeThread将线程退出的退出码作为Dll模块的句柄值。
3、CloseHandle关闭线程句柄
3、CreateRemoteThread将FreeLibraryA注入到远程进程中,参数为第二步获得的句柄值。
4、WaitForSingleObject等待对象句柄返回
5、CloseHandle关闭线程及进程句柄。
其实上述流程的道理并不难,而我们下次课程中的程序的编写,就依据上述流程来进行。
在Ring3层,我们常用的对系统进行全局监控的方式是使用Windows的全局钩子。在操作系统中安装全局钩子以后,只要目标进程符合我们所设定的条件,全局钩子的DLL文件会被操作系统自动或强行地加载到该进程中,而DLL文件存放的正是钩子函数的代码,也即我们想要钩取实现的功能。可见,这种钩子需要使用DLL注入的方式来实现。如果使用这种方式,我们需要使用SetWindowsHookEx()函数来进行钩子的设置,并将该函数的第一个参数设置为WH_GETMESSAGE,即监视被投递到消息队列中的消息。那么关于这种方式的具体实现方法,由于并不是我们讨论的重点,因此大家可以参考相应的资料。我们这次所采用的是一种比较简单的办法,并不需要进行全局监控,而是监控explorer.exe进程。
其实绝大多数的进程都是由explorer.exe进程创建的,比如我们打开ProcessExplorer,然后运行一下我们上面所编写的程序:
图5
可见在explorer.exe进程下有非常多的子进程,而我们的CreateProcessTest.exe正是其子进程的一员,包括由该程序所启动的notepad.exe程序。因此只要我们对该进程进行监控,钩取其CreateProcess()函数,就能够达到我们想要实现的监控目的。当然,使用这种方式并不见得能够起到绝对的监控效果,毕竟方法还是太简单了,但是基本能够达到我们希望的效果。