[Reference] : 위 글은 다음 내용을 제가 공부한 후, 인용∙참고∙정리하여 만들어진 게시글입니다.
- 서론
- Use After Free
- Q&A
- 마치며
'Use After Free'는 메모리 참조에 사용한 포인터를 메모리 해제 후에 적절히 초기화하지 않아서, 또는 해제한 메모리를 초기화하지 않고 다음 청크에 재할당해주면서 발생하는 취약점입니다.
이 취약점은 현재까지도 브라우저 및 커널에서 자주 발견되고 있으며,
익스플로잇 성공률도 다른 취약점에 비해 높아 상당히 위험하다고 알려져 있습니다.
- Dangling Pointer
: 컴퓨터 과학에서, 유효하지 않은 메모리 영역을 가리키는 포인터
malloc
함수는 할당한 메모리의 주소를 반환합니다.
일반적으로, 메모리를 동적할당할 때는
malloc
함수가 할당한 메모리의 주소 저장합니다.
메모리를 해제할 때는 free
함수를 사용합니다.
그런데 free
함수는 청크를 ptmalloc
에 반환하기만 할 뿐, 청크의 주소를 담고 있는 포인터를 초기화하지는 않습니다.
따라서, free
의 호출 이후에 프로그래머가 포인터를 초기화하지 않는다면,
포인터는 해제된 청크를 가리키는 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(UAF)
: 해제된 메모리에 접근할 수 있을 때 발생하는 취약점
이는 Dangling Pointer 외에, 새롭게 할당한 영역을 초기화하지 않고 사용하면서도 발생합니다.
malloc
과 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();
}
}
위의 예제는 UAF 취약점이 있는 코드입니다.
구조체 NameTag
와 Secret
이 정의되어 있는데,
예제에서 그 중 외부에 유출되면 안되는 Secret
구조체를 먼저 할당합니다.
그리고 secret_name
, secret_info
, code
에 값을 입력하고, 해제합니다.
이후, 사원의 정보를 담고 있는 nametag
를 생성합니다.
team_name
, name
에 각각의 값을 입력하고, 입력한 데이터를 출력합니다.
이후, 함수 포인터 func
가 NULL
이 아니라면 포인터가 가리키는 주소를 출력하고, 해당 주소의 함수를 호출합니다.
$ ./uaf
Team Name: security team
Name: S@ssw0rd!@#
Nametag function: 0x1337
Segmentation fault (core dumped)
출력 결과를 보면, Name
으로 secret_info
의 문자열이 출력되고,
값을 입력한 적 없는 함수 포인터가 0x1337
을 가리키는 것을 볼 수 있습니다.
ptmalloc2
는 새로운 할당 요청이 들어오면,
요청된 크기와 비슷한 청크가 bin
이나 tcahche
에 있는지 확인합니다.
만약 있다면, 해당 청크를 재사용합니다.
위의 예제 코드 상에서, Nametag
와 Secret
은 같은 크기의 구조체입니다.
그러므로 앞서 할당한 secret
을 해제하고 nametag
를 할당하면,
nametag
는 secret
과 같은 메모리 영역을 사용하게 됩니다.
이 때, free
는 해제한 메모리의 데이터를 초기화하지 않으므로, nametag
에는 secret
값이 일부 남아있습니다.
다음은 secret
을 해제한 직후, secret
이 사용하던 메모리 영역을 출력한 것입니다.
secret_name
은 적절한 fd
와 bk
값으로 초기화됐지만, secret_info
의 값은 그대로 남아있습니다.
pwndbg> heap
Free chunk (tcache) | PREV_INUSE
Addr: 0x405290
Size: 0x41
fd: 0x00
pwndbg> x/10gx 0x405290
0x405290: 0x0000000000000000 0x0000000000000041
0x4052a0: 0x0000000000000000 0x0000000000405010
0x4052b0: 0x6472307773734050 0x0000000000234021
pwndbg> x/s 0x4052b0
0x4052b0: "P@ssw0rd!@#"
다음으로는, nametag
를 할당하고, printf
함수를 호출하는 시점에서 nametag
멤버 변수들의 값을 확인하겠습니다.
pwndbg> x/10gx 0x405290
0x405290: 0x0000000000000000 0x0000000000000041
0x4052a0: 0x7974697275636573 0x0000006d61657420
0x4052b0: 0x6472307773734053 0x0000000000234021
0x4052c0: 0x0000000000000000 0x0000000000000000
0x4052d0: 0x0000000000001337 0x0000000000020d31
pwndbg> x/s 0x4052a0
0x4052a0: "security team"
pwndbg> x/s 0x4052b0
0x4052b0: "S@ssw0rd!@#"
pwndbg> x/gx 0x4052d0
0x4052d0: 0x0000000000001337
nametag->team_name
에는 "security team"
이 그대로 입력되었으나,
nametag->name
에는 초기화되지 않은 secret_info
의 값이 존재하는 것을 확인할 수 있습니다.
또한, nametag->fuc
위치에 secret->code
에 대입했던 0x1337
이 남아있는 것을 알 수 있습니다.
이 값이 0
이 아니므로, 예제 코드에서 nametag->func
가 호출되고, Segmentation Fault가 발생합니다.
정리하자면!
동적 할당한 청크를 해제한 뒤에는,
해제된 메모리 영역에 이전 객체의 데이터가 남습니다.
이러한 특징을 공격자가 이용한다면, 초기화되지 않은 메모리의 값을 읽어내거나, 새로운 객체가 악의적인 값을 사용하게 유도하여 프로그램의 정상적인 실행을 방해할 수 있습니다.
-
-
[Reference] : 위 글은 다음 내용을 제가 공부한 후, 인용∙참고∙정리하여 만들어진 게시글입니다.