这个月干的是属实有点儿少,感觉特别迷茫,学了那么多结果比赛还是一头雾水,虽然很烦躁但是还是不能放弃啊!
在starCTF的第二道题我始终找不到漏洞点在哪,下来看wp才发现使用的libc不是普通的glibc了,其实在RCTF的一道题目就是基于musl的但是无奈github的仓库当中并没有题目的源码所以没去深入了解,在starCTF过后也算是了解一下这一相较于glic更为轻量的libc了。
首先本文先介绍一下这一libc
musl 1.1.24
musl libc 是一个专门为嵌入式系统开发的轻量级 libc 库,以简单、轻量和高效率为特色。有不少 Linux 发行版将其设为默认的 libc 库,用来代替体积臃肿的 glibc ,如 Alpine Linux(做过 Docker 镜像的应该很熟悉)、OpenWrt(常用于路由器)和 Gentoo 等。
数据结构
这一版本的chunk结构其实是和glibc相差不大的。
1 | struct chunk { |
psize和csize字段都有标志位(glibc 只有size字段有),但只有一种位于最低位的标志位INUSE(glibc 最低三位都有标志位)。若设置INUSE标志位(最低位为1),表示 chunk 正在被使用;若没有设置INUSE标志位(最低位为0),表示 chunk 已经被释放或者通过mmap分配的,需要通过psize的标志位来进一步判断 chunk 的状态。
1 | static struct { |
这个mal结构体很类似main_arena,里面记录着堆的信息,有三个成员:64位无符号整数binmap、链表头部数组bins和锁free_lock。binmap记录每个 bin 是否为非空,若某个比特位为 1,表示对应的 bin 为非空,即 bin 链表中有 chunk。
1 | struct bin { |
bin 链表头部的结构如上。head和tail指针分别指向首部和尾部的 chunk,同时首部 chunk 的prev指针和尾部 chunk 的next指针指向 bin 链表头部,这样构成了循环链表。当链表为空时,head和tail指针等于 0 或者指向链表头部自身。
看mal结构可以看到有64的bin,前面32个bin是类似于small bin的结构,存放的chunk的大小是固定的,但是后面的就类似于large bin存放的是在一定范围的chunk了。
malloc
1 | // src/malloc/malloc.c L284-L331 |
大概步骤就是:
调整n,增加头部的长度然后对齐32位
如果n>MMAP_THRESHOLD,则使用mmap创建一块大小为n的内存返回
如果n<=MMAP_THRESHOLD,计算n对应的bin的i,查找binmap
如果所有可用bin都为空,那么就扩展堆空间,生存一个新的chunk
如果存在非空的bin,则大小最接近n的bin,将bin首部的chunk返回
如果符号pretrime条件,使用pretrime分割
否则使用unbin从链表中取出
最后对chunk进行trim,返回给用户
1 | static void unbin(struct chunk *c, int i) |
这其实就是取出chunk的一个操作,可以看到取出的过程中并没有检测chunk指针的合法性,这也就造成了安全隐患
1 | static int pretrim(struct chunk *self, size_t n, int i, int j) |
pretrim的作用是切割大 chunk,防止把大小超过需求的 chunk 分配给用户。当满足一定条件时,pretrim从 bin 链表首部 chunk 切割出一块大小刚好符合需求的小 chunk,然后将小 chunk 分配给用户,链表首部 chunk 的位置保持不变。
1 | static void trim(struct chunk *self, size_t n) |
malloc 的最后一步是trim,主要作用是回收 chunk 超过需求大小的部分。trim将 chunk 多余的部分切割出来,然后将其释放到 bin 中,减少内存浪费。
free
1 | void free(void *p) |
1 | static void unmap_chunk(struct chunk *self) |
free 先对 chunk 进行 mmap / double free 检查。如果 chunk 的csize字段没有设置INUSE标志位,进入unmap_chunk函数检查psize字段。如果psize字段设置了INUSE标志位,视为 double free,crash;否则视为 mmap chunk,调用__munmap函数释放。
1 | void __bin_chunk(struct chunk *self) |
__bin_chunk函数的作用是将 chunk 插入到 bin 链表中。首先合并 chunk 前后的空闲 chunk、设置 binmap 和 chunk 标志位,最后将 chunk 插入到对应的 bin 链表中。
然后在musl当中的堆管理为了减少内存的使用会直接将libc和程序当中的空闲的内存当作堆内存,而glibc的堆地址一般都是位于内存中的动态内存区域。
XCTF_2020_PWN_musl
可以看到这里确实是直接在libc和process上面有堆的地址。
题目分析
就是很经典的菜单题,并且在create函数里面有一处只能运行一次的0x50的溢出,而且题目只有一处使用exit退出程序,然后show函数也只有一次。
利用分析
其实利用思路就很简单了,存在溢出,unbin又有如此大的安全隐患,所以就是通过溢出修改掉next指针和prev指针从而实现任意地址写,造成FSOP
这里说一下怎么造成的FSOP
1 | _Noreturn void exit(int code) |
1 | static void close_file(FILE *f) |
可以看到最后是有机会调用到file的内部函数指针的
1 | struct _IO_FILE { |
所以我们需要利用exit来执行FSOP,不过难点就是怎么运行到exit,因为需要malloc返回一个0xdeadbeef,在上面可以看到,如果所有的bin都为空,此时malloc就会调用expand_heap来扩展堆,本质还是调用了__expand_heap函数
1 | void *__expand_heap(size_t *pn) |
在__expand_heap函数中,brk是指向数据段末尾位置的指针。__expand_heap函数调用 brk 系统调用__syscall(SYS_brk, brk+n),将数据段末尾向后延展n字节,然后延展部分返回给malloc作为新的 chunk 分配给用户
若程序不开启 PIE,数据段的地址长度为 24 bit(0~0x2000000),内存位置与0xBADBEEF比较接近。若将brk指针修改为0xBADBEEF - n,brk 系统调用就会把数据段延展至0xBADBEEF,使其成为可访问的内存地址。
exp
1 | from pwn import * |
然后就是我这里的libc是自己编译的和题目一直有出入,所以我这个exp可能不能直接用在题目上,同时我也问了其他师傅这个编译该怎么办,还在等回复~