我在IDA的RUNTIME_FUNCTION结构的.pdata段中发现了一个大型数组。 所以,我可以在哪里找到信息:从它编译的内容,我如何创建它以及如何在C ++中使用它。 请给我书籍,或带有良好描述的链接以及使用此结构进行异常处理和展开的教程。
答案 0 :(得分:1)
编译器将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}
值指向。过滤器始终等于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
。如果它用尽了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是否位于定义的BeginAddress
和BeginAddress + 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}}。
x86使用基于堆栈的异常处理,而不是x64使用的基于表的异常处理。这使它容易受到缓冲区溢出攻击///我将在以后继续
答案 1 :(得分:0)
您可以在Microsoft's MSDN找到有关RUNTIME_FUNCTION及相关结构的更多信息。
这些结构由编译器生成并用于实现structured exception handling。在执行代码期间,可能会发生异常,并且运行时系统需要能够在调用堆栈中向上查找该异常的处理程序。为此,运行时系统需要知道函数prolog的布局,它们保存哪些寄存器,以便正确展开各个函数堆栈帧。更多详细信息为here。
RUNTIME_FUNCTION是描述单个函数的结构,它包含展开它所需的数据。
如果您在运行时生成代码并且需要将该代码提供给运行时系统(因为您的代码调用可能引发异常的已编译代码),那么您为每个生成的代码创建RUNTIME_FUNCTION个实例函数,为每个函数填写UNWIND_INFO,然后通过调用RtlAddFunctionTable告诉运行时系统。