malloc 함수의 caching 기능 중 deferred coalescing 속성 존재.
-> free 된 heap이 realloc될 때에 같은 사이즈로 요청 받을 수 잇는데, heap을 병합하거나 분할하는 시간을 절약하고자 free 된 heap을 남겨뒀다가 reuse 할때 free 된 영역을 그대로 사용하게 해주는 방법.
=> 즉, 한번 사용하고 프리를 했다가 동일한 크기의 메모리 할당을 요청하면 방금 프리한 블럭을 넘겨주게되고 여기에는 당연히 이전에 들어있던 데이터가 남아있다. (memset 초기화를 하지 않았기 때문) 여기서 발생하는 것이 바로 Use After Free 취약점이다.
아래는 "Preventing Use-After-Free Attacks with Fast Forward Allocation"이라는 논문에서 나온 예시 코드이다.
typedef struct
{
long used;
char buf[24];
} Array; // 32-byte
typedef struct
{ // 32-byte
long status;
void *start, *current;
int (*handler)(void *buf);
} Parser;
enum Command
{
INVALID,
PARSE,
...
};
int net_parser(void *buf);
int handle_net_input(int client_fd)
{
Parser *parser = (Parser *)malloc(32); // allocation
parser->status = INIT;
parser->start = parser->current = NULL;
parser->handler = &net_parser;
enum Command cmd = INVALID;
read(client_fd, &cmd, sizeof(cmd));
switch (cmd)
{
case INVALID:
free(parser); // missing break;
case PARSE:
Array *array = (Array *)malloc(32); // re-allocation
read(client_fd, array->buf, 24); // content changes
parser->handler(array->buf); // use-after-free
free(parser);
break;
}
}
case INVALID 아래 free(parser)에서 glibc 등의 메모리 관리자는 parser가 사용했던 메모리를 운영체제로부터 즉각 회수할 수 없다. 그러한 상황에서 Array 객체 할당을 요청하는 순간 메모리 관리자는 기존에 parser객체가 쓰던 메모리 내용을 그대로 array 객체에게 재할당해준다. 그렇다면 이제 array와 parser는 동일한 메모리 영역을 가리키게 된다.
그런 상황에서 read(client_fd, array->buf, 24) 함수는 사용자의 신뢰할 수 없는 입력값을 그대로 받아들일것이며 이를통해 parser구조체의 멤버 변수들을 효과적으로 덮어쓸 수 있게 된다. 특히 handler 같은 함수 포인터가 주요 공격 목표물이 될 수 있다. 이후 parser의 handler 함수 호출이 발생하면, 공격자는 자신이 지정한 임의의 어떤 위치라도 점프할 수 있다. (handler 포인터 메모리에 내가 원하는 위치로 점프하는 system call을 넣어놓을 수 있기 때문에)
=> 이는 control flow hijacking 공격이라 볼 수 있다.