Delphi研究之驱动开发篇(七)--利用共享内存与用户模式

2018-10-31

上篇教程我们学习了通过Section在用户进程和内核驱动程序之间共享信息的方法,但是这种方法有一个缺点,就是驱动程序被硬性限制在具体进程的地址上下文中,即驱动程序所使用的虚拟地址位于此进程的地址空间中。我们在本例中使用的方法将没有这个缺点。对于驱动程序来说,这种方法更为自然些。
首先来看看驱动程序。

代码:unit SharingMemory;

interface

uses
nt_status, ntoskrnl, native, winioctl, fcall, macros;

function _DriverEntry(pDriverObject:PDRIVER_OBJECT; pusRegistryPath:PUNICODE_STRING): NTSTATUS; stdcall;

implementation

uses
seh;

var
g_pSharedMemory: PVOID;
g_pMdl: PVOID;
g_pUserAddress: PVOID;
g_fTimerStarted: boolean;
g_usDeviceName, g_usSymbolicLinkName: UNICODE_STRING;

{更新系统时间到共享内存}
procedure UpdateTime; stdcall;
var
SysTime:LARGE_INTEGER;
begin
KeQuerySystemTime(@SysTime);
ExSystemTimeToLocalTime(@SysTime, g_pSharedMemory);
end;

procedure TimerRoutine(pDeviceObject:PDEVICE_OBJECT; pContext:PVOID); stdcall;
begin
UpdateTime;
end;

{清理过程--释放资源}
procedure Cleanup(pDeviceObject:PDEVICE_OBJECT); stdcall;
begin
if g_fTimerStarted then
begin
IoStopTimer(pDeviceObject);
DbgPrint('SharingMemory: Timer stopped'#13#10);
end;

if (g_pUserAddress <> nil) and (g_pMdl <> nil) then
begin
MmUnmapLockedPages(g_pUserAddress, g_pMdl);
DbgPrint('SharingMemory: Memory at address %08X unmapped'#13#10,
g_pUserAddress);
g_pUserAddress := nil;
end;

if g_pMdl <> nil then
begin
IoFreeMdl(g_pMdl);
DbgPrint('SharingMemory: MDL at address %08X freed'#13#10,
g_pMdl);
g_pMdl := nil;
end;

if g_pSharedMemory <> nil then
begin
ExFreePool(g_pSharedMemory);
DbgPrint('SharingMemory: Memory at address %08X released'#13#10,
g_pSharedMemory);
g_pSharedMemory := nil;
end;
end;

function DispatchCleanup(pDeviceObject:PDEVICE_OBJECT; p_Irp:PIRP): NTSTATUS; stdcall;
begin
DbgPrint(#13#10'SharingMemory: Entering DispatchCleanup'#13#10);
Cleanup(pDeviceObject);

p_Irp^.IoStatus.Status := STATUS_SUCCESS;
p_Irp^.IoStatus.Information := 0;
IofCompleteRequest(p_Irp, IO_NO_INCREMENT);

DbgPrint('SharingMemory: Leaving DispatchCleanup'#13#10);
result := STATUS_SUCCESS;
end;

function DispatchCreateClose(p_DeviceObject:PDEVICE_OBJECT; p_Irp:PIRP): NTSTATUS; stdcall;
begin
p_Irp^.IoStatus.Status := STATUS_SUCCESS;
p_Irp^.IoStatus.Information := 0;

IofCompleteRequest(p_Irp, IO_NO_INCREMENT);

result := STATUS_SUCCESS;
end;

function DispatchControl(p_DeviceObject: PDEVICE_OBJECT; p_Irp:PIRP): NTSTATUS; stdcall;
label
SafePlace;
var
dwContext:DWORD;
psl:PIO_STACK_LOCATION;
IOCTL_GIVE_ME_YOUR_MEMORY: DWORD;
pSystemBuffer: PVOID;
begin
DbgPrint(#13#10'SharingMemory: Entering DispatchControl'#13#10);
IOCTL_GIVE_ME_YOUR_MEMORY := CTL_CODE(FILE_DEVICE_UNKNOWN,
$800, METHOD_BUFFERED,
FILE_READ_ACCESS);
p_Irp^.IoStatus.Status := STATUS_UNSUCCESSFUL;
p_Irp^.IoStatus.Information := 0;
psl := IoGetCurrentIrpStackLocation(p_Irp); {取IRP的stack location的指针}
if psl^.Parameters.DeviceIoControl.IoControlCode = IOCTL_GIVE_ME_YOUR_MEMORY then
begin
{是我们控制码就开始处理}
if psl^.Parameters.DeviceIoControl.OutputBufferLength >= sizeof(PVOID) then
begin
g_pSharedMemory := ExAllocatePool(NonPagedPool, PAGE_SIZE);
if g_pSharedMemory <> nil then
begin
DbgPrint('SharingMemory: %X bytes of nonpaged memory allocated at address %08X'#13#10,
PAGE_SIZE, g_pSharedMemory);
g_pMdl := IoAllocateMdl(g_pSharedMemory, PAGE_SIZE,
false, false, nil);
if g_pMdl <> nil then
begin
DbgPrint('SharingMemory: MDL allocated at address %08X'#13#10,
g_pMdl);
MmBuildMdlForNonPagedPool(g_pMdl);
{安装SEH}
asm
push offset DefaultExceptionHandler
push fs:[0]
mov fs:[0], esp

mov sseh.SafeEip, offset SafePlace
mov sseh.PrevEbp, ebp
mov sseh.PrevEsp, esp
end;
g_pUserAddress := MmMapLockedPagesSpecifyCache(g_pMdl,
UserMode, MmCached, nil, 0, NormalPagePriority);
if g_pUserAddress <> nil then
begin
DbgPrint('SharingMemory: Memory mapped into user space at address %08X'#13#10,
g_pUserAddress);
pSystemBuffer := p_Irp^.AssociatedIrp.SystemBuffer;
PVOID(pSystemBuffer^) := g_pUserAddress;
UpdateTime;
if IoInitializeTimer(p_DeviceObject, @TimerRoutine,
@dwContext) = STATUS_SUCCESS then
begin
IoStartTimer(p_DeviceObject);
g_fTimerStarted := true;
DbgPrint('SharingMemory: Timer started');
p_Irp^.IoStatus.Information := sizeof(PVOID);
p_Irp^.IoStatus.Status := STATUS_SUCCESS;
end;
end;
SafePlace: {发生异常安全退出的地方}
asm
pop fs:[0]
add esp, 4
end;
end;
end;
end else
begin
p_Irp^.IoStatus.Status := STATUS_BUFFER_TOO_SMALL;
end;
end else
begin
p_Irp^.IoStatus.Status := STATUS_INVALID_DEVICE_REQUEST;
end;
if p_Irp^.IoStatus.Status <> STATUS_SUCCESS then
begin
DbgPrint('SharingMemory: Something went wrong:'#13#10);
Cleanup(p_DeviceObject);
end;
IofCompleteRequest(p_Irp, IO_NO_INCREMENT);
DbgPrint('SharingMemory: Leaving DispatchControl'#13#10);
result := p_Irp^.IoStatus.Status;
end;

procedure DriverUnload(p_DriverObject:PDRIVER_OBJECT); stdcall;
begin
IoDeleteSymbolicLink(@g_usSymbolicLinkName);
IoDeleteDevice(p_DriverObject^.DeviceObject);
end;

{驱动程序进入点}
function _DriverEntry(pDriverObject:PDRIVER_OBJECT; pusRegistryPath:PUNICODE_STRING): NTSTATUS;
var
status: NTSTATUS;
pDeviceObject: TDeviceObject;
begin
status := STATUS_DEVICE_CONFIGURATION_ERROR;
g_pSharedMemory := nil;
g_pMdl := nil;
g_pUserAddress := nil;
g_fTimerStarted := false;

RtlInitUnicodeString(g_usDeviceName, '\Device\SharingMemory');
RtlInitUnicodeString(g_usSymbolicLinkName, '\DosDevices\SharingMemory');
if IoCreateDevice(pDriverObject, 0, @g_usDeviceName,
FILE_DEVICE_UNKNOWN, 0, true,
pDeviceObject) = STATUS_SUCCESS then
begin
if IoCreateSymbolicLink(@g_usSymbolicLinkName,
@g_usDeviceName) = STATUS_SUCCESS then
begin
pDriverObject^.MajorFunction[IRP_MJ_Create] := @DispatchCreateClose;
pDriverObject^.MajorFunction[IRP_MJ_CLEANUP] := @DispatchCleanup;
pDriverObject^.MajorFunction[IRP_MJ_CLOSE] := @DispatchCreateClose;
pDriverObject^.MajorFunction[IRP_MJ_DEVICE_CONTROL] := @DispatchControl;
pDriverObject^.DriverUnload := @DriverUnload;
status := STATUS_SUCCESS;
end else
begin
IoDeleteDevice(@pDeviceObject);
end;
end;
result := status;
end;

end.
DriverEntry函数:

代码:pDriverObject^.MajorFunction[IRP_MJ_Create] := @DispatchCreateClose;
pDriverObject^.MajorFunction[IRP_MJ_CLEANUP] := @DispatchCleanup;
pDriverObject^.MajorFunction[IRP_MJ_CLOSE] := @DispatchCreateClose;
pDriverObject^.MajorFunction[IRP_MJ_DEVICE_CONTROL] := @DispatchControl;
pDriverObject^.DriverUnload := @DriverUnload;
本例子的DriverEntry函数与以往的不同,除了通常的IRP_MJ_Create、IRP_MJ_CLOSE和IRP_MJ_DEVICE_CONTROL之外,这里还要处理IRP_MJ_CLEANUP。当用户模式代码调用了CloseHandle时,驱动程序开始发出IRP_MJ_CLEANUP,通知系统设备驱动将要关闭句柄。在此之后句柄真正关闭,驱动收到IRP_MJ_CLOSE。在本例中我们希望释放掉之前使用的资源,所以需要处理IRP_MJ_CLEANUP。

代码:g_pSharedMemory := ExAllocatePool(NonPagedPool, PAGE_SIZE);
if g_pSharedMemory <> nil then
begin
获得了控制代码IOCTL_GIVE_ME_YOUR_MEMORY,我们来从非分页内存中分配一个内存页。驱动应该将这部分内存映射到用户进程的地址空间中,在接收到请求的进程上下文中,即在我们的应用程序的地址空间中。使用非分页内存以及使用一个内存页的原因后面会介绍到。
ExAllocatePool返回系统空间中的地址,也就是说驱动程序是与当前上下文无关的。现在需要将这块内存映射到这个进程的地址空间中去,使之被共享。我们的驱动程序是单层的,所以对IRP_MJ_DEVICE_CONTROL的处理我们想放在我们应用程序的地址上下文中。在我们将分配的一个内存页映射到进程地址空间之前必须先分配MDL(Memory Descriptor List。)
MDL是一个结构体,用于描述一片内存区域中的物理内存页。其定义如下:

代码:PMDL = ^TMDL;
TMDL=packed record
Next: PMDL;
Size: CSHORT;
MdlFlags: CSHORT;
Process: PEPROCESS;
MappedSystemVa: PVOID;
StartVa: PVOID;
ByteCount: ULONG;
ByteOffset: ULONG;
end;


更准确地讲,MDL结构体是一个首部(header)。紧随首部之后的是许多物理页的页号(page frame number, PFN)。但是MDL所描述的内存区域在虚拟地址空间中是连续不间断的,而它们所占据的物理页所在的物理内存却可能是按任意的顺序排列。正是因为如此再加上页的数量较大,我们需要维护一个表来记录该内存区中所有的物理页。同时这也用在了直接内存访问(Direct Memory Access, DMA)中。在我们这里物理页总共就一个。

代码:g_pMdl := IoAllocateMdl(g_pSharedMemory, PAGE_SIZE,
false, false, nil);
函数IoAllocateMdl前两个参数定义了虚拟地址和内存区的大小,是建立MDL所必须的。如果MDL不与IRP相关联(我们这里正是这样),则第三个参数就为FALSE。第四个参数定义是否需要减少进程的份额,并只用于位于驱动程序链最上层的驱动程序或是单层的驱动程序(我们这里正是这样)。每一个进程都要获得一定份额的系统资源。当进程为自己分配资源时,这个份额就会减小。如果份额用完,就不能再为其分配相应的资源。我们可不想减少这个用于分配内存的进程份额,所以第四个参数设为FALSE。最后一个参数定义了一个非必要的指向IRP的指针,通过这个指针MDL可以与IRP关联。例如,对于直接I/O,I/O管理器为用户缓冲区建立MDL,并将其地址送至IRP.MdlAddress。我们弄这个MDL可不是用来搞I/O的,所以就没有什么IRP,最后一个参数也就设为NULL。
函数IoAllocateMdl为MDL分配内存并初始化首部。

代码:MmBuildMdlForNonPagedPool(g_pMdl);
MmBuildMdlForNonPagedPool填充物理页号并更新MDL首部的某些范围。
如果我们将要调用的函数MmMapLockedPagesSpecifyCache的参数AccessMode为UserMode且调用失败,系统会抛出一个异常(这是DDK公开说明的),这个异常我们能够处理,所以我们建立SEH-frame用于捕获异常。
MmMapLockedPagesSpecifyCache函数将MDL所描述的内存映射到我们应用程序的地址空间中。
MDL的第一个参数为描述所要映射的内存区域的MDL。第二个参数定义了是否要从用户模式下访问这块内存。第三个参数定义了这块内存被处理器缓存的方式。如果第四个参数为NULL,则系统会自己从用户空间中挑选虚拟地址。第五个参数定义了如果万一系统不能完成请求,是否要出现BSOD,但是这只用在第二个参数为KernelMode时。我们可不想让系统死掉,于是将这个参数赋值为FALSE。最后一个参数定义了成功调用MmMapLockedPagesSpecifyCache的重要性。
借助于MDL,在用户地址空间中只能映射锁定的内存,即位于非分页池中的内存(对于使用分页内存的所有情况我并不全都知道)。这是使用非分页内存的第一个理由。
映射的内存不能少于一页,所以我们需要完整的一个内存页,但是实际上总共只用其中的几个字节。

代码:g_pUserAddress := MmMapLockedPagesSpecifyCache(g_pMdl,
UserMode, MmCached, nil, 0, NormalPagePriority);
if g_pUserAddress <> nil then
begin
DbgPrint('SharingMemory: Memory mapped into user space at address %08X'#13#10,
g_pUserAddress);
pSystemBuffer := p_Irp^.AssociatedIrp.SystemBuffer;
PVOID(pSystemBuffer^) := g_pUserAddress;
MmMapLockedPagesSpecifyCache返回我们的内存页映射到用户空间中的地址。我们将这个地址传递到应用程序中。从这一刻起该内存页就成为共享的了,并且驱动程序对其的使用不依赖于当前的地址上下文,而用户进程也能以自己的地址来访问。
为了直观起见,函数UpdateTime将把当前系统时间放在我们的内存页中。

代码:{更新系统时间到共享内存}
procedure UpdateTime; stdcall;
var
SysTime:LARGE_INTEGER;
begin
KeQuerySystemTime(@SysTime);
ExSystemTimeToLocalTime(@SysTime, g_pSharedMemory);
end;
KeQuerySystemTime取得的是格林威治时间。再用ExSystemTimeToLocalTime将其转换为本地时间。

代码:if IoInitializeTimer(p_DeviceObject, @TimerRoutine,
@dwContext) = STATUS_SUCCESS then
Begin
IoInitializeTimer初始化内核Timer,Timer将与设备对象建立关联。DEVICE_OBJECT结构体中有一个Timer域,其中有指向IO_TIMER结构体的指针。函数IoInitializeTimer的第一个参数定义了Timer要和哪一个设备对象关联。第二个参数是一个指向系统启用Timer时要调用的函数的指针。TimerRoutine函数将调用UpdateTime,在我们的内存页中更新系统时间。TimerRoutine运行在IRQL = DISPATCH_LEVEL(DDK中有记载)。这就是我们使用非分页内存的第一个也是最主要的原因。IoInitializeTimer的最后一个参数是一个指向任意数据的指针。这个指针将被传递到TimerRoutine中。我们这里不需要指定这个值,所以只是随便虚构一个变量。

代码:IoStartTimer(p_DeviceObject);
g_fTimerStarted := true;
启动Timer。现在函数TimerRoutine大约每秒被调用一次。这个时间间隔是不能修改的。

代码:if p_Irp^.IoStatus.Status <> STATUS_SUCCESS then
begin
DbgPrint('SharingMemory: Something went wrong:'#13#10);
Cleanup(p_DeviceObject);
end;
如果上述各阶段有一个发生问题,就要收回资源。

代码:{清理过程--释放资源}
procedure Cleanup(pDeviceObject:PDEVICE_OBJECT); stdcall;
begin
if g_fTimerStarted then
begin
IoStopTimer(pDeviceObject);
DbgPrint('SharingMemory: Timer stopped'#13#10);
end;

if (g_pUserAddress <> nil) and (g_pMdl <> nil) then
begin
MmUnmapLockedPages(g_pUserAddress, g_pMdl);
DbgPrint('SharingMemory: Memory at address %08X unmapped'#13#10,
g_pUserAddress);
g_pUserAddress := nil;
end;

if g_pMdl <> nil then
begin
IoFreeMdl(g_pMdl);
DbgPrint('SharingMemory: MDL at address %08X freed'#13#10,
g_pMdl);
g_pMdl := nil;
end;

if g_pSharedMemory <> nil then
begin
ExFreePool(g_pSharedMemory);
DbgPrint('SharingMemory: Memory at address %08X released'#13#10,
g_pSharedMemory);
g_pSharedMemory := nil;
end;
end;

阅读44