c++异常处理-漏洞利用
字数 1802 2025-08-20 18:17:59

C++异常处理机制与漏洞利用分析

1. C++异常处理基础

1.1 异常处理流程

C++异常处理的基本流程如下:

  1. 使用throw抛出异常
  2. 从当前函数开始查找匹配的catch
  3. 如果当前函数没有匹配的catch块,则清除当前函数的栈帧
  4. 回溯到调用当前函数的函数继续查找
  5. 重复上述过程直到找到匹配的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段包含支持异常处理的数据结构,主要包括:

  1. 函数入口点:每个函数开始执行的位置
  2. 着陆垫(Landing Pad):异常发生后控制流跳转的位置
  3. 调用帧信息:如何恢复调用者状态(堆栈指针、寄存器等)
  4. 异常过滤信息:指定哪些类型的异常应由哪个catch块处理

2.2 运行时异常处理流程

当程序运行并抛出异常时,运行时系统会:

  1. 查找最近的未处理异常的catch
  2. 使用.eh_frame中的信息计算如何跳转到对应的catch
  3. 执行必要的堆栈展开操作,恢复调用者状态

3. 堆栈展开机制

3.1 堆栈展开步骤

  1. 识别异常处理程序:通过.eh_frame找到最近的合适catch
  2. 恢复调用者状态
    • 释放局部变量占用的堆栈空间
    • 调用局部对象的析构函数
    • 重置CPU寄存器到调用前的状态
  3. 控制流转移:跳转到异常处理器(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 基本利用原理

  1. throw函数会析构当前函数的变量
  2. 如果没有合适的catch处理,会进行堆栈展开并跳转到上一个函数的catch部分
  3. __cxa_throw最终执行到_Unwind_Resume时会恢复rbp到原来的值
  4. 通过覆盖rbp和返回地址可以控制程序流

4.2 关键利用点

  1. 返回地址篡改

    • 可以篡改到try部分或trycatch之间的代码
    • _Unwind_RaiseException会根据返回地址搜寻合适的catch
    • 找到合适的catch后才会析构当前函数对象并进入_Unwind_Resume
  2. __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 异常处理详细流程

  1. 异常发生时的堆栈保存:保存PC、SP及其他寄存器状态
  2. 查找.eh_frame信息:根据PC查找对应的FDE(Frame Description Entry)
  3. 解析FDE并恢复CFA:解析CFA(Canonical Frame Address)和寄存器偏移量
  4. 恢复寄存器:根据FDE规则从堆栈恢复寄存器值
  5. 堆栈展开:逐步弹出函数调用栈中的帧,直到找到匹配的catch
  6. 转向异常处理逻辑:控制权转移给catch块代码

6. 防御措施

  1. 输入验证:严格检查所有输入数据的长度和内容
  2. 堆栈保护:启用栈保护机制如Canary、ASLR等
  3. 异常处理安全
    • 确保所有异常都被正确处理
    • 避免在异常处理中使用不安全函数
  4. 代码审计:定期检查异常处理代码的安全性

7. 总结

C++异常处理机制通过.eh_frame段和运行时库的协作实现,了解其内部机制有助于发现和利用相关漏洞。关键点包括:

  1. 异常传播路径和堆栈展开过程
  2. .eh_frame段的结构和作用
  3. __cxa_throw_Unwind_Resume等关键函数的行为
  4. 通过覆盖返回地址和控制寄存器实现利用的方法

通过深入理解这些机制,安全研究人员可以更好地分析和防御相关漏洞。

C++异常处理机制与漏洞利用分析 1. C++异常处理基础 1.1 异常处理流程 C++异常处理的基本流程如下: 使用 throw 抛出异常 从当前函数开始查找匹配的 catch 块 如果当前函数没有匹配的 catch 块,则清除当前函数的栈帧 回溯到调用当前函数的函数继续查找 重复上述过程直到找到匹配的 catch 块或程序终止 1.2 异常处理示例 在这个例子中, 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 堆栈展开示例 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 完整利用示例 编译命令: 5. 分析工具与方法 5.1 readelf工具 使用 readelf 分析 .eh_frame 段: 选项说明: -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 等关键函数的行为 通过覆盖返回地址和控制寄存器实现利用的方法 通过深入理解这些机制,安全研究人员可以更好地分析和防御相关漏洞。