文章转载与看雪论坛.
我觉得,做一个杀毒软件,大概要有以下的驱动程序。下面我给出了杀毒软件的大致设计框架。
由于一些事情的存在,程序代码暂时不能上传到看雪论坛上,以免引起日后产生不必要的法律纠纷。这里还请各位朋友能够原谅。
若有不对和不足的地方,还请见谅。
1)磁盘扫描计算机病毒。
一个扫描引擎主要包含:扫描规则设置、对象设置。
扫描规则设置主要是,允许用户使用一个或全部规则对文件进行扫描。
扫描对象设置主要是,允许用户对要扫描的文件的类型进行设置。
<1>判断是否是PE 文件格式。
若打开的文件不是PE 文件格式规范,则给出提示信息,让用户重新选择要扫描的文件。
若打开的文件是PE文件格式规范,则:进行地址转换。
RVA和地址进行相互转换。
循环得到每个节的起始RVA(相对虚拟地址)、根据节的大小来计算出节的范围。
比较程序入口的RVA(Address Of Entry Point),用来确定当前的节在哪个节区。
当RVA确定了以后,计算出它在该节中的偏移量。
然后把这个偏移量+该节在文件中的偏移量=入口地址在文件中的确切地址。
<2>比较节的名称。
一个PE文件格式会存在有一个或多个的代码节,也会有多个的非代码节。在一般情况下,代码节是只读不可写的、非代码节是可以写的。如果某个非代码节拥有了代码节的属性,那么,这个节存在着代码,或许是病毒代码。
代码节的属性是60000020H,也就是可执行、可读、节中包含代码。如果某个节的节名不是: .text 、.code、.CODE,而它的属性是60000020H,那么可以认为,该节不是代码节并且具有了代码节的属性,那么它就是一个可疑的节。
可以通过IMAGE_SECTION_HEADER结构体的Name字段循环读出每个节的名字,若发现可疑之处,则可以把可疑的节设置为节名:.VIRUS或 .Virus等名字。
接着把该可疑的节的特征保存到数据库里面。可以设定一定的权值,通常在0.1—1之间。
这样做目的是:为扫描引擎提供数据源、提供校正的权值。
<3>文件大小与最后一个节的尾偏移量不相同。
一般情况下,文件的最后一个节是文件的结束,所以最后一个节相对于文件头的偏移量应该和文件的大小相同。如果不相同,可以认为,文件被加入了其它的内容。那么可以认为,这个文件拥有可疑的代码,这个文件是病毒木马文件。
<4>文件的入口点指向文件头、文件尾、其它让人出乎意料的地址。
一个PE文件格式是由:文件头+节区组合而成。一个程序的入口点肯定要指向文件的某个节区。否则,就是不正常的现象。那么可以认为,这个文件拥有可疑的代码,这个文件是病毒木马文件。
<5>节与节之间,它们的首地址和尾地址不是连续的。
两个连续的节之间,不能有间隙。应当是首尾相连的。而一些病毒是按照自定义的方式来设置一些尺寸,这样会造成了节和节之间产生了空隙,所以,可以认为,这个文件是病毒木马文件。
<6>一个文件,拥有2个以上的文件头。
病毒木马程序附加在宿主文件上,一般有3种形式。也就是说,病毒木马程序修改了PE文件的格式。
(1)病毒木马附加在宿主文件的后面。
(2)病毒木马附加在宿主文件里面。
(3)病毒木马附加在宿主文件的前面。
那么,可以认为,这个文件是病毒木马文件。
<7>获取Windows内核控制权。
一些程序会加载到C0001000H地址处,这样可以使系统在运行本程序的时候,获取内核的控制权。
我们安全卫士,可以循环读出每个节区的内存地址RVA,并和C000000H比较,如果相同,那么可以认为这个文件是恶意的程序。
扫描算法的设计。
在扫描算法设计上,应当通过加如新的规则,来进行权值的校正。各条规则权值的校正可以通过算法来完成、或人工修改的方式。
假设Y是扫描结果,
Y=1是权和大于规定值,则扫描为木马病毒。
Y=0 是权和小于规定值,则不是木马病毒。
公式Y=a+B1X1+B2X2+……….+BnXn
其中
Xi是:第i条规则的权值。
a是:整体误差,由规则不全引起的误差。
病毒日志:
主要记录:病毒文件名、病毒来源、扫描等级、扫描日期、路径扫描方式。
病毒扫描数据备份和恢复:
主要根据用户的选择,备份一些数据表,在需要的时候进行恢复。
病毒扫描历史记录:
主要是保存扫描后的扫描结果,为扫描引擎提供数据来源。
2)自我防护。
防止被恶意程序结束。
在32位Windows系统上,可以:
Hook掉 NtOpenProcess()、NtTerminateProcess()、ObReferenceObjectByHandle()
但,这些都不是最深层的函数,我们的安全卫士程序很容易被恶意程序结束掉。
因此,应该Inline Hook PspTerminateThreadByPointer(),防止进程被结束。
在64 位Windows系统上,可以:
调用Microsoft提供的内核函数 ObRegisterCallbacks()。利用提供的Callback函数指针来保护进程不被结束。
注意的事项:
在Windows7或更高版本的Windows系统里面,Callback函数可以拥有64个。若一台计算机里面的Callback函数多于64个,那么Windows系统将不行挂载Callback函数。这时,需要替换Windows系统中的其它Callback函数。这些被替换的Callback函数可能是别的程序拥有的。
3)网络流量监控与管理。
监控电脑上的进程的联网情况。
如果要限制网络速度的快和慢, 可以截取网络数据包,然后做适当的丢包处理。
因为NDIS驱动程序,可以直接操控数据链路层的以太网数据包,可以使用NDIS驱动来进行丢包处理的操作。而进行数据包的丢弃处理,就是可以控制网络速度的快、慢。在程序代码里,我们可以设计一些规则来决定什么时候需要丢弃数据包、什么时候不需要丢弃数据包。
由于NDIS不能够得到进程的信息。所以要配合TDI、WFP驱动来得到进程的信息。
本地端口号、本地IP、远程端口号、远程IP、协议类型、进程路径、进程ID号。如果,想获取这些信息,可以直接在WFP提供的回调函数FWPM_LAYER_ALE_AUTH_CONNECT_V4里面获取。这个回调函数的最后一个参数,可以指定一个值,用来放行还是拦截。
由于在Windows7或更高版本的Windows系统里面,TDI技术已经被废弃了,而使用了WFP技术来代替TDI技术。所以,在开发网络流量监控的时候,最好用上NDIS和WFP技术。
4)监控管理Windows文件系统。
监控Windows系统上,文件的打开、创建、删除、复制、粘贴、剪切、读、写 等状态。
为了达到通用性,不论是在 32位Windows系统上,还是64位Windows系统上,都要使用MiniFilter文件系统过滤驱动的开发技术,来监管文件的这些状态。不应该使用Hook SSDT的钩子技术来监控管理文件系统。
主要是:实现IRP_MJ_CREATE、IRP_MJ_READ、IRP_MJ_WRITE等这些IRP请求的
Pre和Post函数的代码实现。并决定是否允许用户操作、是否禁止。就是在代码里面设置:
STATUS_ACCESS_DENIED、STATUS_SUCCESS这样的标志。
5)监控管理Windows注册表。
监控注册表的键值是否:打开、创建、删除、复制、粘贴、剪切、读、写 等状态。
在32 位的Windows操作系统上:
在 WindowsXP以及更早的Windows操作系统上,在注册表防护的时候,可以进行SSDT Hook的操作。 主要是Hook掉以下几个内核函数:ZwSetValueKey、ZwCreateKey、ZwDeleteValueKey、ZwDeleteKey等等。因此,为了有效地管理注册表,我们的安全卫士还应该Hook掉 KiFastCallEntry()函数。 这个函数是所有SSDT HooK函数的管理中转站。 我们可以禁止别人来Hook一些函数。这样可以有效地防止而已程序来操控注册表。
在64位的Windows操作系统上:
在Windows7、Windows8或更高版本的Windows系统里面,Microsoft 公司提供了注册表专用的管理函数----CmRegisterCallback,我们可以用这个函数来监控注册表。
需要说明的是:
在WindowsXP以及更早的Windows操作系统上,Microsoft公司没有提供CmRegisterCallback内核函数,所以只能使用SSDT Hook的方式来管理注册表。
在Windows7、Windows8或更高版本的Windows系统里,不论是32位还是64位的Windows系统,都已经可以使用CmRegisterCallback函数了。
CmRegisterCallback函数,主要是提供了一个函数指针,在这个指针里面来实现代码,进行注册表的监控管理。
CmRegisterCallback函数,和上面提到的自我防护一样。 也需要注意Callback函数可以拥有的个数。若一台计算机里面的Callback函数多于Windows操作系统所规定的个数,那么
Windows系统将不行挂载Callback函数。这时,需要替换Windows系统中的其它Callback函数。这些被替换的Callback函数可能是别的程序拥有的。
6)沙箱。
沙箱其实就是一个Windows过滤文件驱动,具体来说,就是你把要写的东西写到了硬盘上,但实际上并没有写到硬盘,而是到了一个转存处,读取内容需要判断是沙箱开启之前就存在的内容还是开沙箱之后写入的内容,要分别从不同的地方读取内容,重启之后把转存的地方清零。
在实现沙箱代码的时候,要做以下几个工作:
(1)文件系统虚拟化
重定向文件目录的路径。
重新编写一个Windows文件系统,完全映射真实的Windows文件系统。可以参考WDK 自带的例子FastFat工程代码,并在此基础上进行修改。
(2)注册表虚拟化
重定向注册表相关键值的路径。
重新实现一个注册表系统的驱动程序。完全映射真实的Windows注册表。封装注册表操控的内核函数、定义通用的HIVE数据结构、
(3)内核对象虚拟化
主要是:突破单实例程序在沙箱里和沙箱外同时运行时的限制。比如一部分游戏的多开限制。
(4)服务虚拟化
对于沙箱创建的服务进行虚拟化,主要作用是防止沙箱内的程序通过服务进程来穿透沙箱。
(5)窗口虚拟化
作用同内核对象虚拟化。
(6)DCOM虚拟化
使沙箱内进程的功能更加完善,比如ie浏览器在很多地方就使用了DCOM组件,如不进行虚拟化则ie有些功能会不正常。
(7)多沙箱支持
同时存在多个沙箱,每个沙箱互不干扰,彼此不知道对方的存在。
(8)日志记录
支持系统从WindowsXP到Windows8.1的32位和64位操作系统。
(9)Inline Hook、SSDT Hook、Shadow SSDT Hook、Object Hook
实现钩子引擎。同时要确保能在 Windows x64操作提供上能够正常使用。
(10)服务端进程
用来模拟RPC通信。实现所需要进行RPC交互的各种消息。
沙箱的基础架构:
沙箱在进程级别上操作。所有需要装在沙箱中的东西都需要生存在进程中。最小的沙箱配置需要两个进程:一个是权限控制,称为(代-理),和一个或者多个沙箱化的进程,称为目标。在文档和代码当中,这两个术语始终有精确的内涵。沙箱作为一个动态链接库提供,必须链接到代-理和目标可执行程序中。
在沙箱中,代-理是一个浏览器进程。主要是:一个沙箱进程的权限控制者/管理者。
代-理进程的任务有:
为每一个目标进程提供策略
产生目标进程
安置沙箱策略引擎服务
安置沙箱拦截管理器
安置沙箱IPC服务(发送到目标进程)
执行目标进程请求的策略允许的行为。
代-理进程必须在所有目标进程终止后才能终止。沙箱IPC是一个低层次的机制,它用来透明地发送来自于目标进程的特定的Windows API调用到代-理进程:这些调用被评估是否违反了策略。策略允许的调用被代-理进程执行,结果通过相同的IPC返回到目标进程。拦截管理器的工作是调整要通过IPC发送到代-理进程的Windows API调用。
沙箱的限制:
沙箱的核心依赖于四个windows机制提供的保护
(1)一个限制令牌
(2)Windows 作业对象
(3)Windows desktop对象
(4)限于Vista完整性级别
这些机制在保护OS上是非常高效的,它们的配置和用户数据提供了:
所有的安全资源有一个比空安全描述符更好的描述符。即:没有未配置安全性的关键资源。
令牌:
拥有一个正常工作的进程的令牌和作业如何做限制。
在设计上,沙箱令牌不能保护下面的不安全资源:
挂载的FAT或者是FAT32卷:它们的安全描述符实际上是空的。在目标进程上运行的恶意软件可以读写这些卷,只要它能猜到或者推出卷的路径。
作业对象(Job Objects):
目标进程运行在一个Job对象下面。使用这个Windows机制,一些有趣的没有传统对象或者是安全描述符的限制得以实施:
禁止使用SystemParametersInfo做用户系统修改,函数可以切换鼠标左右键,或者设置屏保超时时间。
(1)禁止创建或者是切换桌面
(2)禁止修改用户显示配置例如分辨率和主显示器。
(3)不允许读写剪切板
(4)禁止广播Windows消息
(5)禁止设置全局钩子
(6)禁止读取全局Atom表
(7)禁止读取Job对象外创建的User句柄
(8)一个活动进程限制(不允许创建子进程)
(9)每一个进程有自己的作业对象。使用作业对象,沙箱可以阻止
<1>过度使用CPU
<2>过度使用内存
<3>过度使用IO
目标进程自启动:
目标进程启动时不带策略指定的限制。它们启动时带着与普通用户进程非常相近的令牌。原因是,在进程启动时,OS加载器要读取许多资源,它们中的大部分确实没有文档描述而且任何时候也不能更改。同样的,大多数应用程序使用标准的CRT(C运行时库)提供标准的开发工具。在进程启动后,CRT还需要做初始化。
因此,在进程启动的过程中实际上使用两个令牌:锁定令牌,是一个进程令牌;初始令牌,作为模拟令牌设置到初始线程。
7)防止被调试。
一些恶意程序,可能会调试我们的安全卫士程序,来破坏、绕过安全卫士等等。被调试的程序可以检测到自己是否被调试器附加了,如果探知自己正在被调试,肯定是有人试图反汇编的方法破解自己。
(1)Windows API
Win32提供了两个API函数, IsDebuggerPresent和CheckRemoteDebuggerPresent可以用来检测当前进程是否正在被调试。
(2)查询进程PEB的BeingDebugged标志位
当进程被调试器所附加的时候,操作系统会自动设置这个标志位,因此在程序里定期查询这个标志位就可以了。
(3)查询进程PEB的NtGlobal标志位
当进程被调试的时候,操作系统除了修改BeingDebugged这个标志位以外,NtDll中一些控制堆(Heap)操作的函数的标志位就会被修改,因此也可以查询这个标志位。
(4)查询进程堆的标志位ForceFlags
只要进程被调试,进程在堆上分配的内存,在分配的堆的头信息里,ForceFlags这个标志位会被修改,因此可以通过判断这个标志位的方式来反调试。因为进程可以有很多的堆,因此只要检查任意一个堆的头信息就可以了。
(5)操作系统,硬件虚拟化。
在电脑里面,当电脑一开机的时候,BIOS就会上电自检测。 而BIOS里面有一个选项VT-X
当VT-X的状态为Enable的时候,表示电脑可以进行硬件虚拟化的操作。
在代码实现的时候,因为Windows驱动程序都是运行在Ring0层,调试、反调试的对抗也是直接在Ring0层进行的。
因此,权限只要比Ring0层高,那么就可以完全阻止其它程序来调试我们的程序。
目的:即使程序代码的实现方法众所周知,其它任何软件也无法探测到它。
硬件虚拟化的实现步骤:
<1>检查当前电脑的CPU是否支持硬件虚拟化技术,使用汇编指令CPUID来查询是否
CPUID.1:ECX.VMX[bit 5]=1,也就是VMX位结果是否为1。
设置CR4.VMXE[bit 13]=1,并且在内存中分配出VMXON区域和VMCS控制块。它们都必须分配在4KB页对齐的内存区域上。
<2>在驱动程序代码里面,通过汇编语言指令VMXON来进入VMX Root模式,这样就开启了虚拟机管理器的运行环境。然后使用VMLAUNCH指令,使目标系统正式运行在虚拟机中。
<3>在程序代码里面,调用Windows内核函数MmInitManager()、MmMapGuestKernelPages()、
MmMapGuestPages()等等来构建私有页表。
<4>按照一般驱动的方法编写代码,来阻止调试。因为是比Ring0层更底层,所以可以非常有效地控制其它程序来调试。