[HeapLab] Unsafe_Unlink

Unsorted Bin

与Top Chunk相邻的chunk,在Free之后如果不属于fastbin,就会直接合并到Top Chunk中

image-20210323151654862

fastbin最大是可以放0x80,这边申请两个大小为0x90的chunk,之后free与top chunk相邻的那个

image-20210323151755991

可以看到这是直接将空间收回了;

下面我们再执行两个malloc,重新申请b的空间,以及再申请一个新的0x20大小的空间;

这时堆中的内容是下面这样

image-20210323152024792

这时如果将a的空间释放,a就被链接到了unsorted bins上面

image-20210323152331967

这里发生变化的主要有三个地方;

  • 被释放的块a的用户空间中的两个字,变成了两个指针(绿色框)
  • 与a靠近的块b,指示位previous_inuse,值从原本的91变成了90(蓝色框)
  • 与a靠近的块b,原本空着的一个字变成了previous_size,也是90(红色框)

unsortedbin 是一个双向链表的结构,因此a中这两个指针分别是backward和forward;

目前只有a一项,所以是同一个值,这个值就是main_arena中的top这个字段;

image-20210323154000475

我们继续执行,把b也free了,这时并不会再unsortedbin中加两项,而是会把ab两个块合并在一起

image-20210323154354084

可以看到直接是修改了块a的大小,变成了120,最后一个chunk的prev_size也是变成了120

free掉一个fastbin是对周边的chunk没有影响的,但是free常规的chunk,是对周围的值有影响的;

Unsafe Unlink

前面一部分介绍了unsortedbin,我们可以注意到它是通过一个链表实现的;

image-20210323165239257

链表大概就是这样的一个结构;

fd、bk分别是双链表的指针,指向前后的块;

如果想要拆下来其中的一个块,只需要把它前一个块的fd改成它自身的fd,把它后一个块的bk改成它自身的bk就可以;

image-20210323170813482

在低版本的GLIBC中没有对这个过程进行检查,并且是通过宏来实现的;

并且由于fd、bk这两个指针本身的位置是一个正常块的user data部分,所以这里是存在伪造的可能的;

示例程序

下面就用一个例子来看一下;

image-20210323190151013

程序和之前一样是一个菜单形式

1是malloc申请空间,2是可以输入对应空间要填充的内容,3可以free掉申请的chunk

这个程序的malloc申请的chunk限制了大小在120~1000字节之间;

0x78是120比特,也就是说正好是fastbin放不下的chunk

另外还有一点就是,这个漏洞unsafe_unlink提出的时候还没有NX,这个程序为了讲最基本的原理也关闭了NX

image-20210327110922700

任意地址写

首先是程序的漏洞所在,就是没有进行输出内容长度的判断,申请a、b后输入很多内容回溢出到下一个chunk

这个类似于之前的House_of_force,但是这个程序限制了malloc的次数,所以没办法像House_of_force一样利用;

image-20210329155553056

那么我们还是先设法获取一个任意地址写的能力;

我们这次一共只能申请两次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)会把两个块合并,实际上执行的操作是

  1. 查看b的prev_inuse,发现位0,前一个块已经被释放;
  2. 查看b的prev_size,找到块a的起始地址;
  3. 把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了

image-20210329161824457

下面把prev_size这个字段也伪造了,只需要修改刚才的edit那句

edit(a,b'a'*0x80+p64(0x90)*2)

image-20210329162148950

接下来我们需要填入两个指针,修改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)

image-20210329163725241

执行完毕之后,用vis可以看到A、B一起都被free掉了

并且可以看到我们输入的两个值都写到内存里了,fd+0x18写入了70b0bk+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

image-20210401101944829

可以看到图中红框是shellcode的内容,绿框是指向shellcode地址的一个指针,即bk

下面执行free(b)

image-20210401102202252

看到堆中a和b都已经被释放掉了,并且bk+0x10的位置已经变成了fd__free_hook的值

使用u可以将__free_hook中的内容当作code打印出来,可以看到已经是我们shellcode的内容了

image-20210401102941350

这时继续执行,再调用一次free就会执行我们的shellcode

最终返回了一个shell

image-20210401102517211

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注