Heap exploit - unsafe unlink

chk_pass·2025년 6월 19일

최신버전 기준 작성

doubly linked list에서 청크를 연결 해제하는 과정인 unlink를 이용한 공격기법

⇒원하는 공간에 값을 쓰거나 leak할 수 있게 해주는 공격 기법이다.


<사용조건>
1. 힙 영역을 전역변수에서 관리 (힙 영역을 전역 변수같이 주소를 알고 있는 위치에 unlink 될 청크의 주소가 저장되어있어야 함)
2. 2개의 Allocated Chunk가 필요하며 한 개는 Fake Chunk를 생성할 수 있어야 함
3. 첫 번째 Chunk를 통해 두 번째 Chunk의 헤더를 조작할 수 있어야 함 (8바이트+null)




<익스 시나리오>

  1. 두 개의 연속된 청크가 존재하며, 그 중 앞의 청크 주소를 저장하고 있는 전역변수가 존재하는 상황이다. 예를 들면, 0x420을 두 번 할당하고, 맨 앞 청크의 주소를 전역변수인 chunk_ptr에 저장한다고 치자
  2. 두 청크는 모두 해제 시에 fastbin이나 tcache에 들어가선 안된다. (크기가 그 이상으로 크던가, tcache를 가득 채우던가)
  3. 앞의 청크의 헤더를 제외한 앞부분에 fake chunk의 헤더를 구성해준다.
    1. 청크의 헤더를 제외한 시작 주소 +0x8에 size값을 넣는다. 원래의 sie값보다 0x10보다 작게 설정한다. 예를 들면 원래 청크의 size값이 0x431이었을 것이므로 0x421을 넣는다.
    2. 청크의 헤더를 제외한 시작주소 +0x10에 fd를 설정해주는데, &chunk_ptr-0x18을 넣는다.
    3. 청크의 헤더를 제이한시작주소 +0x18에 bk를 설정해주는데 &chunk_ptr-0x10을 넣는다.
  4. 두 번째 chunk의 헤더를 조작한다.
    1. prev_size가 직전에 구성한 fake chunk의 size가 되도록한다. 여기서는 0x420이면된다.
    2. 추가로 널을 써줘서 prev_in_use 비트가 0, 즉 직전 청크가 free 청크라고 인식되게 한다.
  5. 두 번째 chunk를 해제한다.
    1. 그러면 두 번째 청크를 해제하는 과정에서 직전 청크(fake chunk)가 free된 chunk라고 인식하고 둘을 병합하려한다. 병합 중 존재하는 unlink 루틴에 의해 chunk_ptr이 원래의 첫 번째 청크가 아니라 &chunk_ptr-0x18을 가리키게 된다.
    2. 즉, chunk_ptr = &chunk_ptr-0x18
  6. chunk_ptr을 통해 해당 주소에접근하여 chunk_ptr+0x18에 우리가 값을 쓰고 싶은 주소의 값을 넣는다.
  7. 그리고 chunk_ptr, 즉 우리가 값을 쓰고싶은 주소에 쓰고싶은 값을 쓴다.




<원리>

우선 가장 기본 상태일때 힙 구조와 전역변수의 상황은 위와 같다.


그리고 fake chunk를 구성하고, 두 번째 청크의 메타데이터를 조작해준 모습이다.

위와 같이 구성해주는 이유는 다음과 같다.

  1. 우선, 두 번째 청크의 마지막 비트를 0으로 바꾸어주어야 직전 인접 청크, 즉 fake chunk를 free된 청크로 인식할 것이며 그래야 두 번째 청크 free 시에 직전 청크와의 병합이 이루어지면서 해당 공격기법의 목적을 달성할 수 있다.
       prevsize = prev_size (p);
       size += prevsize;
       p = chunk_at_offset(p, -((long) prevsize));
+      if (__glibc_unlikely (chunksize(p) != prevsize))
+        malloc_printerr ("corrupted size vs. prev_size while consolidating");
  1. 위의 보호기법은 free와 병합 루틴에 새롭게 추가된 보호기법인데, 따라서 두 번째 청크의 prevsize와 fake chunk의 size값을 통일시켜주어야할 필요가 있다.
  2. fake chunk의 fd와 bk를 각각 &chunk_ptr-0x18, &chunk_ptr-0x10로 설정해주어야 (P->fd->bk != P || P->bk->fd != P) 이라는 보호기법을 우회할 수 있게 된다.
    1) 해당 보호기법은 unlink 루틴 내에 존재하는데, 각 조건을 따라가보면 다음과 같다.
    2) 우선 p→fd는 &chunk_ptr-0x18이고, 이것의 bk는 +0x18의 위치에 존재하므로 결국 원래 chunk_ptr의 위치에 써진 값을 의미하고, 이는 청크의 시작주소이므로 곧 p와 동일하다.
    3) p→bk는 &chunk_ptr-0x10이고, 이것의 fd는 +0x10위치에 존재하므로 결국 원래 chunk_ptr의 위치에 써진 값을 의미하고, 이는 청크의 시작주소이므로 곧 p와 동일하다.



이제 두 번째 청크를 free하면 fake 청크와의 병합이 일어나고, 그 과정에서 fake chunk를 대상으로 unlink가 일어나게 된다.

unlink에는 아래와 같은 루틴이 존재한다.

static void
unlink_chunk (mstate av, mchunkptr p)
{
  if (chunksize (p) != prev_size (next_chunk (p)))
    malloc_printerr ("corrupted size vs. prev_size");

  mchunkptr fd = p->fd;
  mchunkptr bk = p->bk;

  if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
    malloc_printerr ("corrupted double-linked list");

  fd->bk = bk;
  bk->fd = fd;

(위 코드에서 보이는 모든 보호기법은 이미 우회한 상태)

여기서 우리가 주목해야할 것은 마지막 두 줄이다.

일단 fd, bk는 각각 &chunk_ptr-0x18, &chunk_ptr-0x10를 의미한다.

먼저 fd->bk = bk;를 수행한다고 생각해보자.

fd→bk는 &chunk_ptr인데, 여기에 bk, 즉 &chunk_ptr-0x10를 대입한다.

따라서 chunk_ptr위치에 &chunk_ptr-0x10라는 값이 들어간 상태이다.

다음으로는 bk->fd = fd;를 수행할 차례이다.

bk→fd는 &chunk_ptr이고, 여기에 fd, 즉 &chunk_ptr-0x18를 대입한다.

따라서 최종적으로는 chunk_ptr이라는 전역변수의 위치에 &chunk_ptr-0x18주소값이 쓰이게 된다.


보통 익스 상황에서는 해당 전역변수를 대상으로 읽기, 쓰기 등이 가능한 상태일 것이므로 이제 &chunk_ptr-0x18으로의 접근이 가능하고, 이를 이용해 &chunk_ptr에 우리가 접근하고 싶은 주소 값을 쓰고, 또 이를 바탕으로 우리가 접근하고 싶은 주소에 원하는 값을 쓰면 된다. 그러면 aaw or aar이 가능하다.

0개의 댓글