分享一次 C++ PWN 出题经历——深入研究异常处理机制
字数 1728 2025-08-22 12:22:15
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
程序保护:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
漏洞点:
input()函数中的read存在栈溢出- 输入超过0x10字节会抛出异常
利用方法:
- 覆盖RBP为GOT表地址-8
- 异常处理时会从GOT表取函数指针执行
EXP关键部分:
padding = 'a' * 0x30
poc2 = padding + p64(0x404050-0x8) # 覆盖RBP为puts GOT entry-8
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关键部分:
# 通过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 常见防御措施
- 栈保护(Canary)
- 地址随机化(ASLR/PIE)
- 不可执行内存(NX)
- 完整性检查
4.2 绕过技术
- 利用异常处理绕过canary检查
- 通过栈回退绕过栈保护
- 劫持虚表指针实现任意代码执行
- 篡改异常字符串实现参数控制
五、总结与思考
5.1 技术要点总结
- C++异常处理机制提供了绕过传统防御的新途径
- 栈回退过程中存在多个可控点(RBP、返回地址等)
- 异常处理流程可以跳过常规的安全检查
- 析构函数和虚表调用是常见的利用点
5.2 扩展思考
- 如何检测和防御这类异常处理机制的滥用?
- 其他语言(如Java、Python)的异常处理机制是否存在类似问题?
- 现代C++特性(如智能指针)对这类漏洞的影响
- 编译器优化对异常处理流程的改变及其安全影响