首页
社区
课程
招聘
[原创]基于windows软件异常触发原理
2021-3-20 14:03 8395

[原创]基于windows软件异常触发原理

2021-3-20 14:03
8395

基于windows软件异常触发原理

一、基本概况

在使用C++编写的调试器项目时,引发了对Windows软件异常机制的兴趣。

 

分析工具:IDA7.5

 

分析方式:静态分析

 

分析用到的DLL模块以及内核程序:Kernel32.dll、Ntdll.dll 、ntoskrnl.exe

 

初学者飘过,如有错误,还请指正,共同学习。

二、具体分析

1、异常的分类

2、CPU异常

​ CPU捕获

 

→查IDT表,执行中断处理函数(中断处理函数不处理异常,给程序员一次机会)

 

→调用CommonDispatchException(给构建的结构体进行赋值)

 

→在堆栈上构建了一个结构体(记录相关异常信息)

 

 

→KiDispatchExceptin(分发异常:目的是找到异常的处理函数)判断是用户异常还是内核异常

 

KiDispatchException (

 

IN PEXCEPTION_RECORD ExceptionRecord,

 

IN PKEXCEPTION_FRAME ExceptionFrame,

 

IN PKTRAP_FRAME TrapFrame, 备份到context里面

 

IN KPROCESSOR_MODE PreviousMode, 0是内核 1是用户

 

IN BOOLEAN FirstChance ;是否是第一次执行 1是第一次执行

 

)

 

 

→KeContextFormKframes(将Trap_Frame备份到context 为返回3环做准备)

 

 

→RtlDispatchException(如果没有内核调试器,或者调试器没有处理,才会执行)

 

→RtlpGetRegistrationHead注册登记循环遍历

 

→返回0,否则未处理异常

 

→再次判断是否存在内核调试器

→蓝屏

→KiDispatchException 通过IRETD返回3环

 

→返回CommonDispatchException进行第二次异常触发

3、用户异常

​ CPU捕获

 

→查IDT表,执行中断处理函数(中断处理函数不处理异常,给程序员一次机会)

 

→调用CommonDispatchException(给构建的结构体进行赋值)

 

→在堆栈上构建了一个结构体(记录相关异常信息)

 

→KiDispatchExceptin(分发异常:目的是找到异常的处理函数)判断是用户异常还是内核异常

 

→KeContextFormKframes(将Trap_Frame备份到context 为返回3环做准备)

 

​ 前面的处理流程和CPU异常处理的流程是一致的,用户异常到这就开始有所不同

 

→KiDispatchException中修改Trap_Frame里面的寄存器的值

→最关键的修改是在0环中的全局变量KeUserExceptionDispatcher会赋一个值是3环中的KiUserExceptionDispatcher函数

 

​ 修正EIP为KiUserExceptionDispatcher ,通过系统调用返回3环

→返回CommonDispatchException

 

→当线程再次回到3环时,将执行KiUserExceptionDispatcher 函数

4、模拟异常(主动抛出)

为什么叫模拟异常,因为当用户异常第二次产生的时候,就是走的模拟异常的流程,它是系统或编写程序的作者主动产生的。

 

throw关键字(依赖编译器)(主动抛异常)

→CxxThrowException不同编译器所调用的模拟异常的函数

→KERNEL32.DLL!RaiseException(DWORD dwExceptionCode,DWORD dwExceptionFlags,
DWORD nNumberOfArguments,const ULONG_PTR* lpArguments)(填充EXceptionRecord结构体)

 

(KERNEL32.DLL)RaiseException函数分析:

 

​ 和CPU异常的差异:

 

​ ExceptionCode根据编译器的不同,存在不同的异常代码

 

​ ExceptionAddress 存储的是RaiseException的地址

 

​ 相同点:使用相同的_EXCEPTION_RECORD结构体

 

_EXCEPTION_RECORD结构体:

 

​ type struct _EXCEPTION_RECORD

 

​ {

 

​ DWORD ExceptionCode ; 异常代码

 

​ DWORD ExceptionFlags ; 异常状态

 

​ struct _EXCEPTION_RECORD* ExcptionRecord ;下一个异常

 

​ PVOID ExceptionAddress ; 异常发生地址

 

​ DWORD NumberParameters ; 附加参数个数

 

​ ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];附加参数指针

 

}

 

→NTDLL.DLL!RtRaiseException()

 

→NT!NtRaiseException (进内核啦)

 

​ 原型:

→NT!KiRaiseException(EXceptionRecord 最高位清零)

 

原型:

 

→KiDispatchExceptin(分发异常:目的是找到异常的处理函数)判断是用户异常还是内核异常

 

→KeContextFormKframes(将Trap_Frame备份到context 为返回3环做准备)

 

→KiDebugRoutine(内核调试器)(判断是否存在)

→DbgkForwardException(内核调试器不存在或返回FALSE,那么此函数将调用3环调试器)

→如果3环的调试器没有处理这个异常那么就得修改Trap_Frame里面的寄存器的值

 

→最关键的修改是在0环中的全局变量KeUserExceptionDispatcher会赋一个值是3环中的KiUserExceptionDispatcher函数

 

​ 修正EIP为KiUserExceptionDispatcher ,通过系统调用返回3环

 

→返回NT!KiRaiseException

 

→当线程再次回到3环时,将执行KiUserExceptionDispatcher 函数

三、总结

总结:CPU两次触发异常的流程都是一样的,用户异常第一次触发和第二次触发的时候有所不同。最重要的是,模拟异常和CPU异常到了分发异常时,后面的步骤就完全一样了,区分不出到底是模拟异常还是CPU异常了。


阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开 发者可享99元/年,续费同价!

最后于 2021-3-20 14:05 被APT_华生编辑 ,原因:
收藏
点赞10
打赏
分享
最新回复 (4)
雪    币: 3428
活跃值: (3482)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
fengyunabc 1 2021-3-20 16:42
2
0
感谢分享!
雪    币: 137
活跃值: (1240)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
huojier 2021-3-28 14:17
3
0
最近也在学习异常处理机制(目前在学SEH) X64下我有点不解的地方是x64的exception table似乎是全局共享的(加载一个dll,这个dll的exception table会被放到全局的一个表里面),也就是说其他模块能给不属于自己的领空的地址加SEH?
雪    币: 3867
活跃值: (3495)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
APT_华生 1 2021-3-28 15:00
4
0
SEH局部链表,所以在使用的时候只对当前线程有效,VEH才是属于全局链表,当VEH中无法处理此异常时,才会交给SEH
雪    币: 3867
活跃值: (3495)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
APT_华生 1 2021-3-28 15:02
5
0
当然,OD这种调试器为何无法调试其他线程,需要切换才能调试,就因为是使用这种机制实现的
游客
登录 | 注册 方可回帖
返回