• 티스토리 홈
  • 프로필사진
    satorare
  • 방명록
  • 공지사항
  • 태그
  • 블로그 관리
  • 글 작성
satorare
  • 프로필사진
    satorare
    • 분류 전체보기 (67)
      • book (3)
      • Translate (2)
      • bin (62)
      • wargame (0)
      • ctf (0)
  • 방문자 수
    • 전체:
    • 오늘:
    • 어제:
  • 최근 댓글
      등록된 댓글이 없습니다.
    • 최근 공지
      • ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⋯
      등록된 공지가 없습니다.
    # Home
    # 공지사항
    #
    # 태그
    # 검색결과
    # 방명록
    • [how2heap] fastbin_dup_consolidate.c
      2024년 09월 20일
      • satorare
      • 작성자
      • 2024.09.20.:54

      Lecture: fastbin_dup_consolidate.c

      https://github.com/shellphish/how2heap/blob/master/glibc_2.35/fastbin_dup_consolidate.c

       

      how2heap/glibc_2.35/fastbin_dup_consolidate.c at master · shellphish/how2heap

      A repository for learning various heap exploitation techniques. - shellphish/how2heap

      github.com

       

       

      Environment : Ubuntu 18.04 LTS / ldd (Ubuntu GLIBC 2.27-3ubuntu1.6) 2.27

       

      Lecture Code:

       

      #include <stdio.h>
      #include <stdlib.h>
      #include <assert.h>
      
      int main() {
      	// reference: https://valsamaras.medium.com/the-toddlers-introduction-to-heap-exploitation-fastbin-dup-consolidate-part-4-2-ce6d68136aa8
      	puts("This is a powerful technique that bypasses the double free check in tcachebin.");
      	printf("Fill up the tcache list to force the fastbin usage...\n");
      
      	void *ptr[7];
      
      	for(int i = 0; i < 7; i++)
      		ptr[i] = malloc(0x40);
      	for(int i = 0; i < 7; i++)
      		free(ptr[i]);
      
      	void* p1 = calloc(1,0x40);
      
      	printf("Allocate another chunk of the same size p1=%p \n", p1);
        	printf("Freeing p1 will add this chunk to the fastbin list...\n\n");
        	free(p1);
      
        	void* p3 = malloc(0x400);
      	printf("Allocating a tcache-sized chunk (p3=%p)\n", p3);
      	printf("will trigger the malloc_consolidate and merge\n");
      	printf("the fastbin chunks into the top chunk, thus\n");
      	printf("p1 and p3 are now pointing to the same chunk !\n\n");
      
      	assert(p1 == p3);
      
        	printf("Triggering the double free vulnerability!\n\n");
      	free(p1);
      
      	void *p4 = malloc(0x400);
      
      	assert(p4 == p3);
      
      	printf("The double free added the chunk referenced by p1 \n");
      	printf("to the tcache thus the next similar-size malloc will\n");
      	printf("point to p3: p3=%p, p4=%p\n\n",p3, p4);
      
      	return 0;
      }

       

      Background:

       

      이전 Lecture 까지는 DFB를 트리거하기 위해서 중복된 청크 해제 과정 사이에 임의의 청크 하나를 아무거나 해제시켜서 DFB 탐지를 우회하는 방법을 이용했었다.

       

      이번 Lecture에서는 청크를 연속으로 해제하는 대신 힙의 consolidate(병합) 기능을 이용해서 DFB를 트리거하는 방법을 알려준다.

       

      Consolidate 는 사용하지 않는 청크를 병합해서 메모리 단편화를 줄이기 위한 힙 메커니즘이다.

       

      malloc.c 파일에서 malloc_consolidate 함수가 존재하는데, 이 함수에서 병합 기능이 구현된다.

       

      다음은 malloc_consolidate 함수의 스니핏이다. 핵심 부분만 가져왔다.

       

      //malloc.c
      static void *
      _int_malloc(mstate av, size_t bytes) 
      {
      	...
      	if (in_smallbin_range (nb))
      	{
      		...
      	}
      	else 
      	{
      		idx = largebin_index(nb);
      		if (atomic_load_relaxed (&av->have_fastchunks))
      			malloc_consolidate (av);
      	}
      	...
      
      	use_top:
      	// arena의 top chunk를 검사
      	...
      	if ((unsigned long) size >= (unsigned long) (nb + MINSIZE))
      	{
      	...
      	}
      
      	else if (atomic_load_relaxed (&av->have_fastchunks)) 
      	{
      		malloc_consolidate (av);
      		...
      	}
      	
      	...
      }

       

      힙 메모리 할당 시점에는 총 두 번의 병합 기회가 주어진다.

       

      첫 번째는 lin_smallbin_range(nb) 조건을 통과하지 못하고 else 분기로 이동하는 경우, have_fastchunks 라는 함수를 이용해서 av(현재 아레나)의 fastbins에 청크가 존재하는지 확인한다. 청크가 존재한다면 해당 청크들을 병합한다.

       

      두 번째로 (unsigned long) size >= (unsigned long) (nb + MINSIZE) 조건을 통과하지 못한 경우, 똑같이 av(현재 아레나)의 fastbins에 청크가 존재하는지 확인한다. 첫 번째외 똑같이 fastbins에 청크가 존재한다면 해당 청크들을 병합한다.

       

      그렇다면 메모리 해제 시점에는 어떨까?

       

      //malloc.c
      static void
      _int_free (mstate av, mchunkptr p, int have_lock) 
      {
      	...
      	
      	else if (!chunk_is_mmapped(p)) {
      		...
      
      		if ((unsigned long)(size) >= FASTBIN_CONSOLIDATION_THRESHOLD) {
      			if (atomic_load_relaxed (&av->have_fastchunks))
      				malloc_consolidate(av);
      		}
      	}
      	...
      }

       

      (unsigned long) (size) >= FASTBIN_CONSOLIDATE_THRESHOLD 인 경우, 병합을 진행한다.

       

      FASTBIN_CONSOLIDATE_THRESHOLD 매크로는 상수 매크로이며 64kb 값을 가지고 있다. 즉, 해제된 청크의 크기가 64kb보다 큰 경우에 또 다시 fastbins를 검사하여, 내부에 청크가 존재한다면 그들을 병합한다.

       

      할당과 해제 시점의 모든 병합 조건을 알았으니 다음은 malloc_consolidate 함수의 동작을 알아보자.

       

      //malloc.c
      static void malloc_consolidate(mstate av)
      {
        mfastbinptr*    fb;                 /* current fastbin being consolidated */
        mfastbinptr*    maxfb;              /* last fastbin (for loop control) */
        mchunkptr       p;                  /* current chunk being consolidated */
        mchunkptr       nextp;              /* next chunk to consolidate */
        mchunkptr       unsorted_bin;       /* bin header */
        mchunkptr       first_unsorted;     /* chunk to link to */
      
        /* These have same use as in free() */
        mchunkptr       nextchunk;
        INTERNAL_SIZE_T size;
        INTERNAL_SIZE_T nextsize;
        INTERNAL_SIZE_T prevsize;
        int             nextinuse;
        mchunkptr       bck;
        mchunkptr       fwd;
      
        atomic_store_relaxed (&av->have_fastchunks, false);
      
        unsorted_bin = unsorted_chunks(av);
      
        /*
          Remove each chunk from fast bin and consolidate it, placing it
          then in unsorted bin. Among other reasons for doing this,
          placing in unsorted bin avoids needing to calculate actual bins
          until malloc is sure that chunks aren't immediately going to be
          reused anyway.
        */
      
        maxfb = &fastbin (av, NFASTBINS - 1);
        fb = &fastbin (av, 0);
        do {
          p = atomic_exchange_acq (fb, NULL);
          if (p != 0) {
            do {
      	{
      	  unsigned int idx = fastbin_index (chunksize (p));
      	  if ((&fastbin (av, idx)) != fb)
      	    malloc_printerr ("malloc_consolidate(): invalid chunk size");
      	}
      
      	check_inuse_chunk(av, p);
      	nextp = p->fd;
      
      	/* Slightly streamlined version of consolidation code in free() */
      	size = chunksize (p);
      	nextchunk = chunk_at_offset(p, size);
      	nextsize = chunksize(nextchunk);
      
      	if (!prev_inuse(p)) {
      	  prevsize = prev_size (p);
      	  size += prevsize;
      	  p = chunk_at_offset(p, -((long) prevsize));
      	  unlink(av, p, bck, fwd);
      	}
      
      	if (nextchunk != av->top) {
      	  nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
      
      	  if (!nextinuse) {
      	    size += nextsize;
      	    unlink(av, nextchunk, bck, fwd);
      	  } else
      	    clear_inuse_bit_at_offset(nextchunk, 0);
      
      	  first_unsorted = unsorted_bin->fd;
      	  unsorted_bin->fd = p;
      	  first_unsorted->bk = p;
      
      	  if (!in_smallbin_range (size)) {
      	    p->fd_nextsize = NULL;
      	    p->bk_nextsize = NULL;
      	  }
      
      	  set_head(p, size | PREV_INUSE);
      	  p->bk = unsorted_bin;
      	  p->fd = first_unsorted;
      	  set_foot(p, size);
      	}
      
      	else {
      	  size += nextsize;
      	  set_head(p, size | PREV_INUSE);
      	  av->top = p;
      	}
      
            } while ( (p = nextp) != 0);
      
          }
        } while (fb++ != maxfb);
      }

       

      간결하게 정리하면 다음과 같다.

       

      1. fastbins를 불러온다

      2. 이전 청크가 비어있으면 현재 청크와 병합

      3. 다음 청크가 탑 청크가 아니고 비어있으면 현재 청크와 병합

      4. 2,3번을 거친 청크들은 모두 unsorted bin으로 이동 (이후에 다시 small bin, large bin으로 분류한다)

      5. 2, 3번을 거치지 않은 청크들 중에서 다음 청크가 탑 청크인 경우 탑 청크와 병합

      6. 모든 fastbin 인덱스에  대해서 위 과정을 반복 실행

       

      정리해도 헷갈린다면 프로그램을 직접 실행해서 결과를 분석해보자.

       

      Analyze:

       


      한줄씩 분석해보면 다음과 같다.

       

      우선 tcache를 가득채운다

       

      이후 p1 청크(0x557463bdf8a0)를 할당 후 해제해서 fastbin list에 넣는다.

       

      이제 malloc_consolidate 함수를 실행하기 위해서 적절한 크기의 청크를 할당한다.

      (병합 조건은 small bin에 들어가지 않을 정도의 크기를 할당 요청하는 경우, 혹은 모든 bins로 부터 재사용할 청크가 없어서 top chunk로 부터 할당받는 경우이다.)

       

      여기선 0x400 크기의 p3 청크를 할당하였다.

      (출력을 보면 tcache-sized chunk라 되어있는데 나중에 tcache에 넣을거라 그렇다. 어찌됐든 조건은 small bin (최대 1023byte까지 수용)에 안들어가면 되기 때문에 0x400(1024)byte 를 할당하면 된다. 참고로 tcache의 최대 크기 인덱스는 1032byte이므로 small bin과 안 겹침)

       

      조건을 만족했기 때문에 p3를 할당할 때 malloc_consolidate 함수가 실행된다.

       

      현재 fastbins에는 p1만 존재하고, 이 p1의 다음 청크(굳이 리스트로 연결되어있지 않더라도 인접한 주소를 레퍼런스한다.)는 현재 아레나의 top chunk일 것이다.

       

      위에서 알아보았듯이 top chunk와 인접한 fastbin의 청크는 top chunk와 병합된다. 결과적으로 p1은 top chunk와 병합된다.

       

      그리고 p3는 이 병합된 top chunk로 부터 메모리를 할당받는다.

       

      이를 조금 더 가시적으로 보기위해서 pwndbg로 확인해보면 다음과 같다.

       

       

      이 상황은 p3를 할당하기 직전이다. 여기서 top chunk와 병합되는 부분을 캐치하려면 _int_malloc 함수 내부의 malloc_consolidate 함수 실행 직후를 포착해야한다. (메인 함수에 브레이크를 걸면 병합이 순식간에 이루어져서...)

       

      p3를 할당하는 malloc 함수에 브레이크를 걸고 step into 명령으로 함수를 파고들어 다음과 같이 malloc_consolidate 함수가 실행되는걸 포착했다.

       

       

      (조금 시간이 걸리니까 인내심을 갖고 계속 한 스텝씩 실행)

       

      여기까지 실행하고 힙 상태를 확인해보면 다음과 같다.

       

       

      기존의 fastbin에 존재했던 p1(0x555555603890)이 top chunk의 시작 주소(Addr 값)가 되어버렸다. 여기서 p3 할당을 마치면 다음과 같이 top chunk로 부터 p3를 할당받는다.

       

       

      찿ㅁ고로 main 함수를 디스어셈블 해보면 rbp-0x58이 p1, rbp-0x50이 p3인데 두 위치를 비교해보면 값이 똑같다.

      (포인터 정리를 안하기 때문에 이런 검증이 가능하다)

       

       

      힙 메모리 포인터는 청크의 시작 주소가 아니라 데이터 영역을 가리킨다는 점을 유의해야한다. 그래서 위의 Chunk의 주소와 0x10 차이가 난다.

       

      이제 p1을 해제하려면 p3(0x555555603890)의 크기를 참조하므로 0x400 크기의 청크를 해제하는 것과 똑같다. 따라서 fastbin이 아니라 tcache에 들어간다. (아까도 말했듯이 tcache 최대 수용크기는 1032byte)

       

      *tcache는 앞에서 꽉 채운거 아닌가?

      0x40 크기의 인덱스가 꽉찬거지 0x400 크기를 수용하는 인덱스는 하나도 안찼다

       

       

      예상대로 fastbin이 아닌 tcache에 p1(0x555555603890) 청크가 들어갔다.

       

      이걸로 Duplicated Chunk가 되었다. (아래와 같이 p1은 p3로써 Allocated이지만, 동시에 fastbin에도 존재한다.)

       

       

      이제 p4(0x400)을 할당하면 당연히 0x400 크기의 p1을 재사용한다. 결과적으로 p4는 p3와 같은 주소를 공유한다.

       

       

      최종적으로 3개의 포인터 p1(rbp-0x58), p3(rbp-0x50), p4(rbp-0x48)이 같은 청크를 공유하고 되었다.

       

      * 근데 이게 왜 DFB 인가? bins에 같은 청크가 존재한 적이 없는데?

       

      => 딱히 bins에 중복된 청크가 존재해야한다는 법칙은 없다. DFB는 결국 중복 해제를 의미하므로...

       

      이런 병합을 이용한 DFB 트리거 방법은 아주 제한적인 상황에서 의미있다. 예를 들어 청크의 해제 순서를 강제하는 경우, 혹은 같은 크기의 청크를 연속으로 할당할 수 없는 경우, 혹은 무조건 큰 크기의 청크를 할당받아야하는 경우에는 이 방법이 유용할 것이다.

       

      Reference:

       

       

       

      저작자표시 비영리 (새창열림)

      'bin' 카테고리의 다른 글

      ROPgadget 설치할 때 error: externally-managed-environment  (0) 2025.02.17
      [how2heap] fastbin_dup_into_stack.c  (0) 2024.09.11
      [how2heap] fastbin_dup.c  (0) 2024.09.11
      [how2heap] calc_tcache_idx.c  (0) 2024.09.04
      [how2heap] first_fit.c  (0) 2024.09.03
      다음글
      다음 글이 없습니다.
      이전글
      이전 글이 없습니다.
      댓글
    조회된 결과가 없습니다.
    스킨 업데이트 안내
    현재 이용하고 계신 스킨의 버전보다 더 높은 최신 버전이 감지 되었습니다. 최신버전 스킨 파일을 다운로드 받을 수 있는 페이지로 이동하시겠습니까?
    ("아니오" 를 선택할 시 30일 동안 최신 버전이 감지되어도 모달 창이 표시되지 않습니다.)
    목차
    표시할 목차가 없습니다.
      • 안녕하세요
      • 감사해요
      • 잘있어요

      티스토리툴바