[DreamHack] Stage 11 : Memory Corruption - Use After Free
Use-After-Free 취약점 이란?
: 메모리 참조에 사용한 포인터를 메모리 해제 후에 적절히 초기화하지 않아서, 또는 해제한 메모리를 초기화하지 않고 다음 청크에 재할당 해주면서 발생하는 취약점.
이 취약점은 현재까지도 브라우저 및 커널에서 자주 발견되고 있으며, 익스플로잇 성공률도 다른 취약점에 비해 높아 상당히 위험하다고 알려져 있다.
여기서 원인과 취약점이 발생하는 코드의 패턴, 그리고 공격자의 관점에서 해당 취약점을 이용했을 때 얻을 수 있는 효과 를 다룬다고 한다.
<Dangling Pointer>
Dangling Pointer는 유효하지 않은 메모리 영역을 가리키는 포인터이다. 메모리의 동적 할당에 사용되는 malloc 함수는 할당한 메모리의 주소를 반환한다. 일반적으로, 메모리를 동적 할당할 때는 포인터를 선언하고, 그 포인터에 malloc함수가 할당한 메모리의 주소를 저장한다. 그리고 그 포인터를 참조하여 할당한 메모리에 접근한다.
메모리를 해제할 때는 free 함수를 호출한다. 그런데 free 함수는 청크를 ptmalloc에 반환하기만 할 뿐, 청크의 주소를 담고 있던 포인터를 초기화하지는 않는다. 따라서 free의 호출 이후에 프로그래머가 포인터를 초기화하지 않으면, 포인터는 해제된 청크를 가리키는 Dangling Pointer가 된다.
하지만, Dangling Pointer가 생긴다고 해서 프로그램이 보안적으로 취약한 것은 아니다. 그러나 Dangling Pointer는 프로그램이 예상치 못한 동작을 할 가능성을 키우며, 경우에 따라서는 공격자에게 공격 수단으로 활용될 수도 있다고 한다.
아래 코드는 Dangling Pointer의 위험성을 보이는 예제이다. 청크를 해제한 후에 청크를 가리키던 ptr변수를 초기화하지 않는다. 그래서 다음과 같이 청크를 할당하고 해제하면, ptr은 이전에 할당한 청크의 주소를 가리키는 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) {
printf("Already allocated\n");
break;
}
ptr = malloc(256);
break;
case 2:
if (!ptr) {
printf("Empty\n");
}
free(ptr);
break;
default:
break;
}
}
}
$ ./dangling_ptr> 1> 2
ptr이 해제된 청크의 주소를 가리키고 있으므로, 이를 다시 해제할 수 있다.
$ ./dangling_ptr> 1> 2> 2
이를 Double Free Bug라고 하는데, 프로그램에 심각한 보안 위협이 되는 소프트웨어 취약점이다.
<Use After Free>
: 해제된 메모리에 접근할 수 있을 때 발생하는 취약점
원인으로
1) Dangling Pointer로 인해 발생
2) 새롭게 할당한 영역을 초기화하지 않고 사용하면서 발생
malloc과 free는 할당 또는 해제시 메모리의 데이터들을 초기화하지 않아 새롭게 할당한 청크를 프로그래머가 명시적으로 초기화하지 않으면, 메모리에 남아있던 데이터가 유출되거나 사용될 수 있다.
아래는 Use-After-Free 취약점이 있는 예제이다.
// 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();
}
}
구조체 NameTag와 Secret이 정의되어 있는데 여기에서는 그 중 외부에 유출되면 안되는 Secret 구조체를 먼저 할당한다. 그리고 secret_name. secret_info, code에 값을 입력하고 이를 해제한다.
34번쨰 줄 : nametag = malloc(sizeof(struct NameTag)); 에서는 사원의 정보를 담고 있는 nametag를 생성한다. team_name, name에 각각의 값을 입력하고 입력한 데이터를 출력한다.
이후에 함수 포인터 func가 NULL이 아니라면 포인터가 가리키는 주소를 출력하고 해당 주소의 함수를 호출한다.
실행결과는 아래와 같다.
그리고 출력 결과를 살펴보면, Name으로 secret_info의 문자열이 출력되고, 값을 입력한 적 없는 함수 포인터가 0x1337을 가리키는 것을 확인할 수 있다. 2
<Use After Free 동적 분석>
ptmalloc2는 새로운 할당 요청이 들어오면 요청된 크기와 비슷한 청크가 bin이나 tcache에 있는지 확인한다. 그리고 만약에 있으면, 해당 청크를 꺼내 재사용한다.
예제코드에서 Nametag와 Secret은 같은 크기의 구조체이다.
-> 앞서 할당한 secret을 해제하고 nametag를 할당하면, nametag는 secret과 같은 메모리 영역을 사용하게 된다.
이때 free는 해제한 메모리의 데이터를 초기화하지 않으므로, nametag에는 secret이 값이 일부 남아 있게 된다.
gdb를 이용해 이 과정을 자세히 분석해보자.
아래는 secret을 해제한 직후에 secret이 사용하던 메모리 영역을 출력한 것이다.
secret_name은 적절한 gd와 bk값으로 초기화됐지만, secret_info의 값은 그대로 남아있는 것을 확인할 수 있다.
heap은 할당 및 해제된 청크들의 정보를 조회하는 명령어이다.
그리고 다음으로 nametag를 할당하고 printf 함수를 호추라는 시점에서 nametag 멤버 변수들의 값을 확인해보자.
nametag->team_name에는 “security team”이 그대로 입력되었으나, nametag->name에는 초기화되지 않은 secret_info의 값이 존재하는 것을 확인할 수 있다. 또한, nametag->func 위치에 secret->code에 대입했던 0x1337 이 남아있는 것을 알 수 있다. 이 값이 0이 아니므로 예제의 42번째 줄에서 nametag->func이 호출되고, Segmentation Fault가 발생.
예제를 통해 살펴봤듯, 동적 할당한 청크를 해제한 뒤에는 해제된 메모리 영역에 이전 객체의 데이터가 남는다. 이러한 특징을 공격자가 이용한다면 초기화되지 않은 메모리의 값을 읽어내거나, 새로운 객체가 악의적인 값을 사용하게 유도하여 프로그램의 정상적인 실행을 방해할 수 있다.