分享一次 C++ PWN 出题经历——深入研究异常处理机制
字数 1728 2025-08-22 12:22:15

C++异常处理机制与PWN利用技术深度解析

一、C++异常处理机制原理

1.1 基本概念

C++异常处理机制主要由三个关键字组成:

  • throw:抛出异常
  • try:包含可能抛出异常的代码块
  • catch:捕获并处理特定类型的异常

1.2 异常处理流程

  1. 当异常被抛出时,立即引发C++的异常捕获机制
  2. 如果在当前函数内没有匹配的catch块,异常会沿着调用链向上传播
  3. 调用链上的每个函数都会尝试匹配catch块
  4. 如果最终没有找到匹配的catch块,程序调用std::terminate()终止

1.3 栈回退(Stack Unwind)

  • 从抛出异常开始,沿着调用链寻找匹配catch块的过程称为栈回退
  • 栈回退过程中会执行局部对象的析构函数
  • 发生异常的函数的剩余部分不会被执行

二、异常处理机制的漏洞利用

2.1 利用方式一:篡改RBP实现栈迁移

原理

  • 通过溢出漏洞篡改RBP寄存器
  • 在异常处理流程中,handler会执行leave; ret指令
  • 控制RBP可以间接控制返回地址

关键点

  1. 需要函数调用遵循"通过将old_rbp存储在栈中"的约定
  2. 出现异常的函数的调用者(caller)必须存在处理对应异常的catch块
  3. 可以绕过canary保护,因为异常处理跳过了stack_check_fail

利用步骤

  1. 计算输入点与RBP的距离
  2. 构造payload覆盖RBP为目标地址-8
  3. 异常处理时会从[rbp+8]处取返回地址

2.2 利用方式二:劫持RET执行目标catch块

原理

  • _Unwind_RaiseException会检查调用链上的函数是否有匹配的catch块
  • 通过劫持返回地址,可以强制程序执行特定函数的catch块

关键点

  1. 需要目标函数有能处理对应类型异常的catch块
  2. 需要拥有合法的RBP值
  3. 可以执行从未被调用的函数中的catch块

利用步骤

  1. 找到目标catch块的地址范围
  2. 构造payload覆盖返回地址为目标catch块内地址
  3. 确保异常类型与catch块匹配

三、实战案例分析

3.1 案例一:自定义异常处理demo

程序保护

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

漏洞点

  • input()函数中的read存在栈溢出
  • 输入超过0x10字节会抛出异常

利用方法

  1. 覆盖RBP为GOT表地址-8
  2. 异常处理时会从GOT表取函数指针执行

EXP关键部分

padding = 'a' * 0x30
poc2 = padding + p64(0x404050-0x8)  # 覆盖RBP为puts GOT entry-8

3.2 案例二:N1CTF2023 - n1canary

程序特点

  • 自定义canary机制
  • 漏洞点在launch()函数的栈溢出
  • 通过raise()抛出异常绕过canary检查

利用链

  1. 溢出修改栈上的BOFApp对象虚表指针
  2. 触发异常导致栈回退
  3. 析构函数中通过虚表调用执行shellcode

关键点

  • 利用溢出修改存储在栈上的BOFApp对象的虚函数表指针
  • 异常处理绕过了自定义canary和系统canary的检测
  • 最终在析构函数中实现任意代码执行

3.3 案例三:2024羊城杯 - logger

创新点

  • 通过数组越界漏洞篡改抛出异常的字符串
  • 劫持异常处理流程到后门catch块

利用步骤

  1. 通过trace功能的OOB漏洞修改异常字符串为/bin/sh
  2. 通过溢出漏洞劫持异常处理到后门catch块
  3. 执行system("/bin/sh")

EXP关键部分

# 通过OOB修改异常字符串
for i in range(7):
    trace()
trace('a'*0x10, 'n')
payload = '/bin/sh;'
trace(payload)

# 通过溢出劫持异常处理
menu(2)
payload = 'a' * 0x70
payload += p64(0X404300)  # 控制RBP
payload += p64(0x401BC2 + 1)  # 后门catch块地址

四、防御与绕过技术

4.1 常见防御措施

  1. 栈保护(Canary)
  2. 地址随机化(ASLR/PIE)
  3. 不可执行内存(NX)
  4. 完整性检查

4.2 绕过技术

  1. 利用异常处理绕过canary检查
  2. 通过栈回退绕过栈保护
  3. 劫持虚表指针实现任意代码执行
  4. 篡改异常字符串实现参数控制

五、总结与思考

5.1 技术要点总结

  1. C++异常处理机制提供了绕过传统防御的新途径
  2. 栈回退过程中存在多个可控点(RBP、返回地址等)
  3. 异常处理流程可以跳过常规的安全检查
  4. 析构函数和虚表调用是常见的利用点

5.2 扩展思考

  1. 如何检测和防御这类异常处理机制的滥用?
  2. 其他语言(如Java、Python)的异常处理机制是否存在类似问题?
  3. 现代C++特性(如智能指针)对这类漏洞的影响
  4. 编译器优化对异常处理流程的改变及其安全影响
C++异常处理机制与PWN利用技术深度解析 一、C++异常处理机制原理 1.1 基本概念 C++异常处理机制主要由三个关键字组成: throw :抛出异常 try :包含可能抛出异常的代码块 catch :捕获并处理特定类型的异常 1.2 异常处理流程 当异常被抛出时,立即引发C++的异常捕获机制 如果在当前函数内没有匹配的catch块,异常会沿着调用链向上传播 调用链上的每个函数都会尝试匹配catch块 如果最终没有找到匹配的catch块,程序调用 std::terminate() 终止 1.3 栈回退(Stack Unwind) 从抛出异常开始,沿着调用链寻找匹配catch块的过程称为栈回退 栈回退过程中会执行局部对象的析构函数 发生异常的函数的剩余部分不会被执行 二、异常处理机制的漏洞利用 2.1 利用方式一:篡改RBP实现栈迁移 原理 : 通过溢出漏洞篡改RBP寄存器 在异常处理流程中,handler会执行 leave; ret 指令 控制RBP可以间接控制返回地址 关键点 : 需要函数调用遵循"通过将old_ rbp存储在栈中"的约定 出现异常的函数的调用者(caller)必须存在处理对应异常的catch块 可以绕过canary保护,因为异常处理跳过了 stack_check_fail 利用步骤 : 计算输入点与RBP的距离 构造payload覆盖RBP为目标地址-8 异常处理时会从 [rbp+8] 处取返回地址 2.2 利用方式二:劫持RET执行目标catch块 原理 : _Unwind_RaiseException 会检查调用链上的函数是否有匹配的catch块 通过劫持返回地址,可以强制程序执行特定函数的catch块 关键点 : 需要目标函数有能处理对应类型异常的catch块 需要拥有合法的RBP值 可以执行从未被调用的函数中的catch块 利用步骤 : 找到目标catch块的地址范围 构造payload覆盖返回地址为目标catch块内地址 确保异常类型与catch块匹配 三、实战案例分析 3.1 案例一:自定义异常处理demo 程序保护 : 漏洞点 : input() 函数中的 read 存在栈溢出 输入超过0x10字节会抛出异常 利用方法 : 覆盖RBP为GOT表地址-8 异常处理时会从GOT表取函数指针执行 EXP关键部分 : 3.2 案例二:N1CTF2023 - n1canary 程序特点 : 自定义canary机制 漏洞点在 launch() 函数的栈溢出 通过 raise() 抛出异常绕过canary检查 利用链 : 溢出修改栈上的BOFApp对象虚表指针 触发异常导致栈回退 析构函数中通过虚表调用执行shellcode 关键点 : 利用溢出修改存储在栈上的BOFApp对象的虚函数表指针 异常处理绕过了自定义canary和系统canary的检测 最终在析构函数中实现任意代码执行 3.3 案例三:2024羊城杯 - logger 创新点 : 通过数组越界漏洞篡改抛出异常的字符串 劫持异常处理流程到后门catch块 利用步骤 : 通过trace功能的OOB漏洞修改异常字符串为 /bin/sh 通过溢出漏洞劫持异常处理到后门catch块 执行 system("/bin/sh") EXP关键部分 : 四、防御与绕过技术 4.1 常见防御措施 栈保护(Canary) 地址随机化(ASLR/PIE) 不可执行内存(NX) 完整性检查 4.2 绕过技术 利用异常处理绕过canary检查 通过栈回退绕过栈保护 劫持虚表指针实现任意代码执行 篡改异常字符串实现参数控制 五、总结与思考 5.1 技术要点总结 C++异常处理机制提供了绕过传统防御的新途径 栈回退过程中存在多个可控点(RBP、返回地址等) 异常处理流程可以跳过常规的安全检查 析构函数和虚表调用是常见的利用点 5.2 扩展思考 如何检测和防御这类异常处理机制的滥用? 其他语言(如Java、Python)的异常处理机制是否存在类似问题? 现代C++特性(如智能指针)对这类漏洞的影响 编译器优化对异常处理流程的改变及其安全影响