内核驱动漏洞原因和七大忠告
- 不要使用MMIsAddressValid函数,这个函数对于校验内存结果是UNreliable的
首先,他只能判断一个字节地址的有效性 比如:
if(MmIsAdressValid(p1)){
memcmp(p1,p2,len);
}
攻击者只需要传递第一个字节在有效页,而第二个字节在无效页的内存就会导致系统崩溃。比如0x7000是有效也 0x8000是无效也 传入0x7fff
其次,MmIsAddressValid对于 pageout的页面不能准确的判断,所以攻击者可以了利用你的判断失误来绕过你的保护 -
在try_except内完成对于用户态的任何操作
错误写法:
try{
ProbeForRead(Buff, Len, Alig);
}
__except(EXECUTE_HANDLER_EXCEPTION){ ... }
if(memcmp(Buff, Buff2, Len))
....
ProbeForRead只检查 probe 的那一刻, buff地址是在用户态地址范围内 -
留心长度为0 的缓存、为NULL的缓存指针和缓存对齐
a 长度为0:
内存校验函数ProbeForRead 和 ProbeWrite 函数 当ProbeForXXX的参数Length为0 时,这两个函数都不会做任何工作,连微软都犯过错(可以通过某种方式分配出地址为NULL的地址)
所以即使当 ProbeForRead验证了参数,一样要当心Length 为 0 时的情况
__try{
ProbeForRead(Str1,Len,sizeof(WCHAR));
if(wcsnicmp(Str1, Str2,wcslen(Str2)){...}
}
__except(EXECUTE_HANDLER_EXCEPTION){ ... }
当Len=0 时, 这样的函数会导致系统奔溃,需要注意,对于长度为空的缓存不能随意放行,因为系统可能接受为空的缓存参数做特殊用途,比如:对于ObjectAttributes->ObjectName的Length,如果为空,系统会以对应的参数打开ObjectAttributes->RootDirectory的句柄,攻击者可以先以低权限得到一个受保护对象的句柄,再以长度为空的缓存,将句柄填入RootDirectory来获取更高权限的句柄
b 缓存指针为空:
不要使用诸如下面的代码来判断用户态的参数:
if(UserBuffer == NULL)
{
goto pass_request;
}
Windows 操作系统是允许用户态申请一个地址为0 的内存的,攻击者可以利用这个特性来绕过检查或保护
c缓存对齐的问题:
ProbeForRead 的第三个参数 Alig 即对齐,如果没有正确的传递这个参数,也会导致问题,例如对于ObjectAttributes,系统默认按1 来对齐,如果在对其参数处理中使用sizeof(ULONG)来对齐,就会对本来可以使用的参数引发异常,绕过保护或检查 -
不正确的内核函数引发的问题
a.ObReferenceObjectByHandle 未指定类型
对于用户态句柄使用ObReferenceObjectByHandle ,不指定类型仍可以获得对应的对象地址,但如果直接访问这个对象,就会引发漏洞
常见的错误:
ObReferenceObjectByHandle(FileHandle, Access, NULL(ObjectType), ...&fileobject);
if(wcsnicmp(fileobject->FileName))
攻击者可以传入非文件类型的句柄从而造成系统漏洞。
b.不正确的ZwXXX函数调用
注意,不能将任何用户态内存通过ZwXXX函数传递给内核,用户态内存未经过校验,传递给ZwXXX会让系统忽略内存检查(因为ZwXXX调用时认为上个模式已经是内核模式),即使你进行了校验,传递这样的内存给系统也可以引发崩溃(例如内存也在调用时突然无效),即使你在外部有异常捕获,也可能造成内核内存泄漏、对象泄漏、甚至权限提升等严重问题
__try{
ProbeForRead(ObjectAttrbutes,sizeof(OBJECT_ATTRIBUTES),1);
ProbeForRead(ObjectAttrbutes->ObjectName,sizeof(UNICODE_STRING),1);
ProbeForRead(ObjectAttrbutes->ObjectName->Buffer, ObjectAttrbutes->ObjectName->Length,sizeof(WCHAR),1);
//未校验全部参数就传递给Zw函数,ObjectAttributes 还有多个域
//即使全部校验了也不能传递给Zw函数, 例如内存如果是无效的用户态内存,最后可能会被我们的驱动异常捕获,但是内核函数的数据回收很可能没有进行
}
c.不要接受任何用户输入的内核对象给内核函数
接受用户输入的内核对象意味着可以轻易构造内核任意写入地址漏洞,不要再设备控制中接入用户输入的内核对象并将其传递给内核函数
常见的错误:
if(IoControlCode == IOCTL_RELEASE_MUTEX){
KeReleaseMutex((MY_EVT_INFO)(Irp->AssociatedIrp=>SystemBuffer)->Mutex);//将节点从链表中摘掉
}
...
用户态程序只需要传递一个精心构造的Mutex对象(可以位于用户态内存),就可以做到 -
给驱动提供的功能接口必须小心
例如可以对注册表、文件、内核内存、进程线程等操作的功能性接口,一定要非常小心,如果不能完全杜绝存在被恶意利用的可能,一定要限制设备控制的调用者,禁止一切非受信进程的调用,金山网盾的漏洞,腾讯的QQ医生漏洞等 -
设备控制经量使用BUFFERED IO 而且一定要使用SystemBuffer,如果不能用BUFFERED IO ,对于 UserBuffer必须非常小心的Probe,同时注意Buffer指针,字符串引发的严重问题,如果可能,尽量禁止程序调用自己的驱动
设备控制中,可以的话,尽量使用BUFFERED IO ,使用BUFFERED IO 时一定要注意仅使用SystemBuffer(参考超级巡警曾出现的漏洞)如果不能使用 BUFFERED IO, 对于UserBuffer一定要做ProbeForRead 和 Try_except的完整校验,即使使用 Buffered IO ,Buffer中的指针 也可能引发内核DOS提权, 如果BUFFER 中还有指针 ,必须像对待非BUFFERED IO 中 UserBuffer那样仔细校验,同时对于字符串使用也要非常小心,杜绝使用 strcpy 这样会引发栈溢出的函数, 不过分影响产品功能得到前提下,尽一切可能限制对驱动的调用。例如在打开设备时检查是否保护进程,至少要检查是否Admin用户,如果可以的话尽量禁止非Admin 用户打开设备,同时,如果是服务进程或常驻进程使用的驱动设备,可以再IOCreateDevice 时对Exclusive参数传 TRUE ,来杜绝其他进程打开设备 -
使用 verifier(内核校验器) 和 Fuzz工具检查和测试驱动
SecurityCheck 功能就可以检查驱动是否传递了错误的内存、句柄给内核函数
Swap分区:在系统的物理内存不够的时候,把硬盘空间中的一部分空间释放出来,以供当前运行的程序使用
page in:分页(Page)从磁盘重新回到内存的过程
page out:分页(Page)写入磁盘的过程
Page Fault: 当内核需要一个分页时,但发现此分页不在物理内存中(因为已经被Page-out了),此时就发生了分页错误Page Fault ,CPU会产生一个hard page fault 中断。Hard page fault也称为major page fault ,指需要访问的内存不在虚拟空间,也不再物理内存中,需要从慢速设备载入。从swap 回到物理内存也是hard page fault。minor page fault 也称为 soft page fault,指需要访问的内存不在虚拟地址空间,但是在物理内存中,只需要MMU建立物理内存和虚拟地址空间的映射关系即可,比如多个进程访问同一个共享内存中的数据,某些进程还没有建立起映射关系,会出现soft page fault。 invalid fault 也称为 segment fault,指进程需要访问的内存地址不在它的虚拟地址空间范围内,属于越界访问,内核会报segment fault 错误