本文部分图片模糊,重新补发清晰。Ioctl是操作系统为应用层与内核层之间通讯设计的机制,是为了将应用层的数据传输到内核层,因为操作系统内核的系统调用是为了Windows官方API进行开放与创建,但是第三方驱动厂商想开发驱动程序,一般是要自己注册设备对象,并且通过CreateFile方式访问设备对象对应的IRP进行通讯。
具体流程:
图1:IRP流程
上述为大致流程,基本目前市面上的驱动程序都是基于此结构思路与应用层通讯,之间流程及其复杂。但是目前可以通过很多工具去查看每个驱动的设备对象信息。
这个可以通过PCHunter 或 火绒剑 包括大量ARK工具都可以做到枚举功能。当然如果自己写一个那么更好,最好进行权限检测。
一、简介
1.1、Ioctl Fuzz
Ioctl的fuzz是非常多种方式,其中快照式最为推荐。当然还有很多种方式,比如动态式。而Ioctl Fuzze是通过Ioctl进行传输数据,对传输点进行安全测试。原理上就是疯狂向驱动的IRP填充数据。主要是针对内存漏洞。
动态式用得比较多,是直接构造一个ioctl访问,向内核疯狂写数据。但是缺点是一旦检测到崩溃点代表整个操作系统的崩溃,往往只能点到为止。优点是稳定性与精确性包括真实性非常不错。
快照式用得比较少,逆向工作与动态调试需求比较多。对内核拍摄一个快照,使用CPU模拟工具、虚拟机(模拟运行快照)。将目标的内存修改为指定数据。比如进行ioctl,动态式直接构造好数据给DecviceloControl调用,快照式完全不一样,在调用前会将DecviceloControl的参数进行修改,修改为Fuzzer的数据,并且使用Bochs、KVM、QEMU进行模拟。但是这种技术极其复杂,需要挖掘人员需要对某个模糊测试框架足够熟悉。这种方法缺点是:逆向调试需求高,不稳定等等。优点是支持面广泛,精确度高,速度快等等
1.2、动态式测试
动态式,这里动态式式以原生的测试方式进行模糊测试,比如测试一个内存溢出,直接对一个可执行文件进行运行,并且传入垃圾数据进行测试。这种测试性能损耗大,速度慢。基本测试一个例子,1000组数据需要几分钟才行。但是精确度因为使用了系统为平台,并且以最真实的方式进行模拟。任何内存分配都到位包括机制,所以精确性比较准。相当于我使用一个脚本,构造好垃圾数据。然后创建一个子进程(目标)将数据传递给子进程,子进程会接收到数据,会按照正常流程执行,并且执行到漏点触发漏洞,这种还是比较精准的。当然这是以速度换来的。并且有些场景比如内核测试,局限性太高了。
1.3、快照式测试
快照式测试,上面说过通过对测试目标进行快照拍摄,并且加载快照修改内部可控的数据,实现动态的模糊测试,这种方式优点还是可以的,而且对于内核测试是极其不错的方式。其原理与上面动态式测试差异还是很大的,他是先进行快照拍摄,也就是将整体进行dump,将dump出的快照进行加载,并且设置内存状态与堆栈包括寄存器还原代码环境,快照式一般都是在触发漏洞点前进行数据修改,比如动态式例子创建子进程就进行截断,进行导出,在传递好数据开始,将已经传递好的数据进行修改,修改为测试需要的数据,当然这种方式需要修改的点还有很多,比如修改了堆,栈还有一些数据需要修改,所以局限性就是在于逆向工作,当然这种方法虽然很吃力,速度却异常快。这是以电脑性能为主的。一般windows平台需要使用bochs,Linux使用kvm等等。当然这种方法适用于内核层比较多。这种方式在应用层没必要因为大炮打蚊子。
二、Iotcl
这里分析一下Ioctl的基础原理,内容为:Ioctl-Code、IRP、派遣函数、设备对象等等。让读者对Ioctl有具体的熟悉。
2.1、IRP和派遣函数
IRP和派遣函数是Windows非常基础且重要的概念机制。IRP(I/O Request Packet)是 IO操作请求包的的简称。Windows中Ring3和Ring0的交流都会被处理成一个IRP结构体,当驱动接收到了IRP消息,会通过派遣函数进行内容分发等等,实现更多输入输出操作。
图2:IRP结构体
可以访问:https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ns-wdm-_irp 查看完整的IRP结构体。
lMdlAddress
如果驱动程序使用直接I/O,并且IRP主要函数代码是以下之一,则指向描述用户缓冲区的MDL的指针:
nIRP_MJ_READ
MDL描述了设备或驱动程序填充的空缓冲区。
nIRP_MJ_WRITE
MDL描述了一个包含设备或驱动程序数据的缓冲区。
nIRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL
如果IOCTL代码指定了METHOD_IN_DIRECT传输类型,MDL描述的缓冲区包含设备或驱动程序的数据。
如 果IOCTL代码指定了METHOD_OUT_DIRECT传输类型,MDL描述了设备或驱动程序填充的空缓冲区。
有关IOCTL代码中与METHOD_IN_DIRECT和METHOD_OUT_DIRECT传输类型关联的缓冲区的更多信息,请参阅I/O控制代码的缓冲区说明。
如果驱动程序没有使用直接I/O,则此指针为NULL。
lFlags
文件系统驱动程序使用此字段,该字段对所有驱动程序都是只读的。网络,可能也是最高级别的设备驱动程序也可能读取此字段。此字段设置为零,或设置为以下一个或多个系统定义的标志位的按位-OR:
nIRP_NOCACHE
nIRP_PAGING_IO
nIRP_MOUNT_COMPLETION
nIRP_SYNCHRONOUS_API
nIRP_ASSOCIATED_IRP
nIRP_BUFFERED_IO
nIRP_DEALLOCATE_缓冲区
nIRP_INPUT_OPERATION
nIRP_SYNCHRONOUS_PAGING_IO
nIRP_CREATE_OPERATION
nIRP_READ_OPERATION
nIRP_WRITE_OPERATION
nIRP_CLOSE_OPERATION
nIRP_DEFER_IO_COMPLETION
nIRP_OB_QUERY_NAME
nIRP_HOLD_DEVICE_QUEUE
nIRP_UM_DRIVER_INITIATED_IO
lAssociatedIrp
lAssociatedIrp.MasterIrp
指向IRP中主IRP的指针,该IRP是由最高级别的驱动程序调用IoMakeAssociatedIrp创建的。
lAssociatedIrp.SystemBuffer
指向系统空间缓冲区的指针。如果驱动程序使用的是缓冲I/O,缓冲区的目的由IRP主要函数代码确定,如下所示:
nSystemBuffer.IRP_MJ_READ
缓冲区接收来自设备或驱动程序的数据。缓冲区的长度由驱动程序IO_STACK_LOCATION结构中的Parameters.Read.Length指定。
无效。
nSystemBuffer.IRP_MJ_WRITE
缓冲区为设备或驱动程序提供数据。缓冲区的长度由驱动程序IO_STACK_LOCATION结构中的Parameters.Write.Length指定。
无效。
nSystemBuffer.IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL
该缓冲区表示提供给DeviceIoControl和IoBuildDeviceIoControlRequest的输入和输出缓冲区。输出数据覆盖输入数据。
对于输入,缓冲区的长度由驱动程序IO_STACK_LOCATION结构中的Parameters.DeviceIoControl.InputBufferLength指定。
对于输出,缓冲区的长度由驱动程序IO_STACK_LOCATION结构中的Parameters.DeviceIoControl.OutputBufferLength指定。
有关更多信息,请参阅I/O控制代码的缓冲描述。
缓冲区表示提供给DeviceIoControl和IoBuildDeviceIoControlRequest的输入缓冲区。
缓冲区的长度由驱动程序IO_STACK_LOCATION结构中的Parameters.DeviceIoControl.InputBufferLength指定。
有关更多信息,请参阅I/O控制代码的缓冲描述。
如果驱动程序使用直接I/O,缓冲区的目的由IRP主要函数代码确定,如下所示:
lThreadListEntry
lIoStatus
包含IO_STATUS_BLOCK结构,其中驱动程序在调用IoCompleteRequest之前存储状态和信息。
lRequestorMode
指示操作的原始请求者、UserMode或KernelMode之一的执行模式。
lPendingReturned
如果设置为TRUE,则驱动程序已将IRP标记为待处理。每个IoCompletion例程都应该检查此标志的值。如果标志为TRUE,并且IoCompletion例程不会返回STATUS_MORE_PROCESSING_REQUIRED,则例程应调用IoMarkIrpPending,将挂起状态传播到设备堆栈中其上方的驱动程序。
lCancel
如果设置为TRUE,IRP要么取消,要么应该取消。
lCancelIrql
包含调用IoAcquireCancelSpinLock时驱动程序正在运行的IRQL。
lCancelRoutine
包含在取消IRP时调用驱动程序提供的取消例程的切入点。NULL表示IRP目前不可取消。
lUserBuffer
如果满足以下两个条件,则包含输出缓冲区的地址:
uI/O堆栈位置的主要函数代码是IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL。
uI/O控制代码是用METHOD_NEITHER或METHOD_BUFFERED定义的。
对于METHOD_BUFFERED,驱动程序应使用指向 byIrp->AssociatedIrp.SystemBuffer的缓冲区作为输出缓冲区。当驱动程序完成请求时,I/O管理器将此缓冲区的内容复制到Irp->UserBuffer指向的输出缓冲区。驱动程序不应直接写入Irp->UserBuffer指向的缓冲区。有关更多信息,请参阅I/O控制代码的缓冲描述。
lTail
lTail.Overlay.DeviceQueueEntry
如果IRP在与驱动程序的设备对象关联的设备队列中排队,则此字段将设备队列中的IRP链接。这些链接只能在驱动程序处理IRP时使用。
lTail.Overlay.DriverContext
如果IRP没有在与驱动程序设备对象关联的设备队列中排队,则驱动程序可以使用此字段存储最多四个指针。此字段只能在驱动程序拥有IRP时使用。
lTail.Overlay.Thread
指向调用者线程控制块(TCB)的指针。对于源自用户模式的请求,I/O管理器始终将此字段设置为指向发出请求的线程的TCB。
lTail.Overlay.ListEntry
如果驱动程序管理自己的内部IRP队列,它会使用此字段将一个IRP链接到下一个IRP。这些链接只能在驱动程序将IRP保留在其队列中或正在处理IRP时使用。
操作系统会在应用层与内核层通讯时,应用程序会发动IO请求,操作系统将其转化为IRP数据结构,并且根据类型传递给不同的派遣函数里。IRP的两个基本属性MajorFunction和MinorFunction。分别记录IRP主类型与子类。操作系统根据MajorFunction将IRP派遣到不同的派遣函数中,在派遣函数还可以继续判断这个IRP属于MinorFunction。
一般进入驱动程序入口前(DriverEnrty),操作系统会将_IoPInvalidDeviceRequest的地址填满整个MajorFunction数组。
一般应用层使用CreateFile、WriteFile、ReadFile等进行IRP处理。
派遣函数例子:
图3:派遣函数
先设置状态码,并且获取IRP堆栈。并且将irp状态与长度设置对应信息,状态设置STATUS_SUCCUSS(应用层TRUE)。这样子发起的IRP的代码例:WriteFIle就可以接收到TRUE返回值。然后通过IoCompleteRequest将IRP结束。
2.2、I/O控制代码
I/O控制代码(IOCTL)用于用户模式应用程序和驱动程序之间的通信,或用于堆栈中驱动程序之间的内部通信。I/O控制代码使用IRP发送。
用户模式应用程序通过调用DeviceIoControl将IOCTL发送到驱动程序,Microsoft Windows SDK文档中对此进行了描述。调用DeviceIoControl会导致I/O管理器创建IRP_MJ_DEVICE_CONTROL请求并将其发送到最上面的驱动程序。
此外,上层驱动程序可以通过创建和发送IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL请求,将IOCTL发送到较低级别驱动程序。驱动程序在DispatchDeviceControl和DispatchInternalDeviceControl例程中处理这些请求。(用户模式应用程序无法发送IRP_MJ_INTERNAL_DEVICE_CONTROL请求。)
一些IOCTL是“公共的”,有些是“私人的”。公共IOCTL通常由微软在Windows驱动程序套件(WDK)或Windows SDK中进行系统定义和记录。它们可以通过用户模式组件对DeviceIoControl的调用发送,也可以使用IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL请求从一个内核模式驱动程序发送到另一个内核模式驱动程序。公共IOCTL的示例包括SCSI端口I/O控制代码和I8042prt鼠标内部设备控制请求。
另一方面,私人IOCTL仅供供应商的软件组件使用,以相互通信。私有IOCTL通常在供应商提供的头文件中定义,没有公开记录。与公共IOCTL一样,它们可以通过用户模式组件对DeviceIoControl的调用发送,也可以使用IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL请求从一个内核模式驱动程序发送到另一个内核模式驱动程序。
公共和私人IOCTL的编码没有区别。然而,与用于系统定义的IOCTL的内部代码相比,可用于供应商定义的IOCTL的内部代码存在差异。如果可用的公共IOCTL不符合您的需求,您可以定义新的私有IOCTL,您的软件组件可以使用这些IOCTL相互通信。有关更多信息,请参阅定义I/O控制代码。
l定义I/O控制代码
在定义新的IOCTL时,重要的是要记住以下规则:
n如果新的IOCTL可用于用户模式软件组件,IOCTL必须用于IRP_MJ_DEVICE_CONTROL请求。用户模式组件通过调用DeviceIoControl来发送IRP_MJ_DEVICE_CONTROL请求,DeviceIoControl是一个Win32函数。
n如果新的IOCTL仅适用于内核模式驱动程序组件,则IOCTL必须与IRP_MJ_INTERNAL_DEVICE_CONTROL请求一起使用。内核模式组件通过调用IoBuildDeviceIoControlRequest创建IRP_MJ_INTERNAL_DEVICE_CONTROL请求。有关更多信息,请参阅在驱动程序中创建IOCTL请求。
I/O控制代码是一个32位值,由多个字段组成。下图显示了I/O控制代码的布局。
图4:Ioctl Code
使用在Wdm.h和Ntddk.h中定义的系统提供的CTL_CODE宏来定义新的I/O控制代码。新IOCTL代码的定义,无论是用于IRP_MJ_DEVICE_CONTROL还是IRP_MJ_INTERNAL_DEVICE_CONTROL请求,都使用以下格式:
#define IOCTL_Device_Function CTL_CODE(DeviceType, Function, Method, Access)复制
为IOCTL选择一个描述性常量名称,形式为IOCTL_Device_Function,其中设备表示设备类型,函数表示操作。一个示例常量名称是IOCTL_VIDEO_ENABLE_CURSOR。
向CTL_CODE宏提供以下参数:
nDeviceType
识别设备类型。此值必须与驱动程序DEVICE_OBJECT结构的DeviceType成员中设置的值匹配。(请参阅指定设备类型)。小于0x8000的值保留给微软。供应商可以使用0x8000及以上的值。请注意,供应商分配的值设置了公共位。
nFunctionCode
标识驱动程序要执行的功能。小于0x800的值保留给微软。供应商可以使用0x800及以上的值。请注意,供应商分配的值设置了自定义位。
nTransferType
指示系统将在DeviceIoControl(或IoBuildDeviceIoControlRequest)的调用者和处理IRP的驱动程序之间传递数据。
使用以下系统定义的常量之一:
nMETHOD_BUFFERED
指定缓冲I/O方法,通常用于为每个请求传输少量数据。设备和中间驱动程序的大多数I/O控制代码都使用此TransferType值。
有关系统如何为METHOD_BUFFERED I/O控制代码指定数据缓冲区的信息,请参阅I/O控制代码的缓冲描述。
有关缓冲I/O的更多信息,请参阅使用缓冲I/O。
METHOD_IN_DIRECT或METHOD_OUT_DIRECT
指定直接I/O方法,通常用于使用DMA或PIO读取或写入大量数据,这些数据必须快速传输。
如果DeviceIoControl或IoBuildDeviceIoControlRequest的调用者将数据传递给驱动程序,请指定METHOD_IN_DIRECT。
指定METHOD_OUT_DIRECT,如果DeviceIoControl或IoBuildDeviceIoControlRequest的调用者将从驱动程序接收数据。
有关系统如何为METHOD_IN_DIRECT和METHOD_OUT_DIRECT I/O控制代码指定数据缓冲区的信息,请参阅I/O控制代码的缓冲区说明。
有关直接I/O的更多信息,请参阅使用直接I/O。
方法_NEITHER
既不指定缓冲区,也不指定直接I/O。I/O管理器不提供任何系统缓冲区或MDL。IRP提供指定给DeviceIoControl或IoBuildDeviceIoControlRequest的输入和输出缓冲区的用户模式虚拟地址,而无需验证或映射它们。
有关系统如何为METHOD_NEITHER I/O控制代码指定数据缓冲区的信息,请参阅I/O控制代码的缓冲区说明。
只有当驱动程序可以保证在发起I/O控制请求的线程上下文中运行时,才能使用此方法。只有最高级别的内核模式驱动程序才能保证满足此条件,因此METHOD_NEITHER很少用于传递给低级设备驱动程序的I/O控制代码。
使用这种方法,最高级别的驱动程序必须确定是在收到请求时设置缓冲或直接访问用户数据,可能必须锁定用户缓冲区,并且必须将其对用户缓冲区的访问包装在结构化异常处理程序中(请参阅处理异常)。否则,原始用户模式调用者可能会在驱动程序使用缓冲数据之前更改缓冲数据,或者可以在驱动程序访问用户缓冲区时将调用者替换掉。
有关更多信息,请参阅使用缓冲区或直接I/O。
必填访问
指示调用者在打开表示设备的文件对象时必须请求的访问类型(请参阅IRP_MJ_CREATE)。只有当调用者请求指定的访问权限时,I/O管理器才会创建IRP并使用特定的I/O控制代码调用驱动程序。RequiredAccess使用以下系统定义的常量指定:
FILE_ANY_ACCESS
I/O管理器为任何具有句柄的调用者发送IRP,以表示目标设备对象的文件对象。
文件_READ_数据
I/O管理器仅为具有读取访问权限的调用者发送IRP,允许底层设备驱动程序将数据从设备传输到系统内存。
FILE_WRITE_DATA
I/O管理器仅为具有写入访问权限的调用者发送IRP,允许底层设备驱动程序将数据从系统内存传输到其设备。
如果调用者必须同时拥有读写访问权限,FILE_READ_DATA和FILE_WRITE_DATA可以一起ORED。
一些系统定义的I/O控制代码具有FILE_ANY_ACCESS的必填访问值,该值允许调用者发送特定的IOCTL,无论授予设备的访问权限如何。示例包括发送给专属设备驱动程序的I/O控制代码。
其他系统定义的I/O控制代码要求调用者具有读取访问权限、写入访问权限或两者兼而有之。例如,公共I/O控制代码IOCTL_DISK_SET_PARTITION_INFO的以下定义表明,只有当调用方同时拥有读写访问权限时,才能将此I/O请求发送给驱动程序:
#define IOCTL_DISK_SET_PARTITION_INFO\
CTL_CODE(IOCTL_DISK_BASE, 0x008, METHOD_BUFFERED,\
FILE_READ_DATA | FILE_WRITE_DATA)
注意:在为新的IOCTL代码指定FILE_ANY_ACCESS之前,您必须绝对确定允许不受限制地访问您的设备不会为恶意用户破坏系统创建可能的路径。
驱动程序可以使用IoValidateDeviceIoControlAccess执行比IOCTL的Des RequiredAccess位更严格的访问检查。
2.3、I/O 控制代码 缓冲区
I/O控制代码包含在IRP_MJ_DEVICE_CONTROL和IRP_MJ_INTERNAL_DEVICE_CONTROL请求中。I/O管理器通过调用DeviceIoControl和IoBuildDeviceIoControlRequest创建这些请求。
由于DeviceIoControl和IoBuildDeviceIoControlRequest接受输入缓冲区和输出缓冲区作为参数,因此所有IRP_MJ_DEVICE_CONTROL和IRP_MJ_INTERNAL_DEVICE_CONTROL请求都提供输入缓冲区和输出缓冲区。系统描述这些缓冲区的方式取决于数据传输类型。传输类型由创建IOCTL代码值的CTL_CODE宏中的TransferType值指定。
该系统将每个TransferType值的缓冲区描述如下。
lMETHOD_BUFFERED
对于这种传输类型,IRP在Irp->AssociatedIrp.SystemBuffer上提供指向缓冲区的指针。此缓冲区表示调用DeviceIoControl和IoBuildDeviceIoControlRequest时指定的输入缓冲区和输出缓冲区。驱动程序将数据从此缓冲区传输出来,然后传输到此缓冲区。
对于输入数据,缓冲区大小由驱动程序IO_STACK_LOCATION结构中的Parameters.DeviceIoControl.InputBufferLength指定。对于输出数据,缓冲区大小由Parameters.DeviceIoControl.OutputBufferLength在驱动程序的IO_STACK_LOCATION结构中指定。
系统为单个输入/输出缓冲区分配的空间大小是两个长度值中较大的一个。
lMETHOD_IN_DIRECT或METHOD_OUT_DIRECT
对于这些传输类型,IRP在Irp->AssociatedIrp.SystemBuffer上提供指向缓冲区的指针。这表示在调用DeviceIoControl和IoBuildDeviceIoControlRequest中指定的第一个缓冲区。缓冲区大小由驱动程序IO_STACK_LOCATION结构中的Parameters.DeviceIoControl.InputBufferLength指定。
对于这些传输类型,IRP还提供指向Irp->MdlAddress的MDL的指针。这表示在调用DeviceIoControl和IoBuildDeviceIoControlRequest时指定的第二个缓冲区。此缓冲区可以用作输入缓冲区或输出缓冲区,如下所示:
如果处理IRP的驱动程序在调用缓冲区中接收数据,则指定METHOD_IN_DIRECT。MDL描述了输入缓冲区,并指定METHOD_IN_DIRECT确保执行线程具有对缓冲区的读取访问权限。
如果处理IRP的驱动程序在完成IRP之前将数据写入缓冲区,则指定了METHOD_OUT_DIRECT。MDL描述了一个输出缓冲区,并指定METHOD_OUT_DIRECT确保执行线程具有对缓冲区的写入访问权限。
对于这两种传输类型,Parameters.DeviceIoControl.OutputBufferLength指定了MDL描述的缓冲区的大小。
lMETHOD_NEITHER
I/O管理器不提供任何系统缓冲区或MDL。IRP提供指定给DeviceIoControl或IoBuildDeviceIoControlRequest的输入和输出缓冲区的用户模式虚拟地址,而无需验证或映射它们。
输入缓冲区的地址由参数.DeviceIoControl.Type3InputBuffer在驱动程序的IO_STACK_LOCATION结构中提供,输出缓冲区的地址由Irp->UserBuffer指定。
缓冲区大小由驱动程序IO_STACK_LOCATION结构中的Parameters.DeviceIoControl.InputBufferLength和Parameters.DeviceIoControl.OutputBufferLength提供。
2.4、一个Ioctl例子
这里主要是以一个Ring3与Ring0通信为标准进行编写 。首先从内核层开始,首先定义设备对象还有Ioctl Code:
图5:Ioctl Code 与设备对象名称包括驱动链接名
这里Ioctl代码使用0x830进行设置,并且在入口函数对设备对象与派遣函数还有卸载函数进行设置。
(卸载函数需要调用相关设备对象释放操作。)
入口函数:
图6:入口函数内容
这里是以循环来设置,减少代码整体的量。并且最后设置最为核心的派遣函数。也就是设备对象里面的MajorFunction数组里的IRP_MJ_DEVICE_CONTROL,一旦接受到设备控制类的调用,程序会调用MajorFunction的指针也就是ControlThroughDispatch。
图7:派遣函数内部
这里直接打印输入内容。然后通过设置设置OutputData。进行返回值设置操作。如果长度不够,那么就设置内存不足。(是DeviceIoControl调用的参数问题)
然后开始设置应用层的“连接设备对象”的代码,同理先设置设备对象名称与控制代码
定义代码:
图8:设备对象与控制代码定义
然后进行调用,统一在入口函数,释放操作因为篇幅原因不写了。
图9:应用层核心代码
通过编译并且运行,通过DbgView可以得到对应的输出结果,并且调用后也会得到相对应的返回值当然这里指的返回值是通过引用方式实现。
三、模糊测试
模糊测试,我们通过自己的方式去实现。当然自写的只会通过简单粗暴的41填充大法,通过参考:hongfuzz、LibFuzzer 来实现mutator多样式。
可以参考:https://github.com/0vercl0k/wtf/blob/main/src/wtf/mutator.cc来实现。
首先漏洞驱动,这里直接选用HEVD来实现。因为HEVD集成了很多Ioctl漏洞,从栈溢出到内存池溢出包括整数溢出UAF还有属性等等能够想到的任何漏洞类型都基本集成(二进制洞)当然学习难度还是有点东西的,利用起来也很头疼。
HEVD Github: https://github.com/hacksysteam/HackSysExtremeVulnerableDriver
编译直接运行builder里面的bat文件即可,当然需要wdk与cmake环境进行支持。
不用我多说,可以自行到GitHub项目的描述信息去查看。HEVD支持漏洞如下:
图10:漏洞列表
3.1 自写一个傻瓜式模糊测试
这里逻辑就是直接访问HEVD,并且疯狂填充数据。在填充之前把数据进行记录。当然我们只记录长度。核心就是通过while构造无限循环,并且不停对一组堆内存进行加0x10操作,不断增大并且直接传入给HEVD进行处理。先定义IO_CODE。
图11:IO_CODE
并且直接进入入口代码,很简单。
图12:测试代码
这段测试代码并不完美,但是能够测试一些简单很low的栈溢出等等。首先就是连接设备对象,并且申请一个以MutatorLen为长度的堆,并且重新置0,并且全填充为A。接着过直接通过DecviceIoControl传给HEVD即可。后面随便加入日志记录。
最终:
图13:BugCheck
日志记录:
图12:日志记录
至于漏洞原因很简单栈溢出,所以这里不演示动态分析了,大家可以自己Wibdbg配置双机调试,调试一波。
漏洞触发点:
图13:触发点
Windows7可以直接修改EIP跳转到用户空间直接执行shellcode实现修改EPROCESS,因为Windows7内核可以执行用户空间的代码(指的是空间)
图14:Exp核心代码
通过执行以下shellcode即可。
图15:shellcode
通过修改EPROCESS内部的Token,将exp的token修改为system的token即可实现踢权
当然Windows10也可以实现做到这种提权,但是从windows8加入SMEP机制后很多漏洞变得无法复现。因为这个机制会限制内核空间执行用户空间上下文,一般需要修改特定寄存器而且Windows10更新的东西太多了以致于很多shellcode offset等等机制都被改变,所以目前Windows10最佳的提权漏洞是任意地址读写。
3.2、ioctl-bf
(这个框架属于动态测试,是我3.1概念的升级版可以自定义ioctl_code 还有驱动对象名称也是)
IOCTLbf只是一个小工具(概念证明),可以通过执行两项任务来搜索Windows内核驱动程序中的漏洞:
n扫描驱动程序支持的有效IOCTL代码,
n基于代际的IOCTL模糊
该工具的一个优点是它不依赖捕获的IOCTL。因此,它能够检测到驱动程序支持的有效IOCTL代码,这些代码不经常甚至从未被来自用户土地的应用程序使用。
例如,情况可能如下:
nIOCTL在非常特定的条件下调用(不容易发现和/或复制)。
n用于调试目的的IOCTL有时会被放在驱动程序中。
一旦扫描完成并找到给定驱动程序的有效IOCTL,用户可以在列表中选择一个IOCTL来开始模糊过程。请注意,此工具仅执行基于生成的模糊。与基于突变的模糊(包括使用有效的IOCTL缓冲区和添加异常)相比,代码覆盖当然不那么重要。
具体使用:
1、首先,有必要找到目标司机。可以使用DriverView(http://www.nirsoft.net/utils/driverview.html)这样的工具来轻松识别非微软驱动程序(第三方驱动程序)。
2、然后,有必要检查与目标驱动程序关联的设备。例如,做到这一点的好工具是DeviceTree(http://www.osronline.com/article.cfm?文章=97)
3、检查设备的安全属性(DACL)。它应该提供给有限的用户,以便从攻击者的角度使其变得有趣。事实上,驱动程序的漏洞可能导致系统上的本地特权升级,或者在服务不可利用时只是拒绝服务。
4、检索应用程序用于与目标驱动程序的一台设备通信的符号链接。所有符号链接都可以在“全球?”中与Sysinternal的工具WinObj一起列出。部分(http://technet.microsoft.com/zh-us/sysinternals/bb896657)。
5、最后,有必要知道目标驱动程序支持的至少一个有效的IOCTL代码。例如,可以使用OSR的IrpTracker实用程序(http://www.osronline.com/article.cfm?)等工具监控IRP,可以轻松完成。article=199)。确保仅在“DEVICE_CONTROL”上应用过滤器,并仅选择目标驱动程序。当然,也可以通过逆向工程驱动程序直接检索有效的IOCTL代码。
6、检索到有效的IOCTL代码后,可以使用ioctlbf。可以选择以下IOCTL代码扫描模式之一:
n函数代码+传输类型bruteforce
nIOCTL代码范围
n单个IOCTL代码扫描过程返回每个IOCTL代码和可接受的缓冲区大小。
7.下一步只是选择一个IOCTL来模糊。模糊过程实际上遵循以下步骤:
n[如果方法!= METHOD_BUFFERED]输入/输出缓冲区的地址无效
n检查是否有微不足道的内核溢出
n模糊与预定的DWORD(无效地址,指向长ascii/unicode字符串的地址,指向无效地址表的地址)。
n模糊不全随机的数据
3.3、wtf
(Wtf是0vercl0k开发的模糊测试框架,支持分布式、快照式、可定制、跨平台的模糊测试,可以很完美的测出Windows内核的漏洞。而且定制化功能极其方便。)
fuzz或wtf是一个分布式、代码覆盖引导、可定制、跨平台快照的模糊器,旨在攻击在Microsoft Windows上运行的用户和/或内核模式目标。目标的执行可以在具有bochscpu(最慢、最精确)的模拟器内完成,在具有Windows Hypervisor平台API的Windows VM中执行,也可以在具有KVM API的Linux虚拟机内部完成(最快)。
图16:Wtf的主程序
具体使用不多说,这个框架通过对不同的异常触发的方式(比如:异常处理函数指针),通过对这些指针进行断点操作(因为运行环境相当于一个调试环境)如果检测到了触发断点那么等于发生了Crash事件,那么会记录下来。
图17:KeBugCheck2事件
通过对KeBugCheck2 (蓝屏处理)进行断点如果程序检测到了断点触发那么会判定成功触发异常。
因为该框架属于快照式,不妨看一下Mutatot是如何实现传给HEVD的。可以看看hevd_client
图18:HEVD_CLIENT
可以看到HEVD_CLENT在调用DeviceIoControl前进行断点操作了,其实本质上快照就是从调用这个函数之前(CALL前)导出,然后可以看看InsertTestcase函数
图19:InsertTestcase函数
看函数内容,发现是修改r8 rdx r9根据调用约定规则可以看出,就是修改设备控制的参数,并且将完整的数据传入堆栈(Rsp+5*sizeof(unint64_t))偏移上,也就是输入数据里。然后继续运行即可。
四、总结
Ioctl其实非常简单,如果想挖更多二进制安全漏洞,那么需要储备更多fuzz知识(单方面),目前还有很多测试框架可供使用。本文不做多的结尾了。但是还是祝:祝大家0day多多,SRC每天榜一