首页
社区
课程
招聘
[原创]CTF2017 第四题 ReeHY-main 解题报告
2017-6-8 23:29 4421

[原创]CTF2017 第四题 ReeHY-main 解题报告

2017-6-8 23:29
4421

CTF2017 第四题 ReeHY-main

题外话

  1. 没有发现第三题程序的减法被替换,而解了一天错误方程的我
  2. 尽量把 writeup 写的通俗易懂,有可能有些细节没写清楚也多多包涵
  3. 为什么题目不是按照难度打分

初步分析

  1. 从 ctflibc.so.6 中的字符串猜测应该是 CentOS 7 系列,去阿里云买了个主机 CentOS 7.2 对比了一下 so 一模一样

  2. 程序特征

    • 64位 linux 程序
    • 执行 readelf -lW 4-ReeHY-main | grep GNU_STACK,堆栈无可执行权限(默认)
    • 函数没有堆栈保护代码(好耶)
    • PLT 中有这些函数 atoi、exit、fflush、free、malloc、memcpy、puts、read、setvbuf、write
  3. 64位函数参数前几个是寄存器传参(rdi、rsi、rdx),所以利用溢出堆栈来传参变得极为棘手,必须利用程序中现成的堆栈转移寄存器指令(这里考试要考)

  4. 因为堆栈没有可执行权限,所以堆栈调 syscall 可以靠边了(从上一条看 mprotect 的传参太棘手)

  5. 目标是 system("/bin/sh"),但 PLT 中没有 system,因为 ASLR 的关系,在解题过程中,必定要打印出已有堆栈寻找 libc 偏移,从而找到 system 地址来调用,唯一可以利用的函数是 write(这里考试要考)

寻找堆栈溢出点

前面分析了那么多,没有一个堆栈溢出点就全是空话,所谓的堆栈溢出点各位同学都很清楚,就是存在向一个有限的栈空间写入了比它容量更大的数据,超出的数据就能复写当前函数的其他变量,甚至修改程序返回地址(重新定义函数流程),上IDA截图


图1

从图1看出,①buf 是个 128 字节的堆栈数组,整个程序只有在③处,才会发生堆栈的写入。有同学会说,②处有长度验证怎么办,看起来小于等于 112 字节的才会被写入 buf,仔细看你会发现,这2处对比都是有符号的,而 read 函数的 size 参数是无符号的,传入的 size 为负数即可造成堆栈溢出。

找到了溢出点,那么就来构造溢出字串,有了 IDA 的堆栈图,我们就不用反复尝试 "A" 的数量了。


图2

堆栈结构为,从这里开始,会略过基础的函数 call 堆栈概念

buf(128) | dest 拷贝目标地址(8) | cun 槽位(4) | size (4) | s ebp (8) | r 返回 (8)

鹅妹子,我们能够控制这个函数里面所有的堆栈变量,所以这个函数非常重要

我们来个简单的溢出测试,测试的目标是要函数返回崩溃在 0x0000000041414141,我们首先需要过掉 memcpy 的干扰,size 参数复写为 0 即可,所以我们构造如下字串

Ax128 | 0 地址 | 0 槽位 | 0 size | Ax8 | 0x0000000041414141


图3

bingo,马上进行下一步。这里说明一下我是使用 python 的 subprocess.Popen 载入程序,p.stdin.write 写入数据,并使用 gdb attach 进行的调试

打印堆栈

这一步我进行的非常艰难,关键在如何想办法构造 write 的参数上面,write 接受三个参数,想要输出堆栈的话,必须满足:rdi = 1, rsi = 堆栈地址, rdx = 打印长度

既然我们在 create_exploit 函数内那么自由,那么必定要从中找到从堆栈转移到寄存器的关键点,从汇编看


图4

在调用 PLT 函数③ fflush 前,①rsi = buf 地址(堆栈地址),②rdx = cun * 16,前面说的三个寄存器要求满足了两个,那还剩 rdi = 1怎么办呢,别着急,找到 write 的调用处


图5

就在程序一开始的输入名字阶段,rdi = 1是固定传入的,那么接下来只要把 fflush 函数的 PLT 跳转地址改为这里就可以了(这里选第一个 write,别问我为什么)

如何改动 PLT 地址呢,从图1看,只要改动 memcpy 的复制目标 dest,就可以实现向任何有写权限的地方写入任何值,因为源是 buf,关于 PLT 改动的原理这里略过,附上 PLT 表


图6

接下来原本我分了两步进行的,第一次多次调整 cun 的大小来得到最终的堆栈打印效果(0x20),第二次是修改函数返回再重新一次进入一次 create_exploit 函数做之后的事情,这里合并成一步进行,依照之前的堆栈布局、PLT 地址和指令地址,构造如下溢出字串

0x4008E1 write | Ax125 | 0x602048 fflush.plt.addr | 0x20 cun | 3 size | Ax8 | 0x4009D1 create_exploit


图7

图7就是 write 返回的结果,上图我是直接用 python 的 telnetlib.Telnet 连接服务器,用 t.read_eager 读取的,所以之间的 0 都无法读出,但是不碍事,我们用 gdb 打开程序,在输入名字处打印调用堆栈


图8

从中可以看出

system(0x7F..FD0) - libc_start_main(0x7F..B35) = 0x2049B

依据虽有 ASLR,但地址的首尾基本不会变化的特点,从图7中找到 0x7F..B35 形式的地址(高亮部分),得出当前 system 的地址为 0x7FE4E2660FD0

拿到 shell

做了前面的工作后,后面的事情就简单了,system 只要一个字符串参数 edi,程序中有个读取输入整数的函数,上汇编


图9

这个函数太完美了,read 读取 10 个字节,刚好可以存 /bin/sh,atoi 和 system 参数一致,只要替换 atoi 的 PLT 就完美了,和之前的的思路一样,将 fflush 替换成 readInt 函数,构造如下溢出字串

0x400C55 readInt | 0x400736 setvbuf.plt | 0x7FE4E2660FD0 system(atoi.plt) | Ax104 | 0x602048 fflush.plt.addr | 0 cun | 0x18 size

接下来只要在 python 执行 t.write("/bin/sh\0")(注意后面要输出 \0,read 函数不会补充)后,此时的 t.write 就能直接执行任意 shell 命令了,flag 文件在 /root/flag(我 ls root 目录找到的),执行 t.write("cat /root/flag\n\0") 即可输出本题答案(注意后面的 \n,shell 需要,我开始没注意试了起码三个小时)

后记

本文没有把每个步骤写的非常详细,只是把关键点指出,略去了繁琐的 write 交互,另外我是手动出结果的,也没有来得及写个自动脚本发上来(时间宝贵,写这个 writeup 就花了好久),欢迎各种吐槽和围观



[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

收藏
点赞4
打赏
分享
最新回复 (1)
雪    币: 74
活跃值: (10)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
PeSafe 2017-6-16 16:06
2
0
作者很用心,点赞
游客
登录 | 注册 方可回帖
返回