CHOP初探
在2024羊城杯遇见一道利用到C++异常处理的题目,以下对该题目作出探讨
CHOP
CHOP全称Catch Handler Oriented Programming,通过扰乱unwinder来实现程序流劫持的效果
由于C++终止语义,在引发异常点之后的任意代码都不会被执行,故往往可以利用存在异常控制流的代码处控制该函数返回地址为其他的可执行函数的代码来实现攻击,绕过类似于Canary和Shadow stacks等backward-edge保护机制
易受攻击的代码序列如下:
常见攻击手法堆栈布局如下:
logger
基于此题,经过审计,我们先通过函数 sub_4015AB()
存在的
off by null 漏洞修改 src 为 b'/bin/sh\x00'
,
同时发现函数 sub_40178A()
中 buf 存在栈溢出漏洞,故构造
payload 覆盖 rbp 和 ret 返回地址:
这里详细列出异常处理执行流程及程序中异常处理相关代码:
- 触发异常:首先使用
_cxa_allocate_exception
初始化异常,然后通过__cxa_throw
抛出该异常。 - 栈展开与捕获:在异常抛出后,
_Unwind_RaiseException
负责实现栈展开和捕获。如果异常被捕获,程序将返回到对应的catch
块;如果没有捕获,异常信息将输出到stderr
,并导致程序中止。 - 恢复执行流程:进入
catch
块后,使用_Unwind_Resume
来恢复正常的执行流程,将控制权转移,以便程序可以继续从异常发生的位置执行。 - 清理工作:执行完
catch
块后,使用__cxa_begin_catch
初始化与捕获异常相关的上下文,随后调用__cxa_end_catch
进行清理工作,程序将恢复到原有的执行流程。 - 异常捕获的结束:
__cxa_end_catch
用于处理异常捕获结束后的清理工作,执行完成后,控制权将交还给原有程序,注意此时原有的stack_fail检查将不再执行。 - 栈溢出处理:如果在此过程中发生栈溢出,结合异常处理机制,程序可能会绕过canary检查,依然能够正常执行。
1 | if ( v0 > 0x10 ) |
程序存在后门,但开启了 Full RELRO
保护,故考虑栈迁移到可读可写可执行的 bss 段,同时将存储在 rax 中的
b'/bin/sh\x00'
赋值给 rdi 寄存器
在位于 /usr/lib/x86_64-linux-gnu/libstdc++.so.6
的
libstdc++
库中找到 __cxa_throw
的代码,不难发现,__cxa_throw
函数在抛出异常时,会进行一系列操作,其中包括保存当前的
rbp
,并在需要时进行修改:
当异常被抛出后,C++
的异常处理机制会开始展开栈帧,在这个过程中,它会根据栈上的信息恢复寄存器的值,包括但不限于通过
rbp
来遍历调用栈,故我们利用其通过 rbp
回溯调用栈机制,构造 payload 迁移其 rbp
为bss段上可读可写可执行的地址 0x404550
根据异常捕获机制:
- 异常抛出 :在C++中,当一个异常发生时,可以使用
throw
语句抛出一个异常对象。 - 捕获异常 :如果在当前函数中没有对应的
catch
语句来捕获这个异常,程序会沿着函数的调用链向上查找,直到找到一个能够捕获该异常的catch
语句。 - 调用链 :如果在调用当前函数的上层函数中找到了
catch
,那么异常会被捕获并处理。如果没有找到,异常会继续向上抛出,直到到达main
函数。 - 程序中止 :如果在整个调用链中都没有找到匹配的
catch
语句,程序会调用std::terminate()
,导致程序异常终止。
最终需要返回到栈展开时的 __Unwind_Resume
的地址进行清理栈帧并恢复到正确的状态,再往下走去执行
system("/bin/sh\x00");
,否则会因异常未正确捕获或处理而调用
std::terminate()
,导致程序异常终止
exp:
1 | from pwn import * |