Struct RUNTIME_FUNCTION

时间:2013-11-06 09:25:13

标签: c++ exception-handling stack-unwinding

我在IDA的RUNTIME_FUNCTION结构的.pdata段中发现了一个大型数组。 所以,我可以在哪里找到信息:从它编译的内容,我如何创建它以及如何在C ++中使用它。 请给我书籍,或带有良好描述的链接以及使用此结构进行异常处理和展开的教程。

2 个答案:

答案 0 :(得分:1)

Windows x64 SEH

编译器将exception directory放在.exe映像的.pdata节中。编译器用_RUNTIME_FUNCTION填充异常目录。

typedef struct _RUNTIME_FUNCTION {
 ULONG BeginAddress;
 ULONG EndAddress;
 ULONG UnwindData;
} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION;

每个_RUNTIME_FUNCTION在使用SEH的图像中描述一个功能。程序中具有try / except或try / finally块的每个函数都有一个。 BeginAddress指向函数的开始,EndAddress指向函数的结束。

UnwindData指向_UNWIND_INFO表结构

#define UNW_FLAG_NHANDLER 0x0
#define UNW_FLAG_EHANDLER 0x1
#define UNW_FLAG_UHANDLER 0x2
#define UNW_FLAG_CHAININFO 0x4

typedef struct _UNWIND_INFO {
    UBYTE Version         : 3;
    UBYTE Flags           : 5;
    UBYTE SizeOfProlog;
    UBYTE CountOfCodes;
    UBYTE FrameRegister  : 4;
    UBYTE FrameOffset    : 4;
    UNWIND_CODE UnwindCode[1];
    union {
        //
        // If (Flags & UNW_FLAG_EHANDLER)
        //
        OPTIONAL ULONG ExceptionHandler;
        //
        // Else if (Flags & UNW_FLAG_CHAININFO)
        //
        OPTIONAL ULONG FunctionEntry;
    };
    //
    // If (Flags & UNW_FLAG_EHANDLER)
    //
    OPTIONAL ULONG ExceptionData[]; 
} UNWIND_INFO, *PUNWIND_INFO;

如果设置了UNW_FLAG_EHANDLER,则ExceptionHandler指向一个名为_C_specific_handler的通用处理程序,其目的是解析指向ExceptionData结构的SCOPE_TABLE。如果设置了UNW_FLAG_UHANDLER,则它是一个try / finally块,并且还将通过别名_C_specific_handler指向终止处理程序。

typedef struct _SCOPE_TABLE {
 ULONG Count;
 struct
 {
     ULONG BeginAddress;
     ULONG EndAddress;
     ULONG HandlerAddress;
     ULONG JumpTarget;
 } ScopeRecord[1];
} SCOPE_TABLE, *PSCOPE_TABLE;

SCOPE_TABLE结构是一个可变长度结构,每个try块都有一个ScopeRecord,其中包含try块的开始和结束地址(可能是RVA)。 HandlerAddress是括号__except()中异常过滤器函数的偏移量(EXCEPTION_EXECUTE_HANDLER表示始终运行except,所以它与exception类似),而JumpTarget是偏移量到与__except块相关联的__try块中的第一条指令。

  

一旦处理器引发异常,Windows中的标准异常处理机制将为有问题的指令指针找到RUNTIME_FUNCTION并调用ExceptionHandler。对于在当前版本的Windows上运行的内核模式代码,这将始终导致对_C_specific_handler的调用。 _C_specific_handler然后将开始遍历所有SCOPE_TABLE条目以在错误的指令上搜索匹配项,并希望找到覆盖异常代码的__except语句。 (Source

此外,对于嵌套异常,我想它总是会找到覆盖当前故障指令的最小范围,并且会在未处理的较大范围内展开。

也不清楚操作系统异常处理程序如何知道要查找哪个dll的异常目录。我想它可以使用RIP并咨询进程VAD,然后获取特定分配的第一个地址并调用{{1 }}。

异常过滤器

使用SEH的示例函数:

RtlLookupFunctionEntry

假设BOOL SafeDiv(INT32 dividend, INT32 divisor, INT32 *pResult) { __try { *pResult = dividend / divisor; } __except(GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { return FALSE; } return TRUE; } 是在Java中使用的。它完全等同于:

catch (ArithmeticException a){//do something}

括号中的过滤器字符串由前面的__except(GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) {//do something} 值指向。过滤器始终等于ExceptionHandlerEXCEPTION_CONTINUE_SEARCHEXCEPTION_EXECUTE_HANDLEREXCEPTION_CONTINUE_EXECUTIONGetExceptionCode中获取ExceptionCodewindows specific error constant),该_EXCEPTION_RECORD可能是IDT中的特定异常处理程序使用错误代码和异常号创建的。 (_EXCEPTION_RECORD存储在可以通过调用访问的位置)。将其与特定错误进行比较,EXCEPTION_INT_DIVIDE_BY_ZERO将使用ArithmeticException。如果过滤器表达式的计算结果为EXCEPTION_EXECUTE_HANDLER,那么它将跳至JumpTarget,否则我会想像它会寻找范围更广的ScopeRecord。如果它用尽了ScopeRecord来覆盖错误指令的RIP,那么它需要调用在线程创建本身上定义的try块的异常,如下所示:

VOID
WINAPI
BaseProcessStartup(PPROCESS_START_ROUTINE lpStartAddress)
{
 DPRINT("BaseProcessStartup(..) - setting up exception frame.\n");

 _SEH2_TRY
 {
     /* Set our Start Address */
     NtSetInformationThread(NtCurrentThread(),
                            ThreadQuerySetWin32StartAddress,
                            &lpStartAddress,
                            sizeof(PPROCESS_START_ROUTINE));

     /* Call the Start Routine */
     ExitThread(lpStartAddress());
 }
 _SEH2_EXCEPT(UnhandledExceptionFilter(_SEH2_GetExceptionInformation()))
 {
     /* Get the Exit code from the SEH Handler */
     if (!BaseRunningInServerProcess)
     {
         /* Kill the whole process, usually */
         ExitProcess(_SEH2_GetExceptionCode());
     }
     else
     {
         /* If running inside CSRSS, kill just this thread */
         ExitThread(_SEH2_GetExceptionCode());
     }
 }
 _SEH2_END;
 }

如果当前未在调试应用程序,则被调用的未处理过滤器将返回EXCEPTION_EXECUTE_HANDLER,该过滤器除并终止线程/进程外,将进行调用。如果此操作系统异常分发代码必须使用的每个线程都指向上述try / except块,则应该使用ScopeRecord。如果将其存储在ETHREAD结构或其他内容中,或者将_RUNTIME_FUNCTION写入描述了初始化并调用线程(BaseProcessStartup)的函数的图像,这将是有意义的,但是请记住, RIP将位于BaseProcessStartup内部,该内核将具有内核RIP,因此无法在VAD空间中查找模块,因此OS异常处理程序也可能具有检查RIP是否为内核模式的函数。地址,然后知道过滤器的偏移量,因为长度和精确函数是内核函数,因此众所周知。

UnhandledExceptionFilter将调用SetUnhandledExceptionFilter中指定的过滤器,该过滤器以别名GlobalTopLevelExceptionFilter存储在进程地址空间中,该别名在我认为的kernel32.dll动态链接上初始化。 >

序言和结语例外

_RUNTIME_FUNCTION结构描述的函数中,该函数的序言或结尾以及函数主体中都可能发生异常。序言是函数调用的一部分,它协商参数传递,调用约定和将参数CS:RIP,RBP推入堆栈。结语是该过程的逆转,即从函数返回。编译器将在序言中发生的每个动作存储在UnwindCodes数组中;每个动作都由一个2字节的UNWIND_CODE结构表示,该结构包含序言中的偏移量的成员(1个字节),展开操作代码(4位)和operation info(4位)。

在找到{RIP}范围内的_RUNTIME_FUNCTION之后,在调用_C_specific_handler之前,操作系统异常处理代码检查RIP是否位于定义的BeginAddressBeginAddress + SizeOfProlog之间分别位于_RUNTIME_FUNCTION_UNWIND_INFO结构中。如果是,则在UnwindCodes数组中查找第一个条目,其偏移量小于或等于RIP从函数开始的偏移量。然后,它将按顺序撤消阵列中描述的所有操作。这些操作之一可能是UWOP_PUSH_MACHFRAME,它表示已推送陷阱帧。恢复该陷阱帧将使RIP现在变为函数调用之前的RIP。一旦撤消了操作,将在调用函数之前使用RIP重新启动该过程。现在,操作系统异常处理将使用此RIP查找_RUNTIME_FUNCTION,该_C_specific_handler将是调用函数的_UNWIND_INFO。现在它将在调用函数的主体中,因此可以调用父ScopeRecord的{​​{1}}来扫描BeginAddress

如果RIP不在BeginAddress + SizeOfProlog-SizeOfEpilog范围内,则它将检查RIP之后的代码流,如果它与合法尾声的结尾部分匹配,则它在尾声中(奇怪不仅定义了EndAddress并从_CONTEXT_RECORD中减去,而且我们开始了),并且结语的其余部分使用_RUNTIME_FUNCTION结构进行了仿真,并随着每条指令的处理而更新。现在,RIP将紧接在调用函数中的函数调用之后,因此将像在序言中一样再次拾取父级的_C_specific_handler,它将用于处理异常。

如果它既不在序言中也未在结尾中,则它将按原样调用_RUNTIME_FUNCTION中的_RUNTIME_FUNCTION

另一个值得一提的情况是,如果该函数是叶子函数,则它将没有_RUNTIME_FUNCTION记录,因为叶子函数不会调用任何其他函数或在堆栈上分配任何局部变量。因此,RSP直接寻址返回指针。 [RSP]处的返回指针存储在更新的上下文中,模拟的RSP递增8,然后寻找另一个EXCEPTION_CONTINUE_SEARCH

展开

当过滤器返回EXCEPTION_EXECUTE_HANDLER而不是UnwindCode时,它需要从函数中返回,这称为展开。为此,它只需像之前一样通过_RUNTIME_FUNCTION数组,并撤消将函数恢复到函数调用之前的CPU状态的所有操作-不必担心本地变量,因为它们会在以太帧向下移动时会掉到以太身上。然后,它将查找父函数的__C_specific_handler,并将调用JumpTarget。如果异常得到处理,则它将控制权传递给EXCEPTION_EXECUTE_HANDLER处的except块,然后继续正常执行。如果未处理(即,过滤器表达式的计算结果不为BaseProcessStartup,则它将继续展开堆栈,直到到达UnhandledExceptionFilter(_SEH2_GetExceptionInformation())并且RIP处于该函数的范围之内,这意味着未处理异常。就像我之前说的,它可以识别出它是一个内核地址和一个异常过滤器表达式的索引,该索引恰好是SetUnhandledExceptionFilter,如果正在调试进程,则它将在其中传递给调试器。会使用EXCEPTION_EXECUTE_HANDLER调用自定义过滤器集,该过滤器集将执行一些操作,但必须返回EXCEPTION_EXECUTE_HANDLER,否则将仅返回{{1}}。

Windows x86 SEH

x86使用基于堆栈的异常处理,而不是x64使用的基于表的异常处理。这使它容易受到缓冲区溢出攻击///我将在以后继续

答案 1 :(得分:0)

您可以在Microsoft's MSDN找到有关RUNTIME_FUNCTION及相关结构的更多信息。

这些结构由编译器生成并用于实现structured exception handling。在执行代码期间,可能会发生异常,并且运行时系统需要能够在调用堆栈中向上查找该异常的处理程序。为此,运行时系统需要知道函数prolog的布局,它们保存哪些寄存器,以便正确展开各个函数堆栈帧。更多详细信息为here

RUNTIME_FUNCTION是描述单个函数的结构,它包含展开它所需的数据。

如果您在运行时生成代码并且需要将该代码提供给运行时系统(因为您的代码调用可能引发异常的已编译代码),那么您为每个生成的代码创建RUNTIME_FUNCTION个实例函数,为每个函数填写UNWIND_INFO,然后通过调用RtlAddFunctionTable告诉运行时系统。