6.1.8 pwn DCTF2017 Flex

下载文件

题目复现

$ file flex
flex: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=30a1acbc98ccf9e8f4b3d1fc06b6ba6f0cbe7c9e, stripped
$ checksec -f flex
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FORTIFY  Fortified Fortifiable  FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   Yes      0               4       flex

可以看到开启了 Canary,本题的关键就是利用某种神秘机制(C++异常处理机制)绕过它。

随便玩一下,了解程序的基本功能:

$ ./flex
1.start flexmd5
2.start flexsha256
3.start flexsha1
4.test security
0 quit
option:
1
FlexMD5 bruteforce tool V0.1
custom md5 state (yes/No)
No
custom charset (yes/No)
yes
charset length:
10
charset:
a
bruteforce message pattern:
aaaa

把程序跑起来:

C++ 异常处理机制

所以这个程序是一个 C 和 C++ 混合编译的,以便处理异常。

当用户 throw 一个异常时,编译器会帮我们调用相应的函数分配 _cxa_exception 就是头部,exception_obj。异常对象由函数 __cxa_allocate_exception() 进行创建,最后由 __cxa_free_exception() 进行销毁。当我们在程序里执行了抛出异常后,编译器做了如下的事情:

  1. 调用 __cxa_allocate_exception 函数,分配一个异常对象

  2. 调用 __cxa_throw 函数,这个函数会将异常对象做一些初始化

  3. __cxa_throw() 调用 Itanium ABI 里的 _Unwind_RaiseException() 从而开始 unwind,unwind 分为两个阶段,分别进行搜索 catch 及清理调用栈

  4. _Unwind_RaiseException() 对调用链上的函数进行 unwind 时,调用 personality routine(__gxx_personality_v0

  5. 如果该异常如能被处理(有相应的 catch),则 personality routine 会依次对调用链上的函数进行清理。

  6. _Unwind_RaiseException() 将控制权转到相应的 catch 代码

  7. unwind 完成,用户代码继续执行

具体内容查看参考资料。

题目解析

程序的第四个选项很吸引人,但似乎没有发现什么突破点,而第一个选项可以输入的东西较多,问题应该在这里,查看该函数 sub.bruteforcing_start:_500

函数 sub.FlexMD5_bruteforce_tool_V0.1_148

函数 sub.atoi_f45 将字符串转换成长整型数:

可以看到该函数并未对所输入的数字进行验证,所以我们可以输入负数,因为计算机中数字是以补码的形式存在,例如 -2 = 0xfffffffffffffffe。这个数字加 1 后,作为读入字符串个数的判定,因为个数不能为负,我们就可以开心地读入后面的 payload 了。

这个程序中读入操作使用函数 sub.read_e76,该函数内部有一个循环,每次读入一个字符,如果遇到换行符,则完成退出。

分析完了,接下来就写 exp 吧。

漏洞利用

stack pivot

0x004012b4 下断点,以检查溢出点:

所以缓冲区的长度为 288 / 8 = 36。利用缓冲区溢出覆盖掉 rbp,在异常处理过程中,unwind 例程向上一级一级地找异常处理函数,然后恢复相关数据,这样就将栈转移到了新地址:

unwind_addr 必须是调用函数里的一个地址,这样抛出的异常才能被调用函数内的异常处理函数 catch。

get puts address

异常处理函数结束后,执行下面两句:

通常情况下我们构造 rop 调用 read() 读入 one-gadget 来获得 shell,但可用的 gadget 只能控制 rdi 和 rsi,而不能控制 rdx。所以必须通过函数 sub.read_f1e 来做到这一点。

构造 paylode_2 打印出 puts 的地址,并调用 read_f1e 读入 payload_3 到 pivote_addr + 0x50 的位置:

get shell

找到 libc 的 do_system 函数里的 one-gadget 地址为 0x00041ee7

通过泄露出的 puts 地址,计算符号偏移得到 one-gadget 地址,构造 payload_3:

Bingo!!!

exploit

完整的 exp 如下:

最后建议读者自己多调试几遍,以加深对异常处理机制的理解。

参考资料

Last updated

Was this helpful?