windows异常处理学习总结

异常学习

一丶CPU异常

1.1 异常处理简介

此为Windows异常处理。学习异常可以帮助我们更好的调试程序。或者更好的寻找异常点。

先说一下异常产生之后要进行的操作

  • 异常记录

  • 异常分发

  • 异常处理

异常产生后第一步就是要进行异常记录 原因是首先要记录哪里产生了异常。 异常的类型是什么。 第二步就是要分发异常 分发异常指的就是去寻找异常处理函数 找到函数之后就会进行调用,调用的函数我们就称为异常处理

1.2 异常的分类之CPU异常

异常分类有两种,第一种就是CPU产生的异常。 第二种就是软件模拟的产生的异常。

CPU异常如下:

C++代码示例

  1. CPU异常示例
int main(int argc, char **argv)
{
    int x = 10;
    int y = 0;
    int c = x / y;

    return 0;
}

提示我们除零异常 这个是CPU可以检测出来的。

  1. 软件模拟的异常
void fun()
{
    throw 1;
}
int main(int argc, char **argv)
{
    fun();
    return 0;
}

当我们使用高级语言编写代码的时候。多多少少高级语言都会提供异常处理机制。

比如C++ 我们可以使用 throw 关键字来抛出一个异常。

这个就是软件模拟的异常。

1.3 CPU异常的 记录

CPU 检测到异常之后会进行如下操作。

XP下

WIN10 64位下

这里说下64位下的把。

其实最重要的就是 KiExceptionDisPatch 此函数会先 进行异常记录

此函数结构大概为:

__int64 __fastcall KiExceptionDispatch(
        int arg_ExceptionCode,   
        unsigned int arg_ExceptionNumberParameters,
        void *arg_ExceptionAddress,
        unsigned __int64 arg_ExceptionInformation, 
        char a5)  
参数 说明
arg_ExceptionCode 异常记录的代码
arg_ExceptionNumberParameters 异常记录的附加参数大小
arg_ExceptionAddress 异常记录出错的地址
arg_ExceptionInformation 异常记录附加参数数组信息
a5 暂时未知

在调用函数之前代码汇编代码如下:

.text:00000001401C72F3 B9 03 00 00 10                 mov     ecx, 10000003h
.text:00000001401C72F8 33 D2                          xor     edx, edx
.text:00000001401C72FA 4C 8B 85 E8 00+                mov     r8, [rbp+0E8h]
.text:00000001401C72FA 00 00
.text:00000001401C7301 E8 7A 71 00 00                 call    KiExceptionDispatch

传递了一个 10000003 值。此值就是我们的除零异常的的SWITCH值 注意 除零异常的值 应该是 0xC0000094 这个后面说。

下图为 KiExceptionDisPatch 异常记录区域。可以看到会把我们的 switch 值 进行记录 并且会记录我们出现异常的地址

异常记录结构体为如下:

#defien EXCEPTION_MAXIMUM_PARAMETERS 15
typedef struct _EXCEPTION_RECORD32 {
    DWORD    ExceptionCode;
    DWORD ExceptionFlags;
    DWORD ExceptionRecord;
    DWORD ExceptionAddress;
    DWORD NumberParameters;
    DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD32, *PEXCEPTION_RECORD32;

typedef struct _EXCEPTION_RECORD64 {
    DWORD    ExceptionCode;
    DWORD ExceptionFlags;
    DWORD64 ExceptionRecord;
    DWORD64 ExceptionAddress;
    DWORD NumberParameters;
    DWORD __unusedAlignment;
    DWORD64 ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD64, *PEXCEPTION_RECORD64;

含义如下:

参数 说明
ExceptionCode 记录异常的代码
ExceptionFlags 记录异常的状态 确定我们是CPU异常还是 软件模拟的异常 还是嵌套异常。根据不同场合值也不一样。
ExceptionRecord 指向下一个异常记录。当出现嵌套异常的时候会记录下一个异常。
ExceptionAddress 记录异常发生的地址
NumberParameters 附加参数
ExceptionInformation 附加参数指针

观看结构体,其中最重要的其实就是前4个成员。 此结构体 我称为异常记录结构体,此结构体也是我所说的异常产生的时候进行的第一步。记录异常信息。此结构体中我们重点还需要知道 ExceptionCode ExceptionAddress 这两个成员分别记录的异常代码和异常地址。这在我们调试的时候很有帮助。

假设我们是 DIV 0 的异常 那么CPU 回去 IDT表中寻找 KiDivideErrorFaultShadow 此函数会最终调用到 KiDivideErrorFault

如果在WINDBG中调试的时候 输入 !IDT 并且有符号的情况下则可以看到 IDT表中的符号名称。

反汇编查看 KiDivideErrorFault 则可以看到异常流程。

1.4 CPU异常的分发

异常的分发就是去寻找异常处理函数。 我们继续反汇编

KiExceptionDisPatch 可以看到异常分发函数

反汇编

再上图上我们可以看到会继续调用 KiDispatchException

此函数如下:

void KiDispatchException (
    IN PEXCEPTION_RECORD ExceptionRecord,
    IN PKEXCEPTION_FRAME ExceptionFrame,
    IN PKTRAP_FRAME TrapFrame,
    IN KPROCESSOR_MODE PreviousMode,
    IN BOOLEAN FirstChance
    )
参数 说明
ExceptionRecord 异常记录结构体
ExceptionFrame 异常信息框架
TrapFrame 陷阱框架
PreviousMode 先前模式
FirstChance 机会。第几次异常

继续反汇编

在反汇编过程中 可以看到。 他会对比异常记录结构体中记录的switch值 然后进行修复 如果我们是除零异常 则会修复异常错误代码为 0xC0000094;

在后面会着重说一下 异常分发和处理。 这里先抛出问题。知道我们 CPU产生异常了 如何进行 异常记录 异常分发掉哟个你的函数是哪个

1.5 CPU 异常记录小总结

CPU产生除零异常后会调用 IDT表中 KiDivideErrorFaultShadow 然后此函数继续调用 KiDivideErrorFault 这两个函数都不会进行异常处理只是继续往内核层面下发发。

KiDivideErrorFault 继续调用 KiExceptionDispatch 然后继续调用 KiDispatchException

其中异常记录 是在 KiExceptionDispatch 中产生的。 KiExceptionDispatch 会生成一个 结构体 我们称为异常记录结构体(EXCEPTION_RECORD) 此结构体来保存上层传递过来的 异常代码 异常地址 等信息。

最中会交给 KiDispatchException 去分发。

还了解两个函数。 这两个函数可以在调试中帮助我们来快速定位问题。

void KiDispatchException (
    IN PEXCEPTION_RECORD ExceptionRecord,
    IN PKEXCEPTION_FRAME ExceptionFrame,
    IN PKTRAP_FRAME TrapFrame,
    IN KPROCESSOR_MODE PreviousMode,
    IN BOOLEAN FirstChance
    )
__int64 __fastcall KiExceptionDispatch(
        int arg_ExceptionCode,   
        unsigned int arg_ExceptionNumberParameters,
        void *arg_ExceptionAddress,
        unsigned __int64 arg_ExceptionInformation, 
        char a5)  

二丶软件模拟异常

2.1 反汇编下追踪模拟异常

软件模拟异常 我们可以使用上面的C++ 代码。

void fun()
{
    throw 1;
}
int main(int argc, char **argv)
{
    fun();
    return 0;
}

断点下在 throw 1位置 然后反汇编查看代码

代码中会调用 __CxxThrowException@8 函数。并且传递了两个参数 一个参数为 _TI1H 结构 另一个为我们自定义的错误码 在C++ 如何想要专门识别异常处理得专门开一篇文章介绍。 所以在这里不在介绍。后面会单独将。 我们继续往下走。

继续往下走则会调用到Kernel32中得 RaiseException 而Kernel32中的 RaiseException 只是一个stub 函数,它是对 Kernelbase.dll中得RaiseException 做了封装。

所以我们直接IDA分析 KernelBase 主要 要拷贝32位的 Kernelbase.dll分析

此函数结构如下:

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

反汇编如下:

可以看到 还是生成一个 异常记录结构体 并对其填充

如果是软件异常 则异常记录结构体填充为1

记录异常的地址填充为了 RaiseException本身

记录的异常代码为上层传递的。异常由当前编译环境给填充的。

最后调用到 ntdll.dll中的RtlRaiseException

继续反汇编 RtlRaiseException

函数如下:

void __stdcall __noreturn RtlRaiseException(
PEXCEPTION_RECORD ExceptionRecord)

反汇编如下:

可以看到首先 使用 RtlCaptureContext 获取当前环境的异常上下文信息(上下文就是记录寄存器的一些信息) 然后设置 异常记录结构体中的异常地址为返回地址。 最后调用 ZwRaiseException 调用到内核。

在内核中则是KiRaiseException处理异常。然后最终调用到 KiDispatchException

在调用KiDispatchException的时候 会把 EXCEPTION_RECORD.ExceptionCode最高位清零,用来区分CPU异常。 也就是上图所示

2.2 软件模拟异常 产生的总结图

根据2.1小节,跟出了软件模拟一个异常的时候的步骤。

那么画图进行总结一下。

其中还了解到了两个API

void __stdcall RaiseException(
        DWORD dwExceptionCode,
        DWORD dwExceptionFlags,
        DWORD nNumberOfArguments,
        const ULONG_PTR *lpArguments);
void __stdcall __noreturn RtlRaiseException(
PEXCEPTION_RECORD ExceptionRecord)

三丶CPU异常与软件模拟异常图总结

3.1 总结图如下

其中 ring3的 记录异常 会将记录的异常地址设置为 RaiseException的地址

异常代码 由当前编译环境决定

KiRaiseException会为了区分是CPU一行还是软件异常来设置 异常记录.异常代码 的最高位为0

CPU的异常记录函数为 KiExceptionDisPatch 最后都会发送给 KiDispatchException进行异常分发

四丶内核层下的异常分发 KiDispatchException的详解

4.1 内核异常分发 学习

  • 简单了解

    我们知道 不管是 ring3 还是 ring0 出了异常都会走到 KiDispatchException 函数进行异常分发(也就是找异常处理函数) 但是此函数很复杂。但是学好此函数也就是学明白异常的关键。 这里 在内核下发生的异常我们称为 内核异常 反之就是用户模式异常

函数结构如下:

void KiDispatchException (
    IN PEXCEPTION_RECORD ExceptionRecord,
    IN PKEXCEPTION_FRAME ExceptionFrame,
    IN PKTRAP_FRAME TrapFrame,
    IN KPROCESSOR_MODE PreviousMode,
    IN BOOLEAN FirstChance
    )

内核层面的流程

内核层面其实很简单

  1. 保存TrapFrame(一些ring3的上下文信息) 返回ring3做准备

  2. 判断先前模式 0 = 内核 1= ring3

  3. 判断是否是第一次调试机会

  4. 判断是否有内核调试器

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

  6. 调用RtlDispatchException

    7.如果返回值是FALSE 会继续判断是否有内核调试器

    8 根据7 决定发送到内核调试器 还是蓝屏。

所以重点就是新函数 RtlDispatchException 此函数就是专门调用异常处理函数的

此函数结构如下:

BOOLEAN __stdcall RtlDispatchException(
    PEXCEPTION_RECORD ExceptionRecord,
    PCONTEXT Context)
{
    //xxxx
}

参数1 是记录的异常信息

参数2 是记录的上下文信息。 比如记录了 RIP RAX 等寄存器

此函数本质上就是 遍历异常处理链表 来进行异常处理

4.2 异常信息记录链表结构体

异常信息结构体存储在 FS:[0] 在ring3下也就是 TEB的环境块

TEB结构块如下:

nt!_TEB
   +0x000 NtTib            : _NT_TIB   
   +0x01c EnvironmentPointer : Ptr32 Void
   +0x020 ClientId         : _CLIENT_ID
   +0x028 ActiveRpcHandle  : Ptr32 Void
   +0x02c ThreadLocalStoragePointer : Ptr32 Void
   +0x030 ProcessEnvironmentBlock : Ptr32 _PEB

其中第一个成员结构如下:

typedef struct _NT_TIB
{
     PEXCEPTION_REGISTRATION_RECORD ExceptionList;
     PVOID StackBase;
     PVOID StackLimit;
     PVOID SubSystemTib;
     union
     {
          PVOID FiberData;
          ULONG Version;
     };
     PVOID ArbitraryUserPointer;
     PNT_TIB Self;
} NT_TIB, *PNT_TIB;

NT_TIB结构的第一个成员记录的就是异常记录链表异常记录链表中记录的就是异常处理函数
结构如下:

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

RtlDispatchException本质上就是遍历此结构体来进行异常函数寻找调用处理的。

此结构如上 第一个成员是一个NEXT域 指向下一个 异常处理信息结构体的

第二个成员如下指向一个异常处理函数。异常处理函数结构如下

NTAPI
EXCEPTION_ROUTINE (
    _Inout_ struct _EXCEPTION_RECORD *ExceptionRecord,
    _In_ PVOID EstablisherFrame,
    _Inout_ struct _CONTEXT *ContextRecord,
    _In_ PVOID DispatcherContext
    );

异常处理函数 有四个参数 其中第一个参数我们都应该很熟悉了 ,就是异常信息记录结构

参数3 是一个上下文 记录着 寄存器的信息 也是很重要的参数。

异常处理函数 可以进行异常处理。 如果处理了那么 返回值就是1 那么就不会继续寻找异常处理函数了。

4.3 内核异常处理总体流程总结图

其中我们要知道 FS:[0]中指向的是一个异常记录链表

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

异常记录链表中的第二个成员是异常处理函数

NTAPI
EXCEPTION_ROUTINE (
    _Inout_ struct _EXCEPTION_RECORD *ExceptionRecord,
    _In_ PVOID EstablisherFrame,
    _Inout_ struct _CONTEXT *ContextRecord,
    _In_ PVOID DispatcherContext
    );

RtlDispatchException 会循环遍历链表进行调用 异常处理函数

五丶用户模式下的异常分发(内核) KiDispatchException的详解

5.1 反汇编内核中的KiDispatchException 查看用户异常异常流程

观看反汇编总结如下:

  1. KeContextFromKframes 备份Trap_frame 到context中

  2. 判断先前模式 内核调用还是用户调用 0 = 内核 1 = 用户

  3. 是否有第一次机会

  4. 是否有内核调试器

  5. 发送给ring3调试器 DbgkForwardException

  6. 如果3环调试器没有处理异常那么修正EIP为 KiUserExceptionDispatcher

  7. 如果是CPU异常那么通过 IRETD返回3环 如果是模拟异常 那么直接通过系统调用返回3环

  8. 无论哪种方式当线程再次回到3环的时候都会调用 KiUserExceptionDispatcher

5.2 流程图

其中需要注意的是最终由 ring3层的 KiUserExceptionDispatcher 开始分发异常

5.3 3环下的异常处理

ntdll.KiUserExceptionDispatcher 是操作系统返回到3环所进行调用的函数。

所以直接分析此函数即可。

此函数结构如下:

VOID KiUserExceptionDispatcher(
    __in PEXCEPTION_RECORD ExceptionRecord, 
    __in PCONTEXT ContextRecord)

反汇编代码如下:

int __stdcall KiUserExceptionDispatcher(PEXCEPTION_RECORD a1, PCONTEXT a2)
{
  _EXCEPTION_RECORD *pRecord; // ebx
  int v4; // eax
  _DWORD v5[3]; // [esp-8h] [ebp-14h] BYREF
  _EXCEPTION_RECORD *pV6Record; // [esp+4h] [ebp-8h]
  _CONTEXT *v7; // [esp+8h] [ebp-4h]

  if ( !LdrDelegatedKiUserExceptionDispatcher )
  {
    pRecord = pV6Record;
    if ( RtlDispatchException(pV6Record, v7) )
      v4 = ZwContinue(v7, 0);
    else
      v4 = ZwRaiseException((int)pV6Record, (int)v7, 0);
    v5[0] = v4;
    v5[1] = 1;
    v5[2] = pRecord;
    v7 = 0;
    RtlRaiseException((PEXCEPTION_RECORD)v5);
  }
  return ((int (__stdcall *)(PEXCEPTION_RECORD, PCONTEXT))LdrDelegatedKiUserExceptionDispatcher)(a1, a2);
}

此结构进行的流程如下

  1. 调用 RtlDispatchException 查找并执行异常处理函数

  2. 如果Rtlxx返回真 那么则调用 ZwContinue再次进入0环。但是再次返回3环的时候 会从修正的地址开始执行

  3. 如果Rtlxxx返回假 那么会调用 ZwRaiseException进行第二轮的异常分发

六丶用户模式下的VEH

6.1 3环下的RtlDispatchException 分析

在3环下我们分析下RtlDispatchException

在代码中可以看到API调用

RtlpCallVectoredHandlers 这个函数的主要作用就是调用一个链表

函数原型如下:

BOOLEAN
 NTAPI
 RtlpCallVectoredHandlers(IN PEXCEPTION_RECORD ExceptionRecord,
                          IN PCONTEXT Context,
                          IN PLIST_ENTRY VectoredHandlerList)
 {
     ...
 }

它总共三个三次

  1. 参数1 异常记录结构体

  2. 参数2 上下文信息保存了寄存器的信息

  3. 参数3 链表

此链表则是全局链表 我们也称为VEH。 如果它找到了则返回 则 RtlDispatchException 就不再往下执行了。

ReactOS: sdk/lib/rtl/vectoreh.c Source File 此链接有人将这个函数逆向出来了。有兴趣可以看一下。

6.1.2 VEH的全局链表

VEH是全局的,是因为当出现异常的时候首先就是先调用它来进行处理。 假设我们有自己异常要进行处理。我们则可以加入到全局链表中。让其帮我们处理异常。

下面演示如何添加VEH 让其帮我们处理

首先是VEH 处理函数

//#define EXCEPTION_EXECUTE_HANDLER      1       VEH不使用
//#define EXCEPTION_CONTINUE_SEARCH      0       未处理
//#define EXCEPTION_CONTINUE_EXECUTION (-1)      已处理
LONG NTAPI VectExceptionRoutine(PEXCEPTION_POINTERS pExceptionInfo)
{
    ::MessageBoxA(NULL, "Veh Execute", "VehTitle", 0);
    if (pExceptionInfo->ExceptionRecord->ExceptionCode == 0xC0000094)
    {
        pExceptionInfo->ContextRecord->Eip = pExceptionInfo->ContextRecord->Eip + 2;
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    return EXCEPTION_CONTINUE_SEARCH;
}

VEH处理函数的参数是一个 PEXCEPTION_POINTERS 结构,此结构如下:

typedef struct _EXCEPTION_POINTERS {
    PEXCEPTION_RECORD ExceptionRecord;
    PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

此结构的第一个成员是 异常记录结构体。 此结构体上面说过很多次了。记录的就是当前异常的一些信息。(异常代码 异常地址 异常标志等等)

参数2 是一个Context上下文信息 Context上下文信息里面就保存了我们发生异常时候的寄存器。这里面最重要的就是 EIP/RIP

VEH的异常处理函数只能返回两个值。 要么是 已经处理 要么是 未处理

下面就是插入 异常处理函数到VEH链表中了。

这里我们需要使用Ntdll中的函数 RtlAddVectoredExceptionHandler 在之前是使用

Kernel32中的 AddVectoredExceptionHandler 但是在我逆向的时候只发现了Ntdll用有此函数了。

所以我们动态获取来进行使用。

完整代码:

typedef PVOID (NTAPI * PfnRtlAddVectoredExceptionHandler)(int isInsertListHeader, PEXCEPTION_POINTERS arg_poniter);

PfnRtlAddVectoredExceptionHandler RtlAddVectoredExceptionHandler = NULL;
//#define EXCEPTION_EXECUTE_HANDLER      1       VEH不使用
//#define EXCEPTION_CONTINUE_SEARCH      0       未处理
//#define EXCEPTION_CONTINUE_EXECUTION (-1)      已处理
LONG NTAPI VectExceptionRoutine(PEXCEPTION_POINTERS pExceptionInfo)
{
    ::MessageBoxA(NULL, "Veh Execute", "VehTitle", 0);
    if (pExceptionInfo->ExceptionRecord->ExceptionCode == 0xC0000094)
    {
        //注释掉的为第一种修复方式,修复EIP + 2 跳过除0的硬编码。
        //pExceptionInfo->ContextRecord->Eip = pExceptionInfo->ContextRecord->Eip + 2;
        pExceptionInfo->ContextRecord->Ecx = 1;  //第二种方式 修复除数为1 那么则不会触发异常
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    return EXCEPTION_CONTINUE_SEARCH;
}
int main(int argc, char **argv)
{
    HMODULE hModule = NULL;
    hModule = GetModuleHandle("ntdll.dll");
    if (hModule == NULL)
    {
        return 0;
    }
    RtlAddVectoredExceptionHandler =
        reinterpret_cast<PfnRtlAddVectoredExceptionHandler>
        (::GetProcAddress(hModule, "RtlAddVectoredExceptionHandler"));
    if (RtlAddVectoredExceptionHandler == NULL)
    {
        return 0;
    }
    RtlAddVectoredExceptionHandler(0, (PEXCEPTION_POINTERS)&VectExceptionRoutine);

    //构造异常
    __asm
    {
        xor edx,edx
        xor ecx,ecx
        mov eax,0x10
        idiv ecx
    }

    printf("异常处理成功\n");
    system("pause");
    return 0;
 }

实现效果如下:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章