c++异常处理-漏洞利用
字数 1802 2025-08-20 18:17:59
C++异常处理机制与漏洞利用分析
1. C++异常处理基础
1.1 异常处理流程
C++异常处理的基本流程如下:
- 使用
throw抛出异常 - 从当前函数开始查找匹配的
catch块 - 如果当前函数没有匹配的
catch块,则清除当前函数的栈帧 - 回溯到调用当前函数的函数继续查找
- 重复上述过程直到找到匹配的
catch块或程序终止
1.2 异常处理示例
#include <iostream>
#include <stdexcept>
void funcB() {
throw std::runtime_error("Error in function B");
}
void funcA() {
try {
funcB();
} catch (const std::exception& e) {
std::cout << "Caught in A: " << e.what() << std::endl;
}
}
int main() {
try {
funcA();
} catch (...) {
std::cout << "Caught in main" << std::endl;
}
return 0;
}
在这个例子中,funcB抛出异常,funcA尝试捕获并处理。如果funcA没有捕获,异常会继续向上传递到main函数。
2. .eh_frame段的作用
2.1 .eh_frame段内容
.eh_frame段包含支持异常处理的数据结构,主要包括:
- 函数入口点:每个函数开始执行的位置
- 着陆垫(Landing Pad):异常发生后控制流跳转的位置
- 调用帧信息:如何恢复调用者状态(堆栈指针、寄存器等)
- 异常过滤信息:指定哪些类型的异常应由哪个
catch块处理
2.2 运行时异常处理流程
当程序运行并抛出异常时,运行时系统会:
- 查找最近的未处理异常的
catch块 - 使用
.eh_frame中的信息计算如何跳转到对应的catch块 - 执行必要的堆栈展开操作,恢复调用者状态
3. 堆栈展开机制
3.1 堆栈展开步骤
- 识别异常处理程序:通过
.eh_frame找到最近的合适catch块 - 恢复调用者状态:
- 释放局部变量占用的堆栈空间
- 调用局部对象的析构函数
- 重置CPU寄存器到调用前的状态
- 控制流转移:跳转到异常处理器(
catch块)开始执行
3.2 堆栈展开示例
#include <iostream>
#include <unistd.h>
class x {
public:
char buf[0x10];
x(void) { printf("x:x() called\n"); }
~x(void) { printf("x:~x() called\n"); }
};
void test() {
x a;
int cnt = 0x100;
size_t len = read(0, a.buf, cnt);
if (len > 0x10) {
throw "Buffer overflow";
}
}
int main() {
try {
test();
throw 1;
} catch (int x) {
printf("Int: %d\n", x);
} catch (const char* s) {
printf("String: %s\n", s);
}
return 0;
}
4. 异常处理漏洞利用
4.1 基本利用原理
throw函数会析构当前函数的变量- 如果没有合适的
catch处理,会进行堆栈展开并跳转到上一个函数的catch部分 __cxa_throw最终执行到_Unwind_Resume时会恢复rbp到原来的值- 通过覆盖
rbp和返回地址可以控制程序流
4.2 关键利用点
-
返回地址篡改:
- 可以篡改到
try部分或try和catch之间的代码 _Unwind_RaiseException会根据返回地址搜寻合适的catch块- 找到合适的
catch后才会析构当前函数对象并进入_Unwind_Resume
- 可以篡改到
-
__cxa_call_unexpected利用:
- 当异常未被捕获时,控制权会转交给
__cxa_call_unexpected - 在Ubuntu 22.04上会调用
__cxa_call_unexpected_cold - 最终可能进入
__terminate,其执行的函数指针来自寄存器r12
- 当异常未被捕获时,控制权会转交给
4.3 完整利用示例
#include <iostream>
#include <unistd.h>
#include <cstdlib>
class x {
public:
char buf[0x10];
x(void) { printf("x:x() called\n"); }
~x(void) { printf("x:~x() called\n"); }
};
void backdoor() {
system("/bin/sh");
}
void test() {
x a;
int cnt = 0x100;
size_t len = read(0, a.buf, cnt);
if (len > 0x10) {
throw "Buffer overflow";
}
}
int main() {
try {
test();
throw 1;
} catch (int x) {
printf("Int: %d\n", x);
} catch (const char* s) {
printf("String: %s\n", s);
}
return 0;
}
编译命令:
g++ -no-pie -g -static llk.c -o llk
5. 分析工具与方法
5.1 readelf工具
使用readelf分析.eh_frame段:
readelf -wF file
选项说明:
-w:显示DWARF调试信息-F:显示DWARF信息中的框架信息(Frame Information)
5.2 异常处理详细流程
- 异常发生时的堆栈保存:保存PC、SP及其他寄存器状态
- 查找.eh_frame信息:根据PC查找对应的FDE(Frame Description Entry)
- 解析FDE并恢复CFA:解析CFA(Canonical Frame Address)和寄存器偏移量
- 恢复寄存器:根据FDE规则从堆栈恢复寄存器值
- 堆栈展开:逐步弹出函数调用栈中的帧,直到找到匹配的
catch块 - 转向异常处理逻辑:控制权转移给
catch块代码
6. 防御措施
- 输入验证:严格检查所有输入数据的长度和内容
- 堆栈保护:启用栈保护机制如Canary、ASLR等
- 异常处理安全:
- 确保所有异常都被正确处理
- 避免在异常处理中使用不安全函数
- 代码审计:定期检查异常处理代码的安全性
7. 总结
C++异常处理机制通过.eh_frame段和运行时库的协作实现,了解其内部机制有助于发现和利用相关漏洞。关键点包括:
- 异常传播路径和堆栈展开过程
.eh_frame段的结构和作用__cxa_throw和_Unwind_Resume等关键函数的行为- 通过覆盖返回地址和控制寄存器实现利用的方法
通过深入理解这些机制,安全研究人员可以更好地分析和防御相关漏洞。