异常处理利用
本文最后更新于:2025年12月31日 晚上
这不对吧()
原理浅叙
这里主要记录一个C++里关于异常机制的利用原理。其实也算一个很老的trick了,它的主要作用是绕过canary防护。
C++里的异常大概原理为:使用try-catch选定一个代码块,try是监控这里的代码有没有异常出现,catch是用来处理异常。
在编写C++程序的时候可以根据程序运行的情况决定是否要抛出一个异常。这里的用法是使用throw这个关键字。
throw这个特殊关键字会被解析为如下过程和函数调用
- 调用__cxa_allocate_exception,这个函数会为一个异常信息exception指针分配结构。
- 根据throw抛出的异常,把抛出异常信息填入exception结构。
- 调用__cxa_throw,将异常抛出。
异常被抛出后,会立刻转移到相应catch代码块内,剩余的代码都不会继续执行。当然也有可能存在不适配此异常的catch,这样就会导致程序崩溃。
接下来是CTF中的重点:如果异常发生于一个函数中,而这个函数不存在适配catch代码块。程序会到该函数的调用者的代码里寻找catch块。如果找到,则就此执行这个catch的代码,触发异常的函数,和调用者从函数调用点之后直到try结束的代码,都不会被执行。这个过程可以沿着函数调用链不断向上追溯。
实现这个追溯行为的函数似乎叫unwind,在__cxa_throw里有调用这个函数,不过具体的我名字记不清了()那么这个函数是如何实现沿着函数调用链追溯调用者的try-catch呢?这是一个复杂的过程,我还没有彻底研究明白,但是可以确定的一点就是这个过程依赖于函数栈帧结构,对于调用者函数的定位,完全取决于函数的返回地址。同时栈回退的过程也伴随着RBP和RSP的重新设置,因此也有使用栈迁移的机会。
在有溢出但有canary保护的场景中,如果有机会异常处理机制,则可以利用异常处理的回退机制,绕过正常函数返回时的canary保护。这样可以把函数劫持到别的函数try-catch块,或者直接攻击没有canary保护的函数栈帧的返回地址,或者结合栈迁移实现其他功能。
需要注意一点,现在的防护有针对异常处理环境下溢出行为的防护。有时候在__cxa_throw之前程序会检查canary是否被破坏,这种情况就很难发挥了。