我在IDA的RUNTIME_FUNCTION结构的.pdata段中找到了一个大型数组。所以,我可以在哪里找到信息:从它编译的内容,我如何创建它以及如何在C ++中使用它。请给我书,或带有良好描述的链接以及使用此结构进行异常处理和展开的教程。
您可以在Microsoft's MSDN找到有关RUNTIME_FUNCTION和相关结构的更多信息。
这些结构由编译器生成并用于实现structured exception handling。在执行代码期间,可能会发生异常,并且运行时系统需要能够在调用堆栈中向上查找该异常的处理程序。为此,运行时系统需要知道函数prolog的布局,它们保存哪些寄存器,以便正确展开各个函数堆栈帧。更多细节是here。
RUNTIME_FUNCTION是描述单个函数的结构,它包含展开它所需的数据。
如果您在运行时生成代码并且需要将该代码提供给运行时系统(因为您的代码调用已编译的代码可能引发异常),那么您为每个生成的函数创建RUNTIME_FUNCTION实例,填写UNWIND_INFO以获取每个,然后通过调用RtlAddFunctionTable告诉运行时系统。
编译器将exception directory放在.exe映像的.pdata部分中。编译器使用_RUNTIME_FUNCTION
s填充异常目录。
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_SEARCH
,EXCEPTION_EXECUTE_HANDLER
或EXCEPTION_CONTINUE_EXECUTION
。 GetExceptionCode
从ExceptionCode
获取windows specific error constant(_EXCEPTION_RECORD
),这可能是由IDT中的特定异常处理程序使用错误代码和异常编号创建的。 (_EXCEPTION_RECORD
存储在可以通过呼叫访问的某个地方)。它与特定的错误进行比较,EXCEPTION_INT_DIVIDE_BY_ZERO
将被ArithmeticException
使用。如果过滤器表达式的计算结果为EXCEPTION_EXECUTE_HANDLER
,那么它将跳转到JumpTarget
,否则我会想象它会寻找范围更广的ScopeRecord
。如果它用完了覆盖故障指令的RIP的ScopeRecord
s,那么它需要调用在线程创建本身定义的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是否分别位于BeginAddress
和BeginAddress + SizeOfProlog
结构中定义的_RUNTIME_FUNCTION
和_UNWIND_INFO
之间。如果是,那么它会查看第一个条目的UnwindCodes
数组,其偏移量小于或等于RIP与函数start的偏移量。然后它按顺序撤消数组中描述的所有操作。其中一个动作可能是UWOP_PUSH_MACHFRAME
,表示已经推动了陷阱框架。恢复此陷阱帧将导致RIP现在成为函数调用之前的RIP。一旦操作撤消,在函数调用之前使用RIP重新启动该过程; OS异常处理现在将使用此RIP来查找_RUNTIME_FUNCTION
,它将是调用函数的_C_specific_handler
。现在这将在调用函数的主体中,因此现在可以调用父_UNWIND_INFO
的ScopeRecord
来扫描BeginAddress
s。
如果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。
x86使用基于堆栈的异常处理,而不是x64使用的基于表的。这使得它容易受到缓冲区溢出攻击的影响//我将在稍后继续