[0]

이전 포스팅에서, 아래의 대표적인 5가지 Memory error/vulnerability를 살펴보았다. 여기서는 이러한 취약점들을 어떻게 보호할 수 있는지 알아본다. 아래의 [1]~[4]는 이 포스팅을 크게 참고하였다. 

1. buffer overflow
2. null pointer dereference
3. use after free
4. use of uninitialized memory
5. illegal free (of an already-freed pointer, or a non-malloced pointer)

 

[1]

우선 가장 심플한 솔루션을 생각해보자. undefined memory 영역에 대해 접근이 불가능하도록 언어를 정의했다고 생각하자. 이 정의를 통해 자명하게 2,3을 방어할 수 있다. 또한 초기화되지 않은 영역도 undefined로 취급한다면 4를 방어할 수 있다. free()를 defined 된 영역에 한해서만 가능하도록 한다면 5 또한 방어할 수 있다. 

//프로그램 1
int x;
int buf[5];
buf[5] = 1;	//buffer overflow!

하지만 이 정의는 위의 프로그램1과 같은 버퍼 오버플로우 공격을 막을 수 없다. 프로그램 1에선 undefined memory에 대한 접근이 전혀 없기 때문에 공격을 감지할 수 없다.

 

[2]

[1]에서 buffer overflow를 막을 수 있는 심플한 정의만 추가한다면 complete memory safety가 달성되었다고 할 수 있을 것이다. 만약 모든 object가 메모리 공간에서 붙어 있지 않다고, 즉 각 오브젝트들 사이에 undefined memory를 끼고 할당되어 있다고 생각하면 어떨까? 이런 경우 프로그램 1의 x와 buf 사이에 undefined memory가 삽입되게 될 것이므로, buf에서 오버플로우가 발생하더라도, undefined memory에 접근하게 되어 [1]에 의해 에러가 발생하게 된다.

//프로그램 2
int x;
//undefined memory would be inserted here!
int buf[5];
buf[6] = 1;	//buffer overflow!

하지만 이 방식도 완전하지 않다. 공격자가 오브젝트 사이에 삽입되는 undefined memory의 크기를 알고 있다면, 이를 이용해서 쉽게 버퍼 오버플로우로 overwrite할 수 있기 때문이다. 위의 프로그램 2에서는 오브젝트 사이에 sizeof(int)만큼의 undefined memory가 삽입되었지만, 공격자가 이 크기를 미리 알고 있어 buf[6]으로 쉽게 x에 접근할 수 있다. 

 

[3] 

[2]에서 공격자가 삽입되는 undefined memory의 크기를 알 수 없도록 한다면 어떨까? 즉, 삽입되는 메모리 크기를 랜덤화하는 것이다. 이런 경우 100% 완전한 방어는 불가능하지만 충분히 큰 크기의 entropy를 확보한다면(예컨대 1~4GB의 메모리가 삽입되도록 한다.) 메모리 오버헤드는 어마어마하겠지만, 방어 자체는 굉장히 높은 확률로 이뤄낼 수 있을 것이다. 하지만 이 방법도 완전하진 않다. 다음과 같은 공격을 막을 수 없기 때문이다.

//프로그램 3
sturct victim{
	int x;
	int buf[5];
}
strcut * obj = malloc(sizeof(struct victim));
obj->buf[5] = 1;	//buffer overflow!

프로그램 3에서는 같은 오브젝트 내부의 원소들에서 오버플로우를 시도한다. [3]도 이러한 경우를 막을 수는 없는데, 구조체는 하나의 오브젝트로 취급되어 내부의 원소들 사이에 undefined memory를 삽입하지 않기 때문이다. 

 

다만 이를 우회할 수 있는 방법도 있다. C표준에서 구조체 내의 padding값에 제한을 두지 않기 때문에, 여기에도 충분히 큰 랜덤한 padding을 삽입하면 된다. 하지만 이 방법은 구조체 간의 casting 혹은 특정 padding을 가정한 코드들(주로 커널 코드들)의 호환성 문제가 발생하기 때문에 실질적으로 사용하기 어렵다.

 

[4]

이번에는 전혀 다른 접근법을 생각해보자. 근본적으로 memory safety를 침범하는 일은 포인터 역참조에서 발생한다. (배열의 원소 접근 또한 근본적으로 포인터 연산과 역참조이다.) 그렇다면 포인터 자체에 upper bound와 lower bound를 기록하여 접근 범위를 제한하는 것은 어떨까? 이 정의에 따르면 포인터는 (p, b, e)의 세 가지 값을 갖는다. p는 가리키는 주소 값, b는 base(=lower bound), e는 extent(=upper bound)이다. 새로운 포인터를 생성할 때, p, b, e값이 모두 초기화된다. 포인터 연산 시에 p의 값은 변할 수 있지만 b와 e는 절대로 변하지 않는다. p의 주소에 접근 시에, b와 e를 통해 해당 주소가 유효한지를 검증하고, 유효하지 않을 경우 접근을 거부한다. 이제 [1]과 조합될 경우, 위의 1~5의 모든 취약점을 봉쇄할 수 있다. 

 

이 아이디어에 대한 상세한 구현은 Softbound 논문을 참고하자. 구현자체는 그렇게 복잡하지 않아서 이해하기 쉽다.

 

 

참고

1. http://www.pl-enthusiast.net/2014/07/21/memory-safety/

 

What is memory safety? - The PL Enthusiast

Memory safety is a commonly used term - but what is its definition (and why does that matter)?

www.pl-enthusiast.net

2. https://repository.upenn.edu/cgi/viewcontent.cgi?article=1941&context=cis_reports

'Computer Science > Security' 카테고리의 다른 글

Confidential Computing이란  (0) 2020.03.24
double-free 취약점  (0) 2020.02.04
Memory safety - 3  (0) 2019.12.02
Memory Safety - 1  (0) 2019.10.30

+ Recent posts