【第42节】windows双机调试环境搭建和SEH原理剖析
目录
一、windows双机调试环境
1.1 双机调试是什么
1.2 准备工作
1.3 配置步骤
1.3.1 安装 VirtualKD
1.3.2 将target文件夹拷贝到虚拟机
1.3.3 在主机上使用vmmon64.exe监控虚拟机
二、SEH 原理剖析
2.1 TEB 与 FS 概述
2.2 手工注册 SEH
一、windows双机调试环境
1.1 双机调试是什么
双机调试环境是一种通过两台计算机(主机与目标机)协同工作进行软件测试和问题排查的技术。在你的场景中,主机运行Windows 10,目标机为安装了Windows 7的VMware虚拟机,调试工具是WinDbg。以下是详细的配置步骤:
1.2 准备工作
虚拟机 vmware12
目标系统 win7-x32,确保已安装VMware 12及Windows 7(32位)作为目标系统
主机系统 win10
调试器 windbg,主机需安装Windbg及VirtualKD用于内核调试
1.3 配置步骤
1.3.1 安装 VirtualKD
在主机上解压并运行`virtualkd.exe`,选择目录完成安装。
安装后出现下面目录
1.3.2 将target文件夹拷贝到虚拟机
在虚拟机中运行程序
默认安装即可,系统会重新启动。
1.3.3 在主机上使用vmmon64.exe监控虚拟机
设置windbg的调试器路径
选择调试器
运行调试器
即可进行内核调试
注意事项
- 确保所有操作均以管理员权限执行,避免权限不足导致的问题。
- 如果遇到连接失败,检查COM端口设置或尝试重新配置串行端口。
以上步骤将帮助你搭建一个稳定的双机调试环境,便于深入分析和解决系统级问题。
二、SEH 原理剖析
2.1 TEB 与 FS 概述
大家都清楚,程序一旦产生异常,这个异常首先会被发送到内核层。而Windows系统所提供的异常处理机制,它肯定是在内核层把异常发出来以后,才会开始发挥作用去处理异常。
Windows系统特别厉害,仅仅借助`_try`、`_except`这些关键字,就能够成功接管异常。而且在处理异常的过程中,程序还能正常运行,即便跳转来跳转去处理各种情况,也不会出现崩溃的状况。那这到底是怎么做到的呢?
要弄明白这个问题,我们得从分析TEB和异常链入手。对于线程来说,在内核层有一个专门的数据结构,叫做ETHREAD;而在用户层,也有对应的一个数据结构,叫TEB 。
实验:建立双机调试,查看 TEB 结构
kd>dt TEB -b
nt!_TEB
+0x000 NtTib : _NT_TIB
+0x000 ExceptionList : Ptr32
+0x004 StackBase : Ptr32
+0x008 StackLimit : Ptr32
+0x00c SubSystemTib : Ptr32
+0x010 FiberData : Uint4B
+0x010 Version : Uint4B
+0x014 ArbitraryUserPointer : Ptr32
+0x018 Self : Ptr32
+0x01c EnvironmentPointer : Ptr32
+0x020 ClientId : _CLIENT_ID
+0x020 UniqueProcess : Ptr32
+0x024 UniqueThread : Ptr32
+0x028 ActiveRpcHandle : Ptr32
从这里能发现,TEB里的第一个字段是`NtTib` 。而在`NtTib`当中,它的第一个字段是`ExceptionList` 。这个`ExceptionList`其实就是一个链表头结点的地址。链表中的元素是按照下面这样来定义的:
typedef struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD *Next; // 指向下一个 SEH
PEXCEPTION_ROUTINE Handler; // 异常处理函数
} EXCEPTION_REGISTRATION_RECORD;
当异常出现的时候,系统会借助`FS:[0]`找到那条链表。然后,系统会按照顺序挨个调用链表里面的处理函数,用它们来处理异常。这,就是SEH的关键原理所在。至于微软提供的那些`_try`、`_except`等关键字,主要是为了让咱们用户在使用SEH机制的时候,感觉更方便、更顺手罢了 。
2.2 手工注册 SEH
搞懂SEH的大概运作方式之后,咱们就能自己动手注册SEH了。不过在这之前,得先知道SEH异常处理函数长啥样,也就是它的原型:
NTAPI EXCEPTION_ROUTINE(
_Inout_ struct _EXCEPTION_RECORD *ExceptionRecord,
_In_ PVOID EstablisherFrame,
_Inout_ struct _CONTEXT *ContextRecord,
_In_ PVOID DispatcherContext
);
手工注册代码如下:
#include "stdafx.h"
#include <windows.h>
EXCEPTION_DISPOSITION NTAPI seh(struct _EXCEPTION_RECORD *ExceptionRecord,PVOID EstablisherFrame,struct _CONTEXT *ContextRecord,PVOID DispatcherContext)
{
printf("seh\n");
// 继续执行
return ExceptionContinueExecution;
}
int _tmain(int argc, _TCHAR* argv[])
{
// EXCEPTION_REGISTRATION_RECORD node;
/*
* 产生异常后 , 操作系统使用fs段寄存器找到TEB,
* 通过TEB.ExceptionList 找到SEH链表的头节点,
* 通过节点中记录的异常处理函数的地址调用该函数.
*/
// node.Handler = seh;
// node.Next = NULL;
_asm
{
push seh; // 将SEH异常处理函数的地址入栈
push fs:[0];//将SEH头节点的地址入栈
;// esp + 0 -- > [fs:0]; node.Next;
;// esp + 4 -- > [seh]; node.handler;
mov fs:[0], esp;// fs:[0] = &node;
}
*(int*)0 = 0;
// 平衡栈空间
// 还原FS:[0]原始的头节点
_asm{
pop fs : [0]; // 将栈顶的数据(原异常头节点的地址)恢复到FS:[0],然后再平衡4个字节的栈
add esp, 4; // 平衡剩下的4字节的栈.
}
return 0;
}
SEH 机制的基本原理:
当异常产生时,操作系统会通过 fs 段寄存器找到线程环境块(TEB),然后在 TEB 中通过 ExceptionList 找到 SEH 链表的头节点,最后根据头节点中记录的异常处理函数的地址来调用相应的异常处理函数。同时,注释中还提到了原本可以通过定义 EXCEPTION_REGISTRATION_RECORD 结构体变量 node 并设置其成员 Handler 为自定义的异常处理函数 seh,Next 为 NULL 来构建一个 SEH 节点。
上面的代码使用汇编语言手动安装 SEH 节点:
(1)push seh; 将自定义的 SEH 异常处理函数 seh 的地址压入栈中。
(2)push fs:[0]; 将当前 SEH 链表头节点的地址(存储在 fs:[0] 中)压入栈中。
(3)注释部分解释了栈中的数据布局,esp + 0 处是原来的 SEH 链表头节点地址(即新节点的 Next 指针所指向的位置),esp + 4 处是新的异常处理函数地址(即新节点的 Handler 成员)。
(4)mov fs:[0], esp; 将栈顶指针 esp 的值赋给 fs:[0],这样就把新的 SEH 节点插入到了 SEH 链表的头部,fs:[0] 现在指向了新创建的 SEH 节点。
触发异常:
*(int*)0 = 0; 这行代码试图向内存地址 0 写入数据,由于地址 0 通常是受保护的,不能被随意访问和写入,所以这会触发一个异常,从而使系统调用刚刚安装的 SEH 异常处理函数。
异常处理完成后,需要恢复栈空间和原来的 SEH 链表:
(1)pop fs : [0]; 将栈顶的数据(即原来的 SEH 链表头节点地址)弹出并恢复到 fs:[0] 中,这样就还原了原来的 SEH 链表头节点。
(2)add esp, 4; 用于平衡栈空间,因为之前 push 操作使栈增加了 4 个字节(push seh; 操作压入了一个 4 字节的函数地址),这里通过增加 esp 的值来恢复栈的平衡。
上面代码的主要目的是通过手动安装 SEH 节点来展示 Windows 系统中 SEH 机制的工作原理,包括如何注册自定义的异常处理函数,以及在异常处理完成后如何恢复系统的状态。