异常(1)

Windows内核分析索引目录:https://www.cnblogs.com/onetrainee/p/11675224.html

异常(1)

1. 异常种类
2. CPU异常的产生
3. 用户模拟异常
4.CPU异常与用户模拟异常的总结
5. 内核层异常的分发与处理
6. 用户层的异常处理
7.VEH异常
8.SEH异常 《SEH异常拓展
9.当用户层异常未处理时

 

 

1. 异常种类

  1)CPU处理异常 (除零异常)

  2)软件模拟异常(throw 1)

2. CPU异常的产生

 1)处理过程

  CPU指令检测到异常->查IDT表,执行中断处理函数->CommonDIspatchException->KiDispatchException。

 2)KiTrap00函数分析: 

  当出现除零异常时,其会走IDT[0]中断,执行KiTrap00这个函数,该函数保存CPU现场,填写KTRAP_FRAME这个结构。

  之后,其会调用CommonDispatchException来分发异常。  

  

  

 3)DispatchException函数分析:

  该函数目的就是构造_EXCEPTION_RECORD,然后传入KiDispatchException进一步处理异常。

  struct _EXCEPTION_RECORD {

    LONG ExceptionCode; //0x0    

    ULONG ExceptionFlags; //0x4

    struct _EXCEPTION_RECORD* ExceptionRecord; //0x8

    VOID* ExceptionAddress; //0xc ULONG NumberParameters; //0x10

    ULONG ExceptionInformation[15]; //0x14

  };

  

 

3. 用户模拟异常

  1)throw 反汇编

    在程序中使用代码 throw 1 抛出异常。

    0040103F   push        offset __TI1H (00423580)
    00401044   lea         eax,[ebp-4]
    00401047   push        eax    // &ThrowCode
    00401048   call        __CxxThrowException@8 (00401230)

  2)在 __CxxThorException函数中调用Kernel32!RaiseException
    0040125E   push        ecx
    0040125F   mov         edx,dword ptr [ebp-20h]
    00401262   push        edx    // ExceptionCode,这个编译器自己模拟的
    00401263   call        dword ptr [__imp__RaiseException@16 (0042a154)]

    其中要注意的一点:在函数中并没有看到 [ebp-20h] 确定的值,ExceptionCode 并不是ThrowCode,而是编译器固定的值。

    作为Kernel32!RaiseException第一个参数,这个值是编译器自己确定的,并不是像内核异常一样由CPU确定(比如除零异常0xc0000094)。

  3)Kernel32!RaiseException函数分析

    该函数包装好EXCEPTION_RECORD结构体,然后调用RtlRaiseException函数。

    

  4)ntdll!RtlRaiseException函数分析

    该函数调用_CONTEXT保存三环的工作环境,然后记录第几次发生异常,将其传入ntdll!ZwRaiseException函数中。

    

  5)ntdll!ZwRaiseException函数分析

    该函数进入零环,系统服务号0B5h.  

    

   6)nt!NtRaiseException函数分析

    其本质就是调用KiRaiseException,在这之前做了些简单的处理。

    当调用完成时,可以看到调用KiServiceExit来退出函数。

    

   7)nt!KiRaiseException函数分析

    其做了一些基本的判断,首先,判断先前模式三环,做了些准备工作;之后将CONTEXT转换为TRAPFRAME;最后将ExceptionCode最高位置0。

    之前除零异常,c0000094,最高位为1,表示CPU产生的异常;而如果最高位为0,则表示用户模拟异常。

    最后,调用KiDispatchException,同CPU异常一样,进入用户派发函数。

    

 

4.CPU异常与用户模拟异常的总结

  CPU发生异常时,其直接在零环,而当用户模拟异常,其存在一个三环到零环的过程,相对比较复杂。

  其中用户模拟异常从三环进入零环时,有一点特殊,其通过_CONTEXT保存现场而不是_KTRAP_FRAME。

  之前我们学习用户APC的过程处理时,其通过零环返回三环处理用户APC函数通过保存在三环_CONTEXT,但是异常已经提前这么做了,我们之后会深入研究。

  最后,无论CPU异常还是用户模拟异常,最后都会通过KiDispatchException函数派发的,对于操作系统来说没有区别。

  唯一可能区分异常的就是看最高位,如果为1则为CPU异常,如果为0则为用户异常。

  

 

5. 内核层异常的分发与处理

  1)KiDispatchException分析

    无论用户层异常还是内核层异常,最终都是走KiDispatchException这个函数。

    其中内核层处理比较直观,因为不用再返回用户层去处理。

    

   2)核心逻辑判断 

    KeDebugRoutine是内核调试器函数,其判断是否存在内核调试器,如果有就调用内核调试器来处理异常。

    RtlDispatchException是异常分发,其_KPCR+0x0处的 ExceptionList 分析。

    

   3)RtlDispatchException函数分析

    该函数在处理用户异常时会详细分析,现在先做的一个简单介绍。

    _KPCR+0x0 是一个 ExceptionList,里面一个链表,串起来一个_EXCEPTION_REGISTRATION_ROCRD结构,里面存在一个异常处理函数。

    我们可以向其中添加函数,来处理异常。

    

    其中Handler异常处理函数的返回值为_EXCEPTION_DISPOSTION,来判断异常处理的结果

      enum _EXCEPTION_DISPOSITION {

        ExceptionContinueExecution = 0,  // 异常处理成功

        ExceptionContinueSearch = 1,    // 异常没有处理,继续寻找

        ExceptionNestedException = 2,   // 二次异常,存在嵌套异常

        ExceptionCollidedUnwind = 3   // 发生嵌套的展开

      };

    而RtlDispatchException核心就是遍历这张链表,其核心操作如下(用户层发生异常时再回来分析)

     

   4)总结

    在内核中出现异常时,执行结果相对比较简单,因为不需要返回三环来处理。

    其在KiDispatchException函数中的思路比较清晰,不用太过多的来分析。

 

6. 用户层的异常处理

  用户层出现异常时,处理用户层的函数在三环,因此我们必须返回用户层来处理。

  从零环返回三环,其最关键的是堆栈切换,异常返回三环的。

  我们之前从零环返回三环,分析过用户APC的执行流程,其三环异常的执行流程与用户APC的执行流程大体相同。 

  只不过用户APC返回三环的落脚点是 KiUserApcDispatcher,而三环异常的落脚点是KiUserDispatchDispatcher。

  1)KiDispatchException函数分析

    该函数在判断不是内核异常时,则默认是用户异常来执行代码,如下图。

    

   2)ntdll!KiUserExceptionDispatcher函数分析

    返回三环后,可以看到其调用一个RtlDispatchException。

    注意,在处理内核异常时,也有一个同名的RtlDispatchException,那是内核模块,这是三环模块。

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

    这样你就能很好的区分两者的作用了。

    

   3)ntdll!RtlDispatchException函数分析

    其处理两种异常,一种VEH异常,一种SEH异常。

    VEH异常相当于一个全局变量的异常链表,其通过全局变量查找该张表。

    SEH异常相当于局部异常,其Try··catch··就是向这里面添加异常,TEB、KPCR第一个成员都是这个ExceptionList。

    我们先分析这里,之后会分析VEH异常与SEH异常,之后再来分析这个函数。

    

 

7.VEH异常

  1)VEH异常链表是一个全局链表,其模板如下:

    LONG NTAPI VehFunc(struct _EXCEPTION_POINTERS* ExceptionInfo) {
        return  EXCEPTION_CONTINUE_SEARCH;
    }
    int main(int argc, char* argv[])
    {
        // 1-veh链头部,0-veh链尾部。
        AddVectoredExceptionHandler(1,VehFunc);
        return 0;
    }
    其中 _EXCEPTION_POINTER结构体如下:

    typedef struct _EXCEPTION_POINTERS {
        PEXCEPTION_RECORD ExceptionRecord; // 异常记录
        PCONTEXT ContextRecord;  // 异常发生时的各个寄存器的值
    } EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

  2)VEH异常的返回值

     #define EXCEPTION_EXECUTE_HANDLER      1     // 异常被识别,_except模块中处理该异常
     #define EXCEPTION_CONTINUE_SEARCH      0    // 异常未被识别,继续调用下一个Handler来处理异常
     #define EXCEPTION_CONTINUE_EXECUTION (-1)   // 异常已被忽略或修复,不继续往下寻找

     注意:SEH异常与VEH异常返回值是不同的,对于SEH异常,其返回的是一个 enum EXCEPTION_DISPOSITION。

  3)我们根据ContextRecord中保存的寄存器我们就可以实现对我们的代码出现异常的修复,下面是除零异常代码的修复: 

LONG NTAPI VehFunc(struct _EXCEPTION_POINTERS* ExceptionInfo) {
    if (ExceptionInfo->ExceptionRecord->ExceptionCode == 0xc0000094) {
        ExceptionInfo->ContextRecord->Eip += 2;
        printf("除零异常已被处理了!\n");
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    return  EXCEPTION_CONTINUE_SEARCH;
}
int main(int argc, char* argv[])
{
    // 1-veh链头部,0-veh链尾部。
    AddVectoredExceptionHandler(1,VehFunc);
    _asm {
        mov ebx, 0;
        mov eax, 1;
        idiv ebx;
    }
    return 0;
}

 

8.SEH异常

  SEH异常其存在于堆栈中,头部在Fs:[0]这个寄存器中,其在堆栈中的结构如下图:

  

 

  1)返回值:虽然在零环下也存在这类值,但是定义是值是不同的,尤其注意继续寻找是返回的是0.

    我们在编程中,看到的返回值定义如下:

      #define EXCEPTION_EXECUTE_HANDLER      1     // 异常被识别,_except模块中处理该异常
      #define EXCEPTION_CONTINUE_SEARCH      0    // 异常未被识别,继续调用下一个Handler来处理异常
      #define EXCEPTION_CONTINUE_EXECUTION (-1)   // 异常已被忽略或修复,不继续往下寻找

    但是在分析ntdll!RtlDispatchException模块中,我们使用的和内核是一致的:

      enum _EXCEPTION_DISPOSITION {

        ExceptionContinueExecution = 0,  // 异常处理成功

        ExceptionContinueSearch = 1,    // 异常没有处理,继续寻找

        ExceptionNestedException = 2,   // 二次异常,存在嵌套异常

        ExceptionCollidedUnwind = 3   // 发生嵌套的展开

      };

  在分析内核时,应该选取后面那个结构体定义,不要搞混两者。  

  2)ntdll!RtlDispatchException函数分析

    之前我们只是简单的提到过该函数,其先调用VEH异常,如果不成功则调用SEH异常来执行。

    下面我们来详细分析一下其SEH异常的执行流程,其思路还是比较清晰的。

     

  3)手动添加SEH异常

    我们后面使用编译器拓展的SEH,现在手动添加一下,来感受下它的存在

#include <stdio.h>
#include <iostream>
#include <windows.h>
using namespace std;

EXCEPTION_DISPOSITION NTAPI ExceptionRoutine(
    _Inout_ struct _EXCEPTION_RECORD* ExceptionRecord,
    _In_ PVOID EstablisherFrame,
    _Inout_ struct _CONTEXT* ContextRecord,
    _In_ PVOID DispatcherContext
) {
    if (ExceptionRecord->ExceptionCode == 0xc0000094) {
        ContextRecord->Eip += 2;
        printf("检测到除零异常,正在修复...");
        return ExceptionContinueExecution;
    }
    return ExceptionContinueSearch;
}
int main(int argc, char* argv[])
{
    struct _EXCEPTION_REGISTRATION_RECORD SehRecord;
    _EXCEPTION_REGISTRATION_RECORD* temp;
    // 挂入SEH链表中
    _asm {
        mov eax, fs: [0] ; // 获取头部
        mov temp, eax; // 原来的头部存入中间变量中
        lea eax, SehRecord; // 获取结构体地址
        mov fs : [0] , eax;    // 将其变量加入头部
    }
    // 给SEH赋值
    SehRecord.Handler = ExceptionRoutine;
    SehRecord.Next = temp;
    // 触发除零异常
    _asm {
        mov eax, 1;
        mov ebx, 0;
        idiv ebx;
    }
    return 0;
}

 

 9.当用户层异常未处理时

  1)最后一道防线

    当main或其他线程启动之前,其会预先存入一个异常,作为最后一道防线,其会调用 RtlUnhandledExceptionFilter() 过滤表达式进行最后一次判断。

    _try{

      xxx

    }_except(RtlUnhandledExceptionFilter()){

      // 终止线程

      // 终止进程

    }  

  2)RtlUnhandledExceptionFilter函数分析

    其调用 ntdll!RtlUnhandledExceptionFilter2() 函数,其在内核中做三件事:

    ①其先判断是否存在R3调试器,其会查询两次。

      

    ②)如果存在三环调试器,其会根据异常错误码等信息调用DbgPrint()输出调试信息,在调用int 3实现软件中断。

      

    ③) 如果没调试器,尝试调用回调函数 RtlCallKernel32UnhandledExceptionFilter

      如果没有三环调试器异常还未处理,其会执行最后一道,调用一个内核回调函数,我们可以手动来设置这个回调函数进行处理。

      

  3)由 回调函数 来实现的一个反调试思路

    上面我们提到过最后一次拦截,当发现没有三环调试器时,其会自行执行一个回调函数,尝试进行最后一次修复。

    如果存在三环调试器,其会输出调试信息,调用int 3断点暂停执行。

    这样,我们就存在一种反调试思路:我们认为制造一种异常,如果此时存在调试器,其会中断;如果没有调试器,其会运行我们准备好的回调函数,修复好该异常后程序正常执行。

// 123.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <windows.h>
LONG WINAPI func(struct _EXCEPTION_POINTERS *ExceptionInfo){
    ExceptionInfo->ContextRecord->Ecx = 1;
    return EXCEPTION_CONTINUE_EXECUTION;
}

int main(int argc, char* argv[])
{
    SetUnhandledExceptionFilter(func);
    _asm{
        xor ecx,ecx;
        mov eax,0x10;
        idiv ecx;
    }
    
    return 0;
}

 

posted @ 2020-04-02 19:22  OneTrainee  阅读(533)  评论(0编辑  收藏  举报