初探windows异常处理

windows系统里,为了保证系统内核的强壮和稳定,为了保证用户程序的强壮和稳定,提供了异常处理机制,来帮助程序员和系统使用人员处理异常。如果想要更加深入的掌握操作系统,异常处理的知识是必不可少的,不仅如此,软件调试也与异常处理息息相关。

0x00 前言

windows系统里,为了保证系统内核的强壮和稳定,为了保证用户程序的强壮和稳定,提供了异常处理机制,来帮助程序员和系统使用人员处理异常。如果想要更加深入的掌握操作系统,异常处理的知识是必不可少的,不仅如此,软件调试也与异常处理息息相关。

0x01 异常执行流程

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

异常的分类

  • CPU产生的异常
  • 软件模拟产生的异常

0x02 CPU异常

CPU指令检测到异常(例:除0)

查IDT表,执行中断处理函数

CommonDispatchException

KiDispatchException

image-20220327153026523.png

找到IDT表的0号中断

image-20220327153034674.png

首先保存现场

image-20220327153138693.png

然后向下走,但是并没有直接异常处理的代码,这里有一个跳转跟进去。为什么操作系统没有直接将异常处理写进去,这是因为操作系统希望我们自己首先能够将异常给处理掉

image-20220327153256604.png

跟进去后发现调用了CommonDispatchException函数

image-20220327153417571.png

CommonDispatchException主要是把一些异常的信息存储到了自己的结构体_EXCEPTION_RECORD里面,结构如下

type struct _EXCEPTION_RECORD       
{                               
    DWORD ExceptionCode;                //异常代码
    DWORD ExceptionFlags;               //异常状态
    struct _EXCEPTION_RECORD* ExceptionRecord;  //下一个异常
    PVOID ExceptionAddress;             //异常发生地址
    DWORD NumberParameters;         //附加参数个数
    ULONG_PTR ExceptionInformation 
    [EXCEPTION_MAXIMUM_PARAMETERS];     //附加参数指针    
}  

image-20220327153537518.png

然后通过KiDispatchException去找到异常处理函数

image-20220327154532542.png

在前面的跳转中,带过去了两个寄存器eaxebxeax我们可以发现它的值为0c000094,这个值是操作系统定义的

image-20220327153718760.png

image-20220327153832583.png

然后再是ebx,ebp指向的是_Trap_Frame结构体的栈顶,+68指向的就是eip

image-20220327153931139.png

这两个值就对应了结构里面的ExceptionCodeExceptionAddress

image-20220327154129883.png

再看ExceptionFlags,CPU导致的异常这个值为0,软件调试导致的异常这个值为1

image-20220327154213174.png

CPU异常执行的流程:

1、CPU指令检测到异常
2、查IDT表,执行中断处理函数
3、调用CommonDispatchException(构建EXCEPTION_RECORD)
4、KiDispatchException(分发异常:目的是找到异常的处理函数)

0x03 模拟异常记录

调用过程

CxxThrowException

RaiseException(DWORD dwExceptionCode, DWORD dwExceptionFlags, DWORD nNumberOfArguments, const ULONG_PTR *lpArguments)

NTDLL.DLL!RtlRaiseException()

NT!NtRaiseException

NT!KiRaiseException

首先手动抛出异常

image-20220327155751293.png

然后去反汇编发现调用了CxxThrowException

image-20220327155847189.png

CxxThrowException

__CxxThrowException@8:
00401290   push        ebp
00401291   mov         ebp,esp
00401293   sub         esp,20h
00401296   push        esi
00401297   push        edi
00401298   mov         ecx,8
0040129D   mov         esi,offset string "The value of ESP was not properl"...+0E0h (00423118)
004012A2   lea         edi,[ebp-20h]
004012A5   rep movs    dword ptr [edi],dword ptr [esi]
004012A7   mov         eax,dword ptr [ebp+8]
004012AA   mov         dword ptr [ebp-8],eax
004012AD   mov         ecx,dword ptr [ebp+0Ch]
004012B0   mov         dword ptr [ebp-4],ecx
004012B3   lea         edx,[ebp-0Ch]
004012B6   push        edx
004012B7   mov         eax,dword ptr [ebp-10h]
004012BA   push        eax
004012BB   mov         ecx,dword ptr [ebp-1Ch]
004012BE   push        ecx
004012BF   mov         edx,dword ptr [ebp-20h]
004012C2   push        edx
004012C3   call        dword ptr [__imp__RaiseException@16 (0042b15c)]

该代码所做的事情如下:

① 先从内存中拷贝一段0x20字节的固定结构体到堆栈中;
② 将ExceptionList也拷贝到堆栈中(该结构体内部)
③ 传入有关参数调用RaiseException函数。

注意,ThrowCode虽然从用户代码传入进来,但分析其函数并没有用到,而是直接调用一段固定的异常码。而&ThrowCode以及异常链被作为其参数存储,这样通过分析就可以轻易找到其ThrowCode值,其作为参考之后来处理SEH。

RaiseException

跟进去调用了Kernel32.dllRaiseException

image-20220327161000483.png

image-20220327161910401.png

这里跟CPU异常不同的是,CPU异常会将错误代码跟着寄存器一起传入,但是软件异常并没有,这里看一下

image-20220327161459530.png

这里的edx为E06D7363就是软件调试的错误代码,这里注意,随着语言和版本的不同,这里的EDX即错误代码并不固定,取决于编译环境

image-20220327161638930.png

第二个差异就是CPU异常存储的是发生异常的地址,软件异常则是存储RaiseException函数的地址

image-20220327162048454.png

0x04 内核层异常处理流程

前面我们分析过,存在两种异常,CPU异常与用户模拟异常,其异常触发时收集的线路是不同的,但是其最终走经过KiDispatchException函数。

当走到KiDispatchException,CPU异常与用户模拟异常唯一的区别是CPU异常最高位置1(nt!KiRaiseException异常派发时的上一行代码),其余记录的都是一样的。

KiDispatchException的处理是按照其先前模式来处理的,也就是内核异常与用户异常两种,而不是按照CPU异常与用户模拟异常来进行处理。

1) _KeContextFromKframes  将Trap_frame备份到context 为返回3环做准备

2) 判断先前模式  0是内核调用  1是用户层调用

3) 是否是第一次机会

4) 是否有内核调试器

5) 如果没有或者内核调试器不处理

6) 调用RtlDispatchException

7) 如果返回FALSE 也就是0  

8) 再次判断是否有内核调试器 有就调用 没有直接蓝屏

KiDispatchException

首先定位到KiDispatchException函数

image-20220327205416986.png

首先备份Trap_Frame结构,如果是用户层的异常则需要返回3环堆栈

image-20220327205703543.png

首先通过判断先前模式的值来识别是内核异常还是用户层异常,这里有一个是否第一次调用该函数的判断,这是因为这个函数会被调用很多次,如果不是第一次调用则直接跳转

image-20220327210422006.png

这个函数的最后一个参数就是表示这个函数是第几次被调用

image-20220327210439563.png

然后继续判断有没有内核调试器的存在(如windbg)

image-20220327210634934.png

如果有内核调试器的存在就走下面的KiDebugRoutine函数

image-20220327210735087.png

如果内核调试器没有处理返回失败的话就跳转

image-20220327212341854.png

RtlDispatchException调用异常处理函数

image-20220327212316093.png

跟进到RtlDispatchException

image-20220327212548924.png

又调用了RtlGetRegistrationHead

image-20220327212602769.png

跟进去发现取的是fs:[0]

image-20220327212624842.png

_EXCEPTION_REGISTRATION_RECORD

我们知道0环的fs:[0]指向KPCR,KPCR的第一个结构是_NT_TIB_NT_TIB的第一个成员是ExceptionList,是一个_EXCEPTION_REGISTRATION_RECORD类型的结构体

_EXCEPTION_REGISTRATION_RECORD结构如下

typedef struct _EXCEPTION_REGISTRATION_RECORD {
        struct _EXCEPTION_REGISTRATION_RECORD *Next;
        PEXCEPTION_ROUTINE Handler;
    } EXCEPTION_REGISTRATION_RECORD;

_EXCEPTION_REGISTRATION_RECORD里面有两个成员,*Next是一个指针指向下一个_EXCEPTION_REGISTRATION_RECORD结构,而第二个成员Handler指向的就是一个异常处理函数

RtlDispatchException的作用如下:

遍历异常链表,调用异常处理函数,如果异常被正确处理了,该函数返回1

如果当前异常处理函数不能处理该异常,那么调用下一个,以此类推。

如果到最后也没有处理这个异常,返回0。

image-20220327213102327.png

调用异常处理函数得到返回值后跳转到地址

image-20220327213504310.png

然后判断返回值是否为1,1的话就是处理成功,跳转

image-20220327213527703.png

异常被处理成功则把Context结构放回Trap_Frame里面

image-20220327213735559.png

如果没有被处理成功则继续往下走进行有无内核调试器的判断,如果有内核调试器则调用KiDebugRoutine

image-20220327213835219.png

如果没有内核调试器或者有内核调试器但是没有处理异常,则跳转到下面的地方

image-20220327214035176.png

操作系统蓝屏

image-20220327214109874.png

0x05 用户层异常处理流程

定位到KiDispatchException,进入用户异常的函数

image-20220328095342284.png

进入函数首先判断是不是第一次调用,然后继续往下走,如果有内核调试器则直接跳转,没有的话继续往下走

image-20220328100329260.png

然后进行异常的处理,调用DbgkForwardException,这个函数的作用是调用3环的调试器 ,再进行判断有无3环的调试器接收异常,如果没有则返回3环处理

image-20220328100451340.png

然后进行结构体的修改,这里同用户APC执行的修改过程

image-20220328101211189.png

然后修改EIP的值为KeUserExceptionDispatcher函数的地址,这时候EIP的值已经是函数地址,这时候再回到原函数

image-20220328101716892.png

image-20220328101848233.png

注意这里并没有直接进行返回3环的操作,而是KiDispatchException这个函数执行结束过后回到原函数,用不同的方法回到3环

CPU异常:CPU检测到异常  ->  查IDT执行处理函数  ->  CommonDispatchException  <->  KiDispatchException   
        通过IRETD返回3环

模拟异常:CxxThrowException  ->  RaiseException  ->  RtlRaiseException  ->  NT!NtRaiseException  -> NT!KiRaiseException  <->  KiDispatchException   
        通过系统调用返回3

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

KiUserExceptionDispatcher

返回三环后,可以看到其调用一个RtlDispatchException。注意,在处理内核异常时,也有一个同名的RtlDispatchException,那是内核模块,这是三环模块。

RtlDispatchException可以认为是异常的核心,区别是如果在内核模块,则处理零环,如果在ntdll模块,则处理三环。

1827556-20200402110019370-957825440.png

  • 发表于 2022-04-14 09:57:00
  • 阅读 ( 7255 )
  • 分类:漏洞分析

1 条评论

向往阳光的月光
idb文件能给下不~
请先 登录 后评论
请先 登录 后评论
szbuffer
szbuffer

30 篇文章

站长统计