Exploits under exception handling
引入
在栈溢出漏洞中,程序没有控制或错误控制输入的大小导致了该漏洞的产生。那我们很自然会想到能不能用try
throw
catch
的异常机制来捕获栈溢出行为,从而能更直观的获取错误信息,也在一定程度上避免了一些奇怪的错误产生。但在c++的异常处理实现中,如果我们放任输入数据超过缓冲区大小,冀以异常机制来捕获栈溢出,结果将不尽如人意,甚至还会导致canary保护机制的绕过。
原理&题目分析
这里借助题目对其原理进行理解,其中加入了个人的一些推测和理解,有问题处希望师傅们批评指正。
[2024 羊城杯]logger




这里我一开始参考
DASCTF X GFCTF 2024四月-pwn-control【异常机制】这个视频,想要覆盖掉rbp打栈迁移,实际从throw触发异常到catch捕获的主逻辑在__cxa_throw
的_Unwind_RaiseException
函数里,其中
_Unwind_RaiseException
是用于栈展开(stack
unwinding)的关键函数之一,其主要功能如下:
- 这个函数负责执行栈展开,即遍历当前调用栈上的各个栈帧,寻找匹配的
catch
块。 - 栈展开过程中,函数会逐帧回溯,并在每个栈帧上调用编译器生成的处理函数来检查是否存在与异常类型匹配的
catch
块。 - 如果找到匹配的
catch
块,栈展开停止,控制权转移到该块。

从这个函数跳转出来后,会进入0x4019a1
这里执行,我们对照IDA中的内容会发现这里实质上是cleanup
这个子函数,该函数的主要功能如下:
在栈展开的过程中,每个栈帧可能包含需要执行的清理操作(如调用析构函数)。
如果
_Unwind_RaiseException
确定某个栈帧不包含匹配的catch
块,但需要进行清理操作,会调用与该栈帧关联的清理函数。这个清理函数通常会执行栈帧中需要的析构函数或者其他资源释放操作。IDA 中识别为
cleanup()
的函数,就是这些清理操作的函数,它用于处理抛出异常过程中需要释放的资源。
这个vuln函数的命名是因为我一开始以为这个莫名其妙的无作为的函数是解题的关键,但通过以上的理解,其实这个函数更有可能是执行清理的具体函数,但由于该栈帧中没有需要清理的内容,所以显示为空。


然后会调用__Unwind_Resume
,这个函数是 C++
异常处理机制中的一个重要函数,它用于在栈展开过程中恢复异常处理的流程,通常在执行完清理操作后继续展开栈帧。在该函数的末尾,我们发现我们的rbp实际已经被赋值为了想要的值。

但该题目与视频中的那道题最大的不同是,在将rbp控制后,没有机会进行leave;ret了,主函数是一个死循环,退出循环的方式是选项3直接调用exit退出。那么我们就没有机会打栈迁移了。
那么我们需要利用新的方法,这时我参考了羊城杯 2024 pwn writeup这篇wp中给的解法,发现程序中有很多地方另有玄机。其实程序中还有很多try,catch的组合,在没有被调用的函数中。


什么?程序中居然有catch段中存在对system函数的调用!后面我们通过调试其实可以发现上面打印的错误信息的参数存在src中,正常会打印出Buffer Overflow这个信息,而且0x4040a0也会做为system的参数。更巧的是,一直被我们忘在一边的trace函数中边界处理不当,当i=8时,实际可以往src处写入0x10个字节。那么我们就可以把/bin/sh写入src中,然后想办法把程序控制流转向这个catch块。

这里我们的问题又回到了如何劫持程序控制流,参考exp可以发现,是通过覆盖该函数栈帧的返回地址为0x401bc7来劫持的。那么结合前面所分析的函数调用链,其实我们可以这样子从大体上理解:出现调用cleanup是因为当前栈帧不匹配catch块,所以要进行清理,之后用__unwind_resume
继续找匹配的catch块时,由于返回地址被改为了一个catch块的handler,所以可以直接匹配执行。要深入理解为什么改返回地址可行,我们可以通过追踪rcx值的变化实现,因为最后从__unwind_resume
跳转到目标地址是通过mov rsp,rcx;pop rcx;jmp rcx
实现的。提取出与rcx变化有关的指令按顺序如下:
1 | <_Unwind_Resume+316> call 0x7fa75620d6a0 |
我们可以再跟进0x7fb2f2d9dc20
看一下,发现只是对rdx做了一些改变,要再往前找。而前面的0x7fa75620d6a0
执行后恰好使rax变为0x90,和后面赋给rcx值时的rax值相同,说明该函数是关键,进去看看。
其中的逻辑相当复杂,但在有个17次的循环后,有看到原来栈帧的返回地址,而执行过程中rbp始终为0x7ffcc254a5b0,_Unwind_Resume函数开始调用时也并没有改变rbp的值,这里rax实际能计算出一个当前rbp到处理栈帧返回地址附近的一个偏移,然后最后就能使rcx成为返回地址处的内容并跳转过去当作目标catch的handler函数进行执行。


其实这和正常的栈溢出有点像,那我们不禁想如果覆盖成别的其他不属于handler的地址会如何呢?比如我们把返回地址覆盖为main试试,结果是会报出**terminate called after throwing an instance of ‘char*’**,因为异常没有正常匹配。

然后我们可以试试换一个catch(int)的handler进行匹配,发现会有如下错误,rdx是由前面mov rdx, qword ptr [rdx]
得到的,前面的rdx值为0x404208,也就是我们可控的地址,我们可以输入一个合法的地址进去看看。最后还是会报**terminate
called after throwing an instance of
‘char*’**。后面看了别人博客才发现这个匹配的流程需要一定的经验。其他函数的catch块对应不上。
将ret地址修改为backdoor函数的try块地址范围内
0x401252-0x401258
(在我的测试中发现,这个范围是个左开但是右侧不精确的范围,为了保证成功率可以使用左测边界+1的地址)。
我们这里的try块的范围是从0x0000000000401BC2到0x0000000000401BC7的,最终能打通的范围也的确是0x0000000000401BC3到0x0000000000401BC7。


之后还需要注意的一点是,最后进入目标handler后,存在一个mov qword ptr [rbp - 0x18], rax
的赋值,我们需要确保覆盖的rbp的值减去0x18后为一个可写的地址,否则会出错。

最终exp如下:
1 | from pwn import * |
总结
- 主要调用链为
__cxa_throw
->_Unwind_RaiseException
->clearup
->_Unwind_Resume
->对应catch块的handler函数
- 除了CHOP,有两处漏洞可以利用
- 覆盖rbp进行栈迁移(有leave;ret可供使用)
- 覆盖返回地址到其他handler函数
参考内容
- https://blog.csdn.net/jennycisp/article/details/134965719
- https://www.bilibili.com/video/BV1eE421L7ZE/
- https://qanux.github.io/2024/08/28/%E7%BE%8A%E5%9F%8E%E6%9D%AF%202024%20pwn%20writeup/index.html
- 标题: Exploits under exception handling
- 作者: collectcrop
- 创建于 : 2024-09-21 17:00:04
- 更新于 : 2024-09-21 21:13:42
- 链接: https://collectcrop.github.io/2024/09/21/Exploits-under-exception-handling/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。