瑞星卡卡安全论坛

首页 » 技术交流区 » 反病毒/反流氓软件论坛 » 菜鸟学堂 » 反病毒引擎设计(作者不详)Part3
DoctorLc - 2009-6-11 15:20:00
2.4.2依赖标志寄存器指令模拟函数的分析
CW32Asm类中cmp指令的模拟:

void CW32Asm:: cmpw(int c1,int c2)
{
char FlgReg;
__asm {
mov eax,c1 //取得第一个操作数
mov ecx,c2 //取得第二个操作数
cmp eax,ecx //比较
lahf //将比较后的标志结果装入ah
mov FlgReg,ah //保存结果在局部变量FlgReg中
}
FlagReg=FlgReg; //保存结果在全局变量FlagReg中
}
CW32Asm类中jnz指令的模拟:

int CW32Asm::JNE()
{int i;
char FlgReg=FlagReg; //用保存的FlagReg初始化局部变量FlgReg
__asm
{
mov ah,FlgReg //设置ah为保存的模拟标志寄存器值
pushf //保存虚拟机自身当前标志寄存器
sahf //将模拟标志寄存器值装入真实标志寄存器中
mov eax,1
jne l //执行jnz
popf //恢复虚拟机自身标志寄存器
xor eax,eax
l:
popf //恢复虚拟机自身标志寄存器
mov i,eax
}
return(i); //返回值为1代表需要跳转
}
  2.5反虚拟机技术
任何一个事物都不是尽善尽美,无懈可击的,虚拟机也不例外。由于反虚拟执行技术的出现,使得虚拟机查毒受到了一定的挑战。这里介绍几个比较典型的反虚拟执行技术:

首先是插入特殊指令技术,即在病毒的解密代码部分人为插入诸如浮点,3DNOW,MMX等特殊指令以达到反虚拟执行的目的。尽管虚拟机使用软件技术模拟真正CPU的工作过程,它毕竟不是真正的CPU,由于精力有限,虚拟机的编码者可能实现对整个Intel指令集的支持,因而当虚拟机遇到其不认识的指令时将会立刻停止工作。但通过对这类病毒代码的分析和统计,我们发现通常这些特殊指令对于病毒的解密本身没有发生任何影响,它们的插入仅仅是为了干扰虚拟机的工作,换句话说就是病毒根本不会利用这条随机的垃圾指令的运算结果。这样一来,我们可以仅构造一张所有特殊指令对应于不同寻址方式的指令长度表,而不必为每个特殊指令编写一个专用的模拟函数。有了这张表后,当虚拟机遇到不认识的指令时可以用指令的操作码索引表格以求得指令的长度,然后将当前模拟的指令指针(EIP)加上指令长度来跳过这条垃圾指令。当然,还有一个更为保险的办法那就是:得到指令长度后,可以将这条我们不认识的指令放到一个充满空操作指令(NOP)的缓冲区中,接着我们将跳到缓冲区中去执行,这等于让真正的CPU帮我们来执行这条指令,最后一步当然是将执行后真实寄存器中的结果放回我们的模拟寄存器中。这虚拟执行和真实执行参半方法的好处在于:即便在特殊指令对于病毒是有意义的,即病毒依赖其返回结果的情况下,虚拟机仍可保证虚拟执行结果的正确。

其次是结构化异常处理技术,即病毒的解密代码首先设置自己的异常处理函数,然后故意引发一个异常而使程序流程转向预先设立的异常处理函数。这种流程转移是CPU和操作系统相互配合的结果,并且在很大程度上,操作系统在其中起了很大的作用。由于目前的虚拟机仅仅模拟了没有保护检查的CPU的工作过程,而对于系统机制没有进行处理。所以面对引发异常的指令会有两种结果:其一是某些设计有缺陷的虚拟机无法判断被模拟指令的合法性,所以模拟这样的指令将使虚拟机自身执行非法操作而退出;其二虚拟机判断出被模拟指令属于非法指令,如试图向只读页面写入的指令,则立刻停止虚拟执行。通常病毒使用该技术的目的在于将真正循环解密代码放到异常处理函数后,如此虚拟机将在进入异常处理函数前就停止了工作,从而使解密子有机会逃避虚拟执行。因而一个好的虚拟机应该具备发现和记录病毒安装异常过滤函数的操作并在其引发异常时自动将控制转向异常处理函数的能力。

再次是入口点模糊(EPO)技术,即病毒在不修改宿主原入口点的前提下,通过在宿主代码体内某处插入跳转指令来使病毒获得控制权。通过前面的分析,我们知道虚拟机扫描病毒时出于效率考虑不可能虚拟执行待查文件的所有代码,通常的做法是:扫描待查文件代码入口,假如在规定步数中没有发现解密循环,则由此判定该文件没有携带加密变形病毒。这种技术之所以能起到反虚拟执行的作用在于它正好利用了虚拟机的这个假设:由于病毒是从宿主执行到一半时获得控制权的,所以虚拟机首先解释执行的是宿主入口的正常程序,当然在规定步数中不可能发现解密循环,因而产生了漏报。如果虚拟机能增加规定步数的大小,则很有可能随着病毒插入的跳转指令跟踪进入病毒的解密子,但确定规定步数大小实在是件难事:太大则将无谓增加正常程序的检测时间;太小则容易产生漏报。但我们对此也不必过于担心,这类病毒由于其编写技术难度较大所以为数不多。在没有反汇编和虚拟执行引擎的帮助下,病毒很难在宿主体内定位一条完整指令的开始处来插入跳转,同时很难保证插入的跳转指令的深度大于虚拟机的规定步数,并且没有把握插入的跳转指令一定会被执行到。

另外还有多线程技术,即病毒在解密部分入口主线程中又启动了额外的工作线程,并且将真正的循环解密代码放置于工作线程中运行。由于多线程间切换调度由操作系统负责管理,所以我们的虚拟机只能在假定被执行线程独占处理器时间,即保证永远不被抢先,的前提下进行。如此一来,虚拟机对于模拟启用多线程工作的代码将很难做到与真实效果一致。多线程和结构化异常处理两种技术都利用了特定的操作系统机制来达到反虚拟执行的目的,所以在虚拟CPU中加入对特定操作系统机制的支持将是我们今后改进的目标。

最后是元多形技术(MetaPolymorphy),即病毒中并非是多形的解密子加加密的病毒体结构,而整体均采用变形技术。这种病毒整体都在变,没有所谓“病毒体明文”。当然,其编写难度是很大的。如果说前几种反虚拟机技术是利用了虚拟机设计上的缺陷,可以通过代码改进来弥补的话,那么这种元多形技术却使虚拟机配合的动态特征码扫描法彻底失效了,我们必须寻求如行为分析等更先进的方法来解决。

3.病毒实时监控
3.1实时监控概论
实时监控技术其实并非什么新技术,早在DOS编程时代就有之。只不过那时人们没有给这项技术冠以这样专业的名字而已。早期在各大专院校机房中普遍使用的硬盘写保护软件正是利用了实时监控技术。硬盘写保护软件一般会将自身写入硬盘零磁头开始的几个扇区(由0磁头0柱面1扇最开始的64个扇区是保留的,DOS访问不到)并修改原来的主引导记录以使启动时硬盘写保护程序可以取得控制权。引导时取得控制权的硬盘写保护程序会修改INT13H的中断向量指向自身已驻留于内存中的钩子代码以便随时拦截所有对磁盘的操作。钩子代码的作用当然是很明显的,它主要负责由判断中断入口参数,包括功能号,磁盘目标地址等来决定该类型操作是否被允许,这样就可以实现对某一特定区域的写操作保护。后来又诞生了在此基础之上进行改进了的磁盘恢复卡之类的产品,其利用将写操作重定向至目标区域外的临时分区并保存磁盘先前状态等技术来实现允许写入并可随时恢复之功能。不管怎么改进,这类产品的核心技术还是对磁盘操作的实时监控。对此有兴趣的朋友可参看高云庆著《硬盘保护技术手册》。DOS下还有许多通过驻留并截获一些有用的中断来实现某种特定目的的程序,我们通常称之为TSR(终止并等待驻留terminate-and-stay-resident,此种程序不容易编好,需要大量的关于硬件和Dos中断的知识,还要解决Dos重入,tsr程序重入等问题,搞不好就会当机)。在WINDOWS下要实现实时监控决非易事,普通用户态程序是不可能监控系统的活动的,这也是出于系统安全的考虑。HPS病毒能在用户态下直接监控系统中的文件操作其实是由于WIN9X在设计上存在漏洞。而我们下面要讨论的两个病毒实时监控(For WIN9X&WINNT/2000)都使用了驱动编程技术,让工作于系统核心态的驱动程序去拦截所有的文件访问。当然由于工作系统的不同,这两个驱动程序无论从结构还是工作原理都不尽相同的,当然程序写法和编译环境更是千差万别了,所以我们决定将其各自分成独立的一节来详细地加以讨论。上面提到的病毒实时监控其实就是对文件的监控,说成是文件监控应该更为合理一些。除了文件监控外,还有各种各样的实时监控工具,它们也都具有各自不同的特点和功用。这里向大家推荐一个关于WINDOWS系统内核编程的站点:www.sysinternals.com。在其上可以找到很多实时监控小工具,比如能够监视注册表访问的Regmon(通过修改系统调用表中注册表相关服务入口),可以实时地观察TCP和UDP活动的Tdimon(通过hook系统协议驱动Tcpip.sys中的dispatch函数来截获tdi clinet向其发送的请求),这些工具对于了解系统内部运作细节是很有裨益的。介绍完有关的背景情况后,我们来看看关于病毒 实时监控的具体实现技术的情况。


用户系统信息:Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; InfoPath.2)
DoctorLc - 2009-6-11 15:20:00
3.2病毒实时监控实现技术概论
正如上面提到的病毒实时监控其实就是一个文件监视器,它会在文件打开,关闭,清除,写入等操作时检查文件是否是病毒携带者,如果是则根据用户的决定选择不同的处理方案,如清除病毒,禁止访问该文件,删除该文件或简单地忽略。这样就可以有效地避免病毒在本地机器上的感染传播,因为可执行文件装入器在装入一个文件执行时首先会要求打开该文件,而这个请求又一定会被实时监控在第一时间截获到,它确保了每次执行的都是干净的不带毒的文件从而不给病毒以任何执行和发作的机会。以上说的仅是病毒实时监控一个粗略的工作过程,详细的说明将留到后面相应的章节中。病毒实时监控的设计主要存在以下几个难点:

其一是驱动程序的编写不同于普通用户态程序的写作,其难度很大。写用户态程序时你需要的仅仅就是调用一些熟知的API函数来完成特定的目的,比如打开文件你只需调用CreateFile就可以了;但在驱动程序中你将无法使用熟悉的CreateFile。在NT/2000下你可以使用ZwCreateFile或NtCreateFile(native API),但这些函数通常会要求运行在某个IRQL(中断请求级)上,如果你对如中断请求级,延迟/异步过程调用,非分页/分页内存等概念不是特别清楚,那么你写的驱动将很容易导致蓝屏死机(BSOD),Ring0下的异常将往往导致系统崩溃,因为它对于系统总是被信任的,所以没有相应处理代码去捕获这个异常。在NT下对KeBugCheckEx的调用将导致蓝屏的出现,接着系统将进行转储并随后重启。另外驱动程序的调试不如用户态程序那样方便,用象VC++那样的调试器是不行的,你必须使用系统级调试器,如softice,kd,trw等。

其二是驱动程序与ring3下客户程序的通信问题。这个问题的提出是很自然的,试想当驱动程序截获到某个文件打开请求时,它必须通知位于ring3下的查毒模块检查被打开的文件,随后查毒模块还需将查毒的结果通过某种方式传给ring0下的监控程序,最后驱动程序根据返回的结果决定请求是否被允许。这里面显然存在一个双向的通信过程。写过驱动程序的人都知道一个可以用来向驱动程序发送设备I/O控制信息的API调用DeviceIoControl,它的接口在MSDN中可以找到,但它是单向的,即ring3下客户程序可以通过调用DeviceIoControl将某些信息传给ring0下的监控程序但反过来不行。既然无法找到一个现成的函数实现从ring0下的监控程序到ring3下客户程序的通信,则我们必须采用迂回的办法来间接做到这一点。为此我们必须引入异步过程调用(APC)和事件对象的概念,它们就是实现特权级间唤醒的关键所在。现在先简单介绍一下这两个概念,具体的用法请参看后面的每子章中的技术实现细节。异步过程调用是一种系统用来当条件合适时在某个特定线程的上下文中执行一个过程的机制。当向一个线程的APC队列排队一个APC时,系统将发出一个软件中断,当下一次线程被调度时,APC函数将得以运行。APC分成两种:系统创建的APC称为内核模式APC,由应用程序创建的APC称为用户模式APC。另外只有当线程处于可报警(alertable)状态时才能运行一个APC。比如调用一个异步模式的ReadFileEx时可以指定一个用户自定义的回调函数FileIOCompletionRoutine,当异步的I/O操作完成或被取消并且线程处于可报警状态时函数被调用,这就是APC的典型用法。Kernel32.dll中导出的QueueUserAPC函数可以向指定线程的队列中增加一个APC对象,因为我们写的是驱动程序,这并不是我们要的那个函数。很幸运的是在Vwin32.vxd中导出了一个同名函数QueueUserAPC,监控程序拦截到一个文件打开请求后,它马上调用这个服务排队一个ring3下客户程序中需要被唤醒的函数的APC,这个函数将在不久客户程序被调度时被调用。这种APC唤醒法适用于WIN9X,在WINNT/2000下我们将使用全局共享的事件和信号量对象来解决互相唤醒问题。有关WINNT/2000下的对象组织结构我将在3.4.2节中详细说明。NT/2000版监控程序中我们将利用KeReleaseSemaphore来唤醒一个在ring3下客户程序中等待的线程。目前不少反病毒软件已将驱动使用的查毒模块移到ring0,即如其所宣传的“主动与操作系统无缝连接”,这样做省却了通信的消耗,但把查毒模块写成驱动形式也同时会带来一些麻烦,如不能调用大量熟知的API,不能与用户实时交互,所以我们还是选择剖析传统的反病毒软件的监控程序。

其三是驱动程序所占用资源问题。如果由于监控程序频繁地拦截文件操作而使系统性能下降过多,则这样的程序是没有其存在的价值的。本论文将对一个成功的反病毒软件的监控程序做彻底的剖析,其中就包含有分析其用以提高自身性能的技巧的部分,如设置历史记录,内置文件类型过滤,设置等待超时等。

3.3WIN9X下的病毒实时监控
3.3.1实现技术详解
WIN9X下病毒实时监控的实现主要依赖于虚拟设备驱动(VXD)编程,可安装文件系统钩挂(IFSHook),VXD与ring3下客户程序的通信(APC/EVENT)三项技术。

我们曾经提到过只有工作于系统核心态的驱动程序才具有有效地完成拦截系统范围文件操作的能力,VXD就是适用于WIN9X下的虚拟设备驱动程序,所以正可当此重任。当然,VXD的功能远不止由IFSMGR.vxd提供的拦截文件操作这一项,系统的VXDs几乎提供了所有的底层操作的接口--可以把VXD看成ring0下的DLL。虚拟机管理器本身就是一个VXD,它导出的底层操作接口一般称为VMM服务,而其他VXD的调用接口则称为VXD服务。

二者ring0调用方法均相同,即在INT20(CD 20)后面紧跟着一个服务识别码,VMM会利用服务识别码的前半部分设备标识--Device Id找到对应的VXD,然后再利用服务识别码的后半部分在VXD的服务表(Service Table)中定位服务函数的指针并调用之:

CD 20 INT 20H

01 00 0D 00 DD VKD_Define_HotKey

这条指令第一次执行后,VMM将以一个同样6字节间接调用指令替换之(并不都是修正为CALL指令,有时会利用JMP指令),从而省却了查询服务表的工作:

FF 15 XX XX XX XX CALL [$VKD_Define_HotKey]

必须注意,上述调用方法只适用于ring0,即只是一个从VXD中调用VXD/VMM服务的ring0接口。VXD还提供了V86(虚拟8086模式),Win16保护模式,Win32保护模式调用接口。其中V86和Win16保护模式的调用接口比较奇怪:

XOR DI DI
MOV ES,DI
MOV AX,1684 ;INT 2FH,AX = 1684H-->取得设备入口
MOV BX,002A ;002AH = VWIN32.VXD的设备标识
INT 2F
MOV AX,ES ;现在ES:DI中应该包含着入口
OR AX,AX
JE failure
MOV AH,00 ;VWIN32 服务 0 = VWIN32_Get_Version
PUSH DS
MOV DS,WORD PTR CS:[0002]
 
MOV WORD PTR [lpfnVMIN32],DI
MOV WORD PTR [lpfnVMIN32+2],ES ;保存ES和DI
CALL FAR [lpfnVMIN32] ;call gate(调用门)
ES:DI指向了3B段的一个保护模式回调:

003B:000003D0 INT 30 ;#0028:C025DB52 VWIN32(04)+0742

INT30强迫CPU从ring3提升到ring0,然后WIN95的INT30处理函数先检查调用是否发自3B段,如是则利用引发回调的CS:IP索引一个保护模式回调表以求得一个ring0地址。本例中是0028:C025DB52 ,即所需服务VWIN32_Get_Version的入口地址。

VXD的Win32保护模式调用接口我们在前面已经提到过。一个是DeviceIoControl,我们的ring3客户程序利用它来和监控驱动进行单向通信;另一个是VxdCall,它是Kernel32.dll的一个未公开的调用,被系统频繁使用,对我们则没有多大用处。

你可以参看WIN95DDK的帮助,其中对每个系统VXD提供的调用接口均有详细说明,可按照需要选择相应的服务。

可安装文件系统钩挂(IFSHook)就源自IFSMGR.VXD提供的一个服务IFSMgr_InstallFileSystemApiHook,利用这个服务驱动程序可以向系统注册一个钩子函数。以后系统中所有文件操作都会经过这个钩子的过滤,WIN9X下文件读写具体流程如下:

在读写操作进行时,首先通过未公开函数EnterMustComplete来增加MUSTCOMPLETECOUNT变量的记数,告诉操作系统本操作必须完成。该函数设置了KERNEL32模块里的内部变量来显示现在有个关键操作正在进行。有句题外话,在VMM里同样有个函数,函数名也是EnterMustComplete。那个函数同样告诉VMM,有个关键操作正在进行。防止线程被杀掉或者被挂起。

接下来,WIN9X进行了一个_MapHandleWithContext(又是一个未公开函数)操作。该操作本身的具体意义尚不清楚,但是其操作却是得到HANDLE所指对象的指针,并且增加了引用计数。

随后,进行的乃是根本性的操作:KERNEL32发出了一个调用VWIN32_Int21Dispatch的VxdCall。陷入VWIN32后,其 检查调用是否是读写操作。若是,则根据文件句柄切换成一个FSD能识别的句柄,并调用IFSMgr_Ring0_FileIO。接下来任务就转到了IFS MANAGER。

IFS MANAGER生成一个IOREQ,并跳转到Ring0ReadWrite内部例程。Ring0ReadWrite检查句柄有效性,并且获取FSD在创建文件句柄时返回的CONTEXT,一起传入到CallIoFunc内部例程。CallIoFunc检查IFSHOOK的存在,如果不存在,IFS MANAGER生成一个缺省的IFS HOOK,并且调用相应的VFatReadFile/VFatWriteFile例程(因为目前 MS本身仅提供了VFAT驱动);如果IFSHOOK存在,则IFSHOOK函数得到控制权,而IFS MANAGER本身就脱离了文件读写处理。然后,调用被层层返回。KERNEL32调用未公开函数LeaveMustComplete,减少MUSTCOMPLETECOUNT计数,最终回到调用者。

由此可见通过IFSHook拦截本地文件操作是万无一失的,而通过ApiHook或VxdCall拦截文件则多有遗漏。著名的CIH病毒正是利用了这一技术,实现其驻留感染的,其中的代码片段如下:

  lea eax, FileSystemApiHook-@6[edi] ;取得欲安装的钩子函数的地址
push eax
int 20h ;调用IFSMgr_InstallFileSystemApiHook
IFSMgr_InstallFileSystemApiHook = $
dd 00400067h
mov dr0, eax ;保存前一个钩子的地址
pop eax
  正如我们看到的,系统中安装的所有钩子函数呈链状排列。最后安装的钩子,最先被系统调用。我们在安装钩子的同时必须将调用返回的前一个钩子的地址暂存以便在完成处理后向下传递该请求:

mov eax, dr0 ;取得前一个钩子的地址

jmp [eax] ; 跳到那里继续执行

对于病毒实时监控来说,我们在安装钩子时同样需要保存前一个钩子的地址。如果文件操作的对象携带了病毒,则我们可以通过不调用前一个钩子来简单的取消该文件请求;反之,我们则需及时向下传递该请求,若在钩子中滞留的时间过长--用于等待ring3级查毒模块的处理反馈--则会使用户明显感觉系统变慢。

至于钩子函数入口参数结构和怎样从参数中取得操作类型(如IFSFN_OPEN)和文件名(以UNICODE形式存储)请参看相应的代码剖析部分。

我们所需的另一项技术--APC/EVENT也是源自一个VXD导出的服务,这便是著名的VWIN32.vxd。这个奇怪的VXD导出了许多与WIN32 API对应的服务:如_VWIN32_QueueUserApc,_VWIN32_WaitSingleObject,_VWIN32_ResetWin32Event,_VWIN32_Get_Thread_Context,_VWIN32_Set_Thread_Context 等。这个VXD叫虚拟WIN32,大概名称即是由此而来的。虽然服务的名称与WIN32 API一样,但调用规则却大相径庭,千万不可用错。_VWIN32_QueueUserApc用来注册一个用户态的APC,这里的APC函数当然是指我们在ring3下以可告警状态睡眠的待查毒线程。ring3客户程序首先通过IOCTL把待查毒线程的地址传给驱动程序,然后当钩子函数拦截到待查文件时调用此服务排队一个APC,当ring3客户程序下一次被调度时,APC例程得以执行。_VWIN32_WaitSingleObject则用来在某个对象上等待,从而使当前ring0线程暂时挂起。我们的ring3客户程序先调用WIN32 API--CreateEvent创建一组事件对象,然后通过一个未公开的API--OpenVxdHandle将事件句柄转化为VXD可辩识的句柄(其实应是指向对象的指针)并用IOCTL发给ring0端VXD,钩子函数在排队APC后调用_VWIN32_WaitSingleObject在事件的VXD句柄上等待查毒的完成,最后由ring3客户程序在查毒完毕后调用WIN32 API--SetEvent来解除钩子函数的等待。
当然,这里面存在着一个很可怕的问题:如果你按照的我说的那样去做,你会发现它会在一端时间内工作正常,但时间一长,系统就被挂起了。就连驱动编程大师Walter Oney在其著作《System Programming For Windows 95》的配套源码的说明中也称其APC例程在某些时候工作会不正常。而微软的工程师声称文件操作请求是不能被中断掉的,你不能在驱动中阻断文件操作并依赖于ring3的反馈来做出响应。网上关于这个问题也有一些讨论,意见不一:有人认为当系统DLL--KERNEL32在其调用ring0处理文件请求时拥有一个互斥量(MUTEX),而在某些情况下为了处理APC要拥有同样的互斥量,所以死锁发生了;还有人认为尽管在WIN9X下32位线程是抢先多任务的,但Win16子系统是以协作多任务来运行的。为了能平滑的运行老的16位程序,它引入了一个全局的互斥量--Win16Mutex。任何一个16位线程在其整个生命周期中都拥有Win16Mutex而32位线程当它转化成16位代码也要攫取此互斥量,因为WIN9X内核是16位的,如Knrl386.exe,gdi.exe。如果来自于拥有Win16Mutex的线程的文件请求被阻塞,系统将陷入死锁状态。这个问题的正确答案似乎在没有得到WIN9X源码的之前永远不可能被证实,但这是我们实时监控的关键,所以必须解决。

我通过跟踪WIN95文件操作的流程,并反复做实验验证,终于找到了一个比较好的解决办法:在拦截到文件请求还没有排队APC之前我们通过Get_Cur_Thread_Handle取得当前线程的ring0tcb,从中找到TDBX,再在TDBX中取得ring3tcb根据其结构,我们从偏移44H处得到Flags域值,我发现如果它等于10H和20H时容易导致死锁,这只是一个实验结果,理由我也说不清楚,大概是这样的文件请求多来自于拥有Win16Mutex的线程,所以不能阻塞;另外一个根本的解决方法是在调用_VWIN32_WaitSingleObject时指定超时,如果在指定时间里没有收到ring3的唤醒信号,则自动解除等待以防止死锁的发生。

以上对WIN9X下的实时监控的主要技术都做了详细的阐述。当然,还有一部分关于VXD的结构,编写和编译的方法由于篇幅的关系不可能在此一一说明。需要了解更详细内容的,请参看Walter Oney的著作《System Programming For Windows 95》,此书尚有台湾候俊杰翻译版《Windows 95系统程式设计》。
DoctorLc - 2009-6-11 15:21:00
3.3.2程序结构与流程
以下的程序结构与流程分析来自一著名反病毒软件的WIN9X实时监控虚拟设备驱动程序Hooksys.vxd:

1.当VXD收到来自VMM的ON_SYS_DYNAMIC_DEVICE_INIT消息--需要注意这是个动态VXD,它不会收到系统虚拟机初始化时发送的Sys_Critical_Init, Device_Init和Init_Complete控制消息--时,它开始初始化一些全局变量和数据结构,包括在堆上分配内存(HeapAllocate),创建备用,历史记录,打开文件,等待操作,关闭文件5个双向循环链表及用于链表操作互斥的5个信号量(调用Create_Semaphore),同时将全局变量_gNumOfFilters即文件名过滤项个数设置为0。

2.当VXD收到来自VMM的ON_W32_DEVICEIOCONTROL消息时,它会从入口参数中取得用户程序利用DeviceIoControl传送进来的IO控制代码(IOCtlCode),以此判断用户程序的意图。和Hooksys.vxd协同工作的ring3级客户程序guidll.dll会依次向Hooksys.vxd发送IO控制请求来完成一系列工作,具体次序和代码含义如下:

83003C2B:将guidll取得的操作系统版本传给驱动(保存在iOSversion变量中),根据此变量值的不同,从ring0tcb结构中提取某些域时将采用不同的偏移,因为操作系统版本不同会影响内核数据结构。

83003C1B:初始化后备链表,将guidll传入的用OpenVxdHandle转换过的一组事件指针保存在每个链表元素中。

83003C2F:将guidll取得的驱动器类型值传给驱动(保存在DriverType变量中),根据此变量值的不同,调用VWIN32_WaitSingleObject设置不同的等待超时值,因为非固定驱动器的读写时间可能会稍长些。

83003C0F:保存guidll传送的用户指定的拦截文件的类型,其实这个类型过滤器在查毒模块中已存在,这里再设置显然是为了提高处理效率:它确保不会将非指定类型文件送到ring3级查毒模块,节省了通信的开销。经过解析的各文件类型过滤块指针将保存在_gaFileNameFilterArra数组中,同时更新过滤项个数_gNumOfFilters 变量的值。

83003C23:保存guidll中等待查杀打开文件的APC函数地址和当前线程KTHREAD指针。

83003C13:安装系统文件钩子,启动拦截文件操作的钩子函数FilemonHookProc的工作。

83003C27:保存guidll中等待查杀关闭文件的APC函数地址和当前线程KTHREAD指针。

83003C17:卸载系统文件钩子,停止拦截文件操作的钩子函数FilemonHookProc的工作。

以上列出的IO控制代码的发出是固定,而当钩子函数启动后,还会发出一些随机的控制代码:

83003C07:驱动将打开文件链表的头元素即最先的请求打开的文件删除并插入到等待链表尾部,同时将元素的用户空间地址传送至ring3级等待查杀打开文件的APC函数中处理。

83003C0B:驱动将关闭文件链表的头元素即最先的请求关闭的文件删除并插入到备用链表尾部,同时将元素中的文件名串传送至ring3级等待查杀关闭文件的APC函数中处理

83003C1F:当查得关闭文件是病毒时,更新历史记录链表。

下面介绍钩子函数和guidll中等待查杀打开文件的APC函数协同工作流程,写文件和关闭文件的处理与之类似:

当文件请求进入钩子函数FilemonHookProc后,它先从入口参数中取得被执行的函数的代号并判断其是否为打开操作(IFSFN_OPEN 24H),若非则马上将这个IRQ向下传递,即构造入口参数并调用保存在PrevIFSHookProc中前一个钩子函数;若是则程序流程转向打开文件请求的处理分支。分支入口处首先要判断当前进程是否是我们自己,若是则必须放过去,因为查毒模块中要频繁的进行文件操作,所以拦截来自自身的文件请求将导致严重的系统死锁。接下来是从堆栈参数中取得完整的文件路径名并通过保存的文件类型过滤阵列检查其是否在拦截类型之列,如通过则进一步检查文件是否是以下几个须放过的文件之一:SYSTEM.DAT,USER.DAT,\PIPE\。然后查找历史记录链表以确定该文件是否最近曾被检查并记录过,若在历史记录链表中找到关于该文件的记录并且记录未失效即其时间戳和当前系统时间之差不得大于1F4h,则可直接从记录中读取查毒结果。至此才进入真正的检查打开文件函数_RAVCheckOpenFile,此函数入口处先从备用,等待或关闭链表头部摘得一空闲元素(_GetFreeEntry)并填充之(文件路径名域等)。接着通过一内核未公开的数据结构中的值(ring3tcb->Flags)判断可否对该文件请求排队APC。如可则将空闲元素加入打开文件链表尾部并排队一个ring3级检查打开文件函数的APC。然后调用_VWIN32_WaitSingleObject在空闲元素中保存的一个事件对象上等待ring3查毒的完成。当钩子函数挂起不久后,ring3的APC函数得到执行:它会向驱动发出一IO控制码为83003C07的请求以取得打开文件链表头元素即保存最先提交而未决的文件请求,驱动可以将内核空间中元素的虚拟地址直接传给它而不必考虑将之重新映射。实际上由于WIN9X内核空间没有页保护因而ring3级程序可以直接读写之。接着它调用RsEngine.dll中的fnScanOneFile函数进行查毒并在元素中设置查毒结果位,完毕后再对元素中保存的事件对象调用SetEvent唤醒在此事件上等待的钩子函数。被唤醒的钩子函数检查被ring3查毒代码设置的结果位以此决定该文件请求是被采纳即继续向下传递还是被取消即在EAX中放入-1后直接返回,同时增加历史记录。

以上只是钩子函数与APC函数流程的一个简单介绍,其中省略了诸如判断固定驱动器,超时等内容,具体细节请参看guidll.dll和hooksys.vxd的反汇编代码注释。

3.当VXD收到来自VMM的ON_SYS_DYNAMIC_DEVICE_EXIT消息时,它释放初始化时分配的堆内存(HeapFree),并清除5个用于互斥的信号量(Destroy_Semaphore)。

3.3.3HOOKSYS.VXD逆向工程代码剖析
在剖析代码之前有必要介绍一下逆向工程的概念。逆向工程(Reverse Engineering)是指在没有源代码的情况下对可执行文件进行反汇编试图理解机器码本身的含义。逆向工程的用途很多,如摘掉软件保护,窥视其设计和编写技术,发掘操作系统内部奥秘等。本文中我们用到的不少未公开数据结构和服务就是利用逆向的方法得到的。逆向工程的难度可想而知:一个1K大小的exe文件反汇编后就有1000行左右,而我们要逆向的3个文件加起来有80多K,总代码量是8万多行。所以必须掌握一定的逆向技巧,否则工作起来将是非常困难的。

首先要完成逆向工作,必须选择优秀的反汇编及调试跟踪工具。IDA(The Interactive Disassembler)是一款功能强大的反汇编工具:它以交互能力强而著称,允许使用者增加标签,注释及定义变量,函数名称;另外不少反汇编工具对于特殊处理的反逆向文件,如导入节损坏等显得无能为力,但IDA仍可胜任之。当文件被加过壳或插入了干扰指令时 就需要使用调试工具进行动态跟踪。Numega公司的Softice是调试工具中的佼佼者:它支持所有类型的可执行文件,包括vxd和sys驱动程序,能够用热键实时呼出,可对代码执行,内存和端口访问设置断点,总之功能非常之强大以至于连微软总裁比尔盖茨对此都惊叹不已。

其次需要对编译器常用的编译结构有一定了解,这样有助于我们理解代码的含义。

如下代码是MS编译器常用的一种编译高级语言函数的形式:

  0001224A push ebp ;保存基址寄存器
0001224B mov ebp, esp
0001224D sub esp, 5Ch ;在堆栈留出局部变量空间
00012250 push ebx
00012251 push esi
00012252 push edi
......
0001225B lea edi, [ebp-34h] ;引用局部变量
......
0001238D mov esi, [ebp+08h] ;引用参数
......
00012424 pop edi
00012425 pop esi
00012426 pop ebx
00012427 leave
00012428 retn 8 ;函数返回
如下代码是MS编译器常用的一种编译高级语言取串长度的形式:

0001170D lea edi, [eax+1Ch] ;串首地址指针
00011710 or ecx, 0FFFFFFFFh ;将ecx置为-1
00011713 xor eax, eax ;扫描串结束符号(NULL)
00011715 push offset 00012C04h ;编译器优化
0001171A repne scasb ;扫描串结束符号位置
0001171C not ecx ;取反后得到串长度
0001171E sub edi, ecx ;恢复串首地址指针
最后一点是必须要有坚忍的毅力和清晰的头脑。逆向工程本身是件痛苦的工作:高级语言源代码中使用的变量和函数名字在这里仅是一个地址,需要反复调试琢磨才能确定其含义;另外编译器优化更为我们理解代码增加了不少障碍,如上例中那句压栈指令是将后面函数调用时参数入栈提前放置。所以毅力和头脑二者缺一不可。

以下进入hooksys.vxd代码剖析,由于代码过于庞大,我只选择有代表性且精彩的部分进行介绍。代码中的变量和函数及标签名是我分析后自己添加的,可能会与原作者的意图有些出入。

3.3.3.1钩子函数入口代码
C00012E0 push ebp
C00012E1 mov ebp, esp
C00012E3 sub esp, 11Ch
C00012E9 push ebx
C00012EA push esi
C00012EB push edi
C00012EC mov eax, [ebp+arg_4] ; 被执行的函数的代号
C00012EF mov [ebp+var_11C], eax
C00012F5 cmp [ebp+var_11C], 1 ; IFSFN_WRITE
C00012FC jz writefile
C0001302 cmp [ebp+var_11C], 0Bh ; IFSFN_CLOSE
C0001309 jz closefile
C000130F cmp [ebp+var_11C], 24h ; IFSFN_OPEN
C0001316 jz short openfile
C0001318 jmp irqpassdown
钩子函数入口处,堆栈参数分布如下:

ebp+00h -> 保存的EBP值.
ebp+04h -> 返回地址.
ebp+08h -> 提供这个API要调用的FSD函数的的地址
ebp+0Ch -> 提供被执行的函数的代号
ebp+10h -> 提供了操作在其上执行的以1为基准的驱动器代号(如果UNC为-1)
ebp+14h -> 提供了操作在其上执行的资源的种类。
ebp+18h -> 提供了用户串传递其上的代码页
ebp+1Ch -> 提供IOREQ结构的指针。
钩子函数利用[ebp+0Ch]中保存的被执行的函数的代号来判断该请求的类型。同时它利用[ebp+0Ch]中保存的IOREQ结构的指针从该结构中偏移0ch处path_t ir_ppath域取得完整的文件路径名称。
1
查看完整版本: 反病毒引擎设计(作者不详)Part3