ring3和ring0下有常用三种通信方式:
1.缓冲区通信方式
2.直接IO通信方式
3.其它通信方式
我们的ring3和ring0通讯的时候.ring3会给一个虚拟地址. 然后内核中的参数会通过IRP来获取.
其中有个缓冲区. 我们只要操作这个缓冲区.那么对应的就是操作了三环的缓冲区.
例如:
当我们三环和0环通信的时候, 3环如果选择的是缓冲区通信. 那么久类似于上图. 操作系统会在高2G申请一个额外的缓冲区.
然后ring3下的缓冲区拷贝到里面. 然后我们的内核程序操作这个缓冲区之后. 操作系统将这个缓冲区的数据重新写入到ring3下的虚拟缓冲区中.
优点:
优点是安全.操作系统会创建一个中间层的缓冲区让我们进行操作.然后操作中间层就相当于操作ring3的缓冲区.
缺点:
高2G内核中的内存是很宝贵的.如果我们交互的时候.传出的数据太大.那么就会消耗计算机内存资源.
如果想看完整流程图,请查看WDK的帮助文档.
为什么叫做直接通信方式. 原因是 ring3可以直接和ring0进行通讯了.不需要额外的缓冲区进行操作.
ring3的虚拟内存会通过内存映射的方式.映射到高2G的内存. 然后ring3的虚拟内存进行保护. 这样我们操作ring0的高2G缓冲区.就相当于操作ring3的缓冲区.
优点:
如果数据量比较大.可以使用这种
缺点:
不安全.如果我们的ring3不进行保护.那么通过C语言进行对ring3缓冲区越界访问.那么就相当于访问ring0的物理内存了.
不过幸好.内核中提供了宏让我们自己进行操作.
lpBuff = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, NormalPagePriority);
需要注意的是我们这种通信方式获取的缓冲区不是 IRP中的 SystemBuf;
其它通信方式,这是直接使用用户的虚拟内存,也就是IRP中的 userBuf;
以前我们操作设备的时候. 都是通过Read或者Write去操作.
但是现在我们有控制了.
这个时候我们要控制设备,就要编写控制码.
控制码:
CTL_CODE(FILE_DEVICE_UNKNOWN, (CODE_BASE+(code)), METHOD_BUFFERED, FILE_ANY_ACCESS)
VC++6.0给了一个宏.而NTDDK.h中也有这个. 如果你配置好了环境,那么你就要用VC中提供的了.
控制码的格式:
设备类型,控制码,通讯方式. 权限.
如果用我们的上面的宏,则填写即可.内部会自己进行移位运算.
完整代码:
PIO_STACK_LOCATION pIrpStack = NULL;
PVOID lpBuff = NULL;
ULONG IoControlCode; //获取控制的控制码
ULONG InputBufferLength; //用户输入的缓冲区,这个缓冲区一般做参数
ULONG OutputBufferLength; //ring0返回出去的缓冲区.一般做写出.
NTSTATUS status = STATUS_UNSUCCESSFUL;//各种变量.暂时不用管.
ULONG bytes = 0;
KdBreakPoint();
KdPrint(("[FirstWDK] DispatchControl PID:%d TID:%d\n", PsGetCurrentProcessId(), PsGetCurrentThreadId()));
pIrpStack = IoGetCurrentIrpStackLocation(pIrp);
InputBufferLength = pIrpStack->Parameters.DeviceIoControl.InputBufferLength;//获取长度
OutputBufferLength = pIrpStack->Parameters.DeviceIoControl.OutputBufferLength;//获取输出缓冲区
IoControlCode = pIrpStack->Parameters.DeviceIoControl.IoControlCode;//获取控制码
lpBuff = pIrp->AssociatedIrp.SystemBuffer;
switch(IoControlCode)
{
case MYCTL_GET_GDT_SIZE: //自定义的控制码
{
break;
}
case MYCTL_GET_GDT:
{
PCONTORL_PARAMS pParams = (PCONTORL_PARAMS)lpBuff;
char szGDT[6];
if (InputBufferLength < sizeof(CONTORL_PARAMS))
break;
__asm
{
sgdt szGDT
}
KdPrint(("limit:%p GDT:%p\n", *(short*)szGDT, *(int*)(szGDT + 2)));
if (OutputBufferLength >= sizeof(szGDT))
{
RtlCopyMemory(lpBuff, szGDT, sizeof(szGDT));
bytes = sizeof(szGDT);
status = STATUS_SUCCESS;
}
break;
}
case MYCTL_GET_LDT:
{
break;
}
}
pIrp->IoStatus.Status = status;
pIrp->IoStatus.Information = bytes;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return status;
}
关于自定义的控制码.
这些本质来说就是我们的宏替换.
#define CODE_BASE 0x800
#define MY_CTL_CODE(code) CTL_CODE(FILE_DEVICE_UNKNOWN, (CODE_BASE+(code)), METHOD_BUFFERED, FILE_ANY_ACCESS)
#define MYCTL_GET_GDT MY_CTL_CODE(1)
#define MYCTL_GET_GDT_SIZE MY_CTL_CODE(2)
#define MYCTL_GET_LDT MY_CTL_CODE(3)
我们每次定义,都要写CTL_CODE.很麻烦.所以我们用心的宏替换一下即可.
PS:
当控制码为缓冲区方式,直接方式.以及其它方式的时候.我们分别从IRP中获取的参数缓冲区是不同的.
1.当我们的控制码给定的是缓冲区通信方式
如果是缓冲区通信方式,那么获得的就是IRP中的SystemBuf. 这个缓冲区可以当做ring3传递的参数.然后你往这个缓冲区写数据,则是传出的数据
2.当我们的控制码为直接IO的方式
如果是直接IO的方式.那么你要二选一. 如果你指定了用户的输入缓冲区为直接IO方式,那么对应的输出缓冲区则是缓冲区方式,
那么用户缓冲区的获得方式就是使用上面介绍直接方式的API进行获取了.
例如:
lpBuff = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, NormalPagePriority);
3.如果我们的控制码为其它方式
如果是其它方式,那么用户的缓冲区是 IRP中的UserBuf缓冲区.而输入缓冲区则是用IRP中的.Type3InputBuffer
缓冲区.