セッション2つの64ビットカーネル開発。カーネルプログラミングの考慮事項のほか、UNICODE_STRING

ドライバが動作しているかディアン

1.サービス登録ドライブ

私たちはドライブを作成します。実行されて駆動する方法を知って必ず
私たちはドライブをインストールするときに、それがサービスを作成する最初の。(レジストリ)


ます。HKEY_LOCAL_MACHINE \ System \ CurrentControlSet \ Servicesの\
SrvNmae 最後のものは、あなたのドライブの名前です。
次のように:

ありStartTypeそれはに従ったものである起動するGroupOrder順が

StartType、4 - 0の合計の代わりに以前の開始値が小さいです

数の 開始時期 説明
0 システムローダは、コアをロードします システムは、ブート時の優先順位にロードされます。
1 IOサブシステムの負荷 当社のコアドライバをロードする前に少し後にロードされた。完成ロード
2 自動スタート ログイン画面には、ロードを開始するために表示された場合。コンピュータは、ドライバがデスクトップシステムにログオンしていないロードされます。
3 手動スタート ロードしない再起動後3に設定した場合、あなたはあなた自身の時間をリロードする必要があります。
4 始めることはありません 我々のドライバーに代わって。例えば、以下の4缶の開始値を設定してロードされません

私たちが呼ぶとき値スタートは、ドライブリング3セットにロードされたCreateServiceの伝送パラメータを持つドライバのインストールが、あるときにスタートが次のパラメータ値を:

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 \コントロール\ GroupOrderList

この値は、早く起動するドライブのフロント..です
以下のとおりです。

どのように合わせ
スタートが見える、その後0ビット場合GroupOrder誰上に負荷を。
値0.5を起動し、他は0最初のスタートの値です。

2.オブジェクトマネージャは、従動生成します

それは私たちのサービスは、レジストリに配置されます。しかし、我々のドライバーをロードするように書かれているか、それを実行する方法と言います。

オブジェクトマネージャは、ドライブオブジェクト(DriverObject)を生成し、DriverEntry()。DriverEntry()エントリポイントを実行に渡します。

前記制御対象機器作成
制御装置(操作用リング)シンボリックリンクをトランセンド4.
5.結合フィルタドライバを
、我々はフィルタドライバを持っている場合。フィルタデバイスオブジェクトが作成される。そして、バインド。
6.分布関数レジスタ
7。初期化動作が完了します。
8.デバイスオブジェクトは、アプリケーションのシンボリックリンクで開かれた。そしてIRPを経由して要求を読み書き送ります。

ディアンリング3リング0と通信するための2つの方法が

1.IOCTRL_CODE複数のIO制御コード

1.METHOD_BUFFERED通信

METHOD_BUFFEREDは、
コアが、我々は、読み取りおよび書き込みモードを指定する必要が前記コア層に制御コードリング3私たちを転送することができます。
従います:

#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はその後、我々のデータは、SystemBufferのIRPヘッダでカプセル化されます。

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_STRINGANSI_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|资源不足|
这些状态吗都在 <ntstatus.h>中定义的.

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级别

在使用函数的时候.应该查询WDK文档.看看级别.

五丶驱动函数的分类

在驱动中有一些函数前缀都是固定的

如:
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ツールと同様に使用することができるが
チェックされます。

おすすめ

転載: www.cnblogs.com/iBinary/p/10990667.html