Unsorted Bin
与Top Chunk相邻的chunk,在Free之后如果不属于fastbin,就会直接合并到Top Chunk中
fastbin最大是可以放0x80,这边申请两个大小为0x90的chunk,之后free与top chunk相邻的那个
可以看到这是直接将空间收回了;
下面我们再执行两个malloc,重新申请b的空间,以及再申请一个新的0x20大小的空间;
这时堆中的内容是下面这样
这时如果将a的空间释放,a就被链接到了unsorted bins上面
这里发生变化的主要有三个地方;
- 被释放的块a的用户空间中的两个字,变成了两个指针(绿色框)
- 与a靠近的块b,指示位
previous_inuse
,值从原本的91变成了90(蓝色框) - 与a靠近的块b,原本空着的一个字变成了
previous_size
,也是90(红色框)
unsortedbin 是一个双向链表的结构,因此a中这两个指针分别是backward和forward;
目前只有a一项,所以是同一个值,这个值就是main_arena
中的top这个字段;
我们继续执行,把b也free了,这时并不会再unsortedbin中加两项,而是会把ab两个块合并在一起
可以看到直接是修改了块a的大小,变成了120,最后一个chunk的prev_size
也是变成了120
free掉一个fastbin是对周边的chunk没有影响的,但是free常规的chunk,是对周围的值有影响的;
Unsafe Unlink
前面一部分介绍了unsortedbin,我们可以注意到它是通过一个链表实现的;
链表大概就是这样的一个结构;
fd、bk分别是双链表的指针,指向前后的块;
如果想要拆下来其中的一个块,只需要把它前一个块的fd改成它自身的fd,把它后一个块的bk改成它自身的bk就可以;即p.bk->fd=p.fd;p.fd->bk=p.bk;
在低版本的GLIBC中没有对这个过程进行检查,并且是通过宏来实现的;
并且由于fd、bk这两个指针本身的位置是一个正常块的user data部分,所以这里是存在伪造的可能的;
示例程序
下面就用一个例子来看一下;
程序和之前一样是一个菜单形式
1是malloc申请空间,2是可以输入对应空间要填充的内容,3可以free掉申请的chunk
这个程序的malloc申请的chunk限制了大小在120~1000字节之间;
0x78是120比特,也就是说正好是fastbin放不下的chunk
另外还有一点就是,这个漏洞unsafe_unlink提出的时候还没有NX,这个程序为了讲最基本的原理也关闭了NX
任意地址写
首先是程序的漏洞所在,就是没有进行输出内容长度的判断,申请a、b后输入很多内容回溢出到下一个chunk
这个类似于之前的House_of_force,但是这个程序限制了malloc的次数,所以没办法像House_of_force一样利用;
那么我们还是先设法获取一个任意地址写的能力;
我们这次一共只能申请两次chunk
在前面的demo程序中也了解到了,远离top chunk的块a被free之后会加入到unsortedbin
并且会出现这几个修改
- 还在使用中的块b,size filed中prev_inuse位置零;
- 被释放的块a,user data最后一个字变成块a的大小,即prev_size字段;
- 被释放的块a,user data的前两个字节变成两个指针,分别是fd和bk;
这时free(b)
会把两个块合并,实际上执行的操作是
- 查看b的prev_inuse,发现位0,前一个块已经被释放;
- 查看b的prev_size,找到块a的起始地址;
- 把a从unsortedbin list上卸载下来,即按照fd、bk去修改前后块的地址
那么我们一步一步来,首先申请a、b两个chunk
编辑chunk_a的数据,把b的prev_inuse位清0
a = malloc(0x88)
b = malloc(0x88)
edit(a,b'a'*0x88+p64(0x90))
可以看到这时a的最后一个字已经属于b了
下面把prev_size
这个字段也伪造了,只需要修改刚才的edit
那句
edit(a,b'a'*0x80+p64(0x90)*2)
接下来我们需要填入两个指针,修改chunk_a
的fd和bk两个字段;
在unlink的时候实际上是
this.fd->bk = this.bk;
this.bk->fd = this.fd;
这样两个写入操作,如果我们想要用第一个语句来写
那么fd+0x18
是最终写入的地址,bk
是写入的内容;
我们先试着直接利用这个写入堆中的数据
由于测试的时候都关闭了ASLR,直接是用的固定的地址
我们输入的payload直接就拿这个固定的地址了
edit(chunk_A, p64(0x555555757090)+p64(0x5555557570b0)+b"a"*0x70+2*p64(0x90))
free(chunk_B)
执行完毕之后,用vis可以看到A、B一起都被free掉了
并且可以看到我们输入的两个值都写到内存里了,fd+0x18
写入了70b0
,bk+0x10
写入了7090
说明确实是可以写的;
这可以说是一个有限制的任意地址写;
限制在于要写的数据也会被当成地址来做一个解析,并且会尝试往那个地址写内容;
命令执行
接下来尝试将这个转换为一个命令执行的漏洞
如果我们直接在__free_hook
写入system
的话,会导致system之后的内容也被写入一些内容
这样由于尝试在不可写的地方写入导致出错;
不过由于这个程序是2000s时的程序,NX没有开启,因此我们可以直接把shellcode写到内存里面
之后把__free_hook
覆盖成堆里面的地址
edit(chunk_A, p64(fd) + p64(bk) + shellcode + p8(0)*(0x70-len(shellcode)) + p64(0x90)*2
修改一下要edit的内容
shellcode的话由于bk+0x10
会被写入为fd
的值,shellcode中需要空出来一小段区域,这个可以通过汇编加一个小jmp实现
shellcode = asm("jmp shellcode;"+ "nop;"*0x20+ shellcraft.execve("/bin/sh"))
这里的nop至少要把bk+0x10
这里的8个字节都空出来,最小值的话应该是0x18-len("jmp shellcode")
jmp shellcode
汇编代码是两个字节,所以这里至少要是0x16
可以看到图中红框是shellcode的内容,绿框是指向shellcode地址的一个指针,即bk
下面执行free(b)
看到堆中a和b都已经被释放掉了,并且bk+0x10
的位置已经变成了fd
即__free_hook
的值
使用u可以将__free_hook
中的内容当作code打印出来,可以看到已经是我们shellcode的内容了
这时继续执行,再调用一次free就会执行我们的shellcode
最终返回了一个shell