혜랑's STORY

[Dreamhack] Heap Allocator Exploit : Security check 본문

2021 SISS 21기 활동/2학시 시스템

[Dreamhack] Heap Allocator Exploit : Security check

hyerang0125 2021. 9. 11. 15:06

_int_malloc

_int_malloc과 _int_free는 개방자의 실수, 악의적인 행위 등으로 힙이 정상적으로 동작하지 않는 것을 방지하기 위해 검증 코드가 존재한다. (검증 코드를 이해하고 있다면 익스플로잇을 할 때 이를 우회하여 공격할 수 있게 된다.)

malloc() : memory corruption (fast)

#define fastbin_index(sz) \
  ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
idx = fastbin_index (nb);
if (victim != 0)
{
    if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
    {
        errstr = "malloc(): memory corruption (fast)";
       errout:
        malloc_printerr (check_action, errstr, chunk2mem (victim), av);
        return NULL;
    }
}

_int_malloc에서 fastbin 크기의 힙이 할당될 때 호출되는 검증 코드이다. 

할당하려는 fastbin의 크기와 할당될 영역의 크기를 구한 후 두 크기가 같은 bin에 속하는지 검증한다. 같은 bin에 속하는 크기라면 정상적으로 할당될 것이고, 아니라면 "malloc(): memory corruption (fast)" 에러를 출력하고 비정상 종료 할 것이다.

// m_error1.c
// gcc -o m_error1 m_error1.c
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
int main()
{
	uint64_t *ptr,*ptr2;
	ptr = malloc(0x20);
	ptr2 = malloc(0x20);
	free(ptr);
	free(ptr2);
	
	fprintf(stderr,"ptr2 fd: %p: %p\n", ptr2, ptr2);
	*(uint64_t *)ptr2 = *(uint64_t *)ptr2 + 0x100;
	fprintf(stderr,"ptr2 fd: %p: %p\n", ptr2, ptr2);		
	malloc(0x20);
	malloc(0x20);
}

m_error1.c는 ptr2의 FD를 힙 청크가 존재하지 않는 영역의 주소로 조작해 검증 에러를 발생시키는 코드이다.

메모리의 모습은 다음과 같다.

해제된 ptr2의 FD를 올바르지 않은 0x602100 주소로 조작된 것을 확인하였다. 0x602100 주소는 같은 bin의 크기를 가지고 있지 않기 때문에 에러가 발생한다.

malloc(): memory corruption

while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
  bck = victim->bk;
  if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
      || __builtin_expect (victim->size > av->system_mem, 0))
    malloc_printerr (check_action, "malloc(): memory corruption",
        chunk2mem (victim), av);

unsorted bin 힙의 BK가 main_arena의 unsorted bin 주소인지 확인한다. 다른 값일 때, 할당하는 힙의 크기가 2 * SIZE_SZ 보다 작거나 av->system_mem 보다 크면 에러를 출력하고 비정상 종료한다.

// m_error2.c
// gcc -o m_error2 m_error2.c 
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
int main()
{
        uint64_t *ptr,*ptr2,*ptr3;
        ptr = malloc(0x100);
        ptr2 = malloc(0x100);
        free(ptr);
        fprintf(stderr, "BK: %p\n", ptr[1]);
        ptr[1] += 0x40;
        fprintf(stderr, "Corrupted BK: %p\n", ptr[1]);
        malloc(0x21000);
}

m_error2.c는 위에서 언급한 검증 에러를 발생시키는 코드이다. 메모리의 모습은 다음과 같다.

unsorted bin의 BK를 다른 값으로 조작하고 av->system_mem보다 큰 값을 할당했다. unsorted bin의 BK가 smallbin의 주소를 가리키고 있기 때문에 에러가 발생한다.


_int_free

free(): invalid pointer

#define misaligned_chunk(p) \
  ((uintptr_t)(MALLOC_ALIGNMENT == 2 * SIZE_SZ ? (p) : chunk2mem (p)) \
   & MALLOC_ALIGN_MASK)
size = chunksize (p);
if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
    || __builtin_expect (misaligned_chunk (p), 0))
{
  errstr = "free(): invalid pointer";
  errout:
  if (!have_lock && locked)
        (void) mutex_unlock (&av->mutex);
    malloc_printerr (check_action, errstr, chunk2mem (p), av);

힙을 해제할 때 호출되는 검증 코드이다.

p> -size 혹은 misaligned_chunk(p) 조건을 검증한다.

해제하려는 청크의 크기를 음수로 변환하여 청크 주소랑 비교한다. 만약, 작거나 해제하려는 청크의 align이 맞지 않을 경우 "free(): invalid pointer" 에러를 출력하고 비정상 종료할 것이다.

// f_error1.c
// gcc -o f_error1 f_error1.c
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
int main()
{
    uint64_t *ptr;
	// p > -size
    ptr = malloc(0x100);
	ptr[-1] = -0x100;
	free(ptr);	
}

f_error1.c는 위에서 언급한 검증 에러를 발생시키는 코드이다. 메모리의 모습은 다음과 같다.

이는 힙의 size를 음수로 바꾸고 해제했기 때문에 에러가 발생한다.

// f_error2.c
// gcc -o f_error1 f_error1.c
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
int main()
{
    uint64_t *ptr;
	ptr = malloc(0x100);
	free(ptr+0x100);	
}

f_error2.c는 힙 청크가 존재하지 않는 영역을 해제하는 예제이다.

free 함수가 호출될 때의 인자는 다음과 같다.

해제하려는 영역은 malloc_chunck 구조체가 존재하지 않기 때문에 에러가 발생한다.


free(): invalid size

#define MIN_CHUNK_SIZE        (offsetof(struct malloc_chunk, fd_nextsize))
#define MINSIZE  \
  (unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK))
if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size)))
{
  errstr = "free(): invalid size";
  goto errout;
}

힙을 해제할 대 호출되는 검증 코드이다. 

해제하려는 힙의 size가 MINSIZE, 즉 malloc_chunck의 크기보다 크고 힙의 align이 맞는지 검증한다.

malloc(1)을 통해 힙을 할당하면 malloc_chunks 구조체가 함께 할당되기 때문에 최소 크기는 0x20이다.

// f_error3.c
// gcc -o f_error3 f_error3.c
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
int main()
{
    uint64_t *ptr;
	// size < MINSIZE
    ptr = malloc(0x10);
	ptr[-1] = 0x19;
	free(ptr);
}

f_error3.c는 위에서 언급한 검증 에러를 발생시키는 코드이다. 메모리의 모습을 다음과 같다.

힙의 size를 최소 크기보다 작은 0x19 값으로 조작하고 해제하면서 size가 MINSIZE보다 작기 때문에 에러가 발생한다.


free(): invalid next size (fast), free(): invalid next size (normal)

#define chunk_at_offset(p, s)  ((mchunkptr) (((char *) (p)) + (s)))
if (have_lock
      || ({ assert (locked == 0);
      mutex_lock(&av->mutex);
      locked = 1;
      chunk_at_offset (p, size)->size <= 2 * SIZE_SZ
        || chunksize (chunk_at_offset (p, size)) >= av->system_mem;
        }))
{
      errstr = "free(): invalid next size (fast)";
      goto errout;
}

if (__builtin_expect (nextchunk->size <= 2 * SIZE_SZ, 0)
  || __builtin_expect (nextsize >= av->system_mem, 0))
{
  errstr = "free(): invalid next size (normal)";
  goto errout;
}

힙을 해제할 때 호출되는 검증코드이다.

chunck_at_offset (p, size)->size <= 2 * SIZE_SZ와 chuncksize(chunck_at_offset (p, size)) >= av->system_mem 조건을 검증한다.

chunk_at_offset 메크로는 해제하려는 청크의 size를 가져와 해당 포인터에서 size를 더한 주소를 반환한다. 즉, 위 코드는 해제하려는 힙 뒤에 위치하는 또 다른 힙의 size를 2 * SIZE_SZ, av->system_mem과 비교한다.

fast와 normal의 차이는 해제하는 힙의 크기가 fastbin인지 smallbin 크기인지로 나눠진다.

// f_error4.c
// gcc -o f_error4 f_error4.c 
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
int main()
{
        uint64_t *ptr,*ptr2;
        ptr = malloc(0x10);
        ptr2 = malloc(0x10);
        ptr2[-1] = 0x22001;
        free(ptr);
}

f_error4.c는 위에서 언급한 검증 에러를 발생시키는 코드이다. 메모리의 모습은 다음과 같다.

ptr 뒤에 위치는 ptr2의 size를 av->system_mem 크기보다 크게 설정하고 ptr를 해제하였기 때문에 에러가 발생한다.


double free or corruption (fasttop)

mchunkptr old = *fb, old2;
if (__builtin_expect (old == p, 0))
{
  errstr = "double free or corruption (fasttop)";
  goto errout;
}

힙을 해제할 때 호출되는 검증 코드이다. 

old == p 조건을 검증한다. old 포인터는 이전에 해제한 포인터를 저장하고, p는 현재 해제할 포인터이다.

이전의 해제한 포인터와 현재 해제한 포인터가 동일 하다면 "double free or corruption (fasttop)" 에러를 출력하고 비정상 종료한다.

// f_error5.c
// gcc f_error5 f_error5.c 
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
int main()
{
    uint64_t *ptr;
    ptr = malloc(0x10);
	free(ptr);
	free(ptr);
}

f_error5.c는 위에서 언급한 검증 에러를 발생시키는 코드이다. 두 번째 free 함수에서 ptr을 해제할 때 old와 p가 같은 포인터를 가지기 때문에 에러가 발생한다.


double free or corruption (top)

if (__glibc_unlikely (p == av->top))
{
  errstr = "double free or corruption (top)";
  goto errout;
}

p -- av->top 조건을 검증한다. 해제하려는 힙이 꼭대기에 존재하여 top chunk 값을 가지면 "double free or corruption (top)" 에러를 출력하고 비정상 종료한다.

// f_error6.c
// gcc f_error6 f_error6.c 
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
int main()
{
    uint64_t *ptr,*ptr2;
    ptr = malloc(0x100);
	ptr2 = malloc(0x100);
	free(ptr2);
	free(ptr2);
}

f_error6.c는 위에서 언급한 검증 에러를 발생시키는 코드이다. 두 번째 해제하는 ptr2가 av->top 이기 때문에 에러가 발생한다.


double free or corruption (out)

nextchunk = chunk_at_offset(p, size);
if (__builtin_expect (contiguous (av)
        && (char *) nextchunk
        >= ((char *) av->top + chunksize(av->top)), 0))
{
  errstr = "double free or corruption (out)";
  goto errout;
}

해제하려는 청크 뒤에 위치하는 포인터와 av->top + chuncksize(av->top) 를 검증한다.

위 검증은 힙 청크를 해제하였을 때 해제된 청크의 크기 뒤에 힙이 존재해야 한다.조건을 만족하지 못하면 "double free or corruption (out)" 에러를 출력하고 비정상 종료한다.

// f_error7.c
// gcc f_error7 f_error7.c
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
int main()
{
    uint64_t *ptr,*ptr2;
    ptr = malloc(0x100);
	ptr2 = malloc(0x100);
	ptr2[-1] = 0x21001;
	free(ptr2);
}

f_error7.c는 size를 큰 값으로 만들고 해제해 에러를 발생시키는 예제이다.


double free or corruption (!prev)

if (__glibc_unlikely (!prev_inuse(nextchunk)))
{
  errstr = "double free or corruption (!prev)";
  goto errout;
}

해제하려는 힙 뒤에 위치하는 힙의 size 멤버에 prev_inuse 비트가 설정되어 있는지 확인한다.

이전 청크의 해제 유무를 나타내는 prev_inuse가 설정되어 있지 않다면 "double free or corruption (!prev)" 에러를 출력하고 비정상 종료한다.

// gcc f_error8 f_error8.c 
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
int main()
{
    uint64_t *ptr,*ptr2;
    ptr = malloc(0x100);
	ptr2 = malloc(0x100);
	ptr2[-1] = 0x110;
	free(ptr);
}

f_error8.c는 ptr2 힙 청크의 prev_inuse를 0으로 조작해 에러를 발생시키는 예제이다.


unlink

corrpted double-linked list

unlink 메크로는 두 개의 힙 청크가 연속적으로 해제되었을 때 두 청크를 병합하기 위하여 사용된다.

// gcc -o unlink1 unlink1.c
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
int main()
{
    uint64_t *ptr,*ptr2;
    ptr = malloc(0x100);
	ptr2 = malloc(0x100);
	free(ptr);
	free(ptr2);
}

unlink1.c는 두 개의 인접해있는 힙 청크를 해제하여 unlink 매크로를 호출하는 코드이다.

다음은 smallbin을 처리하는 unlink 매크로의 코드이다.

#define unlink(AV, 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, AV);  \
    else {								      \
        FD->bk = BK;							      \
        BK->fd = FD;

해제하려는 청크의 FD->bk와 BK->fd가 해제하려는 주소인지 검증한다. 만약 FD->bk와 BK->fd가 해제하려는 힙의 주소와 다르면 "corrpted double-linked list" 에러를 출력하고 비정상 종료한다.

// gcc -o unlink2 unlink2.c
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
int main()
{
    uint64_t *ptr,*ptr2;
    ptr = malloc(0x100);
	ptr2 = malloc(0x100);
	free(ptr);
	ptr[0] = ptr+0x100;
	ptr[1] = ptr+0x100;
	free(ptr2);
}

unlink2.c는 연결 리스트를 조작해 에러를 발생시키는 예제이다.


corrpted double-linked list (not small)

#define unlink(AV, P, BK, FD) {                                            \
    FD = P->fd;                     \
    BK = P->bk;                     \
    else {                      \
      FD->bk = BK;                    \
      BK->fd = FD;                    \
      if (!in_smallbin_range (P->size)              \
            && __builtin_expect (P->fd_nextsize != NULL, 0)) {          \
        if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)        \
            || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))    \
              malloc_printerr (check_action,              \
                        "corrupted double-linked list (not small)",    \
                          P, AV);

in_smallbin_range (P->size) 코드로 smallbin에서 허용하는 크기보다 큰 힙이 연속적으로 해제되었을 때 호출된다.

P -> fd_nextsize -> bk_nextsize != P와 P -> bk_nextsize -> fd_nextsize != P 조건을 검증한다.

large bin에서는 FD, BK를 포함하면서 fd_nextsize와 bk_nextsize 또한 관리하기 때문에 해당 조건을 만족하지 못하면 "corrupted double-linked list (not small)" 에러를 출력하고 비정상 종료한다.

// gcc -o unlink3 unlink3.c
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
int main()
{
    uint64_t *ptr,*ptr2;
    ptr = malloc(0x1000);
	ptr2 = malloc(0x1000);
	free(ptr);
	ptr[2] = ptr+0x100;
	ptr[3] = ptr+0x100;
	free(ptr2);
}

unlink3.c는 fd_nextsize와 bk_nextsize를 조작해 에러를 발생시키는 예제이다.