본문 바로가기

DreamHack: System Hacking/F Stage 11

Use After Free 개념 설명

1. Dangling Pointer

Dangling Pointer는 유효하지 않은 메모리 영역을 가리키는 포인터를 뜻한다.

 

메모리의 할당

메모리를 동적으로 할당해줄 때 사용하는 malloc()은 특정 크기만큼의 영역을 할당해주고, 그 영역의 주소를 반환해준다. 따라서 우리는 malloc함수를 사용할 때

void *k = malloc(0x40)

와 같이 포인터 변수에 malloc함수가 할당한 메모리의 주소를 저장한다.

 

메모리의 해제

할당 받았던 메모리를 해제할 때는 free()를 사용한다. free함수할당받았던 청크를 다시 ptmalloc에게 반환하는 역할을 한다.

free(k)

 

문제점

그러나 free()를 사용할 때 문제가 발생할 수 있다. 

 

1. free()는 할당되어있던 청크를 ptmalloc에게 반환해주기만 할뿐, 청크의 주소를 담고 있던 포인터를 초기화 해주지는 않는다. 따라서 free() 호출 이후 포인터를 따로 초기화 해주지 않으면, 해당 포인터는 Dangling Pointer가 돼버린다.

*Dangling PointerDouble Free Bug와 같은 여러 취약점에 이용될 수 있기 때문에 여러 위험성을 가진다. Double Free Bug에 대해서는 Stage12의 글에서 자세히 다루도록 하겠다.

 

2. free()는 청크를 반환해주며, 해당 청크의 내용을 초기화 해주지 않는다. 이는 Use After Free Bug로 이어질 수 있다.(이는 본 글의 밑에서 다루겠다)

 

 

2. Use After Free

Use After Free는 해제된 청크에 접근할 수 있을 때 발생하는 취약점을 뜻한다.

Use After Free가 발생하는 이유는 크게 두 가지가 있다.

1. Dangling Pointer로 인해 발생

2. 새롭게 할당한 영역 또는 해제할 때 영역을 초기화 하지 않아서 발생

 

두 번째 이유에 대해 자세히 알아보자면, malloc()과 free()는 메모리를 할당 또는 해제 할 때 해당 메모리의 데이터들을 초기화 해주지 않는다. 따라서 새롭게 할당한 청크를 프로그래머가 명시적으로 초기화 해주지 않으면, 초기화 되지 않은 메모리의 데이터가 유출되거나 사용될 수 있다.

 

 

Use After Free Bug를 통한 데이터 유출 예제

// 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;
  
  struct NameTag *nametag;
  struct Secret *secret;
  
  secret = malloc(sizeof(struct Secret));
  
  strcpy(secret->secret_name, "ADMIN PASSWORD");
  strcpy(secret->secret_info, "P@ssw0rd!@#");
  secret->code = 0x1337;
  
  free(secret);
  secret = NULL;
  
  nametag = malloc(sizeof(struct 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);
  
  if (nametag->func) {
    printf("Nametag function: %p\n", nametag->func);
    nametag->func();
  }
}

 

코드 설명

1. 같은 크기의 구조체 NameTag와 Secret이 존재한다.

2. 먼저 Secret 구조체malloc()을 통해 동적 할당하고 구조체 변수에

-secret_name = "ADMIN PASSWORD"

-secret_info = "P@ssw0rd!@#"

-code = 0x1337

을 할당해준다.

3. 이후 free()를 통해 secret 청크를 해제해주고, secret ptr를 NULL로 초기화해준다.

4. NameTag 구조체를 malloc()을 통해 동적할당 하고, 구조체 변수에

-team_name = "security team"

-name = "S"

를 할당해준다

5. 마지막으로 nametag의 변수 team_name, name, func의 값을 출력한다.

 

 

코드 실행 결과

출력 결과를 보면 secret_info의 내용이 일부 출력되고, 초기화 해준적 없는 nametag->func가 0x1337을 가리키고 있다.

 

 

이러한 실행 결과가 나타나는 이유

0. ptmalloc은 새로운 공간 할당 요청이 들어왔을 때, 요청된 크기와 비슷한 크기의 청크가 bin 또는 tcache에 있는지 먼저 확인하고, 비슷한 청크가 있다면 그를 우선적으로 할당해준다. NameTag와 Secret은 같은 크기의 구조체이다.

 

1. 따라서 secret을 해제하고 nametag를 할당하면, nametagsecret과 같은 크기의 메모리 공간 할당을 요청하기 때문에

malloc(sizeof(struct Secret)) -> malloc(sizeof(struct NameTag))

nametag는 앞서 해제됐던 secret과 같은 메모리 영역을 사용하게 된다.

 

2. secret을 해제할 때 사용했던 free()는 청크를 반환해주기만 할뿐, 해당 청크의 데이터를 초기화해주지는 않으므로, nametag에는 앞서 해제됐던 secret의 값이 일부 남아있게 된다.

 

 

디버거를 통한 분석

1. secret을 할당한 후 heap의 모습

secret = malloc(sizeof(struct Secret));

0x405290에 우리가 할당한 secret 청크가 존재하는 것을 알 수 있다.

 

2. secret을 해제한 후 heap의 모습

free(secret);

 

이때 해당 청크의 데이터엔 무엇이 들어있는지 살펴보도록 하자.

 

struct NameTag {
	char team_name[16];
	char name[32];
	void(*func)();
};

team_name에 해당하는 0x4052a0은 fd와 bk 값으로 초기화 되었지만, name에 해당하는 0x4052b0부터는 데이터가 남아있음을 확인할 수 있다. 이를 출력해보면

우리가 name에 넣어줬던 "P@ssw0rd!@#"가 그대로 남아있음을 확인할 수 있다.

 

3. nametag를 할당한 후 heap의 모습

nametag = malloc(sizeof(struct NameTag));

우리가 앞서 해제했던 secret 청크의 주소였던 0x405290의 청크가 그대로 재할당 되었음을 확인할 수 있다.

 

if (nametag->func) {
		printf("Nametag function: %p\n", nametag->func);
		nametag->func();
	}

직전까지 실행한 후 nametag 청크의 모습이다.

struct NameTag {
	char team_name[16];
	char name[32];
	void(*func)();
};

 

-team_name에 해당하는 0x4052a0은 "security team"으로 초기화 됐지만,

-name에 해당하는 0x4052b0

memcpy(nametag->name, "S", 1);

에 따라 1바이트만큼만이 "S"로 초기화 됐으며

-void(*func)()에 해당하는 0x4052d0nametag를 할당한 후 우리가 따로 초기화 한 적이 없지만 secret에서 쓰였던 0x1337이 그대로 남아있음을 확인할 수 있다.

 

 

마무리

살펴본 바와 같이 malloc(), free() 등은 청크를 ptmalloc으로부터 할당하고 ptmalloc에게 반환해주는 역할만 할뿐, 해당 청크를 그리고 청크를 가리키는 포인터를 초기화 해주는 역할을 하진 않는다.

 

따라서 청크를 할당하고 해제할 때 청크 내의 데이터들을 프로그래머가 명시적으로 초기화 해주지 않는다면, 적절히 초기화 되지 않은 청크를 다시 재사용하여 초기화 되지 않은 청크의 데이터들을 악의적으로 사용할 수 있는 Use-After-Free 버그로 악용될 수 있다. 

'DreamHack: System Hacking > F Stage 11' 카테고리의 다른 글

uaf_overwrite  (0) 2023.08.01
Background: ptmalloc2  (0) 2023.07.12