目录
- 一丶驱动是如何运行的
- 1.服务注册驱动
- 二丶Ring3跟Ring0通讯的几种方式
- 1.IOCTRL_CODE 控制代码的几种IO
- 2.非控制 缓冲区的三种方式.
- 三丶Ring3跟Ring0开发区别
- 1.什么是Ring3 什么是Ring0
- 四丶IRQL中断级别
- 五丶驱动函数的分类
- 六丶编写内核中的注意事项
一丶驱动是如何运行的
1.服务注册驱动
我们编写驱动.一定要知道驱动是如何运行的
首先在我们安装一个驱动的时候,会创建一个服务.(注册表)
在
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\SrvNmae
最后一个是是你驱动的名字.
如下:
里面有一个StartType 它是按照GroupOrder顺序来启动的
StartType值越小.代表越早启动.总共是0 - 4
数值 | 启动时机 | 说明 |
---|---|---|
0 | 系统核心装载器装载 | 系统在启动的时候优先加载. |
1 | IO子系统装载 | 稍微晚一些加载.加载完核心驱动才加载我们的 |
2 | 自动启动 | 在登录界面出现的时候开始加载.电脑好驱动还没加载也会登录到桌面系统中. |
3 | 手工启动 | 如果设置为3.重启之后不会再加载,你需要自己重新加载一次 |
4 | 禁止启动 | 代表我们驱动不会加载.比如设置start值小于4才可以 |
其中设置Start的值是在我们3环加载驱动的时候设置的 调用 CreateService安装驱动的时候,传递的参数值.其中有个参数就是Start.如下:
SC_HANDLE CreateServiceA(
SC_HANDLE hSCManager,
LPCSTR lpServiceName,
LPCSTR lpDisplayName,
DWORD dwDesiredAccess,
DWORD dwServiceType,
DWORD dwStartType, 这个值设置
DWORD dwErrorControl,
LPCSTR lpBinaryPathName,
LPCSTR lpLoadOrderGroup, GroupOrder注意这个值
LPDWORD lpdwTagId,
LPCSTR lpDependencies,
LPCSTR lpServiceStartName,
LPCSTR lpPassword
);
关于如何使用代码加载我们的驱动.前边也有说过.请参考前面资料.
https://www.cnblogs.com/iBinary/p/8280912.html
GroupOrder值
这个值是在注册表
HEEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GroupOrderList
这个值是越靠前的驱动.启动越早.
如下:
怎么结合的.
如果Start 值位0那么就看 GroupOrder 那个在上边就加载谁.
Start值为0. 另一个为1 则值为0的先启动.
2.对象管理器生成驱动对象
上面说了我们的服务会放在注册表中. 但是我们的写的驱动是怎么加载或执行的那.
对象管理器生成驱动对象 (DriverObject)并且把它传递给DriverEntry(). 执行 DriverEntry()入口函数.
3.创建控制设备对象
4.创见控制设备符号链接(Ring可以操作的)
5.绑定过滤驱动
如果我们有过滤驱动.则会创建过滤设备对象.并且进行绑定.
6.注册分发函数
7.完成初始化动作.
8.应用程序通过符号链接打开设备对象.并且通过IRP发送读写请求.
二丶Ring3跟Ring0通讯的几种方式
1.IOCTRL_CODE 控制代码的几种IO
1.METHOD_BUFFERED 通讯方式
METHOD_BUFFERED
在我们内核中 Ring3可以传递控制码给内核层.其中需要指明我们的读写方式
如下:
#define IOCTRL_BASE 0x800
#define MYIOCTRL_CODE(i)\
CTL_CODE(FILE_DEVICE_UNKNOWN,IOCTRL_BASE+i,METHOD_BUFFERED,FILE_ANY_ACCESS)
4个参数:
参数1: 驱动的类型
参数2: 驱动的控制码.
参数3: 以哪种缓冲方式进行通讯
参数4: 权限
其中我们这里说的就是参数3.指定什么方式进行通讯.
ME_THOD_BUFFERED 则我们的数据都会会封装在IRP头部的SystemBuffer中.
pIrp->AssociatedIrp.SystemBuffer;
2.METHOD_IN_DIRECT 只读缓冲 METHOD_OUT_DIRECT 只写缓冲
如果我们的CTRL_CODE指定的是这两种的其中一种.看如下解释
METHOD_IN_DIRECT
只读缓冲的方式.则我们的缓冲区还是会封装到IRP头部的SystemBuffer缓冲区.
IN pIrp->AssociatedIrp.SystemBuffer;
如上.我们 ring3 输入的数据都会放在这个SystemBuffer中.
METHOD_OUT_DIRECT
只写缓存
如果是这种方式.则我们的数据也会封装到IRP头部.但是会设置的 IRP
头部MdlAddress中.我们输出的数据都要放在MDL中.
如下:
OUT PVOID pOutBuffer = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress,NormalPagePriority);
MDL是 3环虚拟地址,映射到内核中的物理地址. 我们不能直接使用.
比如通过MmGetSystemAddressForMdlSafe这个函数.将映射的物理地址. 转换为内核中的 "虚拟地址" 可以这样理解. 然后对这个内存进入输出即可.我们Ring3则可以接受到数据.
区别:
如果是只读权限打开设备的时候.METHOD_IN_DIRECT则会成功.METHOD_OUT_DIRECT则会失败.
如果是读写的方式.则都会成功.
3.METHOD_NEITHER 其它方式
使用其它方式.则我们Ring3发送过来的数据 会在IRP堆栈中
我们获取Ring3的数据
PIRPSTACK_LOCATIO pIrpStack;
pIrpStack = IoGetCurrentIrpStackLocation(pIrp);
pIrpStack->Parameters.DeviceIoControl.Type3InputBuffer
则我们的输出的数据要放在 IRP头部的UserBuffer中传递给三环
pIrp->UserBuffer;
2.非控制 缓冲区的三种方式.
我们上面说了.控制派遣函数可以传递不同的缓冲区方式.进而在内核中.
进行不同的 取缓冲区 写缓冲区的操作.那么如果不是控制.则会有3种方式.
1.缓冲IO DO_BUFFERED_IO
当我们创建完毕设备对象的时候.可以给设备对象的 Flags字段设置一个缓冲区的方式..分别有三种.
如我们设置 缓冲区读取.
pDevice->Flags |= DO_BUFFERED_IO;
如果是缓冲区方式.则 我们Ring3发送的数据就会封装到
IRP头部的SystemBuffer中.
也就是说说我们只需要取出 IRP头部的SystemBuffer就可以了.
缓冲IO的意思就是 3环 发送数据到0环. 0环开辟一个空间.用来接收.
这种方式很安全.但是效率差.如果一次发送很多字节.不建议使用这种方式.
因为你进入了内核.那么内核空间就是共享了.如果你在创建这种很多字节的缓冲区.那么就让原本已经很紧张的内核空间负担更重.而且如果内存来不及释放.则会永久占据.除非你重启电脑.
2.DO_DIRECT_IO 直接IO
直接IO的方式就是 将ring3数据所在的虚拟地址,映射到内核空间中.
内核进行读取.这种方式效率快.一般内核厂家都是这种.
听到映射.大家应该知道数据怎么传递了.
如果使用这种方式.那么数据 会在IRP头部的MdlAddress中.
我们取出来就是用 "API"进行获取即可.
这里的API指的是使用内核API.不是ring3的.注意
PVOID pBuffer = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress,NormalPagePriority);
3.其它IO方式.
如果是其它IO方式.
我们输入的数据则会在 IRP堆栈中的DeviceContrl字段中Type3InputBuffer中
pIrpStack->DeviceControl.Type3InputBuffer中.
输出的数据则会在 IRP的头部中的UserBuffer中
PIrp->UserBuffer
使用这种方式很危险.这种方式是内核直接读取ring3虚拟地址数据.
必须保证ring3进程跟内核进程处于同一运行状态中.
对此我们对其内存必须进行检查.
有两个API函数
ProbeForRead(); 检查内存是否可读
ProbeForWrite(); 检查内存是否可写.
三丶Ring3跟Ring0开发区别
1.什么是Ring3 什么是Ring0
CPU提供了4层. 而微软只用了2层.
分别是Ring0到Rign3.
而微软只用Ring0. 因为这个设计所以我们写的驱动才能跟操作系统的权限一样.这样做也是因为不过与依赖Cpu的设计.否则以后CPU架构一改.操作系统就废了.
X64系统
在X64系统中.CPU厂商因为操作系统没有使用. 所以CPU直接把
RING1 RING2给去掉了.
所以在X64下,只剩下ring0 跟 ring3了.
虚拟技术 (VT技术)
虚拟技术有三种方式 0 1 3
内核运行在0层. 虚拟机运行在1层 应用程序运行在3层.
VT模式: 虚拟机运行在 -1级别. 根模式. -1就是在0环旁边加了一个新的模式. -1级别就是权限很大的.比内核权限更大.
2.RING3 与 Ring0开发的区别
在内核中
printf scanf fopen fclose fwrite fread malloc free
都不可以使用.
但是与内存相关与字符串相关的可以使用
strcmp strcpy wcslen memset
但是不建议使用.在内核中有专门的操作函数.
这种函数是Rtl开头.
如:
RtlStringcbCatA /W 字符串拼接
RtlStringcbCopy();字符串拷贝
RtlStringcbLength();求长度
RtlStringcbPrnt(); 字符串打印
如果是UNICODE_STRING则如下
RtlStringcbCopyUnicodeString();
RtlUnicodeStringCat();
在内核中我们的字符串数据结构有得新的数据类型
UNICODE_STRING 跟 ANSI_STRING
其实就是一个结构体.
如下:
typedef struct _STRING {
USHORT Length;
USHORT MaximumLength;
PCHAR Buffer;
} ANSI_STRING, *PANSI_STRING;
记录了长度.最大长度.以及一个缓冲区. UNICODE_STRING也一样.
对此针对这个结构体有了新的初始化 函数
ANSI_STRING string;
RtlInitAnsiString(string,"HelloWorld"");
UNICODE则是
RtlInitUnicodeString(string,L"Hello");
3.返回值的判断
在内核中使用函数.则会返回一个状态值.
如:
ntStatuse =IoCreateDevice();
需要使用宏判断
if (!NT_SUCESS(ntStatus))
xxxx
常见的几个状态值
|状态|含义|
|---|---||
|STATUS_SUCCESS|代表此次调用成功|
|STATUS_UNSUCCESSFUL|代表失败|
|STATUS_ACCESS_DENIED|代表访问拒绝|
|STATUS_INSUFFICIENT_RESOURCES|资源不足|
这些状态吗都在
4.内存的使用与申请
在内核中申请内存跟ring3不同.
内核中申请内存使用
PVOID
ExAllocatePoolWithTag(
IN POOL_TYPE PoolType, 申请内存的类型.内存是分页还是不可以
IN SIZE_T NumberOfBytes, 申请的字节
IN ULONG Tag 4个字节的内存标识,随便写.
);
其中参数1需要你指定类型.
分页内存.就是内存可以交换到磁盘使用.
非分页内存.就是内存不能交换.就是固定的.不能变.但是非分页内存很宝贵.只有100-200MB.给我们操作系统使用.所以建议使用分页内存.
四丶IRQL中断级别
这一讲我很多博客也说过了.就是说我们调用的 内核函数都有级别一说.
如下表:
了解:
编码 | 级别 | 说明 |
---|---|---|
PASSIVE_LEVE | 无中断 | 常规线程执行 |
APC_LEVEL | 软中断 | 异步过程调用执行 |
DISPATC_LEVEL | 软中断 | 线程调度延时过程调用执行 |
DIRQL | 硬中断 | 设备中断请求级处理程序执行 |
PROFILE_LEVEL | 硬中断 | 配置文件定时器 |
CLOCK2_LEVEL | 硬中断 | 时钟 |
SYNCH_LEVEL | 硬中断 | 同步级 |
IPI_LEVE | 硬中断 | 处理器之间的中断级 |
POWER_LEVEL | 硬中断 | 电源故障级别 |
除了硬中断是最高的. 我们只看软中断.
PASSIVER_LEVE APC_LEVEL DISPATCH_LEVEL 级别. 分别是 0 1 2
在软中断中Dispath级别最高.
如我们编写内核的时候给的派遣函数.以及入口点函数.
可以如下图:
调用源函数 | 级别 |
---|---|
DriverEntry,DriverUnLoad0 | Passive级别 |
各种分发函数 | Passiver级别 |
完成函数 | Dispatch级别 |
各种NDIS回调函数 | Dispatch级别 |
五丶驱动函数的分类
在驱动中有一些函数前缀都是固定的
如:
Ex开头的.
函数 | 函数说明 |
---|---|
ExAllocatePoolWithTag | 分配内存 |
ExAcquireFastMutex | 获取快速互斥锁 |
ExGetPreviousMode | 获取前一个请求者的运行模式. 判断来自Ring0还是Ring3,拦截ring3.过滤ring0 |
Io开头的 属于Io管理器的
函数 | 函数说明 |
---|---|
IoCreateDevice | 创建设备对象 |
IoCreateSymbolicLink | 创建符号链接 |
IoGetCurrentIrpStackLocation | 获取Irp堆栈 |
IoAttachDeviceToDeviceStack | 设备绑定,自己生成的设备绑定到别人的设备上,做过滤用的. |
IoAllocateIrp | 自己分配一个IRP. |
IoSetCompletionRoutine | 为IRP设置完成例程的. |
Ke开头的
函数 | 函数说明 |
---|---|
KeWaitForSingleObject | 等待事件发生.做同步用的. |
KeSetEvent | 设置事件信号 |
KeInitializeEvent | 初始化一个事件对象. |
Mm开头的. 与Memory相关的.
函数 | 函数说明 |
---|---|
MmGetSystemRoutineAddress | 内核中获取函数的内存地址. 跟ring3 GetProcAddress相似.一个ring3一个ring0 |
MmIsAddressValid | 判断函数地址是否无效. |
Ob开头 与内核对象相关的.
函数 | 函数说明 |
---|---|
ObReferenceObjectByHandle | 把一个内核对象的Handle转化成内核它的内核对象. 句柄不能夸进程.所以转换为内核对象. |
ObQueryNameString | 查询名字跟路径.可能会死锁 |
Ps开头的. 与进程相关的.
函数 | 函数说明 |
---|---|
PsGetCurrentProcess | 获取当前进程的EPROCESS未导出的结构 |
PsGetCurrentProcessId | 获取当前进程Pid |
PsCreateSystemThead | 在内核中创建线程的 |
PsLookUpProcessByProcessId | 进程进程PID获取这个PID的EPROCESS结构 |
Rtl开头的 一组重写的函数.可以操作内存跟字符串
函数 | 函数说明 |
---|---|
RtlZeroMemory | 对一块内存清空位0.跟memset一样. |
RtlInitUnicodeString | 初始化UNICODE字符串 |
Zw开头. 系统服务的. 文件系统.注册表.
函数 | 函数说明 |
---|---|
ZwOpenKey | 打开注册表键 |
ZwCreateFile | 创建文件或打开一个文件 |
ZwOpenProcess | 打开一个进程 |
ZwQuerySystemInformation | 遍历进程的一些信息 |
Flt开头的. 文件过滤相关的一组函数 (minfilter)
Ndis 防火墙中用的一些函数
六丶编写内核中的注意事项
1 不要使用 MmIsAddressValid函数.这个函数对于校验内存没有意义
这个函数只能判断一个字节地址的有效性
if (MmisAddressValid(Buffer))
{
memcmp(BUffer,BufferTwo,Length);
}
它只判断地址字节的第一个地址.只要你的地址在这个分页.那么可以.
但是就怕分页.后面分页不对就会出错.
他还会对 Page Out不能准确的判断. 所以攻击者可以利用你的判断.来绕过你的保护.
2.保证我们的代码在 tye _except中完成.否则蓝屏.
编写驱动代码一定要注意不要产生异常.否则就会蓝屏.
如:
try
{
ProbeFroRead(Buffer,len,alig);
if (memcpy(Buffer,buffer2,len){};//这行出错就会在except.
}
_except(EXECUTE_HANDLER_EXCEPTION)
{
//如果出错就会在这.
}
3.注意长度为0的缓存. 以及为NULL的缓存指针与缓存对其
缓存长度为0
ProbeForread跟Write. 如果我们Buffer长度穿的为0.这两个函数是不工作的.很容易被别人攻击.所以要小心len为0的情况.
如下漏洞代码:
try
{
ProbeForRead(Buffer1,len,sizeof(char));
if (strcmp(Buffer1,Buffer2,len){}
}_except(EXECUTE_HANDLER_EXCEPTION)
{
xxx
}
上面的代码会产生问题.因为当ProbeForRead的时候.长度传递为0
则这个函数不工作.但是我们的strcmp至少会访问一个字节.这样就造成了崩溃蓝屏. 绕过你的保护.所以最好使用Rtl之类的函数操作.
缓存指针如空
不要使用下面的代码
if (userBuffer == NULL){xxx};
windows操作系统运行用户态申请的一个地址为0的内存. 攻击者可以以它来绕过检查过保护.
在我们以前讲调用们的时候也说过. ring3可以使用0内存.
在Windows8以后内存不能申请为NULL.
缓存对齐
ProbeForRead(Address,length,Alignm);
在函数的第三个参数是对其. 默认是按照1对齐.如果使用 Sizeof(ULONG) 也会出问题.导致过保护.
4.注意不正确的内核调用引发的问题
如函数:
ObReferenceObjectByHandle();
如果使用这个函数.不指定类型.任然可以获得对应的对象地址.但是如果你直接访问这个对象.就会引发漏洞.
如:
HookZwSetInformationFile();
ObReferenceObjectByHandle(FileHandle,Access,NULL);
//ObReferenceObjectByHandle(FileHandle,Access,&Fileobject);
if (wscnicmp(fileObject->FileName){}
如上,参数如果传递为NULL. 攻击者可以传入非文件类型的句柄.如果你没有校验.就会导致悲剧.所以使用必须给指定对象类型.会影响第一个参数.
第一个参数攻击者可以传入任何Handle.
这就是拒绝服务攻击.一句话你就蓝屏.
不正确的Zw函数使用
使用Zw函数的时候.不能将用户态的内存给它. 因为Zw函数不会进行校验.
就算你进行了校验.传递这样的内存给系统也可以引发崩溃. 比如内存也在调用的时候突然无效. 就算你进行异常驳货.也可以造成内存泄漏.对象泄漏.甚至权限提升等问题.
不要下发内核对象给内核
我们Ring3的内核对象.不要通过 DeviceControl 进行传递.
如果这样写.很可能让攻击者可以做到任意地址写入.提升权限.
5.给驱动提供的功能性接口必须小心
如果对注册表 文件 内核内存.进程线程等操作的功能性接口.一定要非常小心才可以.禁止一切受信进程的调用. 不然你暴露接口就会被利用.
6.数据传输尽量使用 BUFFERED_IO 缓存的方式.
我们内核中的最好使用缓冲IO.也就是说使用SystemBuffer.如果不使用BUFFERED_IO而使用UserBuffer一定注意使用 Pro等检查函数.
7.发布的驱动必须通过内核校验
微软提供的驱动校验工具: verifier
在CMD命令中输入即可.打开后界面如下:
使用的时候
创建自定义设置(供程序开发人员使用) -> 从一个完整列表选择单个设置
->出来很多检查. -> 从一个列表中选择驱动程序 ->有的话你选择.没有的话你自动选择一个.选中之后则会重启.自动进行检测. 如果出错.就会蓝屏.
队友挂钩内核函数的驱动. 还可以使用 BSOD HOOK 一类的Fuzz工具
来进行检查.