rosieblue
article thumbnail
728x90

Heap 시리즈

[Heap] Background : Memory Allocator

[Heap] Background : Chunk

[Heap] Background : Bin (Fastbin, Unsorted bin, Small bin, Large bin)

[Heap] Memory Corruption : Use After Free(UAF)

 

들어가며

오늘은 UAF(Use After Free)에 대해 알아보도록 하겠다.
혹시 UAF를 공부하기에 앞서 Heap에서의 메모리 할당/해제를 모른다면 먼저 공부하고 와야한다! 이에 대한 포스트를 올리고 있는데 너무 양이 많아서 시간이 꽤 걸리고 있다.. 그래서 일단 UAF부터 올리려고 한다. 일단 UAF를 이해하기 위해 필요한 아주 간단한 사전 지식만을 언급하고 가겠다. 

  • 메모리를 할당하려고 할 때 해제된 메모리에서 필요한 메모리를 찾는데, tcache -> fastbin/smallbin -> unsorted bin ->  large bin 순으로 찾음 
  • tcache, fastbin은 LIFO, smallbin은 FIFO 방식
더보기

주저리) 이걸 알기 전에 원래는 heap에서의 메모리 할당 및 해제 과정을 꼭 알아야해서 이에 대해서 포스트를 올리려고 했다,, 사실 heap 글을 안 올린지도 꽤 오래됐는데, 그 이유가 heap 공부해야할 양이 생각보다 많아서 이를 어디서부터 어디까지 정리해야하나..했다. chunk,bin,tcache 등등.. 컴구도 아직 안 들은 내가 아직 아는 것이 많이없어서 올리는데 시간이 좀 걸릴 것 같다ㅠㅠ 나는 정리는 노션에다가 하고 여기는 정말 내 힘으로 설명하려고 하기 때문에 ㅜㅜ 그래도 빨리 올려보도록 하겠다. 암튼 그래서 UAF에 대해서 먼저 다뤄보도록 하겠다

 
이제 Use After Free를 간단한 비유를 통해 먼저 이해하고 가보자. Memory Allocator를 우리는 부동산 중개업자로 비유했었다. 이때 어떤 사람이 집을 내놓고 새로운 사람이 그 내놓은 집에 들어가는 상황을 가정하자.
 
여기서 일어날 수 있는 문제 2가지가 있다.
 
1. 예전 세입자의 권한을 삭제하지 않아서 그 세입자가 계속 새집에 들어올 수 있는 경우
2. 집을 제대로 청소하지 않아 새 세입자가 기존 세입자의 물건들을 볼 수 있는 경우
 

1번 상황(왼쪽-포인터 초기화 x) / 2번 상황(오른쪽-메모리공간 초기화 x)

각각의 상황을 실제 메모리 할당/해제 과정에 적용하면 아래와 같다.
1. 메모리 참조에 이용한 포인터를 초기화하지 않아 그 주소에 계속 접근할 수 있는 경우
2. 할당(해제)하려는 메모리의 내용을 초기화하지 않고 다음 청크(Chunk)에 재할당해주어서 이전 내용을 볼 수 있는 경우
 
똑같은 내용이지만 표로 정리해봤다. (1->a. 2->b.로 대응)

부동산 비유 실제 메모리 할당/해제 상황
1. 예전 세입자의 권한을 삭제하지 않아서 그 세입자가 계속 새집에 들어올 수 있는 경우 a. 메모리 참조에 이용한 포인터를 초기화하지 않아 그 주소에 계속 접근할 수 있는 경우
2. 집을 제대로 청소하지 않아 새 세입자가 기존 세입자의 물건들을 볼 수 있는 경우 b. 할당(해제)하려는 메모리의 내용을 초기화하지 않고 다음 청크(Chunk)에 재할당해주어서 이전 내용을 볼 수 있는 경우

 
결국 포인터와 메모리를 잘 초기화해주면 된다. 하지만 이것은 함수가 해주는게 아니라 우리가 프로그래밍할 때 명시적으로 해주어야하기 때문에 이런 취약점들이 많이 나타나는 것이다.
 

Use After Free (UAF)

Dangling Pointer

메모리를 할당할 때 우리는 보통 malloc()으로 할당한다. 이때 malloc의 반환값은 할당된 메모리의 주소인데 보통 ptr=(자료형*)malloc(메모리크기) 로 해서 ptr이 할당된 메모리의 주소를 가리키게 된다. 그리고 그 메모리를 다 쓴 경우 free()를 통해 메모리를 해제한다.
 
여기서 문제점은 아래에서 발생한다.

  • C에서의 free()포인터도, 메모리도 초기화해주지 않는다. 단순히 해제만 할 뿐이다.. (진짜 누가 이렇게 함수 만듦?)
  • malloc()도 할당할 메모리를 초기화하지 않는다

아주 간단한 코드를 만들어봤다. (Dobby is free!)

#define N 10
#include <stdio.h>
#include <malloc.h>

int main(){
    int* ptr=(int*)malloc(sizeof(int)*N);
    *ptr=1234;
    printf("Allocated memory address: %p, Data:%d\n",ptr,*ptr);
    
    free(ptr);
    printf("Dobby is FREE.........\n");
    printf("Allocated memory address: %p, Data:%d\n",ptr,*ptr);
    return 0;
}

코드 실행 후 결과

free를 했음에도 불구하고 ptr가 담고 있는 주소는 여전한 것을 볼 수 있다.
그런데 Data를 찍어봤는데 이상한 값이 나온다. free는 딱히 메모리 초기화를 해주지 않는데 말이다.. 이건 왜 그런것일까? 이건 아마 fd,bk으로 초기화되어서 그런 것 같다. 밑에서 확인해보겠다.
 
아무튼 이처럼 유효하지 않는 주소를 가리키는 포인터를 Dangling Pointer라고 한다.

Dangling Pointer : 유효하지 않은 주소를 가리키는 포인터

 
다른 Dangling Pointer 예시를 보자.

// Name: dangling_ptr.c
// Compile: gcc -o dangling_ptr dangling_ptr.c

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

int main() {
  char *ptr = NULL;
  int idx;
  
  while (1) {
    printf("> ");
    scanf("%d", &idx);
    
    switch (idx) {
      case 1:
        if (ptr) { //ptr가 NULL이 아닌 경우
          printf("Already allocated\n");
          break;
        }
        ptr = malloc(256); //메모리 할당
        break;
      case 2:
        if (!ptr) { //ptr가 NULL인 경우
          printf("Empty\n");
        }
        free(ptr); //free
        break;
      default:
        break;
    }
  }
}

위 예제는 free(ptr)를 한 후에 포인터를 따로 초기화해주고 있지 않다. 따라서 다시 이 포인터에 접근할 수도 있고 여러번 free할 수도 있다. 이를 Double-Free Bug라고 하는데 이는 다음에 다루도록 하겠다.
 

실행 결과

free 두번 했더니 역시나 에러가 떴다.
 
이번에는 1,2,1을 해서 malloc, free, malloc을 순서대로 실행해주었더니 나타난 결과이다.

실행 결과

이미 free를 한 후에 malloc을 했음에도 Already Allocated라고 떴다. 이는 우리가 ptr를 다시 초기화시키지 않아서 그렇다. 따라서 프로그래머들은 이를 방지하기 위해 포인터를 코드 상에서 명시적으로 초기화해주어야한다.
 

Use After Free(UAF)

이제 정말 UAF가 뭔지 살펴보자. 위 Dangling Pointer 예제에서 포인터를 초기화하지 않아 생기는 문제에 대해에 대해 이야기했다면, UAF는 해제된 메모리를 초기화하지 않아 생기는 문제에 대해서 다룬다.
Use After Free라는 말 자체가 free 이후 사용한다는 것이니 쉽게 기억할 수 있을 것이다.
 
아래 코드는 UAF 취약점을 가진 코드이다.

// Name: uaf.c
// Compile: gcc -o uaf uaf.c -no-pie

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

struct NameTag { //개인 정보 담는 구조체
  char team_name[16];
  char name[32];
  void (*func)();
};

struct Secret { //유출되면 안되는 정보를 담고 있는 구조체
  char secret_name[16];
  char secret_info[32];
  long code;
};

int main() {
  int idx;
  
  //NameTag, Secret 구조체 포인터 각각 선언
  struct NameTag *nametag;
  struct Secret *secret;
  
  //secret에 malloc을 통해 메모리 할당
  secret = malloc(sizeof(struct Secret));
  
  //secret 구조체 멤버에 값 할당
  strcpy(secret->secret_name, "ADMIN PASSWORD");
  strcpy(secret->secret_info, "P@ssw0rd!@#");
  secret->code = 0x1337;
  
  //해제 후 포인터 초기화
  free(secret);
  secret = NULL;
  
  //nametag 구조체 할당
  //취약점 : NameTag 구조체와 Secret 구조체 size 동일
  nametag = malloc(sizeof(struct NameTag));
  
  //nametag 구조체 멤버에 값 할당
  strcpy(nametag->team_name, "security team");
  memcpy(nametag->name, "S", 1);
  
  //출력
  printf("Team Name: %s\n", nametag->team_name);
  printf("Name: %s\n", nametag->name);
  
  //nametag->func이 NULL이 아니라면 nametag->func 출력
  if (nametag->func) {
    printf("Nametag function: %p\n", nametag->func);
    nametag->func();
  }
}

 
이 코드의 실행 결과는 아래와 같다.

$ gcc -o uaf uaf.c -no-pie
$ ./uaf
Team Name: security team
Name: S@ssw0rd!@#
Nametag function: 0x1337
Segmentation fault (core dumped)

흐음 그런데 Name과 Nametagfunction에서 nametag의 멤버가 아닌 secret의 멤버가 출력된다. 이 이유가 무엇일지 아래에서 하나하나 분석해보자.
 

코드 분석

  //secret에 malloc을 통해 메모리 할당
  secret = malloc(sizeof(struct Secret));
  
  //secret 구조체 멤버에 값 할당
  strcpy(secret->secret_name, "ADMIN PASSWORD");
  strcpy(secret->secret_info, "P@ssw0rd!@#");
  secret->code = 0x1337;
  
  //해제 후 포인터 초기화
  free(secret);
  secret = NULL;

 
위 코드에서는 secret 포인터가 가리키는 구조체에 담겨있는 정보는 아래같이된다.

secret->name="ADMIN PASSWORD"
secret->secret_info="P@ssw0rd!@#"

그리고 이를 free해주고 secret 포인터까지 초기화해준다. 여기까지는 문제 없어 보인다.
 

하지만.. secret이 가리키는 주소의 메모리, 즉 malloc을 통해 할당받았던 메모리 초기화는 이루어지지 않는다. 따라서 누군가가 secret이 가리켰던 주소에 접근할 수 있게된다면 비밀 정보가 모두 유출되는 것이다.

이 이유는 free와 malloc이 따로 "메모리 초기화"는 하지 않아서이다.
 
어쩌면 포인터까지 초기화해주었기에 이제 secret구조체가 있던 주소에 접근하기는 무적 확률이 낮을 것이라고 생각할 수 있다.. 불행히도 이 예제에서는 그 확률이 100%가 된다. 그 이유를 살펴보자.
 

  //nametag 구조체 할당
  //취약점 : NameTag 구조체와 Secret 구조체 size 동일
  nametag = malloc(sizeof(struct NameTag));

굉장히.. 아무 문제도 없어 보이는 코드이다. 하지만 여기서 문제점은 NameTag 구조체와 Secret 구조체 사이즈가 동일하다는 것이다. 이게 왜 문제냐고? heap에서의 메모리 할당 및 해제를 한번 복습하고 가자. 여기서는 간단히만 알아보겠다. 
 
ptmalloc에서는 할당되었던 메모리가 해제되고 난 후에 해당 메모리(청크)의 정보를 tcache와 bin에 저장한다. 그리고 다시 메모리를 할당할 때 tcache와 bin에서 현재 할당하려는 메모리의 크기에 따라 알맞은 청크를 선택에 해당 위치에 할당한다. 이를 통해 메모리를 효율적으로 관리할 수 있게된다.
 
한편 tcache나 bin들은 각각 저장할 수 있는 사이즈가 다른데, 일단 32byte 이상 1040byte 이하의 메모리 할당이 들어오면 ptmalloc은 tcache부터 찾는다. 이때 한 tcache에 들어갈 수 있는 청크는 7개이다. 그리고 tcache는 LIFO 방식을 사용한다.
 
이제 다시 코드의 흐름을 보자.

우리는 free(secret)을 통해 메모리를 해제했다. 이때 secret 구조체의 크기는 32byte이상 1040byte이하이므로 해제된 주소는 tcache에 저장된다. 

그리고 다시 우리는 nametag 구조체를 malloc()으로 할당한다. 이때 그러면 ptmalloc은 nametage의 사이즈에 맞는 청크를 tcache와 bin에서 찾을 것이다. 이때 tcache가 꽉 차있지 않으므로 tcache로 갈것이다. 그런데 tcache는 FIFO를 이용하므로 바로 전에 해제했던 메모리의 주소(secret 구조체의 주소)가 반환된다.

따라서 nametag 구조체가 할당받은 메모리 주소는 이전에 secret 구조체의 주소가 되는 것이다. 
이때 malloc과 free는 메모리를 초기화하지 않으므로
secret의 내용이 일부 남아있게된다.

 

동적 분석

한번 gdb로 동적분석을 하면서 heap 메모리를 확인해보자.
 

  • secret을 free한 후의 메모리를 확인한다.

disass main으로 주소를 찾는다

   0x00000000004011bd <+103>:   call   0x401030 <free@plt> ;secret 메모리 해제
   0x00000000004011c2 <+108>:   mov    QWORD PTR [rbp-0x8],0x0

main+108에 break을 걸어줄 것이다.
이후 heap 명령어를 통해 heap 안의 청크들을 확인한다.

free(secret) 후의 청크들

여기서 우리가 살펴봐야할 것은 Free Chunk이다. 역시나 tcache에 들어가있는 것도 확인할 수 있었다.
 

청크 정보를 알아보기 전에 전에 청크의 구조를 알아야되는데, Chunk는 해제되면 data부분에 fd,bk부분을 덮어씌운다. fd,bk는 free chunk에만 존재하는 부분으로 fd(FowarD)는 이전 청크의 주소, bk(BacKward)는 이후 청크의 주소를 나타낸다. 이것은 쉽게 청크들을 병합하기 위해서이다.
 
그렇기 때문에 secet 구조체의 데이터부분에서 16byte(fd 8byte + bk 8byte)는 덮어씌워졌을 것이다.
따라서 시작주소 + 24byte부분 부터는 데이터가 그대로 남아있다는 소리가 된다.
 
이제 x/10gx를 통해 0x405290의 메모리 구조(Free Chunk의 정보)를 출력한다.
 

fd,bk로 데이터부분이었던 것이 초기화되어있는 것을 확인할 수 있다.
그리고 0x4052b0 부분에서는 어떤 데이터가 남아있는지 확인해보자.
 

secret info가 남아있다

이부분이 이제 name_tag 부분의 name에 들어가게되는 것이다. 그 이유는 secret_info 부분이 name부분이랑 오프셋이 딱 일치하기 때문! (아래 코드 확인)

struct NameTag { //개인 정보 담는 구조체
  char team_name[16];
  char name[32];
  void (*func)();
};

struct Secret { //유출되면 안되는 정보를 담고 있는 구조체
  char secret_name[16];
  char secret_info[32];
  long code;
};

 
 
글리고 *func 부분에는 위에서 code로 입력한 0x1337이 남아있다! 위 gdb 사진에서도 1337 볼 수 있다
 

  • 이제 nametag를 할당하고 멤버변수 또한 다 입력한 후의 청크를 보자

또한 fd,bk부분이 다른 값으로 바뀌어졌다. 이값을 출력해보자. 
 

team_name이 정상적으로 입력되어있다

 
그렇다면 name부분은?
 

우리가 입력한 S를 제외한 secret_info의 값이 그대로 남아있는 것을 확인할 수 있다.
 
 

code 부분도 위에 확인했듯이 1337로 남아있다. 이 값이 0이 아니므로 아래 코드가 실행된다.

  //nametag->func이 NULL이 아니라면 nametag->func 출력
  if (nametag->func) {
    printf("Nametag function: %p\n", nametag->func);
    nametag->func();
  }

그런데 nametag->func()이 0x1337이므로 segmentation fault가 일어날 것이다.
 
 

결과

위에서 분석한 내용과 동일하게 결과가 출력된 것을 볼 수 있다.
 
예제를 통해 살펴봤듯, 동적 할당한 청크를 해제한 뒤에는 해제된 메모리 영역에 이전 객체의 데이터가 남는다. 이러한 특징을 공격자가 이용한다면 초기화되지 않은 메모리의 값을 읽어내거나, 새로운 객체가 악의적인 값을 사용하도록 유도하여 프로그램의 정상적인 실행을 방해할 수 있으니 주의해야한다.
 
 

이 개념을 사용한 워게임 :[Pwnable/Wargame] - [드림핵(Dreamhack)] uaf_overwrite

 

[드림핵(Dreamhack)] uaf_overwrite

⬇ 사용한 배경지식 ⬇ [Heap] Memory Corruption : Use After Free(UAF) (1) [Heap] Exploitation : Unsorted Bin Memory Leak // Name: uaf_overwrite.c // Compile: gcc -o uaf_overwrite uaf_overwrite.c #include #include #include #include struct Human { ch

hannahsecurity.tistory.com

 

Reference

https://dreamhack.io/lecture/courses/106

 

Memory Corruption: Use After Free

이번 강의에서는 메모리 오염 취약점 중 하나인 Use After Free에 대해 설명합니다.

dreamhack.io

 

profile

rosieblue

@Rosieblue

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!