Delphi研究之驱动开发篇(六)--利用Section与用户模式程序通讯(上)

2018-10-31

在进入主题之前,先来简单地看一下结构化异常处理(Structured Exception Handling, SEH),本篇的程序需要这个东东。
结构化异常处理
这里我并不打算详细讲结构化异常处理,关于SEH,在网上你能找到相关的内容,SHE能用于所有的异常处理,也就是说,SEH既能用于用户模式又能用于内核模式。但这两种模式下的异常处理有一个本质上的差别:
在内核模式下,借助于seh,并非所有的异常都能得到处理!比如说,即使使用了seh,用零作除数作除法也会使系统崩溃。最为可怕的是,引用未定义的内核内存也会导致蓝屏死机BSOD。而对未定义的用户模式内存的引用异常,seh却可以轻松处理。因此避免系统崩溃的唯一办法就是所编写的代码不要导致无法处理的异常。
以下是个使用结构化异常的例子:
代码:unit seh;

interface

uses
nt_status;

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

implementation

uses
ntoskrnl;

const
SEH_SafePlaceCounter = 0;
SEH_INSTALLED = 0;

type
_SEH = record
SafeEip: DWORD; { 线程继续执行的地方 }
PrevEsp: DWORD; { 以前esp的值 }
PrevEbp: DWORD; { 以前ebp的值 }
end;

var
sseh: _SEH;

function DefaultExceptionHandler(pExcept:PEXCEPTION_RECORD;
pFrame:DWORD;
pContext:PCONTEXT;
pDispatch:DWORD): DWORD; cdecl;
begin
DbgPrint(#13#10'SEH: An exception %08X has occured'#13#10,
pExcept^.ExceptionCode);
if pExcept^.ExceptionCode = $0C0000005 then
begin
{如果发生了EXCEPTION_ACCESS_VIOLATION类型的异常,}
{则输出以下信息.}
DbgPrint(' Access violation at address: %08X'#13#10,
pExcept^.ExceptionAddress);
if pExcept^.ExceptionInformation[0] <> nil then {试图读还是写?}
begin
DbgPrint(' The code tried to write to address %08X'#13#10#13#10,
DWORD(pExcept^.ExceptionInformation[4]));
end else
begin
DbgPrint(' The code tried to read from address %08X'#13#10#13#10,
DWORD(pExcept^.ExceptionInformation[4]));
end;
end;
asm
lea eax, sseh
push (_SEH PTR [eax]).SafeEip
push (_SEH PTR [eax]).PrevEsp
push (_SEH PTR [eax]).PrevEbp

mov eax, pContext
pop (CONTEXT PTR [eax]).regEbp
pop (CONTEXT PTR [eax]).regEsp
pop (CONTEXT PTR [eax]).regEip
end;
result := 0;
end;

procedure BuggyReader; assembler;
asm
xor eax, eax
mov eax, [eax] {!!! 没有SEH的话 - BSOD !!!}
end;

procedure BuggyWriter; assembler;
asm
mov eax, offset MmUserProbeAddress
mov eax, [eax]
mov eax, [eax]

mov byte ptr [eax], 0 {!!!没有SEH的话 - BSOD !!!}
end;

function _DriverEntry(pDriverObject:PDRIVER_OBJECT;
pusRegistryPath:PUNICODE_STRING): NTSTATUS; stdcall;
label
SafePlace;
begin
DbgPrint(#13#10'SEH: Entering DriverEntry'#13#10);
{ "手工"安装SEH }
asm
push offset DefaultExceptionHandler {我们的SEH程序}
push fs:[0]
mov fs:[0], esp

mov sseh.SafeEip, offset SafePlace {SafePlace是处理完异常后继续执行的地方}
mov sseh.PrevEbp, ebp
mov sseh.PrevEsp, esp
end;
BuggyReader;
BuggyWriter;

SafePlace:
asm
pop fs:[0]
add esp, 4
end;

DbgPrint(#13#10'SEH: Leaving DriverEntry'#10#13);
result := STATUS_DEVICE_CONFIGURATION_ERROR;
end;

end.
安装SHE-Frame
由于在内核模式下我们无法直接使用Delphi自身的异常处理机制,因为在驱动程序中我们要自己手工安装SHE,这里我们使用Delphi的BASM来做这件事情,了解SHE的朋友都知道,做这件事情是非常简单的。

代码:asm
push offset DefaultExceptionHandler {我们的SEH程序}
push fs:[0]
mov fs:[0], esp

mov sseh.SafeEip, offset SafePlace {SafePlace是处理完异常后继续执行的地方}
mov sseh.PrevEbp, ebp
mov sseh.PrevEsp, esp
end;

为了在异常处理之后我们的处理程序能恢复线程的执行,我们应该保存esp、ebp寄存器的内容以及线程继续执行的地址。我们将这三项信息保存在seh结构体中并调用函数BuggyReader。BuggyReader函数试图从地址00000000读取一个DWORD。

代码:procedure BuggyReader; assembler;
asm
xor eax, eax
mov eax, [eax] {!!! 没有SEH的话 - BSOD !!!}
end;
nil指针引用是一个十分常见的错误,微软在00000000-0000FFFFh划出了64k字节的内存区,并使此区域无法访问。访问此区域中的任何一个字节都会导致EXCEPTION_ACCESS_VIOLATION类型的异常。
异常处理
函数BuggyReader从地址00000000读取引发了异常,我们就进入了我们指定的处理程序。
代码:function DefaultExceptionHandler(pExcept:PEXCEPTION_RECORD;
pFrame:DWORD;
pContext:PCONTEXT;
pDispatch:DWORD): DWORD; cdecl;
begin
DbgPrint(#13#10'SEH: An exception %08X has occured'#13#10,
pExcept^.ExceptionCode);
if pExcept^.ExceptionCode = $0C0000005 then
begin
{如果发生了EXCEPTION_ACCESS_VIOLATION类型的异常,}
{则输出以下信息.}
DbgPrint(' Access violation at address: %08X'#13#10,
pExcept^.ExceptionAddress);
if pExcept^.ExceptionInformation[0] <> nil then {试图读还是写?}
begin
DbgPrint(' The code tried to write to address %08X'#13#10#13#10,
DWORD(pExcept^.ExceptionInformation[4]));
end else
begin
DbgPrint(' The code tried to read from address %08X'#13#10#13#10,
DWORD(pExcept^.ExceptionInformation[4]));
end;
end;
asm
lea eax, sseh
push (_SEH PTR [eax]).SafeEip
push (_SEH PTR [eax]).PrevEsp
push (_SEH PTR [eax]).PrevEbp

mov eax, pContext
pop (CONTEXT PTR [eax]).regEbp
pop (CONTEXT PTR [eax]).regEsp
pop (CONTEXT PTR [eax]).regEip
end;
result := 0;
end;

我们处理的第一件事就是输出相应的调试信息,如果发生的是EXCEPTION_ACCESS_VIOLATION类型的异常,还要输出一些额外的信息。之后开始真正的异常处理(这里了使用了BASM,你可以从中体会到BASM的强大功能)。
代码:asm
lea eax, sseh
push (_SEH PTR [eax]).SafeEip
push (_SEH PTR [eax]).PrevEsp
push (_SEH PTR [eax]).PrevEbp

mov eax, pContext
pop (CONTEXT PTR [eax]).regEbp
pop (CONTEXT PTR [eax]).regEsp
pop (CONTEXT PTR [eax]).regEip
end;
在本例中异常处理只是简单地恢复esp、ebp寄存器的值并将eip寄存器的值置为一个能使线程安全地继续执行的地址。这些处理信息都是由系统从我们预先填充的seh结构体中取出并保存在了CONTEXT结构体中,CONTEXT结构体的指针又传递给了系统。
最后异常处理函数返回ExceptionContinueExecution,该值为零就是告诉系统应该恢复线程的上下文并继续执行。即eip的值等于标记SafePlace的地址,而esp和ebp寄存器的值恢复为原值,线程从标记SafePlace处继续其执行。还有一点要注意,异常处理程序是C调用约定的。
有了结构化异常处理的知识后,让我们继续我们的内核之旅。接下来的例子中我们要从驱动程序中转到用户模式内存里。这个转换最好能包含在SEH-frame里。
内存共享
Windows提供了许多机制来进行进程间通讯(Interprocess Communications, IPC):通讯缓冲、DDE、通讯窗口(WM_COPYDATA就在这里)、邮槽(mailslot)、sockets等等。所有这些机制都是基于文件映射对象(file-mapping object)的,该对象本身是一块两个或多个进程可以访问的内存区,用DDK的术语,映射文件就是section对象,不要把它和PE文件中的section混淆起来。
section对象是最底层的通讯机制,这种对象被系统用来将可执行映象加载到内存,而缓存调度程序用它来访问缓存文件中的数据。section对象还能将磁盘上的文件映射到进程的地址空间中,而且用起来不像是在用文件,而是在用内存块。
借助于section对象来共享数据的情形如下:一个进程调用函数CreateFileMapping创建了一个内存映射文件。之后调用函数MapViewOfFile(如果层次更低就调用NtMapViewOfSection)将其视图(view)映射到自己的地址空间中,而另一个进程通过OpenFileMapping打开这个映射文件,并将其映射到自己的地址空间中。结果同一组物理内存页变为由两个进程访问,这就使得它们能通过这个区域轻松地传递较大量的数据,一个进程对这些页内容的修改会反映到另一个进程中。
共享section这种通讯方法不止可以用在用户进程间,还可以用在驱动程序里。在下面的例子里我们用命名section来在用户进程和驱动程序之间进行通讯。
老规矩,先来看看驱动程序。

代码:unit SharedSection;

interface

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

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

implementation

uses
seh;

const
SECTION_SIZE = $1000;

var
g_usDeviceName, g_usSymbolicLinkName, g_usSectionName: UNICODE_STRING;

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
status:NTSTATUS;
IOCTL_SHARE_MY_SECTION: DWORD;
psl:PIO_STACK_LOCATION;
oa:OBJECT_ATTRIBUTES;
hSection:HANDLE;
pSectionBaseAddress:PVOID;
liViewSize:LARGE_INTEGER;
begin
IOCTL_SHARE_MY_SECTION := CTL_CODE(FILE_DEVICE_UNKNOWN, $800, 0, 0);
psl := IoGetCurrentIrpStackLocation(p_Irp); {取IRP的stack location的指针}
if psl^.Parameters.DeviceIoControl.IoControlCode = IOCTL_SHARE_MY_SECTION then
begin
{是我们控制码就开始处理}
DbgPrint('SharedSection: Opening section object'#10#13);
RtlInitUnicodeString(g_usSectionName, '\BaseNamedObjects\UserKernelSharedSection');
InitializeObjectAttributes(oa, @g_usSectionName,
OBJ_CASE_INSENSITIVE, 0, nil);
status := ZwOpenSection(@hSection,
SECTION_MAP_WRITE or SECTION_MAP_READ,
@oa);
if status = STATUS_SUCCESS then
begin
DbgPrint('SharedSection: Section object opened'#13#10);
pSectionBaseAddress := nil;
liViewSize.HighPart := 0;
liViewSize.LowPart := 0;
status := ZwMapViewOfSection(hSection, HANDLE(NtCurrentProcess),
pSectionBaseAddress, 0,
SECTION_SIZE, nil,
@liViewSize, ViewShare, 0,
PAGE_READWRITE);
if status = STATUS_SUCCESS then
begin
DbgPrint('SharedSection: Section mapped at address %08X'#13#10,
pSectionBaseAddress);
{安装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;
_strrev(pSectionBaseAddress);
p_Irp^.IoStatus.Status := STATUS_SUCCESS;
DbgPrint('SharedSection: String reversed'#13#10);
SafePlace:
asm
pop fs:[0]
add esp, 4
end;
ZwUnmapViewOfSection(HANDLE(NtCurrentProcess), pSectionBaseAddress);
DbgPrint('SharedSection: Section at address %08X unmapped '#13#10,
pSectionBaseAddress);
end else
begin
DbgPrint('SharedSection: Couldn''t map view of section. Status: %08X'#13#10,
status);
end;
ZwClose(hSection);
DbgPrint('SharedSection: Section object handle closed'#13#10);
end else
begin
DbgPrint('SharedSection: Couldn''t open section. Status: %08X'#13#10, status);
end;
end else
begin
status := STATUS_INVALID_DEVICE_REQUEST;
end;
p_Irp^.IoStatus.Status := status;
IofCompleteRequest(p_Irp, IO_NO_INCREMENT);
DbgPrint('SharedSection: Leaving DispatchControl'#13#10);
result := 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;
RtlInitUnicodeString(g_usDeviceName, '\Device\SharedSection');
RtlInitUnicodeString(g_usSymbolicLinkName, '\DosDevices\SharedSection');

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_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.

阅读18