본문으로 바로가기

* 이 글은 glibc 2.23 기준으로 작성되었습니다.

* 이 글은 glibc의 heap management가 어떻게 동작하는지 알고 있어야 수월하게 읽을 수 있습니다.

* 이 글은 shellphish 팀의 how2heap repository를 참고했습니다.

* 컴파일 command line: gcc test.c -o test -Wl,-z,relro -no-pie -fno-stack-protector -O0 -ggdb

 

 

Last update: 6/26/2020

 

Analysis

malloc_consolidate의 특성을 사용해 동일한 chunk를 두 번 할당받는 기법이다.

_int_malloc에서는 large chunk 에 해당하는 크기의 할당 요청을 받으면 malloc_consolidate를 실행한다.

 

3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
  /*
     If this is a large request, consolidate fastbins before continuing.
     While it might look excessive to kill all fastbins before
     even seeing if there is space available, this avoids
     fragmentation problems normally associated with fastbins.
     Also, in practice, programs tend to have runs of either small or
     large requests, but less often mixtures, so consolidation is not
     invoked all that often in most programs. And the programs that
     it is called frequently in otherwise tend to fragment.
   */


  else
    {
      idx = largebin_index (nb);
      if (have_fastchunks (av))
        malloc_consolidate (av);
    }
/malloc/malloc.c

 

주석으로 설명이 되어있긴 하지만, 간략하게 설명하자면 chunk의 단편화 (fragmentation) 을 방지하기 위해, 어느 정도 큰 크기의 할당 요청이 들어오면 fastbin을 전부 병합하는 과정을 거치는 것이다.

 

이 글의 malloc_consolidate 분석 내용을 요약하면, 다음 위치에 있는 chunk가 top chunk가 아니면 이 chunk를 unsorted bin으로 옮긴다. chunk가 unsorted bin으로 옮겨지고 fastbin이 비워지면, 동일한 chunk를 다시 free해 unsorted bin과 fastbin에 이 chunk를 올려놓을 수 있다. 이후 malloc으로 해당 크기를 두 번 요청하면 둘 다 같은 주소를 반환하게 된다.

 

실제 동작은 Example 파트에서 설명한다.

 

Example

0x10 크기 chunk를 2개 만들고, 0x800 크기 할당을 요청해 malloc_consolidate를 실행시킨다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <malloc.h>

int main() {
    void *s1 = malloc(0x10);
    void *s2 = malloc(0x10);

    free(s1);
    malloc(0x800);
    free(s1);

    s1 = malloc(0x10);
    s2 = malloc(0x10);

    return 0;
}

 

첫 번째 free 이후 main_arena는 아래와 같은 상태가 된다.

 

gdb-peda$ heapinfo
(0x20)     fastbin[0]: 0x602000 --> 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x602040 (size : 0x20fc0)
       last_remainder: 0x0 (size : 0x0)
            unsortbin: 0x0

 

0x800 크기를 할당하고 나면 특이하게 smallbin에 0x602000의 chunk가 들어가 있는 걸 볼 수 있다.

 

gdb-peda$ heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x602040 (size : 0x20fc0)
       last_remainder: 0x0 (size : 0x0)
            unsortbin: 0x0
(0x020)  smallbin[ 0]: 0x602000

 

0x602000을 다시 free 하면, 이 chunk는 fast chunk 크기에 해당하기 때문에 fastbinsY[0] 을 확인하고, 이 리스트가 비어있기 때문에 double free 검사 로직을 우회할 수 있다.

 

gdb-peda$ heapinfo
(0x20)     fastbin[0]: 0x602000 --> 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x602850 (size : 0x207b0)
       last_remainder: 0x0 (size : 0x0)
            unsortbin: 0x0
(0x020)  smallbin[ 0]: 0x602000 (invalid memory)

 

0x602000 chunk가 fastbin에 들어가면서 fd 필드가 0으로 초기화되기 때문에 smallbin으로서 0x602000 chunk는 고장난 상태가 된다.

이 상태에서 malloc을 두 번 실행하면 s1와 s2에는 같은 주소가 할당된다.

 

gdb-peda$ p s1
$1 = (void *) 0x602010
gdb-peda$ p s2
$2 = (void *) 0x602010