首页
社区
课程
招聘
[原创]NepCTF 2025 Pwn赛题canutrytry解析:利用cpp异常处理与栈迁移实现输出攻破
发表于: 2025-8-3 19:46 3921

[原创]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分析:

image-20250803144819157

跟进Init()函数分析:

image-20250803144850000

发现它读入flag到全局变量flag中,然后开启沙箱保护。我们使用seccomp-tools工具查看保护情况:

只允许我们使用readwriteclosefutex函数。

继续分析menu()函数,打印菜单,提供两个选项:

image-20250803145331572

先来分析visit()函数:

image-20250803145438915

该函数提供三个选项leftrightstright

其中,stright没有检查输入的idx是否合法,我们可以输入任意位置,只要满足条件就可以实现任意地址写(本题解未使用,可能存在非预期解修改IO)。

并且,left会检查size[i]是否大于0,若不合法会抛出char const*类型的异常,异常值为invalid size

再来分析leave()函数:

image-20250803150333394

允许调用1次,允许我们输入idx,若content[idx]非null,则调用:

content[idx]指向的堆块的内容拷贝给栈上变量dest(rbp-0x20),存在栈溢出漏洞。

同时,若size[idx] > 16,抛出char const*类型的异常,异常值为stack overflow

可以看到,这里非常可疑,按照正常逻辑,应该先判断大小再进行拷贝操作。我们可能需要通过c++的异常进行一些危险操作。

visit()leave()函数中,都可能抛出异常,那异常是在什么地方处理的呢?

我们找到main()函数,打开汇编视图:

image-20250803153945260

发现函数调用被try包围,并且下面有一些catch代码块,IDA没有正确的显示反编译结果:

我们手动将其简单还原伪代码,如下所示:

menu()函数中出现异常时,catch块首先调用sub_4016EC()函数:

image-20250803154225134

这个函数提示我们输入ROP,它会调用read函数读取最多0x300字节大小的数据到bss段的全局变量中。

随后,catch块会继续调用 sub_401652()函数:

image-20250803155847322

允许我们输入内容到bss段的全局变量中,接着关闭stdout标准输出流,并调用sub_4015D4()函数:

image-20250803155956059

该函数允许我们输入最多24字节的数据到栈上变量buf中,这里存在栈溢出漏洞,使得我们可以覆盖栈上的old_rbp

visit()函数和leave()函数出现异常时,会触发gift,泄露libc和栈地址。

查看汇编代码发现,sub_401652()函数在调用sub_4015D4()函数时也进行了异常处理,如果在sub_4015D4()函数中发生异常会回溯到这里进行处理:

image-20250803181641356

在前面的逆向分析中,我们找到了两个明显的漏洞:

我们可以先将下标为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()函数:

image-20250803154225134

允许我们输入rop到bss段中,看来后续我们应该想办法将程序迁移到这个位置继续执行,目前我们先不关心它。

输入完rop后,catch块会继续调用 sub_401652()函数:

image-20250803155847322

在这个函数中,允许我们输入一些字符串到bss段中,看起来目前好像也没什么用。然后调用close(1)关闭stdout标准输出流,接着调用sub_4015D4()函数:

image-20250803155956059

这个函数中存在一个漏洞,我们可以在buf中输入超过8字节的数据,覆盖old_rbp,然后触发异常。异常会被上层函数sub_401652()捕获:

image-20250803181641356

这个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 KILL
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>>);
    }
}
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实战!

最后于 2025-8-3 19:52 被Real返璞归真编辑 ,原因: 补充题目附件
上传的附件:
收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回