从 ctflibc.so.6 中的字符串猜测应该是 CentOS 7 系列,去阿里云买了个主机 CentOS 7.2 对比了一下 so 一模一样
程序特征
64位函数参数前几个是寄存器传参(rdi、rsi、rdx),所以利用溢出堆栈来传参变得极为棘手,必须利用程序中现成的堆栈转移寄存器指令(这里考试要考)
因为堆栈没有可执行权限,所以堆栈调 syscall 可以靠边了(从上一条看 mprotect 的传参太棘手)
目标是 system("/bin/sh")
,但 PLT 中没有 system,因为 ASLR 的关系,在解题过程中,必定要打印出已有堆栈寻找 libc 偏移,从而找到 system 地址来调用,唯一可以利用的函数是 write(这里考试要考)
前面分析了那么多,没有一个堆栈溢出点就全是空话,所谓的堆栈溢出点各位同学都很清楚,就是存在向一个有限的栈空间写入了比它容量更大的数据,超出的数据就能复写当前函数的其他变量,甚至修改程序返回地址(重新定义函数流程),上IDA截图
从图1看出,①buf 是个 128 字节的堆栈数组,整个程序只有在③处,才会发生堆栈的写入。有同学会说,②处有长度验证怎么办,看起来小于等于 112 字节的才会被写入 buf,仔细看你会发现,这2处对比都是有符号的,而 read 函数的 size 参数是无符号的,传入的 size 为负数即可造成堆栈溢出。
找到了溢出点,那么就来构造溢出字串,有了 IDA 的堆栈图,我们就不用反复尝试 "A" 的数量了。
堆栈结构为,从这里开始,会略过基础的函数 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
bingo,马上进行下一步。这里说明一下我是使用 python 的 subprocess.Popen 载入程序,p.stdin.write 写入数据,并使用 gdb attach 进行的调试
这一步我进行的非常艰难,关键在如何想办法构造 write 的参数上面,write 接受三个参数,想要输出堆栈的话,必须满足:rdi = 1, rsi = 堆栈地址, rdx = 打印长度
既然我们在 create_exploit 函数内那么自由,那么必定要从中找到从堆栈转移到寄存器的关键点,从汇编看
在调用 PLT 函数③ fflush 前,①rsi = buf 地址(堆栈地址),②rdx = cun * 16,前面说的三个寄存器要求满足了两个,那还剩 rdi = 1怎么办呢,别着急,找到 write 的调用处
就在程序一开始的输入名字阶段,rdi = 1是固定传入的,那么接下来只要把 fflush 函数的 PLT 跳转地址改为这里就可以了(这里选第一个 write,别问我为什么)
如何改动 PLT 地址呢,从图1看,只要改动 memcpy 的复制目标 dest,就可以实现向任何有写权限的地方写入任何值,因为源是 buf,关于 PLT 改动的原理这里略过,附上 PLT 表
接下来原本我分了两步进行的,第一次多次调整 cun 的大小来得到最终的堆栈打印效果(0x20),第二次是修改函数返回再重新一次进入一次 create_exploit 函数做之后的事情,这里合并成一步进行,依照之前的堆栈布局、PLT 地址和指令地址,构造如下溢出字串
0x4008E1 write | Ax125 | 0x602048 fflush.plt.addr | 0x20 cun | 3 size | Ax8 | 0x4009D1 create_exploit
图7就是 write 返回的结果,上图我是直接用 python 的 telnetlib.Telnet 连接服务器,用 t.read_eager 读取的,所以之间的 0 都无法读出,但是不碍事,我们用 gdb 打开程序,在输入名字处打印调用堆栈
从中可以看出
system(0x7F..FD0) - libc_start_main(0x7F..B35) = 0x2049B
依据虽有 ASLR,但地址的首尾基本不会变化的特点,从图7中找到 0x7F..B35 形式的地址(高亮部分),得出当前 system 的地址为 0x7FE4E2660FD0
做了前面的工作后,后面的事情就简单了,system 只要一个字符串参数 edi,程序中有个读取输入整数的函数,上汇编
这个函数太完美了,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 就花了好久),欢迎各种吐槽和围观
图1
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!