-
-
[原创]NepCTF 2025 Pwn赛题canutrytry解析:利用cpp异常处理与栈迁移实现输出攻破
-
发表于: 2025-8-3 19:46 3921
-
题目名: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覆盖为任意可写地址。
构造rop时,由于程序已经关闭标准输出流,我们只能借助标准错误流进行输出:write(2, &flag, 0x100)。
try { throw "error";} catch (const char* e) { std::cout << "Caught const char*: " << e << std::endl;} catch (...) { std::cout << "Caught unknown exception" << std::endl;}try { throw "error";} catch (const char* e) { std::cout << "Caught const char*: " << e << std::endl;} catch (...) { std::cout << "Caught unknown exception" << std::endl;}➜ canutrytry seccomp-tools dump ./canutrytry line CODE JT JF K================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 0004: 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff) goto 0010 0005: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009 0006: 0x15 0x02 0x00 0x00000001 if (A == write) goto 0009 0007: 0x15 0x01 0x00 0x00000003 if (A == close) goto 0009 0008: 0x15 0x00 0x01 0x000000ca if (A != futex) goto 0010 0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0010: 0x06 0x00 0x00 0x00000000 return KILL➜ canutrytry seccomp-tools dump ./canutrytry line CODE JT JF K================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 0004: 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff) goto 0010 0005: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009 0006: 0x15 0x02 0x00 0x00000001 if (A == write) goto 0009 0007: 0x15 0x01 0x00 0x00000003 if (A == close) goto 0009 0008: 0x15 0x00 0x01 0x000000ca if (A != futex) goto 0010 0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0010: 0x06 0x00 0x00 0x00000000 return KILLif ( op == 1 ){ for ( i = 0; i <= 1; ++i ) { if ( *((_QWORD *)&content + i) ) { if ( i == 1 ) { v10 = std::operator<<<std::char_traits<char>>(&std::cout, "no free chunks"); std::ostream::operator<<(v10, &std::endl<char,std::char_traits<char>>); } } else { if ( size[i] ) { if ( size[i] <= 0 ) { exception = __cxa_allocate_exception(8uLL); *exception = "invalid size"; __cxa_throw(exception, (struct type_info *)&`typeinfo for'char const*, 0LL); } *((_QWORD *)&content + i) = malloc(size[i]); if ( *((_QWORD *)&content + i) ) v7 = std::operator<<<std::char_traits<char>>(&std::cout, "malloc success"); else v7 = std::operator<<<std::char_traits<char>>(&std::cout, "malloc failed"); std::ostream::operator<<(v7, &std::endl<char,std::char_traits<char>>); break; } v9 = std::operator<<<std::char_traits<char>>(&std::cout, "invalid size"); std::ostream::operator<<(v9, &std::endl<char,std::char_traits<char>>); } } } if ( op == 2 ) { for ( j = 0; j <= 1; ++j ) { if ( !size[j] ) { std::operator<<<std::char_traits<char>>(&std::cout, "size:"); __isoc99_scanf("%d", &size[j]); break; } if ( j == 1 ) { v11 = std::operator<<<std::char_traits<char>>(&std::cout, "no more size"); std::ostream::operator<<(v11, &std::endl<char,std::char_traits<char>>); } } } if ( op == 3 ) { std::operator<<<std::char_traits<char>>(&std::cout, "index:"); __isoc99_scanf("%d", &v15); if ( *((_QWORD *)&content + v15) ) { std::operator<<<std::char_traits<char>>(&std::cout, "content:"); if ( read(0, *((void **)&content + v15), size[v15]) > 0 ) { v13 = std::operator<<<std::char_traits<char>>(&std::cout, "success"); std::ostream::operator<<(v13, &std::endl<char,std::char_traits<char>>); } } else { v12 = std::operator<<<std::char_traits<char>>(&std::cout, "invalid index"); std::ostream::operator<<(v12, &std::endl<char,std::char_traits<char>>); }}if ( op == 1 ){ for ( i = 0; i <= 1; ++i ) { if ( *((_QWORD *)&content + i) ) { if ( i == 1 ) { v10 = std::operator<<<std::char_traits<char>>(&std::cout, "no free chunks"); std::ostream::operator<<(v10, &std::endl<char,std::char_traits<char>>); } } else { if ( size[i] ) { if ( size[i] <= 0 ) { exception = __cxa_allocate_exception(8uLL); *exception = "invalid size"; __cxa_throw(exception, (struct type_info *)&`typeinfo for'char const*, 0LL); } *((_QWORD *)&content + i) = malloc(size[i]); if ( *((_QWORD *)&content + i) ) v7 = std::operator<<<std::char_traits<char>>(&std::cout, "malloc success"); else v7 = std::operator<<<std::char_traits<char>>(&std::cout, "malloc failed"); std::ostream::operator<<(v7, &std::endl<char,std::char_traits<char>>); break; } v9 = std::operator<<<std::char_traits<char>>(&std::cout, "invalid size"); std::ostream::operator<<(v9, &std::endl<char,std::char_traits<char>>); } } } if ( op == 2 ) { for ( j = 0; j <= 1; ++j ) { if ( !size[j] ) { std::operator<<<std::char_traits<char>>(&std::cout, "size:"); __isoc99_scanf("%d", &size[j]); break; } if ( j == 1 ) { v11 = std::operator<<<std::char_traits<char>>(&std::cout, "no more size"); std::ostream::operator<<(v11, &std::endl<char,std::char_traits<char>>); } } } if ( op == 3 ) { std::operator<<<std::char_traits<char>>(&std::cout, "index:"); __isoc99_scanf("%d", &v15); if ( *((_QWORD *)&content + v15) ) { std::operator<<<std::char_traits<char>>(&std::cout, "content:"); if ( read(0, *((void **)&content + v15), size[v15]) > 0 ) { v13 = std::operator<<<std::char_traits<char>>(&std::cout, "success"); std::ostream::operator<<(v13, &std::endl<char,std::char_traits<char>>); } } else { v12 = std::operator<<<std::char_traits<char>>(&std::cout, "invalid index"); std::ostream::operator<<(v12, &std::endl<char,std::char_traits<char>>); }}memcpy(dest, *((const void **)&content + idx), size[idx]);if ( size[idx] > 16 ){ exception = __cxa_allocate_exception(8uLL); *exception = "stack overflow"; __cxa_throw(exception, (struct type_info *)&`typeinfo for'char const*, 0LL);}memcpy(dest, *((const void **)&content + idx), size[idx]);if ( size[idx] > 16 ){ exception = __cxa_allocate_exception(8uLL); *exception = "stack overflow"; __cxa_throw(exception, (struct type_info *)&`typeinfo for'char const*, 0LL);}int main() { Init(); // 仅供参考,实际情况是分为多个try,catch具体匹配哪一个try是由编译时生成的LSDA决定。 while (true) { try { menu(); int op; std::cin >> op; if (op == 1) { visit(); } else if (op == 2) { leave(); } else { _exit(0); } } catch (const char* err1) { // 只处理 menu() 抛出的异常 sub_4016EC(); sub_401652(); break; } catch (const char* err2) { // 只处理 visit() 和 leave() 抛出的异常 std::cout << "you catch the error " << err2 << std::endl; std::cout << "here is a gift for you!" << std::endl; printf("setbufaddr:%p\n", setbuf_ptr); printf("stackaddr:%p\n", &op); } } return 0;}int main() { Init(); // 仅供参考,实际情况是分为多个try,catch具体匹配哪一个try是由编译时生成的LSDA决定。 while (true) { try { menu(); int op;[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!