这里主要介绍一下Windows驱动开发的一些基础知识,善用Windows驱动可以做到很多事情,比如常见的Rootkit隐藏文件、进程等,其实不仅仅是隐藏自身的目的,驱动可以做很多事情,获取到内核之后基本无所不能,但是很多情况下是理论上的,实际做到可能门槛很高,代价大,下面首先介绍一下驱动开发环境的配置(PS:本文是2020年初把以前内部交流的PPT整理了下发布,描述比较简单仅为个人整理记录备忘用,部分图片及文字描述如IRP部分来自麦洛克菲内核编程培训,其它有一部分忘记来源,当时是内部交流没注意保留来源,后来找不到出处了,如有侵权请与我联系标注来源)。
环境配置
驱动编程开发环境
首先需要安装WDK:
IDE的话可以选择VS2013 Or VS 2010+VisualDDK:
VS2013及以后的VS都自带驱动编程解决方案。
下面介绍驱动调试环境。
驱动调试环境
Vmware、VirtualBox
Windbg :包含在WDK里
VirtualKD :快速搭建双机调试环境
调试符号:对象文件调试信息
Windbg简介
功能无比强大的调试器,支持内核态、用户态,以及源码调试等。
IDA简介
功能无比强大的静态分析神器。
反编译、交叉引用、图形模式…
驱动加载
这里只介绍最常规的NT驱动的加载,
- INF文件安装
- DriverMonitor.exe
- 自己瞎编
OpenSCManager
CreateService
OpenService
StartService
自己编写驱动使用DriverMonitor就足够了,DriverMonitor使用也比较简单。
如果是自己的木马加载驱动的话就需要自己写程序了。
数据交互
下面介绍开始介绍驱动加载到系统之后,应用层程序如何与驱动进行数据交互,在这之前先介绍一下一些概念。
IRP:
应用层API调用之后底层发生了什么?
IRP可以先简单理解为驱动分发函数的参数。重点看一下IRP头部紧挨着的就是IO_STACK_LOCATION,从n-0代表下面每一层的参数。
再来看一下过滤驱动的实现代码里怎么处理的IRP。
看下这段代码,DispatchCommon一般实现就是不处理直接下发到下一层设备。
从这里Create/Read/Write/DeviceIoControl/Close也可以看出驱动一般对读写DeviceIoControl等进行处理,也就是数据交互的地方
这是读写分发函数,看代码,这里就是从IRP中取出参数,然后返回数据。
重点看一下IRP头部紧挨着的就是IO_STACK_LOCATION,从n-0代表下面每一层的参数。
所以IoGetCurrentIrpStackLocation函数获取本层的参数。
前面介绍完读写的处理,可能觉得不够灵活,下面介绍的DeviceIoControl就是最常用的数据传递手段。
DeviceIoControl:
用户态调用DeviceIoControl函数向驱动发送特定CTL_CODE,向内核态传递数据。
CTL code 包含控制请求的相关信息,驱动程序接受到之后根据此进行数据的获取。看代码。
三种数据传递方式原理:
- Buffer IO
系统会将用户提供的输入缓冲区的内容复制到IRP中,使用完毕之后重新拷贝至输出缓冲区。 - Direct IO
将DeviceIoControl指定的输出缓冲区锁定,在内核模式地址下重新映射一段地址。
METHOD_IN_DIRECT及METHOD_OUT_DIRECT - Neither IO
直接访问用户模式地址,危险!
CTL code 包含三种数据传递方式,下面介绍下区别。
- BufferIO:安全 效率略差
METHOD_BUFFERED
输入输出缓冲区:
pIrp->AssociatedIrp.SystemBuffer
缓冲区大小: - DirectIO:
METHOD_DIRECTED
输入缓冲区:
pIrp->AssociatedIrp.SystemBuffer
输出缓冲区
pIrp->MdlAddress
MDL:内存描述符列表,将同一块内存同时映射到用户态空间和内核。
缓冲区大小:
Parameters.DeviceIoControl.InputBufferLength;
Parameters.DeviceIoControl.OutputBufferLength;
MDL:刚才介绍锁定重新映射的概念,主要通过MDL实现
如何使用户态进程与核心态驱动共享内存呢 ?32位Windows中,默认状态下虚拟空间有4G,前2G是每个进程私有的,也就是说在进程切换的时候会变化,后2G是操作系统的,所以是固定的。既然用户态进程和核心态驱动在同一个进程空间里,是不是只要直接传个内存地址过来,就可以访问了?理论上可以但实际上不行,因为用户态的进程在不断地切换,使驱动运行时没法保证前面的用户态进程是哪个,也就不确定前2G虚拟地址空间的映射情况,那么用户态进程传来的地址也许不是合法的。
3. NeitherIO:。
METHOD_NEITHER
输入缓冲区:
Parameters.DeviceIoControl.Type3InputBuffer;
输出缓冲区
pIrp->UserBuffer
缓冲区大小:
Parameters.DeviceIoControl.InputBufferLength;
Parameters.DeviceIoControl.OutputBufferLength;
这一段完毕,下面介绍文件系统过滤驱动,在此之前先简单介绍一下Windbg的一些命令
Windbg命令-1
- 断点类
Bp 软断点
例:bp 0x401000;bp nt!NtEnumerateValueKey
Ba 硬件断点
例:ba r4/w2/e1 0x401000;读、写、执行断点- 查看数据
dd 查看数据
例:dd 0x400000
dt 结构体方式查看
例:dt _PEB
断点的实现原理:CC软断点,写程序断点 硬件断点:调试寄存器,有兴趣可以研究调试器原理。
Dt后面可以加地址。
文件过滤驱动
sFilter框架
- 生成控制设备
- 注册分发函数
- 绑定设备
- 读写…过滤
文件过滤分发函数及对应功能
- FilterCreate 创建/打开
- FilterRead 读、加解密处理
- FilterWrite 写、加解密处理
- FilterSetInfo 删、重命名等操作
- FilterClean 写关闭
其它:MiniFilter 隐藏文件网络过滤驱动
框架种类:
TDI,NDIS驱动(小端口驱动/中间层驱动/协议驱动),WFP……
应用:
构建隐蔽通道,流量监控,网络防火墙…
TDi 驱动可以实现一套内核层的隐蔽链接。
IM驱动和过滤驱动相比,有什么利弊之分?
1.IM有最好的通用性,可以在WINXP,WIN2K,VISTA,WIN7中全面兼容,但它对VISTA与WIN7中的支持,是通过在通信函数中加入从NDIS_PACKET至NET_BUFFER转换层实现,所以性能在WIN7下应该是反而减弱了。
2.过滤驱动是直接使用NET_BUFFER来进行通信的,这种方式会有更好性能,主要的原因是NET_BUFFER使用了新的数据表示方法,避免的频繁的内存复制。具体可以参考MSDN。但很明显,它无法支持WINXP及之前的系统。
NDIS Filter框架:
微软建议使用NDIS Filter替代NDIS IMD。
特点:
- VS2013+WDK集成
- 灵活方便
- 性能更好
NDIS Filter框架:
- DriverEntry
- NDIS_FILTER_DRIVER_CHARACTERISTICS
指定一些Filter驱动的特性 - NdisFRegisterFilterDriver
注册驱动
- FilterSendNetBufferLists
过滤一个NET_BUFFER_LIST的发送。 - FilterReceiveNetBufferLists
过滤一个NET_BUFFER_LIST的接收。
LWF中用它自己的方式存储以太网中的数据帧,这就是NBL(NET_BUFFER_LIST),也就是在代码中,通过你注册的回调函数的参数系统将数据以PNET_BUFFER_LIST形式会传递给你,这时候你就能做数据过滤的操作了。这是个LIST,其中每个NBL又包含若干个NB(NET_BUFFER),每个NB有包含若干个MDL。真正的数据在这MDL中。
内核反调试
KdDisableDebugger
KdDebuggerNotPresent
KdSendPacket、KdRecivePacket
Windbg命令-2
- 查看符号
x命令:- 查看汇编
u 显示8条指令
uf 显示整个函数
ub 显示eip前面的代码
SSDT HOOK
SSDT:
System Services Descriptor Table,系统服务描述符表。Windows系统中存在两个:
- KeServiceDescriptorTable
主要处理Kernel32.dll 中系统调用。 - KeServiceDescriptorTableShadow
主要处理 User32.dll 和 GDI32.dll 中系统调用。
并且 KeServiceDescriptorTable 在 ntoskrnl.exe(Windows 操作系统内核文件,包括内核和执行体层)是导出的,
而 KeServiceDescriptorTableShadow 则是没有被 Windows 操作系统所导出,
而关于 SSDT 的全部内容则都是通过 KeServiceDescriptorTable 来完成的
Demo:
SSDT HOOK能做什么?
- 文件/注册表/进程隐藏
- 进程保护
- 监控模块加载
- …..
怎么编程定位SSDT地址?
- KeServiceDescriptorTable 在 ntoskrnl.exe是导出的,直接声明引用即可。
如何修改SSDT表项?
- 系统对 SSDT 都是只读的,不能直接写
示例:古老的ssdt hook隐藏文件
Tips:Zw?Nt?
区别
ba e1 nt!NtEnumerateValueKey查看
Ntdll.dll中两者一样
Ntoskrnl.exe中nt函数更底层,Zw函数会把PreviousMode设置为KernelMode 然后再调用Nt函数,因此在Nt函数中就不会进行参数检查。如果直接调用Nt函数的话 , 必须自己将PreviousMode设置为KernelMode,否则PreviousMode很可能仍然是UserMode, 这样的话 Nt*函数就会产生问题。
Tips:xuetr(pc hunter)
Tips:PatchGuard
Tips:APC相关结构
- _EPROCESS执行体进程块
进程对象,与用户态的PEB结构联系很大,除了包含许多与进程有关的属性之外,还包含和指向许多其他的相关数据结构。
- _THREAD执行体线程块
ETHREAD(执行体线程块)是执行体层上的线程对象的数据结构。在windows内核中,每个进程的每一个线程都对应着一个ETHREAD数据结构。
APC注入
APC简介:
异步过程调用,给程序在本函数返回后还能执行的机会。包括内核APC及用户APC。
APC注入步骤:
- 遍历进程列表寻找合适进程、线程
- 在目标进程中申请空间布置Shellcode
- KeInitializeApc初始化APC
- KeInsertQueueApc插入APC
内核如何遍历进程列表:
PsGetCurrentProcess()
ActiveProcessLinks
在目标进程中申请空间:
KeStackAttachProcess/KeUnstackDetachProcess
MDL或ZwAllocateVirtualMemory
相关事项及说明:
当线程在alertable状态时才执行这些APC函数
Win7系统内核在目标进程中分配内存不要使用MDL,没有执行权限,进程会崩溃,可以使用ZwAllocateVirtualMemory,WinXP上不受影响。
比较简单,还有其他Usermodecallback等方式的注入,可以了解下。
强杀进程
- 暴力搜索PspTerminateProcess函数地址
- PspTerminateProcess函数未导出
- NtQuerySystemInformation两次调用获取nt基址
- 搜索特征获取PspTerminateProcess地址并调用
NtQuerySystemInformation Win8好像还能用后面就禁用了。
强删文件
文件独占如何删?
- 枚举句柄表 ZwQuerySystemInformation
- 复制句柄 ZwDuplicateObject然后ZwClose
- 使用DUPLICATE_CLOSE_SOURCE再次ZwDuplicateObject,然后ZwClose
正在运行如何删?
- NtfsSetDispositionInfo->MmFlushImageSection检查
- ImageSectionObject 和 DataSectionObject设为0
构建IRP包发送
删除文件时会先到NTFS.sys的分派例程,NtfsSetDispositionInfo-》MmFlushImageSection。MmFlushImageSection检查这个文件对象的SECTION_OBJECT_POINter结构是不是为空,没有在运行直接返回TRUE。所以如果想要删除正在运行的文件,一种方法是设置SECTION_OBJECT_POINter结构里的变量都为0。这样MmFlushImageSection为返回TRUE,表示能删除。另一种方法是HOOK NTFS.sys的导入表中的MmFlushImageSection()函数,在HOOK函数中检查是不是我们要删除的文件,是的话直接返回TRUE也行
如何构建IRP包发送:
- IoBuildSynchronousFsdRequest
创建同步的 IRP 请求,但是只可以创建IRP_MJ_PNP ,IRP_MJ_READ,IRP_MJ_WRITE,IRP_MJ_FLUSH_BUFFERS 和IRP_MJ_SHUTDOWN 类型。 - IoBuildAsynchronousFsdRequest
创建异步的 IRP 请求。 - IoBuildDeviceIoControl
创建同步的 IRP,类型为:IRP_MJ_DEVICE_CONTROL 和 IRP_MJ_INTERNAL_DEVICE_CONTROL。 - IoAllocateIrp:创建后自行填写参数
IoAllocateIrp 函数的使用比较灵活,他可以创建任意类型的 IRP ,但不是由参数指定,而是创建后自行填写,要求用户对 IRP 的结构有比较熟悉的理解。
Bootkit
鬼影。TDL4。
Bootkit种类:
- Bios Bootkit
代表:谍影、 BMW病毒、Hacking Team
过于依赖硬件,通用性差。 - MBR Bootkit
代表:鬼影、TDL4、暗云
隐蔽性差,易发现。 - VBR Bootkit
代表: GrayFish
Other
MBR类Bootkit的调试-Bochs:
MBR类Bootkit的调试-IDA远程调试:
驱动签名
DSE 机制:
Win7 64位操作系统开始引入驱动强制签名机制(Driver Signature Enforcement),必须重启在Bios中关闭设置等,才能加载未经过签名的驱动程序。
申请方式:
个人
企业
怎么办?
已签名驱动程序的漏洞
散列碰撞伪装签名(火焰病毒)
怎么办?
利用内核提权漏洞突破
Win7修改nt!g_CiEnabled为0
Win8修改ci!g_CiOptions为8
过期、吊销的签名
提权漏洞
内核提权不单单指Windows内核,杀软驱动等等都可以含有提权漏洞
Tips:Windbg命令
切换进程
!process 0 0 explorer.exe
.process /p /r ……
查看栈回溯
kb
CVE-2014-4113:
微软内核中xxx开头的函数都会调用KeUserModeCallback
回调函数进入用户态时释放掉该Menu对象会导致后续再使用这个对象出问题
Tips:KeUserModeCallback
漏洞流程:
为什么Shellcode会执行:
申请0页内存:
Shellcode:替换system进程Token
其它类型Shellcode:
覆写SEP_TOKEN_PRIVILEDGES
覆写 HalDispatchTable