我在 IDA 的 RUNTIME_FUNCTION 结构的 .pdata 段中发现了一个大数组。所以,我可以在哪里找到信息:从它的编译内容,我如何创建它以及如何在 C++ 中使用它。请给我书籍,或带有良好描述和教程的链接,用于异常处理和展开这种结构。
2 回答
Windows x64 SEH
编译器将异常目录放置在图像的.pdata
部分中.exe
,但它也可以放置在任何部分中,例如.rdata
PE 标头所指向的部分NtHeaders64->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION].VirtualAddress
。编译器用 s 填充异常目录RUNTIME_FUNCTION
。
typedef struct _RUNTIME_FUNCTION {
ULONG BeginAddress;
ULONG EndAddress;
ULONG UnwindData;
} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION;
每个都RUNTIME_FUNCTION
描述了图像中的一个功能。程序中的每个函数(叶函数除外)都有一个,无论其中是否存在 SEH 异常子句,因为异常可能发生在被调用函数中,因此您需要展开代码以获取可能具有的调用函数SEH 处理程序,因此大多数函数将具有展开代码,但没有范围表。BeginAddress
指向函数的开始和函数EndAddress
的结束。
UnwindData 指向一个_UNWIND_INFO
表结构。
typedef struct _UNWIND_INFO {
UBYTE Version : 3;
UBYTE Flags : 5;
UBYTE SizeOfProlog;
UBYTE CountOfCodes; //so the beginning of ExceptionData is known as they're both FAMs
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;
标志可以是以下之一:
#define UNW_FLAG_NHANDLER 0
#define UNW_FLAG_EHANDLER 1
#define UNW_FLAG_UHANDLER 2
#define UNW_FLAG_FHANDLER 3
#define UNW_FLAG_CHAININFO 4
如果UNW_FLAG_EHANDLER
设置了则ExceptionHandler
指向一个名为__C_specific_handler
(从 libcmt.lib 导入)的通用处理程序,其目的是解析ExceptionData
类型为 的灵活数组成员SCOPE_TABLE
。如果UNW_FLAG_UHANDLER
设置了,则表示__C_specific_handler
也将用于调用finally 块,即函数内有finally 块。如果UNW_FLAG_CHAININFO
设置了标志,则展开信息结构是次要的,并且在共享异常处理程序/链接信息地址字段中包含一个图像相关指针,该指针RUNTIME_FUNCTION
指向指向主要展开信息的条目。这用于不连续的功能。UNW_FLAG_FHANDLER 表示它是一个“帧处理程序”,我不知道那是什么。
typedef struct _SCOPE_TABLE {
ULONG Count;
struct
{
ULONG BeginAddress;
ULONG EndAddress;
ULONG HandlerAddress;
ULONG JumpTarget;
} ScopeRecord[1];
} SCOPE_TABLE, *PSCOPE_TABLE;
该SCOPE_TABLE
结构是一个可变长度结构,ScopeRecord
函数中的每个 try 块都有一个,并且包含 try 块的开始和结束地址(可能是 RVA)。HandlerAddress
是代码的偏移量,用于计算__except
(EXCEPTION_EXECUTE_HANDLER
表示始终运行异常,因此它类似于 except Exception) 括号中的异常过滤器表达式,并且是与块关联的块JumpTarget
中的第一条指令的偏移量。需要,因为它也是一个灵活的数组成员,并且没有其他方法可以知道这个灵活数组成员之后的数据从哪里开始。如果是 try/finally 块,那么因为 finally 中没有过滤器,而不是__except
__try
CountOfCodes
UnwindCode
HandlerAddress
JumpTarget
用于指向 finally 块的副本,该副本装饰有序言和结尾(在异常上下文中调用它时需要副本,而不是通常在到达 try 块的末尾之后 - 这可以'不会发生异常,因为它在成功完成后永远不会运行,因此异常块始终是独立的并且没有原始副本)。
一旦处理器引发异常,IDT 中的异常处理程序会将异常信息传递给 Windows 中的主要异常处理函数,该函数将找到RUNTIME_FUNCTION
违规指令指针并调用ExceptionHandler
. 如果异常属于函数而不是结尾或序言,那么它将调用__C_specific_handler
. __C_specific_handler
然后将开始遍历所有SCOPE_TABLE
条目以搜索错误指令的匹配项,并希望找到一个__except
涵盖违规代码的语句。(来源)
除此之外,对于嵌套异常,我想__C_specific_handler
总是会找到覆盖当前错误指令的最小范围,并且会在未处理的更大范围的异常中展开。上面源代码的实现__C_specific_handler
显示了对记录的简单迭代,这在实践中不会发生。
也不清楚操作系统异常处理程序如何知道要查看哪个 dll 的异常目录。我想它可以使用 RIP 并咨询进程 VAD,然后获取特定分配的第一个地址并调用RtlLookupFunctionEntry
它。RIP 也可能是驱动程序或 ntoskrnl.exe 中的内核地址;在这种情况下,Windows 异常处理程序将查询这些图像的异常目录,但我不确定它如何从 RIP 获取图像库,因为 VAD 中没有跟踪内核分配。
异常过滤器
使用 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 中是 C++ 代码,它将转换为以下 C++ 代码然后编译(仅理论上,因为实际上EXCEPTION_INT_DIVIDE_BY_ZERO
编译器似乎不会为任何异常对象生成)
__except(GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) {//do something}
HandlerAddress
括号中的过滤器字符串由try 块的范围记录中的值指向。过滤器总是等于EXCEPTION_CONTINUE_SEARCH
或。从可能由 IDT 中的特定异常处理程序使用错误代码和异常编号创建的(windows 特定错误常量) 。(存储在某个地方,以便可以通过调用访问)。它与特定错误进行比较,即. 如果过滤器表达式的计算结果为,那么它将跳转到; 否则,如果它评估为 EXCEPTION_CONTINUE_SEARCH 并且我会想象看起来EXCEPTION_EXECUTE_HANDLER
EXCEPTION_CONTINUE_EXECUTION
GetExceptionCode
ExceptionCode
_EXCEPTION_RECORD
_EXCEPTION_RECORD
EXCEPTION_INT_DIVIDE_BY_ZERO
ArithmeticException
EXCEPTION_EXECUTE_HANDLER
JumpTarget
__C_specific_handler
ScopeRecord
范围更广。如果它用完了ScopeRecord
覆盖错误指令的 RIP 的 s,则__C_specific_handler
返回EXCEPTION_CONTINUE_SEARCH
和 windows 异常处理程序展开堆栈序言并继续使用上下文记录中的新 RIP,它在展开时更改,检查_RUNTIME_FUNCTION
结构。
中有一个 SEH 块,mainCRTStartup
但没有BaseThreadInitThunk
。最终,将到达堆栈的底部 - ,它有一个过滤器表达式,其中包含OSRtlUserThreadStart
的调用(初始化为in ,并由传递给过滤器表达式),它将调用在 中指定的过滤器,这是变量(这是什么设置),它在. 如果应用程序当前没有被调试,那么用户指定的未处理过滤器将被调用并且必须返回,这会导致 except 块被调用并且 except 块使用终止整个进程RtlpUnhandledExceptionFilter(GetExceptionInformation())
RtlpUnhandledExceptionFilter
UnhandledExceptionFilter
kernel32!_BaseDllInitialize
GetExceptionInformation
rcx
HandlerAddress
_C_specific_handler
SetUnhandledExceptionFilter
BasepCurrentTopLevelFilter
SetUnhandledExceptionFilter
kernel32.dll
EXCEPTION_EXECUTE_HANDLER
__C_specific_handler
ZwTerminateProcess
.
序言和尾声例外
在由_RUNTIME_FUNCTION
结构描述的函数中,异常可能发生在函数的序言或尾声以及函数体中,可能在也可能不在 try 块中。序言是函数的一部分,它保存寄存器,将参数存储在堆栈上(如果 - O0
)。结语是这个过程的逆转,即从函数中返回。编译器将序言中发生的每个动作存储在一个UnwindCodes
数组中;每个动作由一个 2 字节UNWIND_CODE
结构表示,其中包含序言中的偏移量(1 字节)、展开操作代码(4 位)和操作信息(4 位)的成员。
在找到 RIP 位于 和 之间的 a 之后,RUNTIME_FUNCTION
在调用 之前,操作系统异常处理代码检查 RIP 是否位于和结构中定义的函数的和之间。如果是,那么它在序言中,并在数组中查找第一个条目,其偏移量小于或等于 RIP 从函数开始的偏移量。然后它按顺序撤消数组中描述的所有操作。这些行动之一可能是BeginAddress
EndAddress
__C_specific_handler
BeginAddress
BeginAddress + SizeOfProlog
RUNTIME_FUNCTION
_UNWIND_INFO
UnwindCodes
UWOP_PUSH_MACHFRAME
这表示已经推送了一个陷阱帧,这可能是内核代码中的情况。最终结果是通过最终撤消调用指令将 RIP 恢复到执行调用指令之前的状态,以及将其他寄存器的值恢复到调用之前的状态。这样做时,它会更新CONTEXT_RECORD
. 一旦动作被撤消,该过程在函数调用之前使用 RIP 重新启动;操作系统异常处理现在将使用此 RIP 来查找RUNTIME_FUNCTION
调用函数的 RIP。这现在将在调用函数的主体中,因此现在可以调用__C_specific_handler
父函数来扫描s 即函数中的 try 块。_UNWIND_INFO
ScopeRecord
如果 RIP 不在范围内BeginAddress
-BeginAddress + SizeOfProlog
然后它检查 RIP 之后的代码流,如果它与合法结尾的尾随部分匹配,则它在结尾,结尾的剩余部分被模拟,并在CONTEXT_RECORD
每条指令时更新被处理。RIP 现在将是调用函数中 call 指令之后的地址,因此它将搜索RUNTIME_FUNCTION
此 RIP 并且它将是父级的 RIP RUNTIME_FUNCTION
,然后其中的范围记录将用于处理异常。
如果它既不在序言也不在尾声中,则它调用__C_specific_handler
展开信息结构中的 来检查 try 块范围记录。如果函数中没有 try 块,则不会有处理程序(当设置UNW_FLAG_EHANDLER
位时,结构的ExceptionHandler
字段UNWIND_INFO
被假定为有效,在这种情况下它将是有效的UNW_FLAG_EHANDLER
),如果有 try 块但RIP 不在任何 try 块的范围内,则展开整个序言。HandlerAddress
如果它在一个 try 块中,那么它会根据该代码返回的值来评估指向的过滤器评估代码,__C_specific_handler
如果返回值是,则要么查找父范围记录EXCEPTION_CONTINUE_SEARCH
(如果没有,展开序幕并寻找父母RUNTIME_FUNCTION
) (通过父我的意思是一个封装的 try 范围和一个调用函数),如果返回值是,EXCEPTION_EXECUTE_HANDLER
那么它会跳转到JumpTarget
. 如果这是一个 try/finally 块,它只会跳转到HandlerAddress
(而不是评估过滤器表达式),这是 finally 代码,然后它就完成了。
另一个值得一提的场景是,如果函数是叶函数,它将没有RUNTIME_FUNCTION
记录,因为叶函数不会调用任何其他函数或在堆栈上分配任何局部变量。因此,RSP 直接寻址返回指针。[RSP] 处的返回指针存储在更新后的上下文中,模拟的 RSP 递增 8,然后查找另一个RUNTIME_FUNCTION
.
放卷
当return而不是时,它需要从函数中__C_specific_handler
返回,这称为展开——它需要撤消函数的序言。放松的反面是“模拟”,这是在结尾处完成的。为此,它调用 中的处理程序,即,它通过前面所述的数组并撤消所有操作以将 CPU 状态恢复到函数调用之前 - 它不必担心本地人因为当它向下移动堆栈帧时,它们会丢失到以太中。展开代码数组用于展开(修改)最初由 Windows 异常处理程序快照的上下文记录。然后它会查看上下文记录中的新 RIP,该 RIP 将落入EXCEPTION_CONTINUE_SEARCH
EXCEPTION_EXECUTE_HANDLER
ExceptionHandler
__C_specific_handler
UnwindCode
RUNTIME_FUNCTION
的父函数,它将调用__C_specific_handler
. 如果异常得到处理,那么它将控制权传递给 except 块,JumpTarget
然后继续正常执行。如果它没有被处理(即过滤器表达式没有计算结果,EXCEPTION_EXECUTE_HANDLER
那么它将继续展开堆栈,直到它到达RtlUserThreadStart
并且 RIP 在该函数的边界内,这意味着异常未处理。
此页面上有一个非常好的图解示例。
__unwind{}
当存在异常或终止处理程序并且函数具有展开代码时,IDA pro 似乎会显示一个子句。
Windows x86 SEH
x86 使用基于堆栈的异常处理,而不是 x64 使用的基于表的异常处理。这使它容易受到缓冲区溢出攻击//我稍后会继续
您可以在Microsoft 的 MSDN中找到有关 RUNTIME_FUNCTION 和相关结构的更多信息。
这些结构由编译器生成并用于实现结构化异常处理。在您的代码执行期间,可能会发生异常,并且运行时系统需要能够沿着调用堆栈查找该异常的处理程序。为此,运行时系统需要知道函数序言的布局,它们保存了哪些寄存器,以便正确展开各个函数堆栈帧。更多细节在这里。
RUNTIME_FUNCTION 是描述单个函数的结构,它包含展开它所需的数据。
如果您在运行时生成代码并且需要使该代码可用于运行时系统(因为您的代码调用了可能引发异常的已编译代码),那么您为每个生成的函数创建RUNTIME_FUNCTION实例,填写UNWIND_INFO为每个,然后通过调用RtlAddFunctionTable告诉运行时系统。