본문 바로가기

3. Pwnable (포너블)

[2024.11.16] PWN PWN 해 6주차 활동

6주차는 드림핵의 System Hacking 로드맵에서 Use After Free, Double Free Bug 강의를 공부했다.

https://dreamhack.io/lecture/roadmaps/2


Background: ptmalloc2

Memory Allocator: 프로세스의 요청에 따라 동적으로 메모리를 할당 및 해제해주는 주체, 또는 관련된 알고리즘들의 집합.

실습 환경 - Dockerfile

 

ptmalloc2

ptmalloc2(pthread malloc 2) Wolfram Gloger가 개발한 Memory Allocator, Doug Lea dlmalloc을 개선한 ptmalloc의 두 번째 버전.

ptmalloc의 구현 목표는 메모리의 효율적인 관리.

  1. 메모리 낭비 방지
  2. 빠른 메모리 재사용
  3. 메모리 단편화 방지

 

메모리 낭비 방지

메모리 할당 요청이 발생하면, 먼저 해제된 메모리 공간 중에서 재사용할 수 있는 공간이 있는지 탐색.

해제된 메모리 공간 중에서 요청된 크기와 같은 크기의 메모리 공간이 있다면 이를 그대로 재사용. 작은 크기의 할당 요청이 발생했을 때, 해제된 메모리 공간 중 매우 큰 메모리 공간이 있으면 그 영역을 나누어 주기도 함.

 

빠른 메모리 재사용

메모리 공간을 해제할 때, tcache 또는 bin이라는 연결 리스트에 해제된 공간의 정보를 저장.

tcache bin은 여러 개가 정의되어 있으며, 각각은 서로 다른 크기의 메모리 공간들을 저장합니다. 이렇게 하면 특정 크기의 할당 요청이 발생했을 때, 그 크기와 관련된 저장소만 탐색하면 되므로 더욱 효율적으로 공간을 재사용.

 

메모리 단편화 방지

내부 단편화(Internal Fragmentation): 할당한 메모리 공간의 크기에 비해 실제 데이터가 점유하는 공간이 적을 때.

외부 단편화(External Fragmentation): 할당한 메모리 공간들 사이에 공간이 많아서 발생하는 비효율.

단편화를 줄이기 위해 정렬(Alignment)과 병합(Coalescence) 그리고 분할(Split)을 사용.

비슷한 크기의 요청에 대해서는 모두 같은 크기의 공간을 반환해야 해제된 청크들의 재사용률을 높이고, 외부 단편화도 줄일 수 있음.

잘게 나뉜 영역을 병합하고, 필요할 때 구역을 다시 설정함으로써 해제된 공간의 재사용률을 높이고, 외부 단편화를 줄일 수 있음.

 

ptmalloc의 객체

청크

: ptmalloc이 할당한 메모리 공간을 의미, 헤더와 데이터로 구성. (헤더는 청크 관리에 필요한 정보를 담고 있으며, 데이터 영역에는 사용자가 입력한 데이터가 저장)

사용 중인 청크(in-use)의 헤더와 해제된 청크(freed)의 헤더는 구조가 다름. 사용 중인 청크는 fd bk를 사용하지 않고, 그 영역에 사용자가 입력한 데이터를 저장.

이름 크기 의미
prev_size 8바이트 인접한 직전 청크의 크기. 청크를 병합할 때 직전 청크를 찾는 데 사용.
size 8바이트 현재 청크의 크기. 헤더의 크기도 포함한 값.
flags 3비트 64비트 환경에서 청크는 16바이트 단위로 할당되므로, size의 하위 4비트는 의미를 갖지 않음. size의 하위 3비트를 청크 관리에 필요한 플래그 값으로 사용.
fd 8바이트 연결 리스트에서 다음 청크를 가리킴. (해제된 청크에만)
bk 8바이트 연결 리스트에서 이전 청크를 가리킴. (해제된 청크에만)

 

 

 

 

 

 

 

 

 

bin

: 사용이 끝난 청크들이 저장되는 객체.

ptmalloc에는 총 128개의 bin이 정의. 이 중 62개는 smallbin, 63개는 largebin, 1개는 unsortedbin으로 사용되고, 나머지 2개는 사용되지 않음.

 

smallbin

: 32 바이트 이상 1024 바이트 미만의 크기를 갖는 청크들이 보관.

하나의 smallbin에는 같은 크기의 청크들만 보관되며, index가 증가하면 저장되는 청크들의 크기는 16바이트씩 커짐. , smallbin[0] 32바이트 크기의 청크를, smallbin[61] 1008 바이트 크기의 청크를 보관.

smallbin은 원형 이중 연결 리스트(circular doubly-linked list)이며, 먼저 해제된 청크가 먼저 재할당.

smallbin에 청크를 추가하거나 꺼낼 때 연결 고리를 끊는 과정이 필요, 이 과정을 unlink라고 부름.

메모리상에서 인접한 두 청크가 해제되어 있고, 이들이 smallbin에 들어있으면 이 둘은 병합, 이 과정을 consolidation이라고 부름.

 

fastbin

: 32 바이트 이상 176 바이트 이하 크기의 청크들이 보관.

이에 따라 16바이트 단위로 총 10개의 fastbin이 있습니다. 리눅스는 이 중에서 작은 크기부터 7개의 fastbin만을 사용.

fastbin은 속도는 빠르지만 다른 방법에 비해 파편화가 심한 LIFO의 방법으로 사용. 이에 따라 나중에 해제된 청크가 먼저 재할당. 서로 병합 X.

 

largebin

: 1024 바이트 이상의 크기를 갖는 청크들이 보관.

63개의 largebin이 있는데, smallbin, fastbin과 달리 한 largebin에서 일정 범위 안의 크기를 갖는 청크들을 모두 보관. 이 범위는 largebin의 인덱스가 증가하면 로그적으로 증가.

재할당 요청이 발생했을 때 ptmalloc은 그 안에서 크기가 가장 비슷한 청크(best-fit)를 꺼내 재할당. 크기 내림차순, unlink 동반, 병합.

 

unsortedbin

: 분류되지 않은 청크들을 보관하는 bin. unsortedbin은 하나만 존재하며, fastbin에 들어가지 않는 모든 청크들은 해제되었을 때 크기를 구분하지 않고 unsortedbin에 보관.

smallbin 크기에 해당하는 청크를 할당 요청하면, ptmalloc fastbin 또는 smallbin을 탐색한 뒤 unsortedbin을 탐색. largebin의 크기에 해당하는 청크는 unsortedbin을 먼저 탐색.

 

arena

: fastbin, smallbin, largebin 등의 정보를 모두 담고 있는 객체.

멀티 쓰레드 환경에서 ptmalloc은 레이스 컨디션(어떤 공유 자원을 여러 쓰레드나 프로세스에서 접근할 때 발생하는 오동작)을 막기 위해 arena에 접근할 때 arena에 락을 적용.

생성할 수 있는 갯수가 64개로 제한되어 있으므로 과도한 멀티 쓰레드 환경에서는 결국 병목 현상이 발생, tcache를 추가적으로 도입.

 

tcache (thread local cache)

: 각 쓰레드에 독립적으로 할당되는 캐시 저장소를 지칭.

각 쓰레드는 64개의 tcache를 가짐. LIFO 방식으로 사용되는 단일 연결 리스트이며, 하나의 tcache는 같은 크기의 청크들만 보관. tcache에 보관할 수 있는 청크의 갯수를 7개로 제한. 병합 X.

32 바이트 이상, 1040 바이트 이하의 크기를 갖는 청크들이 보관. 이 범위에 속하는 청크들은 할당 및 해제될 때 tcache를 가장 먼저 조회.

각 쓰레드가 고유하게 갖는 캐시이기 때문에, ptmalloc은 레이스 컨디션을 고려하지 않고 이 캐시에 접근.

보안 검사가 많이 생략되어 있어서 공격자들에게 힙 익스플로잇의 좋은 도구로 활용.

 

Memory Corruption: Use After Free

Use-After-Free: 메모리 참조에 사용한 포인터를 메모리 해제 후에 적절히 초기화하지 않아서, 또는 해제한 메모리를 초기화하지 않고 다음 청크에 재할당해주면서 발생하는 취약점

 

Dangling Pointer

유효하지 않은 메모리 영역을 가리키는 포인터.

메모리를 해제할 때는 free 함수를 호출. 그런데 free 함수는 청크를 ptmalloc에 반환하기만 할 뿐, 청크의 주소를 담고 있던 포인터를 초기화하지는 않음. 따라서 free의 호출 이후에 프로그래머가 포인터를 초기화하지 않으면, 포인터는 해제된 청크를 가리키는 Dangling Pointer가 됨.

 

Use After Free

Use-After-Free (UAF)는 문자 그대로, 해제된 메모리에 접근할 수 있을 때 발생하는 취약점.

malloc free 함수는 할당 또는 해제할 메모리의 데이터들을 초기화하지 않음. 그래서 새롭게 할당한 청크를 프로그래머가 명시적으로 초기화하지 않으면, 메모리에 남아있던 데이터가 유출되거나 사용될 수 있음.

 

uaf 동적 분석

새로운 할당 요청이 들어왔을 때, 요청된 크기와 비슷한 청크가 bin이나 tcache에 있는지 확인. 그리고 만약 있다면, 해당 청크를 꺼내어 재사용.

Nametag Secret은 같은 크기의 구조체입니다. 그러므로 앞서 할당한 secret을 해제하고 nametag를 할당하면, nametag secret과 같은 메모리 영역을 사용. 이때 free는 해제한 메모리의 데이터를 초기화하지 않으므로, nametag에는 secret의 값이 일부 남아있게 됨.

동적 할당한 청크를 해제한 뒤에는 해제된 메모리 영역에 이전 객체의 데이터가 남음. 이러한 특징을 공격자가 이용한다면 초기화되지 않은 메모리의 값을 읽어내거나, 새로운 객체가 악의적인 값을 사용하도록 유도하여 프로그램의 정상적인 실행을 방해할 수 있음.

 

Memory Corruption: Double Free Bug

 

Double Free Bug

같은 청크를 두 번 해제할 수 있는 버그.

dangling pointer Double free bug를 유발하는 대표적인 원인.

Double free bug를 이용하면 duplicated free list를 만드는 것이 가능한데, free list의 각 청크들은 fd bk로 연결.

해제된 청크에서 fd bk 값을 저장하는 공간은 할당된 청크에서 데이터를 저장하는 데 사용. 만약 어떤 청크가 free list에 중복해서 포함된다면, 첫 번째 재할당에서 fdbk를 조작하여 free list에 임의 주소를 포함시킬 수 있음.

 

Tcache Double Free

// Name: dfb.c
// Compile: gcc -o dfb dfb.c
 
#include <stdio.h>
#include <stdlib.h>
 
int main() {
  char *chunk;
  chunk = malloc(0x50);
 
  printf("Address of chunk: %p\n", chunk);
 
  free(chunk);
  free(chunk); // Free again
}

위 코드는 같은 청크를 두 번 해제하는 예제 코드. 컴파일하고 실행하면 tcache에 대한 double free가 감지되어 프로그램이 비정상 종료되는 것을 확인할 수 있음.

$ ./dfb
Address of chunk: 0x55ce62641260
free(): double free detected in tcache 2
zsh: abort      ./dfb

 

Mitigation for Tcache DFB

정적 패치 분석

tcache_entry

해제된 tcache 청크들이 갖는 구조. 일반 청크의 fd next로 대체되고, LIFO 형태로 사용되므로 bk에 대응되는 값은 없음.

double free를 탐지하기 위해 key포인터가 tcache_entry에 추가.

typedef struct tcache_entry {
  struct tcache_entry *next;
+ /* This field exists to detect double frees.  */
+ struct tcache_perthread_struct *key;
} tcache_entry;

 

tcache_put

해제한 청크를 tcache에 추가하는 함수. tcache_put 함수는 해제되는 청크의 key tcache라는 값을 대입하도록 변경됨. 여기서 tcache tcache_perthread라는 구조체 변수를 가리킴.

tcache_put(mchunkptr chunk, size_t tc_idx) {
  tcache_entry *e = (tcache_entry *)chunk2mem(chunk);
  assert(tc_idx < TCACHE_MAX_BINS);
 
+ /* Mark this chunk as "in the tcache" so the test in _int_free will detect a
+      double free.  */
+ e->key = tcache;
  e->next = tcache->entries[tc_idx];
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}

 

tcache_get

tcache에 연결된 청크를 재사용할 때 사용하는 함수. tcache_get함수는 재사용하는 청크의 key값에 NULL을 대입하도록 변경.

 

_int_free

청크를 해제할 때 호출되는 함수. 20번째 줄 이하를 보면, 재할당하려는 청크의 key값이 tcache이면 Double Free가 발생했다고 보고 프로그램을 abort시킴.

 

동적 분석

먼저, 청크 할당 직후에 중단점을 설정하고 실행.

$ gdb -q double_free
pwndbg> disass main
...
pwndbg> b *main+18
pwndbg> r

heap 명령어로 청크들의 정보를 조회

pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555756000
Size: 0x251
 
Allocated chunk | PREV_INUSE
Addr: 0x555555756250
Size: 0x61
 
Top chunk | PREV_INUSE
Addr: 0x5555557562b0
Size: 0x20d51

이 중 malloc(0x50)으로 생성한 chunk의 주소는 0x555555756250 입니다. 해당 메모리 값을 덤프하면, 아무런 데이터가 입력되지 않았음을 확인할 수 있음.

이후의 참조를 위해 청크를 gdb에서 chunk 변수로 정의.

chunk를 해제할 때까지 실행하고, 청크의 메모리를 출력.

pwndbg> disass main
   0x0000555555554703 <+41>:    call   0x5555555545a0 <printf@plt>
   0x0000555555554708 <+46>:    mov    rax,QWORD PTR [rbp-0x8]
   0x000055555555470c <+50>:    mov    rdi,rax
   0x000055555555470f <+53>:    call   0x555555554590 <free@plt>
   0x0000555555554714 <+58>:    mov    rax,QWORD PTR [rbp-0x8]
pwndbg> b *main+58
Breakpoint 2 at 0x0000555555554714
pwndbg> c
pwndbg> print *chunk
$1 = {
  next = 0x0,
  key = 0x555555756010
}

chunk key값이 0x555555756010로 설정된 것을 확인할 수 있음.

이 주소의 메모리 값을 조회하면, 해제한 chunk의 주소 0x555555756260 entry에 포함되어 있음을 알 수 있음.

print *(tcache_perthread_struct *)0x555555756010
$2 = {
  counts = "\000\000\000\000\001", '\000' <repeats 58 times>,
  entries = {0x0, 0x0, 0x0, 0x0, 0x555555756260, 0x0 <repeats 59 times>}
}

이 상태에서 실행을 재개하면 key값을 변경하지 않고, 다시 free를 호출하므로, abort가 발생.

 

우회 기법

if (__glibc_unlikely (e->key == tcache))만 통과하면 tcache 청크를 double free 시킬 수 있음. 다시 말해, 해제된 청크의 key값을 1비트만이라도 바꿀 수 있으면, 이 보호 기법을 우회할 수 있음.

 

Tcache Duplication

// Name: tcache_dup.c
// Compile: gcc -o tcache_dup tcache_dup.c
 
#include <stdio.h>
#include <stdlib.h>
 
int main() {
  void *chunk = malloc(0x20);
  printf("Chunk to be double-freed: %p\n", chunk);
 
  free(chunk);
 
  *(char *)(chunk + 8) = 0xff;  // manipulate chunk->key
  free(chunk);                  // free chunk in twice
 
  printf("First allocation: %p\n", malloc(0x20));
  printf("Second allocation: %p\n", malloc(0x20));
 
  return 0;
}

이를 컴파일하고 실행한 결과,

$ ./tcache_dup
Chunk to be double-freed: 0x55d4db927260
First allocation: 0x55d4db927260
Second allocation: 0x55d4db927260

chunk tcache에 중복 연결되어 연속으로 재할당되는 것을 확인할 수 있음.

 

Exploit Tech: Tcache Poisoning

 

Tcache Poisoning

tcache를 조작하여 임의 주소에 청크를 할당시키는 공격 기법.

 

원리

중복으로 연결된 청크를 재할당하면, 그 청크는 할당된 청크이면서, 동시에 해제된 청크가 됨.

할당된 청크에서 데이터를 저장하는 부분이 해제된 청크에서는 fd bk 값을 저장하는 데 사용. 따라서 공격자가 중첩 상태인 청크에 임의의 값을 쓸 수 있다면, 그 청크의 fd bk를 조작할 수 있으며, 이는 다시 말해 ptmalloc2 free list에 임의 주소를 추가할 수 있음을 의미.

ptmalloc2는 동적 할당 요청에 대해 free list의 청크를 먼저 반환하므로, 이를 이용하면 공격자는 임의 주소에 청크를 할당할 수 있음.

 

효과

Tcache Poisoning으로 할당한 청크에 대해 값을 출력하거나, 조작할 수 있다면 임의 주소 읽기(Arbitrary Address Read, AAR)임의 주소 쓰기(Arbitrary Address Write, AAW)가 가능.