题目名:canutrytry解题数:32题目描述:来吧,你敢trytry吗 flag格式为Nn{}知识点:C++异常处理、栈溢出、栈迁移、stderr替代已关闭的stdout本题的难点在于了解C++异常处理机制的执行流程,漏洞利用比较常规。C++ 的异常处理机制基于两个核心操作:栈展开(Stack Unwinding):当程序中发生异常并执行 throw 语句时,C++ 运行时会沿调用栈逐层向上查找匹配的 catch 块,这就叫做“栈展开”。通俗的来说,就是先在当前函数查找catch块,如果没有就一直向主调函数回溯。如果栈展开过程中一直没有找到合适的 catch 块来处理异常,程序会调用 std::terminate() 异常终止。类型匹配(Type Matching):catch 块用于捕获异常对象。C++ 会根据异常对象的类型,在多个 catch 块中按顺序匹配:throw 抛出的是一个异常对象(如字符串、整型、自定义类等)。catch 使用类型匹配机制:只有当 throw 抛出的类型与 catch 参数类型一致或可转换时,该 catch 才会执行。catch(...) 是兜底机制,可以捕获所有类型的异常。那如果一个函数中有多个try-catch代码块,C++如何匹配它们的关系呢(确保A的try不会匹配到B的catch)?异常抛出时,C++会记录异常对象的类型信息、异常抛出的位置(程序计数器 PC 值)、当前栈帧结构。接下来:其中,LSDA通常包含:该函数内异常代码块(try/catch区间)、异常类型信息(type info)、跳转目标(catch块入口地址)、其它辅助信息等。需要注意,在栈展开时会遵循unwinding规则:恢复旧的 rbp恢复旧的 rsp弹出当前 return address跳转回调用者(继续 unwinding)拖入IDA分析:跟进Init()函数分析:发现它读入flag到全局变量flag中,然后开启沙箱保护。我们使用seccomp-tools工具查看保护情况:只允许我们使用read、write、close和futex函数。继续分析menu()函数,打印菜单,提供两个选项:先来分析visit()函数:该函数提供三个选项left、right和stright:其中,stright没有检查输入的idx是否合法,我们可以输入任意位置,只要满足条件就可以实现任意地址写(本题解未使用,可能存在非预期解修改IO)。并且,left会检查size[i]是否大于0,若不合法会抛出char const*类型的异常,异常值为invalid size。再来分析leave()函数:允许调用1次,允许我们输入idx,若content[idx]非null,则调用:将content[idx]指向的堆块的内容拷贝给栈上变量dest(rbp-0x20),存在栈溢出漏洞。同时,若size[idx] > 16,抛出char const*类型的异常,异常值为stack overflow。可以看到,这里非常可疑,按照正常逻辑,应该先判断大小再进行拷贝操作。我们可能需要通过c++的异常进行一些危险操作。在visit()和leave()函数中,都可能抛出异常,那异常是在什么地方处理的呢?我们找到main()函数,打开汇编视图:发现函数调用被try包围,并且下面有一些catch代码块,IDA没有正确的显示反编译结果:我们手动将其简单还原伪代码,如下所示:当menu()函数中出现异常时,catch块首先调用sub_4016EC()函数:这个函数提示我们输入ROP,它会调用read函数读取最多0x300字节大小的数据到bss段的全局变量中。随后,catch块会继续调用 sub_401652()函数:允许我们输入内容到bss段的全局变量中,接着关闭stdout标准输出流,并调用sub_4015D4()函数:该函数允许我们输入最多24字节的数据到栈上变量buf中,这里存在栈溢出漏洞,使得我们可以覆盖栈上的old_rbp。当visit()函数和leave()函数出现异常时,会触发gift,泄露libc和栈地址。查看汇编代码发现,sub_401652()函数在调用sub_4015D4()函数时也进行了异常处理,如果在sub_4015D4()函数中发生异常会回溯到这里进行处理:在前面的逆向分析中,我们找到了两个明显的漏洞:我们可以先将下标为0数组的预留,然后通过下标为1的数组构造visit的异常,利用题目提供的gift泄露出libc和栈地址:随后,如何继续利用成为了关键。目前,唯一的方法是利用leave()函数中的memcpy()函数实现栈溢出覆盖rbp和返回地址,但这会触发异常无法真的返回到目标地址。我们之前介绍过,在C++异常处理机制中,如果当前函数无匹配的catch块,它会顺着返回地址回溯到上层函数继续寻找catch块。而menu()函数中的catch块包含一些漏洞,我们需要想办法跳转到这里。我们可以考虑通过leave()函数的memcpy()函数栈溢出,覆盖leave()函数的返回地址为menu()函数的返回地址。这样,栈溢出后触发异常,由于leave()函数中并没有catch块,因此它会顺着函数返回地址回溯到main()函数继续寻找catch块。此时,程序会错误的认为异常是由menu()函数所在的try代码块触发,而进入menu()函数对应的catch块中进行处理。具体做法为调用visit()函数中的read向预留的content[0]指针指向的区域写入内容,然后执行leave()函数进行memcpy()操作:此时,程序会进入menu()函数的catch块继续执行,它先调用sub_4016EC()函数:允许我们输入rop到bss段中,看来后续我们应该想办法将程序迁移到这个位置继续执行,目前我们先不关心它。输入完rop后,catch块会继续调用 sub_401652()函数:在这个函数中,允许我们输入一些字符串到bss段中,看起来目前好像也没什么用。然后调用close(1)关闭stdout标准输出流,接着调用sub_4015D4()函数:这个函数中存在一个漏洞,我们可以在buf中输入超过8字节的数据,覆盖old_rbp,然后触发异常。异常会被上层函数sub_401652()捕获:这个catch块基本上没有做什么事情,直接调用leave; ret;返回了。现在,我们的思路就是写入rop,然后想办法将程序迁移到rop处执行。在leave()函数中,我们触发异常回溯到menu()函数的catch块,调用sub_401652()函数 -> sub_4015D4()函数。而sub_4015D4()函数存在栈溢出漏洞,写入超过8个字节,会触发异常回到sub_401652()函数的catch块。如果我们将栈上的old_rbp覆盖为&rop_bss,在sub_401652()函数返回时进行Unwinding操作会还原rbp为我们覆盖的假的old_rbp地址。此时,程序栈迁移到&rop_bss+4继续执行。我们还需要注意一个问题:所有catch基本上会去访问[rbp-xxx]的内存存储异常指针,所以我们在覆盖的时候要将rbp覆盖为任意
...(已截断)
---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-287865.htm
[原创]NepCTF 2025 Pwn赛题canutrytry解析:利用cpp异常处理与栈迁移实现输出攻破
146 浏览
0 回复
暂无回复,快来抢沙发吧!