Heap_기초 2

c0wb3ll ㅣ 2020. 12. 1. 18:54

Heap_기초 2

정리

오늘 정리는 힙 공부를 하면서 여기저기 많이 들려보았지만 fkillrra님의 블로그를 보고 추천영상을 받아 그것을 기초로 정리하였다.

마지막 부분에 reference를 첨부했으니 그 글을 보는 것을 추천한다.

또한 이 글은 힙에 대한 기초적인 내용의 이해를 위해 glibc_2.23버전에서 실습하였다. (ubuntu16.04)


dlmalloc의 알고리즘

  • boundary tag

    chunk의 크기 정보가 chunk의 앞뒤에 저장

  • binning

    재사용가능한 chunk를 크기 단위로 관리

boundary tag

각 chunk들이 할당될 때 크기 정보는 각 chunk의 size부분에 저장된다.

각 청크가 해제될 때 크기 정보가 바로 뒤 chunk의 prev_size부분에 저장된다.

위 그림을 예시로 들면 B chunk가 해제되었을 때는 바로 뒤 chunk인 C chunk에 prev_size에 B chunk의 크기 정보가 기록된다.

이를 통해 알 수 있는 정보는 allocated chunk 와 freed chunk가 존재할 때 인접한 앞/뒤 chunk의 주소를 계산할 수 있다는 점이다.

#include <stdio.h>
#include <stdlib.h>

void main(){
    char *heap1 = malloc(0x90);
    char *heap2 = malloc(0x90);
    char *heap3 = malloc(0x90);

    free(heap1);
    free(heap2);
    free(heap3);
}

이를 실제 코드를 통해 확인해 보도록 하자.

인접한 chunk가 해제되었을 때 prev_size를 이용하여 인접한 앞의 chunk의 주소 계산을 할 수 있다.

위 사진에서 보면 A chunk가 해제된 경우 B chunk의 address와 prev_size를 이용하여 A chunk의 주소를 알아낼 수 있다.

A chunk address = B chunk address - B chunk prev_size
0x1710000       = 0x17100a0       - 0xa0

또한 chunk의 size를 알고 있다면 인접한 뒤의 chunk를 알아낼 수 있다.

위 사진에서 보면 B chunk의 address와 size를 이용하여 C chunk의 주소를 알아낼 수 있다.

C chunk address = B chunk address + B chunk size
0x1710140       = 0x17100a0       + 0xa0

이것을 다른 관점으로 생각해보면

  • prev_size를 조작하면, 이전 chunk의 위치를 조작 가능하다.
  • size를 조작하면, 다음 chunk의 위치를 조작 가능하다.
  • prev_inuse bit를 조작하면 이전 chunk의 할당/해제 여부를 조작 가능하다.

binning

binning이란 dlmalloc이 메모리를 효율적으로 관리하기 위해서 free된 chunk를 크기 단위로 관리하는 방식이다.

이렇게 free된 chunk들을 크기 단위로 관리함으로써 재사용하고 효율적으로 메모리를 관리하게 됩니다.

bin의 종류

  • Fast bin
    • 작은 크기의 chunk들을 빠르게 관리
    • chunk의 크기
      • 32bit : 64 byte 미만
      • 64bit : 128 byte 미만
    • linked list 종류
      • single
    • fit 알고리즘
      • exact fit
    • 개수
      • 10
    • 입출력 방식
      • LIFO
  • Small bin
    • 일반적인 크기의 chunk를 관리
    • chunk의 크기
      • 512 byte 미만
    • linked list 종류
      • double
    • fit 알고리즘
      • exact/best fit
    • 개수
      • 62
    • 입출력 방식
      • FIFO
  • Large bin
    • 큰 크기의 chunk를 관리
    • chunk의 크기
      • 512 byte 이상
    • linked list 종류
      • double
    • fit 알고리즘
      • best fit
    • 개수
      • 63
    • 입출력 방식
      • FIFO
  • Unsorted bin
    • 다양한 용도로 사용
    • chunk의 크기
      • variable
    • linked list 종류
      • double
    • fit 알고리즘
      • exact fit
    • 개수
      • 10,000
    • 입출력 방식
      • FIFO

Fastbin

  • size가 다른 10개의 chunk를 single linked list로 관리
  • LIFO 선입후출 구조
  • chunk의 크기
    • 32bit : 64byte 이하
    • 64bit : 128byte 이하
  • 해제가 되어도 prev_inuse bit가 변경되지 않음 (중요)
    • 인접한 chunk들과 병합되지 않음
#include <stdio.h>
#include <stdlib.h>

void main(){
    char *heap1 = malloc(16);
    char *heap2 = malloc(16);
    char *heap3 = malloc(16);

    free(heap1); 
    free(heap2); // bp
}

실제로 첫번째 free 이후에 힙 영역을 보면 인접한 chunk가 free되었음에도 불구하고 size + prev_inuse bit 값으로 0x21이라는 값을 가지고 있는것을 확인할 수 있다.

그리고 앞서 말한것과 같이 single linked list로 관리되고 있는것을 확인할 수 있었다.

Small bin

  • size가 다른 63개의 chunk를 double linked list로 관리
  • FIFO 선입선출 구조
  • chunk의 size
    • 512byte 이하
  • 해제가 되면 뒤 chunk의 prev_inuse_bit 변경
    • free할 때 인접한 freed chunk와 병합시킴
#include <stdio.h>
#include <stdlib.h>

void main(){
    char *heap1 = malloc(0x100);
    char *heap2 = malloc(0x100);
    char *heap3 = malloc(0x100);

    free(heap2);
    free(heap1);
}

다음과 같이 인접한 이전 chunk가 해제되자 prev_inuse_bit가 변경되는 것을 볼 수 있다.

또한 인접한 chunk가 한번 더 free()되자 병합되는 것 또한 확인할 수 있었다.

Large bin

  • 특정 범위의 size가 다른 63개의 chunk를 double linked list로 관리
  • FIFO 선입선출 구조
  • chunk size
    • 512byte 초과
  • 범위 내에서 size가 가장 큰 chunk가 제일 앞
  • 해제가 되면 뒤 chunk의 prev_inuse_bit 변경
    • free할 때 인접한 freed chunk와 병합시킴

Unsorted bin

Unsorted bin 에 들어가는 경우

  • free했을 때 bin(small bin, large bin)으로 들어가기 전
  • fast bin 들이 병합(malloc_consolidate)되어 합쳐지면
  • bestfit으로 할당된 chunk의 남은 부분인 remainder
  • 인접한 chunk도 이미 free되어 있어, 병합(consolidate) 된 free chunk

Unsorted bin 에서 나오는 경우

  • 사용자가 malloc을 호출하여 요청한 size와 동일한 chunk가 존재하면
  • 이 위에 여러가지 이유가 존재하지만 대표적인 이유만 적음

병합(consolidate)

해제(free)하려는 chunk의 앞, 뒤 chunk가 이미 해제되어 있을 때 병합이 발생한다.

A chunk가 이미 해제된 상태에서 B chunk를 해제하려 할 때 병합이 발생한다.

B chunk가 해제되면 이전 A chunk에서 prev_inuse bit를 확인한 뒤 할당/해제 여부를 판단한다.

또한 이전 chunk가 해제 되어있다면 현재 chunk의 prev_size가 초기화 되어있기 때문에 계산을 통해 A chunk의 주소를 구할 수 있게되고 이 두개를 합치게 된다.

반대로 B chunk가 해제된 상태에서 A chunk가 해제될 때도 병합이 일어난다.

다만 A chunk가 해제될 때는 B chunk의 주소를 안 뒤 C chunk의 주소까지 알아낸다. 이유는 B chunk의 해제 여부인 prev_inuse bit 또는 prev_size가 C chunk에 존재하기 때문이다.

unlink

binlist에 등록된 chunk를 제거 하기 위해 포인터 fd, bk를 정리하는 작업

위 그림과 같이 bin list에서 chunk가 제거 되어야 하는 상황에 제거되는 chunk의 fd와 bk를 이용하여 계산하고, 채우는 과정을 unlink라고 한다.

위와 같은 상황에서 가운데 chunk가 제거되면 더 이상 첫번재 chunk의 bk는 B가 아닌 C여야 하기 때문에 B의 bk를 가지고 와 A의 bk에 세팅한다. fd도 마찬가지이다.

삽질

어렴다....

fastbin 할 때는 첫번째 free때는 link가 안생기는게 정상인데 Top chunk를 보고 오해해서 링크인 줄 알고 왜 링크가 안생기지? 하며 몇 시간을 삽질하거나...했다...

 

솔직히 Large bin 부터는 실습이 안되어서 이해가 안되었다... 왜인지 모르겠지만 fd_next_size와 bk_next_size가 생성되지 않았다... 이해하길 포기해버렸다.... 아니 글로는 이해 되었는데 눈으로 보질 못했다고 하는게 맞는것 같다.

 

일단 다시 초심으로 돌아가 ROP에 대해서 좀 더 공부한 뒤 이번에 한 heap basic이 아닌 heap 공격 기법으로 heap공부를 같이 해야겠다.

Reference

https://www.youtube.com/watch?v=l0GVitgBPf0&ab_channel=koreangang

https://hackstoryadmin.tistory.com/entry/What-is-heap-part1

https://velog.io/@woounnan/SYSTEM-Heap-Basics-Bin