最近在整理线下赛用的工具,顺便看到了这道很有意思的题目。接下来我们就来调试一下这个巨坑的题目、
开始尝试
首先,这个程序是X64的。拖入64位的ida查看源码
这是程序主逻辑的源码,可以看见我们有很多选项来进行选择
根据语义可以看见,程序的前三个选项是用来进行加密解密的,第四个选项是用来进行安全漏洞测试的。优先选择第4个漏洞进行测试,但是不排除前3个函数有漏洞。
首先在虚拟机里面进行检查,这个程序启用了什么防护措施
开启了栈不可执行和Canary,几乎不可能利用栈。所以尝试格式化字符串
格式化字符串的利用也失败。
发现目标
经过查看writeup,我惊讶的发现,在第一个函数的开头,有一个异常捕获程序。有一个函数中存在一个整型溢出的漏洞
在sub_400e76的地方有一个转化函数,将输入加一后进行无符号整数的强制转化
能够发现,在12行有一个栈溢出漏洞
但是我们还是无法解决关于canary的问题。但是这个函数是处于C++额异常捕获机制中的,所以我们可以尝试利用C++的异常捕获机制绕过。
C++的异常捕获机制
C++ 函数的调用和返回
首先异常机制中最重要的三个关键字就是:throw try catch,Throw抛出异常,try 包含异常模块,catch 捕捉抛出的异常,三者各有各的分工,集成在一起就构成了异常的基本机制
首先澄清一点,这里说的 “C++ 函数”是指: 1.该函数可能会直接或间接地抛出一个异常:即该函数的定义存放在一个 C++ 编译(而不是传统 C)单元内,并且该函数没有使用“throw()”异常过滤器。 2.该函数的定义内使用了 try 块。 只需要满足一点即可,
异常抛出
在编译一段 C++ 代码时,编译器会将所有 throw 语句替换为其 C++ 运行时库中的某一指定函数,这里我们叫它 __CxxRTThrowExp(与本文提到的所有其它数据结构和属性名一样,在实际应用中它可以是任意名称)。该函数接收一个编译器认可的内部结构(我们叫它 EXCEPTION 结构)。这个结构中包含了待抛出异常对象的起始地址、用于销毁它的析构函数,以及它的 type_info 信息。对于没有启用 RTTI 机制(编译器禁用了 RTTI 机制或没有在类层次结构中使用虚表)的异常类层次结构,可能还要包含其所有基类的 type_info 信息,以便与相应的 catch 块进行匹配。
__CxxRTThrowExp 首先接收(并保存)EXCEPTION 对象;然后从 TLS:Current ExpHdl 处找到与当前函数对应的 piHandler、nStep 等异常处理相关数据;并按照前文所述的机制完成异常捕获和栈回退。由此完成了包括“抛出”->“捕获”->“回退”等步骤的整套异常处理机制。
异常捕获机制
一个异常被抛出时,就会立即引发 C++ 的异常捕获机制: 根据 c++ 的标准,异常抛出后如果在当前函数内没有被捕捉(catch),它就要沿着函数的调用链继续往上抛,直到走完整个调用链,或者在某个函数中找到相应的 catch。如果走完调用链都没有找到相应的 catch,那么std::terminate() 就会被调用,这个函数默认是把程序 abort,而如果最后找到了相应的 catch,就会进入该 catch 代码块,执行相应的操作。
程序中的 catch 那部分代码有一个专门的名字叫作:Landing pad(不十分准确),从抛异常开始到执行 landing pad 里的代码这中间的整个过程叫作 stack unwind,这个过程包含了两个阶段: 1)从抛异常的函数开始,对调用链上的函数逐个往前查找 landing pad。
2)如果没有找到 landing pad 则把程序 abort,否则,则记下 landing pad 的位置,再重新回到抛异常的函数那里开始,一帧一帧地清理调用链上各个函数内部的局部变量,直到 landing pad 所在的函数为止。
为了能够成功地捕获异常和正确地完成栈回退(stack unwind)
栈回退(Stack Unwind)机制
“回退”是伴随异常处理机制引入 C++ 中的一个新概念,主要用来确保在异常被抛出、捕获并处理后,所有生命期已结束的对象都会被正确地析构,它们所占用的空间会被正确地回收。