这次所讨论的三个问题,比如DLL以及HOOK,很容易被病毒木马所利用,因此必须要比较全面地进行了解。而异常处理机制,则往往与漏洞相关联。它们自身的概念并不难理解,只是由之引申而来的问题,在计算机安全领域则是比较高级的技术,也是需要掌握的。尽管面试的时候,往往是理论性比较强,但是实际上最重要的还是动手能力。作为一名反病毒工程师(尽管我目前还不是),不单单要掌握恶意程序的编写技术,更需要清楚知道如何对付这些病毒木马。从这里也能够看出来,反病毒工程师的技术要求是比较高的,毕竟不单单要知道如何以正常手段使用计算机技术,更要知道如何以非正常手段编写出恶意程序,知己知彼,才能更好地与病毒木马作斗争。
问题10——问题12如下:
**10、请简述你对动态链接库(DLL)的理解。**
答:(以下内容选自**《VC++深入详解》**第19.1节——动态链接库概述)
自从微软推出第一个版本的Windows操作系统以来,动态链接库(DLL)一直就是Windows操作系统的基础。动态链接库通常都不能直接运行,也不能接收消息。它们是一些独立的文件,其中包含能被可执行程序或其他DLL调用来完成某项工作的函数。只有在其他模块调用动态链接库中的函数时,它才发挥作用。在实际编程时,我们可以把完成某种功能的函数放在一个动态链接库中,然后提供给其他程序调用。
WindowsAPI中所有的函数都包含在DLL,其中有3个最重要的DLL。
▪Kernel32.dll
它包含那些用于管理内存、进程和线程的函数,例如CreateThread函数;
▪User32.dll
它包含那些用于执行用户界面任务(如窗口创建和消息的传送)的函数,例如CreateWindow函数;
▪GDI32.dll
它包含那些用于画图和显示文本的函数。
**知识扩展:**
(以下内容选自**《C++黑客编程揭秘与防范》**第3.7.1节——DLL注入)
木马或者病毒编写质量的好坏取决于其隐藏程度,而不在于其功能多少。无论是病毒或是木马,它们都是可执行程序,如果它们是EXE文件的话,那么就必将会产生一个进程,产生进程就很容易被发现。为了不被发现,可以选择为DLL文件,DLL文件加载到进程的地址空间中,不会有进程名,因此其隐蔽性相对较好。DLL文件如果不被进程加载又如何在进程中呢?方法是强制让某进程加载DLL文件,这个强制的手段就是下面要介绍的通过创建远程线程将DLL注入到某个指定的进程中。
(以下内容选自**《Windows核心编程第5版》**第22.4节——使用远程线程来注入DLL)
……
从根本上说,DLL注入技术要求目标进程中的一个线程调用LoadLibrary来载入我们想要的DLL。由于我们不能轻易地控制别人进程中的线程,因此这种方法要求我们在目标进程中创建一个新的线程。由于这个线程是我们自己创建的,因此我们可以对它执行的代码加以控制。幸运的是,Windows提供了如下所示的CreateRemoteThread函数,它使得在另一个进程中创建线程变得非常容易:
……
现在我们已经理解了我们要做什么,让我们来总结一下必须采取的步骤。
(1)用VirtualAllocEx函数在远程进程的地址空间中分配一块内存。
(2)用WriteProcessMemory函数把DLL的路径名复制到第1步分配的内存中。
(3)用GetProcAddress函数来得到LoadLibraryW或LoadLibraryA函数(在Kernel32.dll中)的实际地址。
(4)用CreateRemoteThread函数在远程进程中创建一个线程,让新线程调用正确的LoadLibrary函数并在参数中传入第1步分配的内存地址。这时,DLL已经被注入到远程进程的地址空间中,DLL的DllMain函数会收到DLL_PROCESS_ATTACH通知并且可以执行我们想要执行的代码。当DllMain返回的时候,远程线程会从LoadLibraryW/A调用返回到BaseThreadStart函数(在第6章中介绍)。BaseThreadStart然后调用ExitThread,使远程线程终止。
现在远程进程中有一块内存,它是我们在第1步分配的,DLL也还在远程进程的地址空间中。为了对它们进行清理,我们需要在远程线程退出之后执行后续步骤。
(5)用VirtualFreeEx来释放第1步分配的内存。
(6)用GetProcAddress来得到FreeLibrary函数(在Kernel32.dll中)的实际地址。
(7)用CreateRemoteThread函数在远程进程中创建一个线程,让该线程调用FreeLibrary函数并在参数中传入远程DLL的HMODULE。
**11、请简述你对HOOK的理解。**
答:(以下内容选自**《VC++深入详解》**第20.1.1节)
首先回顾一下第一章中讲述的Windows消息传递机制。当在应用程序窗口中按下鼠标左键时,操作系统会感知到这一事件,然后产生鼠标左键按键消息,接着把此消息放到应用程序的消息队列中,应用程序通过调用GetMessage函数取出消息,然后调用DispatchMessage函数将这条消息调度给操作系统,操作系统会调用在设计窗口类时指定的应用程序窗口过程对这一消息进行处理。这一过程就是所有运行在Windows平台下的窗口应用程序的消息传递过程,如图20.1所示。
![](https://box.kancloud.cn/2016-02-17_56c428a7561e8.jpg)
图20.1 Windows应用程序的消息传递机制
在实际应用中,有时可能需要对某个特殊消息进行屏蔽,例如,我们开发了一个应用程序,不想让它对键盘上的回车键和空格键做出响应,就需要截获所有消息,然后进行判断,如果是回车或空格按键消息,就将这两种消息屏蔽掉,也就是说,不让这样的消息继续传递下去。另一种情况,例如,我们开发了一个安装程序,在安装过程中希望安装程序不能响应用户的鼠标和键盘的按键消息,以免影响软件的安装过程,那么也需要截获这两类消息,然后让它们不再继续向下传递。
为了实现这一功能,可以安装一个HOOK过程,称为钩子过程。操作系统在传递消息时,将我们感兴趣的消息先传递给HOOK过程,在此函数中进行检查,然后再决定是否放行该消息。这就好像逃犯在逃亡时可能会经过许多路段,为了抓住他,警察要在某些地方设置检查站,以便检查过往的车辆和行人。我们可以把车辆和行人看做是消息,检查站就是HOOK过程。如果在某个检查站发现了这个逃犯,就会把他抓起来。这样就相当于阻止了逃犯的逃亡过程,让他无法再继续逃亡下去了。这个道理和钩子过程是一样的,操作系统将我们感兴趣的消息都先交给钩子过程,后者实际上就是一个函数,在此函数中进行判断,如果是我们希望屏蔽掉的消息,那么就直接处理掉,不让它再继续向下传递。如果是其他我们不感兴趣的消息,就直接放弃对它们的处理。这就好像对于那些不是逃犯的行人和车辆一样,警察将会让他们继续前进。
(以下内容选自**《C++黑客编程揭秘与防范》**第5.1节——HOOK知识前奏)
在DOS时代进行编程,那时操作系统提供的编程接口不称为API函数,而称为中断服务向量。也就是说,当时的操作系统提供的编程接口只有中断,要进行写文件要调用系统中断,要进行读文件也要调用系统中断(当然,也不调用操作系统的中断直接调用更底层的中断)……中断服务向量类似于Windows下的API函数,中断服务向量在操作系统的某个地址保存着,它是以数组形式保存着的,我们也称其为中断向量表。DOS时代的HOOK技术也就是修改中断向量表中的中断地址。比如,要捕获写操作,那么就修改中断向量表中关于写文件的地址,将写文件的中断地址保存好,然后替换为我们的地址,这样当程序调用写文件中断时,我们的函数就被执行了;当程序执行完可以继续调用原来的中断地址,从而完成写文件的操作。
在Windows下HOOK技术的方法比较多,使用比较灵活,常见的应用层的HOOK方法有Inline Hook、IAT HOOK、Windows钩子……HOOK技术涉及到了DLL相关的知识。因为HOOK其他进程的时候需要访问其他进程的地址空间,使用DLL是必然的。HOOK技术也涉及到了注入的知识,想要把完成HOOK功能的DLL加载到目标进程空间中就要使用注入的知识了。让我们来学习常用的HOOK技术吧。
**知识扩展:**
**(以下内容选自**《**C++黑客编程揭秘与防范》**第5.2节——内联钩子:InlineHook)
API函数都保存在操作系统提供的DLL文件中,当在程序中使用某个API函数时,在运行程序后,程序会隐式地将API所在的DLL加载入进程中。这样,程序就会像调用自己的函数一样调用API
……
假设要对某进程的kernel32.dll的CreateFile()函数进行HOOK,首先需要在指定进程中的内存中找到CreateFile()函数的地址,然后修改CreateFile()函数的首地址的代码为jmp MyProc的指令。这样,当指定的进程调用CreateFile()函数时,就会首先跳转到我们的函数当中去执行,这样就完成了我们的HOOK了。
……
下面来梳理一下InlineHook的流程吧。流程如下。
(1)构造跳转指令。
(2)在内存中找到欲HOOK函数地址,并保存欲HOOK位置处的前5个字节。
(3)将构造的跳转指令写入需HOOK的位置处。
(4)当被HOOK位置被执行时会转到我们的流程执行。
(5)如果要执行原来的流程,那么取消HOOK,也就是还原被修改的字节。
(6)执行原来的流程。
(7)继续HOOK住原来的位置。
这就是Inline Hook的大概的流程。
(以下内容选自**《C++黑客编程揭秘与防范》**第5.3.5节——IAT HOOK介绍)
在前面的内容中提到这样一个问题,在IMAGE_IMPORT_DESCRIPTOR中,有两个IMAGE_THUNK_DATA结构体,第一个为导入名字表,第二个为导入地址表(IAT)。两个结构体在文件当中是没有差别的,但是当PE文件被装载内存后,第二个IMAGE_THUNK_DATA的值会被修正,该值为一个RVA,该RVA加上映像基址后,虚拟地址就保存了真正的导入函数的入口地址。
在这个描述当中我们知道,要对IAT进行HOOK大概分为3个步骤,首先是获得要HOOK函数的地址,第二步是找到该函数所保存的IAT中的地址,最后一步是把IAT中的地址修改为HOOK函数的地址。这样就完成了IAT HOOK。也许这样的描述不是很清楚,那么下面就来举例说明一下。
比如要在IAT中HOOK系统模块kernel32.dll中的ReadFile()函数,那么首先是获得ReadFile()函数的地址,第二步是找到ReadFile()所保存的IAT地址,最后一步是把IAT中的ReadFile()函数的地址修改为HOOK函数的地址。
(以下内容选自**《C++黑客编程揭秘与防范》**第5.4.1节——钩子原理)
Windows下的应用程序大部分是基于消息模式机制的,一些CUI的程序不是基于消息的。Windows下的应用程序都有一个消息函数,根据不同的消息来完成不同的功能。Windows操作系统提供的钩子机制的作用是用来截获、监视系统中的消息的。Windows操作系统提供了很多不同种类的钩子,不同的钩子可以处理不同的消息。
钩子分为局部钩子和全局钩子。局部钩子是针对一个线程的,而全局钩子则是针对整个操作系统内基于消息机制的应用程序的。全局钩子需要使用DLL文件,DLL文件里存放了钩子函数的代码。
在操作系统中安装了全局钩子以后,只要进程接收到可以发出钩子的消息后,全局钩子的DLL文件会被操作系统自动或强行地加载到该进程中。由此可见,设置消息钩子也是一种可以进行DLL注入的方法。
**12、请简述你对异常处理机制的理解。**
答:(以下内容选自**《Windows环境下32位汇编语言程序设计 典藏版》**第14.1节——异常处理的用途)
Windows操作系统对异常的处理流程相对比较复杂,与DOS操作系统相比,最大的区别在于DOS的异常处理是被动的,一般仅用来处理操作系统内部的异常,对于其他层次的异常是无法处理的,比如,使用INT 21h去读盘的时候发生错误会激发INT 24h中断,但在BIOS服务程序级别用INT 13h去读盘时发生错误就不会激发INT 24h中断,对应用程序胡作非为引发的异常更是束手无策;而Windows的异常处理机制是依靠80x86处理器的保护机制来主动捕获异常,所以Win32下异常处理程序的用途不仅仅局限于防止程序被Windows野蛮地终止,合理利用它们可以让有些功能的实现方式变得更加简单,一般来说,可以在下面这些情况下使用异常处理程序。
●用来处理非致命的错误
程序执行中发生某些异常时只需要终止发生异常的模块(或子程序),并没有必要终止整个程序的运行,这时可以在异常处理程序中指定让程序转移到一个“安全”的地方去执行,并在这里完成资源释放、删除临时文件、显示错误提示等扫尾工作后从出错模块返回。
●处理“计划内”的异常
程序中的有些功能本来就是设计在异常处理模块中实现的。Windows系统中虚拟内存的实现就是一个绝好的例子,第10章中介绍的内存映射文件也是以同样的方法实现的。在这些情况下,“异常”是作为一个触发条件使用的。
另外,在WindowsAPI中使用异常处理程序进行参数的合法性检测也是很常见的。一般来说,大部分子程序都需要对输入的参数进行合法性检测,特别是对于指针类型的参数,但是当参数涉及的数据结构太复杂的时候,合法性检测会大大降低程序的效率,这时可以假定参数全部合法并尝试直接使用这些参数,如果异常处理程序没有捕获到错误,那么表示参数是合法的,这样要比在每个步骤中检测参数(或操作结果)的合法性要简洁得多。
●处理致命错误
虽然捕获到致命错误的时候终止程序是最好的选择,但是程序在退出之前,可以在异常处理程序中进行释放资源、删除临时文件等操作,甚至可以详细记录产生异常的指令位置和环境,以便用来分析产生异常的原因。
(以下内容选自**《加密与解密 第三版》**第11.1节——基本概念)
所谓异常就是在应用程序的正常执行过程中发生的不正常事件。CPU引发的异常称为硬件异常,例如访问一个无效的内存地址。操作系统或应用程序引发的异常称为软件异常。
……
除了CPU捕获一个事件并引发一个硬件异常外,在代码中也可以强制引发一个软件异常。只需调用RaiseException函数:
~~~
VOID RaiseException(
DWORD dwExceptionCode, //标识所引发异常的代码
DWORD dwExceptionFlags, //异常继续是否执行的标识
DWORD nNumberOfArguments, //附加信息
CONST DWORD *lpArguments //附加信息
);
~~~
程序捕获软件异常的方法与捕获硬件异常的完全相同。
……
首先来看看一个应用程序发生错误后,Windows是如何结合SEH机制进行处理的。
(1)因为有多种异常,系统首先判断异常是否应发送给目标程序,如果应该发送,并且目标程序正处于被调试状态,则系统挂起程序,填写如下结构:
~~~
typedef _EXCEPTION_DEBUG_INFO{
EXCEPTION_RECORD ExceptionRecord;
DWORD dwFirstchance;
}EXCEPTION_DEBUG_INFO;
~~~
将成员dwFirstchance置为1,并向调试器发送EXCEPTION_DEBUG_EVENT消息。剩下的事情就由调试器全权负责了,调试器可能处理这个异常,也可能无法处理。
(2)如果调试器未能处理异常或程序根本没有被调试,系统就会查找是否存在与线程相关的异常处理过程,如果目标程序中存在与线程相关的异常处理程序,系统就调用程序的线程相关的SEH异常处理例程,交由其处理。
(3)与线程相关的异常处理过程可以有一个或多个,每个可以选择处理或者不处理异常,如果它不处理并且存在多个线程相关的异常处理过程,可交由链起来的其他异常处理过程进行处理,依此类推。
(4)如果程序线程的异常处理均选择不处理异常,如果程序处于被调试状态,操作系统仍会再次挂起程序通知调试器,这次EXCEPTION_DEBUG_INFO结构的dwFirstchance成员置为0。
(5)如果程序未处于被调试状态或者调试器仍然未能够处理,并且程序调用了API函数SetUnhandledExceptionFilter设置了与进程相关的异常处理过程的话,系统转向对它的调用。
(6)如果程序没有设置相关的异常处理过程或者进程相关的异常处理过程也未能处理这个异常,系统会调用默认的系统异常处理程序,通常显示一个对话框,可以选择“确定”或者最后将其附加到调试器上的“取消”按钮。如果没有调试器能被附加于其上或调试器还是处理不了异常,系统就调用ExitProcess终结程序。
(7)不过在终结之前,系统再次调用发生异常的线程中所有的异常处理过程,这是线程异常处理过程获得的最后清理未释放资源的机会,其后程序就终结了。
学习SEH要树立一个最基本的概念:SEH是系统发现异常或错误时,在终结应用程序之前给应用程序的一个最后改正错误的机会,从程序设计的角度来说,就是系统在终结程序之前给程序的一个执行其预设定的回调函数的机会。
……
一般地,按作用域(即其监视范围)分,SHE可分为两类:一类是监视某线程中某段代码是否发生异常的异常处理过程,一般称为线程相关的异常处理过程,或称为Per_Thread类型的异常处理过程,有时也简称为线程异常处理。另一类是监视整个进程中所有线程是否发生异常的异常处理过程,称为进程相关的异常处理过程,或称为“Final”型异常处理过程。有人也称之为筛选器,源于其对应于设置其的API函数SetUnhandledExceptionFilter中的Filter一词,在Win32 API文档中,称之为顶层(top-level)异常处理。
**本篇文章参考资料:**
1、孙鑫、余安萍,**《VC++深入详解》**,电子工业出版社。
2、冀云,**《C++黑客编程揭秘与防范》**,人民邮电出版社。
3、[美]Jeffrey Richter、[法]Christophe Nasarre(著),葛子昂、周靖、廖敏(译),**《Windows核心编程(第5版)》**,清华大学出版社。
4、段钢(主编),Blowfish、沈晓斌、丁益青、单海波、王勇、赵勇、唐植明、softworm、afanty、李江涛、林子深、印豪、冯典、罗翼、林小华、郭春杨(编委),**《加密与解密(第三版)》**,电子工业出版社。