Coding Memo

Use After Free 본문

Language/C++

Use After Free

minttea25 2023. 11. 2. 13:10

Use After Free는 해제된 메모리를 더 이상 사용하지 않아야 되는 상황에서 사용하려고 할 때 나타나는 매우 큰 문제이다.

(Java나 C#같은 high-level 언어들은 이 문제에 신경을 덜 써도 된다.)

 

 

Use After Free 문제는 다음과 같은 상황에서 일어날 수 있다.

 

메모리를 delete(free)한 후에 그 영역에 접근하려고 할 때

 

즉, 객체의 관점에서 본다면, 객체를 delete(free) 했지만 객체에 대한 포인터가 그대로 남아있어 그 포인터에 접근하려고 할 때 일어난다.


다음 코드를 살펴보고 문제가 되는 점을 찾아보자.

using namespace std;

class A
{
public:
	A()
	{
		cout << "constructor" << endl;
	}

	~A()
	{
		cout << "destructor" << endl;
	}

public:
	int value;
};

int main()
{
	A* a = new A();
    
	a->value = 10;
	cout << a->value << endl;
    
	delete a;

	cout << a->value << endl; // Use After Free

}

 

a를 동적할당 후에 명시적으로 `delete a'를 통해 명시적으로 메모리를 해제한 후에 다시 a 포인터의 value 맴버에 접근하고 있다. 바로 이부분이 UAF 문제이다.

 

더 큰 문제는 따로 있다.

 

컴파일러가 이 문제를 잡아내지 못할 가능성이 있다는 것이다.

(실제로 문제가 없어서 컴파일이 정상적으로 실행되기도 한다.)

(IDE에서는 a에 마우스를 갖다 대면 `할당되지 않은 a에 접근하고 있습니다`라는 경고가 나올 것이다.)

 

실제로 위 코드를 실행 시켜보면 처음 a는 제대로 10이 출력되지만 메모리 할당 이후, a->value의 값에 대해서는 이상한 값이 출력되는 것을 알수 있을 것이다.

 

(만약, 어떤 게임에서 몬스터 처치 경험치가 10이었는데, 몬스터에 대한 메모리를 해제하고 다시 그 몬스터에 대한 경험치를 얻으려고하면 난리가 날 것이다!)

 

 

그렇다면 이 문제를 예방하고 해결할 수 있는 방법에는 무엇이 있을까?


1. delete 후에 포인터를 nullptr로 둔다.

 

솔직히 말해서 좀 애매한(?) 방법이기는 하다. delete까지 했는데 굳이 nullptr로 명시적으로 코드를 작성해야한다니...

int main()
{
	A* a = new A();
	a->value = 10;
	cout << a->value << endl;
	delete a;
    
	a = nullptr;

	cout << a->value << endl; // crash!! - read access is violation; a was nullptr
}

delete 후에 `a = nullptr` 코드를 명시적으로 작성하여 런타임 중에 이제 a는 nullptr니까 접근하지 말라고 알려준다.

위 코드를 실행하면 런타임 중에 에러가 나타날 것이다.

 

 

2. 스마트 포인터 사용

unique_ptr와 shared_ptr이라는 좋은 포인터가 있다! 

 

shared_ptr로 선언된 객체는 (강한)참조가 더 이상 없으면 (=use_count가 0이 되고 약한 참조가 없으면) 메모리가 자동으로 해제된다. (자동으로 nullptr가 들어간다.)

아래 예시는 참조 수를 강제로 reset 시키고 다시 a에 접근하려고 시도했을 때의 코드이다. (오류가 나타난다.)

reset시에 use_count가 0이 되면서 소멸자까지 호출되는 것을 확인할 수 있다.

int main()
{
	shared_ptr<A> a = make_shared<A>();
	a->value = 10;
	cout << a->value << endl;
	cout << "Use count: " << a.use_count() << endl;

	a.reset();
	cout << "Use count after reset: " << a.use_count() << endl;
	cout << a->value << endl; // crash!!; a was nullptr
}

 

unique_ptr로 선언된 객체는 해당 객체에 대한 포인터가 단 하나밖에 존재할 수 없다. 마찬가지로 이 포인터에 대해 메모리가 해제되거나 소유권이 이전되었을 때 해당 객체는 nullptr를 가리키게 된다.

int main()

	unique_ptr<A> a = make_unique<A>();
	a->value = 10;
	cout << a->value << endl;
	//A* b = a.release();
	unique_ptr<A> b = std::move(a); // 소유권 이전
    
	cout << a->value << endl; // crash!!; a was nullptr
}

 

Note: 소유권 이전이므로 소멸자는 호출 되지 않는다.

 

 

3. VirtualAlloc으로 메모리 플래그 지정

 

VirtualAlloc은 Windows에서 제공하는 함수로 가상 메모리를 할당하고 제어하는데 사용된다. 할당하면서 그 메모리 공간에 대한 특성(플래그)를 지정할 수 있다.

먼저, 짚고 넘어가야 할 내용이 있다.

 

VirtualAlloc은 요청한 size에 맞게 메모리를 할당하는 것이 아니라 granularity의 배수로 메모리 블록을 할당한다.

(예를 들어 granulairty가 4kb인데, 1kb사이즈의 크기를 할당 요청을 할 경우 실제 할당되는 크기는 4kb이다. 따라서 3kb에 대해서는 사용하지 않는 공간이 되므로, 메모리 파편화에 주의해야 한다.)

 

따라서 아래 예시 코드처럼 그대로 사용하면 안된다는 것을 기억하자. (예시를 위해서 짠 코드이다.)

int main()
{
	LPVOID p = ::VirtualAlloc(NULL, sizeof(A), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); // 실제 할당되는 메모리 공간은 sizeof(A)가 아니다.
	A* a = static_cast<A*>(p);
	new(p) A();

	a->value = 10;
	cout << a->value << endl;
    
	a->~A();
	VirtualFree(p, NULL, MEM_RELEASE);

	cout << a->value << endl; // Exception thrown: read access violation.
}

 

Note: 명시적으로 생성자와 호출자를 작성했다는 것을 확인하자.

Note2: `new(p) A()` 코드는 placement new로 p라는 메모리 공간에 A를 할당하는 코드이다.


결론적으로는 메모리 풀을 이용할 것이 아니라면, shared_ptr를 쓰는 것이 가장 좋을 수 있겠다. unique_ptr도 마찬가지로 상황에 맞게 사용하면 되겠다.

'Language > C++' 카테고리의 다른 글

[C++] 가상 소멸자  (0) 2024.03.06
[C++] pack pragma (메모리 정렬)  (0) 2024.03.05
값 범주 (value categories) (rvalue, lvalue, xvalue)  (0) 2023.10.27
Visual Studio glog 사용  (2) 2023.10.26
메타 함수, 템플릿 1  (1) 2023.10.26