Struct RUNTIME_FUNCTION

问题描述 投票:0回答:2

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

c++ exception-handling stack-unwinding
2个回答
0
投票

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

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

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

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


1
投票

Windows x64 SEH

编译器将exception directory放在.exe映像的.pdata部分中。编译器使用_RUNTIME_FUNCTIONs填充异常目录。

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

为了补充这一点,对于嵌套异常,我认为它总是会找到覆盖当前故障指令的最小范围,并且不会处理更大范围的异常。

还没有明确OS异常处理程序如何知道要查看哪个dll的异常目录。我想它可以使用RIP并查询进程VAD然后获取特定分配的第一个地址并在其上调用RtlLookupFunctionEntry

异常过滤器

使用SEH的示例函数:

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;
} 

假设catch (ArithmeticException a){//do something}用于java。它完全等同于:

__except(GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ? 
         EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) {//do something}

括号中的过滤字符串由前面的ExceptionHandler值指向。过滤器总是等于EXCEPTION_CONTINUE_SEARCHEXCEPTION_EXECUTE_HANDLEREXCEPTION_CONTINUE_EXECUTIONGetExceptionCodeExceptionCode获取windows specific error constant_EXCEPTION_RECORD),这可能是由IDT中的特定异常处理程序使用错误代码和异常编号创建的。 (_EXCEPTION_RECORD存储在可以通过呼叫访问的某个地方)。它与特定的错误进行比较,EXCEPTION_INT_DIVIDE_BY_ZERO将被ArithmeticException使用。如果过滤器表达式的计算结果为EXCEPTION_EXECUTE_HANDLER,那么它将跳转到JumpTarget,否则我会想象它会寻找范围更广的ScopeRecord。如果它用完了覆盖故障指令的RIP的ScopeRecords,那么它需要调用在线程创建本身定义的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并终止线程/进程。如果这个操作系统异常调度代码必须使用的每个线程都有一个ScopeRecord指向上面的try / except块,那将是有意义的。如果它存储在ETHREAD结构中或某些东西或者如果_RUNTIME_FUNCTION被写入描述初始化和调用线程的函数的图像(BaseProcessStartup),那将是有意义的,但请记住,RIP将在BaseProcessStartup内部,这将具有内核RIP,因此查找VAD空间中的模块将无法工作,因此OS Exception处理程序可能还有一个函数可以检查RIP是否是内核模式地址,然后它知道过滤器的偏移量,因为长度和精确函数是预先知道的,因为它是一个核函数。

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

Prologue和Epilogue例外

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

在找到RIP在其范围内的_RUNTIME_FUNCTION之后,在调用_C_specific_handler之前,OS异常处理代码检查RIP是否分别位于BeginAddressBeginAddress + SizeOfProlog结构中定义的_RUNTIME_FUNCTION_UNWIND_INFO之间。如果是,那么它会查看第一个条目的UnwindCodes数组,其偏移量小于或等于RIP与函数start的偏移量。然后它按顺序撤消数组中描述的所有操作。其中一个动作可能是UWOP_PUSH_MACHFRAME,表示已经推动了陷阱框架。恢复此陷阱帧将导致RIP现在成为函数调用之前的RIP。一旦操作撤消,在函数调用之前使用RIP重新启动该过程; OS异常处理现在将使用此RIP来查找_RUNTIME_FUNCTION,它将是调用函数的_C_specific_handler。现在这将在调用函数的主体中,因此现在可以调用父_UNWIND_INFOScopeRecord来扫描BeginAddresss。

如果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,如果不是它将只返回qazxswpoi。

Windows x86 SEH

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

© www.soinside.com 2019 - 2024. All rights reserved.