首页
社区
课程
招聘
[原创][原创]内核学习-异常处理
发表于: 2021-10-30 18:26 27801

[原创][原创]内核学习-异常处理

2021-10-30 18:26
27801

异常产生后,首先是要记录异常信息(异常的类型、异常发生的位置等),然后要寻找异常的处理函数,称为异常的分发,最后找到异常处理函数并调用,称为异常处理

围绕 异常处理异常分发异常处理展开

异常的分类:

(1)cpu产生的异常
在这里插入图片描述

代码执行时,CPU检测到除数为零,cpu抛出异常

(2)软件模拟产生的异常

在C++或者是C#等一些高级语言中,在程序需要的时候也可以主动抛出异常,这种高级语言抛出的异常就是模拟产生的异常,并不是真正的异常。

异常记录

在这里插入图片描述

找中断表的0号处理函数,

截图百度百科:
在这里插入图片描述

IDA打开ntoskrnl.exe 搜索字符串 _IDT 进行定位

IDT表:

在这里插入图片描述

执行流程

4018c7函数:没有对异常进行处理,函数内部调用CommonDispatchException函数

CommonDispatchException函数内部调用_KiDispatchExceptio,CPU这么设计的目的是为了让程序员有机会对异常进行处理。

总结

该函数构造一个_EXCEPTION_RECORD结构体 并赋值

CommonDispatchException主要就做了一件事情,就是将异常相关的信息存储到一个_EXCEPTION_RECORD结构体里,这个结构体的作用就是用来记录异常信息

ebx中存储的是发生异常时的EIP,eax:0C0000094h ,这个是CPU自己在内部定义的异常代码

ExceptionFlags异常状态:通过异常状态可以区分是CPU产生的异常还是软件模拟产生的异常。所有的CPU产生的异常这个位置的值是0,所有软件模拟产生的异常这个位置存储的值是1,如果堆栈错误里面存储的值是8,如果出现嵌套异常,里面存储的值是0x10

ExceptionRecord下一个异常:通常是空的,但是出现嵌套异常,通过指针指向下一个异常。

CPU异常的执行流程:

示例代码:通过代码抛出异常 throw关键字

在这里插入图片描述

当通过软件抛出异常的时候,实际上就是调用了CxxThrowException

CxxThrowException调用(KERNEL32.DLL)RaiseException

在堆栈里构建一个EXCEPTION_RECORD结构体,然后对结构体进行赋值

CPU产生的异常和软件模拟产生的异常都填充EXCEPTION_RECORD结构体,但是两者有差异

差异:

ExceptionCode 异常代码

当CPU产生异常时会记录一个ErrorCode,通过查表可以查到ErrorCode具体的含义,不同的异常对应不同的错误代码,但是软件抛出的ErrorCode是根据编译环境决定的,如下图的EDX中存储的值即为当前编译环境的ErrorCode

ExceptionAddress 异常发生地址

第二个区别就是ExceptionAddress,CPU异常记录的位置是真正的异常发生时的地址

CPU记录异常的地址是真正发生异常的地址,软件模拟产生的异常里面存储的是RaiseException这个函数的地址。

RaiseException调用RtlRaiseException (ntdll.dll)

RtlRaiseException调用_ZwRaiseException

_ZwRaiseException调用NtRaiseException

NtRaiseException调用_KiRaiseException

这个函数主要做了两件事

img

RaiseException在初始化一个EXCEPTION_RECORD结构体之后,开始调用NTDLL中的RtlRaiseException; 之后,开始调用内核中NtRaiseException, NtRaiseException再调用另外一个内核函数KiRaiseException。接下来KiRaiseException会调用KiDispatchException开始异常的分发。

https://www.cnblogs.com/Winston/archive/2010/03/16/1687649.html

http://runxinzhi.com/Ox9A82-p-5374527.html

cpu异常和模拟异常只有在异常记录不同,调用KiDispatchException开始异常的分发后步骤都相同。

异常处理

异常可以发生在用户空间,也可以发生在内核空间。无论是CPU异常还是模拟异常,是用户层异常还是内核层异常,都要通过KiDispatchException函数进行分发。

KiDispatchException函数执行流程总结

1.将Trap_Frame备份到context为返回三环做准备
2.判断先前模式 0是内核调用 1是用户层调用
3.判断是否是第一次调用
4.判断是否有内核调试器
5.如果没有内核调试器则不处理
6.调用RtlDispatchException处理异常
7.如果RtlDispatchException返回FALSE,再次判断是否有内核调试器,没有直接蓝屏

(ntoskrnl.dll)

1.将Trap_frame备份到context为返回3环做准备(由于操作系统不知道分发的异常到底是3环的异常还是0环的异常,如果是3环的异常,进0环前的环境会被备份到Trap_Frame中,如果要在中途回到3环的话,就需要将 Trap_frame 备份到 context,为返回3环做准备)

如果没有内核调试器或者内核调试器不处理,都跳转到loc_4309A1

调用_RtlDispatchException处理异常

如果RtlDispatchException返回FALSE,再次判断是否有内核调试器,没有直接蓝屏

RtlDispatchException函数作用:遍历异常链表,调用异常处理函数,如果异常被正确处理了,该函数返回1.如果当前异常处理函数不能处理该异常,那么调用下一个,以此类推。如果到最后也没有人处理这个异常,返回0。

RtlDispatchException调用_RtlpGetRegistrationHead

RtlpGetRegistrationHeadFS:0保存到eax之后返回。我们知道FS:0在零环的时候指向的是KPCR,而KPCR的第一个成员就是ExceptionList(具体信息可看以往文章apc和系统调用)

ExceptionList,指向了一个结构体 _EXCEPTION_REGISTRATION_RECORD

1._EXCEPTION_REGISTRATION_RECORD结构体必须位于当前线程的堆栈中

2.这个结构体有两个成员,第一个成员指向下一个_EXCEPTION_REGISTRATION_RECORD,如果没有下一个_EXCEPTION_REGISTRATION_RECORD结构体,那么这个地方的值是-1。第二个成员是异常处理函数。

3.当调用RtlDispatchException时,按顺序执行异常处理函数,若其中一个异常处理函数返回结果为真,就不再继续向下执行

4.若执行完所有异常处理函数后,异常仍然没有被处理,那么就返回FALSE

在这里插入图片描述

1.上述内核层异常处理中,异常处理函数也在0环,不用切换堆栈,用户层异常发生在三环,异常处理函数也在3环,所以要切换堆栈(因为KiDispatchException在内核,从0环返到三环)回到3环执行异常处理函数

2.切换堆栈的处理方式与用户APC的执行过程几乎是一样的,惟一的区别就是执行用户APC时返回3环后执行的函数是KiUserApcDispatcher,而异常处理时返回3环后执行的函数是KiUserExceptionDispatcher

3.理解用户APC的执行过程是理解3环异常处理的关键

Trap_Frame备份到context

判断previousmode内核调用 还是 用户层调用,如果是用户层调用,jnz到loc_42COC2

当不存在内核调试器或者内核调试器没有处理时,跳转到42C10A

调用DbgkForwardException函数将异常发送给3环调试器

3环调试器如果不存在或者没有处理的话,就会开始修改寄存器,准备返回3环

这里eax的值是一个全局变量KeUserExceptionDispatcher;在操作系统初始化的时候,会给这个全局变量赋一个值,这个值就是ntdll.KiUserExceptionDispatcher函数

总结:

1._KeContextFromKframes将Trap_frame被分到context为返回3环做准备
2.判断席安全模式,0是内核调用,1是用户层调用
3.是否都是第一次机会
4.是否有内核调试器
5.发送给3环调试
6.如果3环调试器没有处理这个异常,修正EIP为KiUserExceptionDispatcher
7.KiUserExceptionDispatcher函数执行结束:CPU异常与模拟异常返回地点不同

8.无论通过那种方式,但线程再次回到3环时,将执行KiUserExceptionDispatcher函数

当用户异常产生后,内核函数KiDispatchException并不是像处理内核异常那样在0环直接处理,而是修正3环EIP为KiUserExceptionDispatcher函数后就结束了

这样,当线程再次回到3环时,将会从KiUserExceptionDispatcher函数开始执行

调用_RtlDispatchException找到当前异常的处理函数,处理成功,再次调用ZwContinue重新进入0环(回到3环时,修改了eip,将返回地址更改为当前函数,因此进入0环需要再次修正eip,ZwContinue再次进入零环目的就是把修正后的context写进trap_frame里,再次返回3环后,就会从修正后的位置再次执行)

没有找到当前的处理函数,跳转,调用_ZwRaiseException对异常进行第二次分发

3环调用了RtlCallVectoredExceptionHandlers,0环没有调用此函数。

作用

注意:与内核调用时的区别!

VEH:全局链表,无论哪一个线程出现问题,都会先找这个全局链表,VEH中没有异常处理函数,在查找SEH链表,

这个异常处理函数的参数是一个结构体指针,有两个成员

有了这个参数我们就可以捕获异常发生时的相关信息并且修改异常发生时的寄存器环境。

代码中是先判断异常代码是否为除0异常,然后修改发生异常的Eip和Ecx,接着返回异常已处理。如果不是除0异常就返回异常未处理。

>

CPU捕获异常
通过KiDispatchException进行分发(3环异常将EIP修改为KiUserExceptionDispatcher)
KiUserExceptionDispatcher调用RtlDispatchException
RtlDispatchException查找VEH处理函数链表,并调用相关处理函数
代码返回到ZwContinue再次进入0环
线程再次返回3环后,从修正的位置开始执行

​ 当用户异常产生后,内核函数KiDispatchException并不是像处理内核异常那样在0环直接进行处理,而是修正3环EIP为KiUserExceptionDispatcher函数后就结束了。

​ 这样,当线程再次回到3环时,将会从KiUserExceptionDispatcher函数开始执行。

​ KiUserExceptionDispatcher会调用RtlDispatchException函数来查找并调用异常处理函数,查找顺序:

1.先查全局链表:VEH

2.再查局部链表:SEH

SEH是线程相关的,存储在当前线程的堆栈中。

三环,FS:0指向的是TEB,TEB的第一个成员是一个子结构体_NT_TIB,这个子结构体的第一个成员就是ExceptionList,异常处理链表

在这里插入图片描述

VEH异常处理,全局的链表,想要插入异常处理函数,调用AddVectoredExceptionHandler函数;

SEH异常处理,想要插入异常处理函数的线程,需要自行插入_EXCEPTION_REGISTRATION_RECORD 这中结构,

在这里插入图片描述

handler必须遵循调用约定,有自己的格式。

自定义SEH

SEH的处理流程实际上就是在当前的堆栈中挂一个链表,

使用SEH这种方式来处理异常要创建一个结构体,挂到当前TEB的ExceptionList链表里,编写异常处理函数

编译器支持的SEH

except里的过滤表达式用于异常过滤,只能有以下三个值:

EXCEPTION_EXECUTE_HANDLER(1) 异常已经被识别,控制流将进入到 _except模块中运行异常处理代码
EXCEPTION_CONTINUE_SEARCH(0) 异常不被识别,也即当前的这个 _except模块不是这个异常错误所对应的正确的异常处理模块。 系统将继续到上 _try except域中继续查找一个恰当的 _except模块。
EXCEPTION_CONTINUE_EXECUTION(-1) 异常被忽略,控制流将在异常出现的点之后,继续恢复运行。

过滤表达式只能有三种写法:

手动挂入链表

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
type struct _EXCEPTION_RECORD       
{                               
    DWORD ExceptionCode;                //异常代码
    DWORD ExceptionFlags;                //异常状态
    struct _EXCEPTION_RECORD* ExceptionRecord;    //下一个异常
    PVOID ExceptionAddress;                //异常发生地址
    DWORD NumberParameters;            //附加参数个数
    ULONG_PTR ExceptionInformation
    [EXCEPTION_MAXIMUM_PARAMETERS];        //附加参数指针   
}
type struct _EXCEPTION_RECORD       
{                               
    DWORD ExceptionCode;                //异常代码
    DWORD ExceptionFlags;                //异常状态
    struct _EXCEPTION_RECORD* ExceptionRecord;    //下一个异常
    PVOID ExceptionAddress;                //异常发生地址
    DWORD NumberParameters;            //附加参数个数
    ULONG_PTR ExceptionInformation
    [EXCEPTION_MAXIMUM_PARAMETERS];        //附加参数指针   
}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
VOID KiDispatchException(ExceptionRecord, ExceptionFrame,TrapFrame,PreviousMode, FirstChance)
VOID KiDispatchException(ExceptionRecord, ExceptionFrame,TrapFrame,PreviousMode, FirstChance)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
typedef struct _EXCEPTION_REGISTRATION_RECORD
{
    struct _EXCEPTION_REGISTRATION_RECORD *Next;
    PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD;
typedef struct _EXCEPTION_REGISTRATION_RECORD
{
    struct _EXCEPTION_REGISTRATION_RECORD *Next;
    PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2021-11-17 17:05 被pyikaaaa编辑 ,原因:
收藏
免费 5
支持
分享
最新回复 (6)
雪    币: 68
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
等到了,最近几天我也在学异常,学习了
2021-10-30 22:48
0
雪    币: 68
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
这里的EDX,写错了吧,应该是ECX吧?(........如下图的EDX中存储的值即为当前编译环境的ErrorCode.......)
2021-10-30 22:55
0
雪    币: 2219
活跃值: (5495)
能力值: ( LV12,RANK:210 )
在线值:
发帖
回帖
粉丝
4
是ECX 我改一下,感谢提醒
2021-10-31 09:53
0
雪    币: 246
活跃值: (4427)
能力值: ( LV4,RANK:45 )
在线值:
发帖
回帖
粉丝
5
mark
2021-10-31 10:08
0
雪    币: 576
活跃值: (2035)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
mark,用心写文章,感谢分享。
2021-10-31 11:05
0
雪    币: 1
活跃值: (124)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
真心感谢,看雪论坛人才济济。。。受益匪浅
2022-1-21 22:40
0
游客
登录 | 注册 方可回帖
返回
//