• 异常(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("除零异常已被处理了!
    ");
            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;
    }
  • 相关阅读:
    CodeForces 682B Alyona and Mex (排序+离散化)
    CodeForces 682A Alyona and Numbers (水题)
    CodeForces 682E Alyona and Triangles (计算几何)
    CodeForces 176B Word Cut (计数DP)
    CodeForces 173C Spiral Maximum (想法、模拟)
    Spring源码剖析3:Spring IOC容器的加载过程
    Spring源码剖析2:初探Spring IOC核心流程
    深入理解JVM虚拟机13:再谈四种引用及GC实践
    深入理解JVM虚拟机12:JVM性能管理神器VisualVM介绍与实战
    深入理解JVM虚拟机11:Java内存异常原理与实践
  • 原文地址:https://www.cnblogs.com/onetrainee/p/12611120.html
Copyright © 2020-2023  润新知