0%

2018-02-06-Windows内核驱动与Rootkit杂记

    这里主要介绍一下Windows驱动开发的一些基础知识,善用Windows驱动可以做到很多事情,比如常见的Rootkit隐藏文件、进程等,其实不仅仅是隐藏自身的目的,驱动可以做很多事情,获取到内核之后基本无所不能,但是很多情况下是理论上的,实际做到可能门槛很高,代价大,下面首先介绍一下驱动开发环境的配置(PS:本文是2020年初把以前内部交流的PPT整理了下发布,描述比较简单仅为个人整理记录备忘用,部分图片及文字描述如IRP部分来自麦洛克菲内核编程培训,其它有一部分忘记来源,当时是内部交流没注意保留来源,后来找不到出处了,如有侵权请与我联系标注来源)。

环境配置

驱动编程开发环境

首先需要安装WDK:

IDE的话可以选择VS2013 Or VS 2010+VisualDDK:

VS2013及以后的VS都自带驱动编程解决方案。

下面介绍驱动调试环境。

驱动调试环境

Vmware、VirtualBox
Windbg :包含在WDK里
VirtualKD :快速搭建双机调试环境
调试符号:对象文件调试信息

Windbg简介

功能无比强大的调试器,支持内核态、用户态,以及源码调试等。

IDA简介

功能无比强大的静态分析神器。
反编译、交叉引用、图形模式…

驱动加载

这里只介绍最常规的NT驱动的加载,

  1. INF文件安装
  2. DriverMonitor.exe
  3. 自己瞎编
    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 包含控制请求的相关信息,驱动程序接受到之后根据此进行数据的获取。看代码。


三种数据传递方式原理:

  1. Buffer IO
    系统会将用户提供的输入缓冲区的内容复制到IRP中,使用完毕之后重新拷贝至输出缓冲区。
  2. Direct IO
    将DeviceIoControl指定的输出缓冲区锁定,在内核模式地址下重新映射一段地址。
    METHOD_IN_DIRECT及METHOD_OUT_DIRECT
  3. Neither IO
    直接访问用户模式地址,危险!

CTL code 包含三种数据传递方式,下面介绍下区别。

  1. BufferIO:安全 效率略差
    METHOD_BUFFERED
    输入输出缓冲区:
    pIrp->AssociatedIrp.SystemBuffer
    缓冲区大小:
  2. 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

  1. 断点类
    Bp 软断点
    例:bp 0x401000;bp nt!NtEnumerateValueKey
    Ba 硬件断点
    例:ba r4/w2/e1 0x401000;读、写、执行断点
  2. 查看数据
    dd 查看数据
    例:dd 0x400000
    dt 结构体方式查看
    例:dt _PEB
    断点的实现原理:CC软断点,写程序断点 硬件断点:调试寄存器,有兴趣可以研究调试器原理。
    Dt后面可以加地址。

文件过滤驱动


sFilter框架

  1. 生成控制设备
  2. 注册分发函数
  3. 绑定设备
  4. 读写…过滤

文件过滤分发函数及对应功能

  1. FilterCreate 创建/打开
  2. FilterRead 读、加解密处理
  3. FilterWrite 写、加解密处理
  4. FilterSetInfo 删、重命名等操作
  5. 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框架:

  1. DriverEntry
  • NDIS_FILTER_DRIVER_CHARACTERISTICS
    指定一些Filter驱动的特性
  • NdisFRegisterFilterDriver
    注册驱动
  1. FilterSendNetBufferLists
    过滤一个NET_BUFFER_LIST的发送。
  2. 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

  1. 查看符号
    x命令:
  2. 查看汇编
    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注入步骤:

  1. 遍历进程列表寻找合适进程、线程
  2. 在目标进程中申请空间布置Shellcode
  3. KeInitializeApc初始化APC
  4. KeInsertQueueApc插入APC

内核如何遍历进程列表:
PsGetCurrentProcess()
ActiveProcessLinks

在目标进程中申请空间:
KeStackAttachProcess/KeUnstackDetachProcess
MDL或ZwAllocateVirtualMemory

相关事项及说明:
当线程在alertable状态时才执行这些APC函数
Win7系统内核在目标进程中分配内存不要使用MDL,没有执行权限,进程会崩溃,可以使用ZwAllocateVirtualMemory,WinXP上不受影响。

比较简单,还有其他Usermodecallback等方式的注入,可以了解下。

强杀进程

  1. 暴力搜索PspTerminateProcess函数地址
  2. PspTerminateProcess函数未导出
  3. NtQuerySystemInformation两次调用获取nt基址
  4. 搜索特征获取PspTerminateProcess地址并调用

NtQuerySystemInformation Win8好像还能用后面就禁用了。

强删文件



文件独占如何删?

  1. 枚举句柄表 ZwQuerySystemInformation
  2. 复制句柄 ZwDuplicateObject然后ZwClose
  3. 使用DUPLICATE_CLOSE_SOURCE再次ZwDuplicateObject,然后ZwClose

正在运行如何删?

  1. NtfsSetDispositionInfo->MmFlushImageSection检查
  2. 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