结构RUNTIME_FUNCTION

jDe*_*Dek 1 c++ exception-handling stack-unwinding

我在IDA的RUNTIME_FUNCTION结构的.pdata段中找到了一个大数组。因此,在哪里可以找到信息:从它的编译,如何创建以及如何在C ++中使用信息。请给我书籍,或提供具有良好描述和教程的链接,以使用此结构来处理异常和消除异常。

Lew*_*sey 6

Windows x64 SEH

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

typedef struct _RUNTIME_FUNCTION {
 ULONG BeginAddress;
 ULONG EndAddress;
 ULONG UnwindData;
} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION;
Run Code Online (Sandbox Code Playgroud)

每个_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;
Run Code Online (Sandbox Code Playgroud)

如果UNW_FLAG_EHANDLER设置了if ,则ExceptionHandler指向一个名为_C_specific_handler的通用处理程序,其目的是解析ExceptionDatawhich指向一个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;
Run Code Online (Sandbox Code Playgroud)

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

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

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

还不清楚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;
} 
Run Code Online (Sandbox Code Playgroud)

假设catch (ArithmeticException a){//do something}在Java中使用过。它完全等同于:

__except(GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ? 
         EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) {//do something}
Run Code Online (Sandbox Code Playgroud)

括号中的过滤器字符串由之前的ExceptionHandler值指向。该过滤器是始终或者等于EXCEPTION_CONTINUE_SEARCHEXCEPTION_EXECUTE_HANDLEREXCEPTION_CONTINUE_EXECUTIONGetExceptionCodeExceptionCode特定Windows的错误常数)获取_EXCEPTION_RECORDID,该ID可能是由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;
 }
Run Code Online (Sandbox Code Playgroud)

如果当前未在调试应用程序,则未调用的过滤器将被调用,但将EXCEPTION_EXECUTE_HANDLER终止并终止线程/进程。如果ScopeRecord每个线程都有一个此OS异常分派代码必须使用的指向上面的try / except块,这将是有道理的。如果将其存储在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位)和操作信息(4位)的成员。

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

如果RIP不在范围内BeginAddress- BeginAddress + SizeOfProlog那么它将检查RIP之后的代码流,并且如果它与合法尾声的结尾部分匹配,那么它就在尾声中(奇怪的是,它不只是定义SizeOfEpilog并从中减去,EndAddress而是在那里转到结尾),并用_CONTEXT_RECORD其构建的结构模拟结语的其余部分,并在处理每条指令时对其进行更新。现在,RIP将立即在调用函数中的函数调用之后,因此_RUNTIME_FUNCTION将像在序言中一样再次拾取父级的RIP ,并将其用于处理异常。

如果它既不在序言中也不在序言中,那么它将_C_specific_handler_RUNTIME_FUNCTION原样调用的。

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

放卷

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

Windows x86 SEH

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


Jen*_*ens 1

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

这些结构由编译器生成并用于实现结构化异常处理。在代码执行期间可能会发生异常,运行时系统需要能够遍历调用堆栈以找到该异常的处理程序。为此,运行时系统需要知道函数序言的布局以及它们保存的寄存器,以便正确展开各个函数堆栈帧。更多详细信息请参见此处

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

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