在进入主题之前,先来简单地看一下结构化异常处理(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.
由于在内核模式下我们无法直接使用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;
异常处理
函数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;
最后异常处理函数返回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.
使用共享资源通常情况下需要考虑同步问题,即读写线程不能同时访问共享资源。在本例中总是只有一个线程,所以不需要同步。
代码:
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);
这里视图的虚拟地址与应用程序中视图的地址将有所不同,但共享section所在的物理页是同一个。我们这里有一个内存页,其中还保存着倒着写的一行文字。
代码:
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;
以上建立SEH-frame,并调用_strrev函数将内存里的字符串反转过来。
下面来看看在用户模式下如何加载和调用这个驱动程序。
代码:
program SharedSection; {$APPTYPE CONSOLE} uses SysUtils, Windows, Dialogs, WinSvc, nt_status, native, macros, ntdll; const SECTION_SIZE = $1000; str = '.revird ecived a dna sessecorp resu neewteb yromem erahs ot euqinhcet emas eht esu nac uoy ,revewoH .sessecorp resu gnoma yromem gnirahs rof desu euqinhcet nommoc a si elif gnigap eht yb dekcab elif deppam-yromem A'; _DELETE = $10000; var hSection:HANDLE; liSectionSize: LARGE_INTEGER; oa:OBJECT_ATTRIBUTES; pSectionBaseAddress:PVOID; liViewSize: LARGE_INTEGER; g_usSectionName: UNICODE_STRING; status:NTSTATUS; sTemp: array[0..255] of char; function CallDriver: boolean; var fOk: boolean; hSCManager:HANDLE; hService:HANDLE; acModulePath: string; _ss:SERVICE_STATUS; hDevice:HANDLE; dwBytesReturned: DWORD; IOCTL_SHARE_MY_SECTION: DWORD; lpTemp: PChar; begin fOk := false; IOCTL_SHARE_MY_SECTION := CTL_CODE(FILE_DEVICE_UNKNOWN, $800, 0, 0); hSCManager := OpenSCManager(nil, nil, SC_MANAGER_ALL_ACCESS); if hSCManager <> 0 then begin acModulePath := GetCurrentDir + '\' + ExtractFileName('SharedSection.sys'); hService := CreateService(hSCManager, 'SharedSection', 'One way to share section', SERVICE_START or SERVICE_STOP or _DELETE, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, PChar(acModulePath), nil, nil, nil, nil, nil); if hService <> 0 then begin if StartService(hService, 0, lpTemp) then begin; hDevice := CreateFile(PChar('\\.\SharedSection'), 0, 0, nil, OPEN_EXISTING, 0, 0); if hDevice <> INVALID_HANDLE_VALUE then begin if DeviceIoControl(hDevice, IOCTL_SHARE_MY_SECTION, nil, 0, nil, 0, dwBytesReturned, nil) then begin fOk := true; end else begin ShowMessage('Can''t send control code to device.'); end; CloseHandle(hDevice); end else begin ShowMessage('Device is not present.'); end; ControlService(hService, SERVICE_CONTROL_STOP, _ss); end else begin ShowMessage('Can''t start driver.'); end; DeleteService(hService); CloseServiceHandle(hService); end else begin ShowMessage('Can''t register driver.'); end; CloseServiceHandle(hSCManager); end else begin ShowMessage('Can''t connect to Service Control Manager.'); end; result := fOk; end; begin liSectionSize.HighPart := 0; liSectionSize.LowPart := SECTION_SIZE; RtlInitUnicodeString(g_usSectionName, '\BaseNamedObjects\UserKernelSharedSection'); InitializeObjectAttributes(oa, @g_usSectionName, OBJ_CASE_INSENSITIVE, 0, nil); status := ZwCreateSection(@hSection, SECTION_MAP_WRITE or SECTION_MAP_READ, @oa, @liSectionSize, PAGE_READWRITE, SEC_COMMIT, 0); if status = STATUS_SUCCESS then begin 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 //RtlInitUnicodeString(g_szStrToReverse, str); strcpy(pSectionBaseAddress, PChar(str)); if CallDriver then begin strcpy(sTemp, pSectionBaseAddress); ShowMessage(sTemp); ZwUnmapViewOfSection(HANDLE(NtCurrentProcess), pSectionBaseAddress); end; end else begin ShowMessage('Can''t map section.'); end; ZwClose(hSection); end else begin ShowMessage('Can''t create section.'); end; end.
这是个控制台应用程序,我们也只讲几个关键的地方,其他地方很容易理解。
liSectionSize.HighPart := 0;
liSectionSize.LowPart := SECTION_SIZE;
建立section需要指明其大小,对于大小值使用LARGE_INTEGER类型的变量的程序来说,这个值可以超过4GB。我们将这个值初始化为一个内存页的大小即4KB。
代码:
InitializeObjectAttributes(oa, @g_usSectionName, OBJ_CASE_INSENSITIVE, 0, nil);
我们准备使用的section应该取个名字,这样就可以用名字来打开它。section的名字必须就unicode字符串,通过RtlInitUnicodeString(g_usSectionName, '\BaseNamedObjects\UserKernelSharedSection');来创建。
section对象在对象管理器名字空间的BaseNamedObjects目录下,用户进程创建的命名对象一般都在这个目录下。
代码:
status := ZwCreateSection(@hSection, SECTION_MAP_WRITE or SECTION_MAP_READ, @oa, @liSectionSize, PAGE_READWRITE, SEC_COMMIT, 0);
代码:
if status = STATUS_SUCCESS then begin pSectionBaseAddress := nil; liViewSize.HighPart := 0; liViewSize.LowPart := 0; status := ZwMapViewOfSection(hSection, HANDLE(NtCurrentProcess), pSectionBaseAddress, 0, SECTION_SIZE, nil, @liViewSize, ViewShare, 0, PAGE_READWRITE);
代码:
.revird ecived a dna sessecorp resu neewteb yromem erahs ot euqinhcet emas eht esu nac uoy ,revewoH .sessecorp resu gnoma yromem gnirahs rof desu euqinhcet nommoc a si elif gnigap eht yb dekcab elif deppam-yromem A
代码:
if status = STATUS_SUCCESS then begin strcpy(pSectionBaseAddress, PChar(str)); if CallDriver then begin strcpy(sTemp, pSectionBaseAddress); ShowMessage(sTemp); ZwUnmapViewOfSection(HANDLE(NtCurrentProcess), pSectionBaseAddress); end;
最后ZwUnmapViewOfSection(HANDLE(NtCurrentProcess), pSectionBaseAddress);把系统恢复成初始的样子。