스택 가드
1. 개요
1. 개요
스택 가드는 소프트웨어 개발 과정에서 스택에 할당된 변수들에 대한 버퍼 오버플로를 탐지하고, 이를 심각한 보안 취약점으로 악용되는 것을 막음으로써 실행 파일의 보안을 강화시키는 다양한 기법을 통칭한다. 다른 이름으로 버퍼 오버플로 보호라고도 불린다. 주된 목적은 스택 버퍼 오버플로 공격, 즉 스택 스매싱으로부터 프로그램을 보호하는 것이다.
이를 위한 핵심 기법으로는 카나리, 경계 검사, 태깅 등이 있다. 특히 카나리 기법은 스택의 버퍼와 중요한 제어 데이터 사이에 특별한 값을 위치시켜, 버퍼 오버플로가 발생하면 이 값이 먼저 손상되도록 한다. 이후 이 값을 검증하여 오버플로를 탐지하고 프로그램 실행을 안전하게 종료시킨다.
주요 컴파일러인 GNU 컴파일러 모음(GCC), 마이크로소프트 비주얼 스튜디오, 그리고 Clang/LLVM에는 이러한 보호 기법들이 구현되어 있다. 이들은 컴파일 시 특정 옵션을 활성화함으로써 개발자가 애플리케이션에 스택 가드 보호 기능을 쉽게 적용할 수 있도록 지원한다.
스택 가드는 힙 기반 오버플로를 방어하지는 못하지만, 가장 흔하고 오래된 공격 방식 중 하나인 스택 스매싱에 대한 효과적인 방어 계층을 제공한다. 연구에 따르면 이 기법이 성능에 미치는 영향은 대부분의 경우 무시할 수 있을 정도로 작다.
2. 카나리
2. 카나리
2.1. Terminator 카나리
2.1. Terminator 카나리
Terminator 카나리는 카나리 기법 중 하나로, 버퍼 오버플로 공격이 특정 문자열 연산에 의존한다는 점을 활용한 초기 형태의 보호 방법이다. 이 기법은 공격자가 스택 상의 반환 주소를 덮어쓰기 전에 카나리 값을 먼저 손상시켜야 한다는 원리에 기반한다. Terminator 카나리는 널(null) 종결자, 캐리지 리턴(CR), 라인 피드(LF), 그리고 -1(EOF)과 같은 문자열 종결 문자들로 구성된다.
대부분의 버퍼 오버플로 공격은 strcpy()와 같은 문자열 함수를 이용하며, 이 함수들은 이러한 종결 문자를 만나면 복사를 중단한다. 따라서 공격자가 반환 주소를 조작하려면 카나리 영역을 먼저 덮어써야 하는데, Terminator 카나리에 포함된 종결 문자를 복사하게 되면 공격 코드의 전달이 중단되어 공격이 실패하게 된다. 이는 널 바이트를 사용하는 공격 기법을 특히 효과적으로 차단한다.
그러나 이 방법은 공격자가 카나리의 예상 값을 알고, 해당 값을 정확히 재현한 후 덮어쓸 수 있다면 우회될 가능성이 있다. 또한, 힙 기반 오버플로나 함수 포인터를 대상으로 하는 공격에는 효과가 없다. Terminator 카나리는 이후 등장한 Random 카나리나 Random XOR 카나리와 같은 더 강력한 무작위화 기법의 토대를 제공했다.
2.2. Random 카나리
2.2. Random 카나리
Random 카나리는 카나리 기법 중 하나로, 공격자가 값을 예측하거나 알 수 없도록 프로그램 시작 시 난수 생성기를 통해 무작위로 생성된 값을 사용한다. 이 값은 스택 상의 버퍼와 중요한 제어 데이터(예: 반환 주소) 사이에 배치된다. 만약 버퍼 오버플로 공격이 발생하여 버퍼를 넘어 데이터를 덮어쓸 경우, 공격자는 반환 주소를 조작하기 전에 이 무작위 카나리 값을 먼저 덮어쓰게 된다. 함수가 종료되기 직전에 시스템은 이 카나리 값이 원래의 무작위 값과 여전히 일치하는지 검증한다. 값이 변경되었다면 버퍼 오버플로가 발생한 것으로 간주하고 프로그램 실행을 중단시켜 추가적인 피해를 막는다.
Random 카나리의 주요 강점은 그 예측 불가능성에 있다. 공격자가 공격 코드(익스플로잇)를 작성할 때 정확한 카나리 값을 알지 못하면, 값을 보존하면서 반환 주소만 덮어쓰는 것이 불가능해진다. 이 무작위 값은 일반적으로 전역 변수에 저장되며, 때로는 읽기 시도를 막기 위해 메모리 보호 기법을 적용한 페이지에 위치시키기도 한다. 이를 통해 카나리 값을 읽으려는 시도는 세그먼테이션 오류를 유발하여 프로그램을 종료시킨다.
이 기법은 GNU 컴파일러 모음(GCC)의 -fstack-protector 옵션과 같은 컴파일러 수준의 보안 기능으로 구현된다. 그러나 Random 카나리도 완벽하지는 않다. 만약 공격자가 정보 유출 취약점 등을 통해 실행 중인 프로세스의 메모리에서 카나리 값을 읽어낼 수 있다면, 이 공격을 우회할 가능성이 있다. 또한 이 방법은 주로 스택 기반 오버플로를 방어하며, 힙 기반 오버플로나 함수 포인터 오염과 같은 다른 형태의 공격에는 직접적인 보호를 제공하지 않는다.
2.3. Random XOR 카나리
2.3. Random XOR 카나리
랜덤 XOR 카나리는 랜덤 카나리의 개념을 확장한 기법이다. 이 방법은 단순히 랜덤 값을 스택에 배치하는 것을 넘어, 해당 랜덤 값과 스택의 제어 데이터(예: 반환 주소)를 XOR 연산하여 최종 카나리 값을 생성한다. 이렇게 생성된 값이 함수의 프롤로그에서 스택에 저장되고, 함수 종료 시 에필로그에서 원래의 제어 데이터와 다시 XOR 연산을 수행해 원본 랜덤 값과 비교함으로써 무결성을 검증한다.
이 방식의 핵심 장점은 공격 난이도의 상승에 있다. 공격자가 카나리 값을 정확히 예측하거나 우회하려면 단순히 랜덤 값뿐만 아니라, XOR의 대상이 되는 원본 제어 데이터(예: 반환 주소)의 값과 XOR 알고리즘 자체를 모두 알아내고 제어해야 한다. 이는 스택 읽기 공격을 훨씬 더 복잡하게 만든다. 또한, 제어 데이터 자체가 공격에 의해 변경되면 XOR 결과인 카나리 값도 함께 변경되어 검증 시 탐지될 수 있다.
그러나 랜덤 XOR 카나리도 완벽한 방어는 아니다. 함수 포인터와 같이 스택에 저장된 제어 데이터가 아닌 다른 데이터 영역을 대상으로 하는 공격에는 효과가 제한적일 수 있다. 이 기법은 주로 스택가드와 같은 초기 보호 도구에서 구현되었으며, 이후 더 발전된 스택 보호 기법들의 기반 아이디어를 제공했다.
3. 경계 검사
3. 경계 검사
경계 검사는 버퍼 오버플로 공격을 방어하는 컴파일러 기반 기법 중 하나이다. 이 기법은 프로그램이 실행되는 동안 할당된 메모리 블록에 대한 모든 접근을 검사하여, 실제로 할당된 공간의 경계를 넘어서는 접근이 발생하지 않도록 보장한다. C와 C++ 같은 언어에서는 포인터 계산 시점이나 포인터가 역참조되는 시점에 이러한 검사를 수행할 수 있다.
구현 방식은 크게 두 가지로 나뉜다. 하나는 할당된 각 메모리 블록의 경계 정보를 중앙 저장소에 유지하고, 포인터 접근 시 이 정보를 참조하여 유효성을 검증하는 방식이다. 다른 하나는 팻 포인터를 사용하는 방식으로, 포인터 자체에 가리키는 메모리 영역의 범위 정보를 추가로 포함시켜 검사를 수행한다. 이러한 경계 검사는 스택 뿐만 아니라 힙 영역에서 발생하는 오버플로도 탐지할 수 있는 잠재력을 지닌다.
4. 태깅
4. 태깅
태깅은 메모리에 저장된 데이터의 타입이나 속성을 식별하기 위한 메타데이터를 부여하는 기법이다. 이는 주로 타입 검사를 위해 사용되지만, 버퍼 오버플로 공격을 방어하는 용도로도 활용될 수 있다. 태깅을 통해 데이터 저장을 위해 할당된 메모리 영역을 실행 불가능하게 마킹하면, 공격자가 해당 영역에 악성 코드를 주입하고 실행시키는 것을 효과적으로 차단할 수 있다.
태깅은 컴파일러나 하드웨어 수준에서 구현될 수 있으며, 이를 위해서는 태그를 지원하는 특별한 메모리 아키텍처가 필요하다. 역사적으로 태깅은 LISP와 같은 고수준 프로그래밍 언어를 구현하는 데 사용되어 왔다. 운영체제의 적절한 지원 하에, 이 기법은 메모리의 특정 영역을 할당 불가능하게 표시하여 버퍼 오버플로를 탐지하고 방지하는 데 적용될 수 있다.
하드웨어 기반 태깅의 대표적인 예로는 NX 비트가 있다. 이 기능은 인텔, AMD, ARM 등의 현대 프로세서에서 지원되며, 데이터 페이지를 실행 코드로부터 분리함으로써 코드 인젝션 공격을 막는 데 기여한다. 태깅은 카나리나 경계 검사와 같은 다른 버퍼 오버플로 보호 기법을 보완하는 추가적인 보안 계층으로 작동한다.
5. 구현
5. 구현
5.1. GNU 컴파일러 모음(GCC)
5.1. GNU 컴파일러 모음(GCC)
GNU 컴파일러 모음(GCC)은 스택 가드 기법을 구현한 주요 컴파일러 중 하나이다. GCC에서의 스택 보호 구현은 역사적으로 여러 발전 단계를 거쳤다. 최초의 구현은 1997년 StackGuard에 의해 이루어졌으며, 이는 인텔 x86 백엔드를 위한 GCC 2.7 패치로 도입되었다. 이후 IBM은 ProPolice라는 이름으로 스택 스매싱 방어를 위한 개선된 GCC 패치를 개발했는데, 이는 스택 프레임에서 버퍼를 로컬 변수와 함수 매개변수 뒤에 재배치하여 포인터 오염을 방지하는 방식이었다.
GCC 4.1 버전부터는 레드햇 엔지니어들이 ProPolice의 아이디어를 재구현하여 기본 컴파일러에 통합했다. 이때 -fstack-protector와 -fstack-protector-all이라는 두 가지 컴파일 플래그가 도입되었다. 전자는 취약한 것으로 판단되는 일부 함수만을 보호하는 반면, 후자는 모든 함수에 대해 보호 기능을 적용한다. 이후 2012년 구글 엔지니어들은 보호 범위와 성능 간의 균형을 개선한 -fstack-protector-strong 플래그를 제안했으며, 이는 GCC 4.9 버전부터 사용할 수 있게 되었다.
GCC의 스택 보호 메커니즘은 주로 카나리 값을 활용한 검사를 기반으로 한다. 이는 함수의 프롤로그에서 스택에 특수 값을 저장하고, 함수가 종료되기 전인 에필로그에서 해당 값이 변경되었는지 확인하는 방식으로 동작한다. 값이 변경되었다면 버퍼 오버플로가 발생한 것으로 간주하고 프로그램 실행을 중단시킨다. 이 기법은 스택 스매싱 공격으로부터 반환 주소나 중요한 제어 데이터가 덮어쓰이는 것을 효과적으로 방지한다.
5.2. 마이크로소프트 비주얼 스튜디오
5.2. 마이크로소프트 비주얼 스튜디오
마이크로소프트 비주얼 스튜디오는 2003년 버전부터 /GS 컴파일러 명령줄 스위치를 통해 스택 가드 기법을 구현해 왔다. 이 기능은 마이크로소프트 비주얼 스튜디오 2005 버전부터 기본적으로 활성화되어 있으며, 버퍼 오버플로 공격으로부터 실행 파일을 보호하는 역할을 한다. /GS 스위치는 스택에 할당된 버퍼와 중요한 제어 데이터(예: 함수의 반환 주소) 사이에 무작위로 생성된 카나리 값을 삽입하여, 버퍼 오버플로가 발생해 이 값이 손상되면 프로그램 실행을 중단시킨다.
구체적인 구현 방식은 GNU 컴파일러 모음(GCC)의 접근법과 유사하지만, 마이크로소프트 고유의 최적화와 윈도우 응용 프로그램 이진 인터페이스(ABI)에 맞춰져 있다. 개발자는 보호 기능을 명시적으로 비활성화해야 하는 경우 /GS- 스위치를 사용할 수 있다. 이 기법은 스택 스매싱 공격을 효과적으로 방어하지만, 힙 기반 오버플로나 포인터를 통한 다른 메모리 손상 공격까지는 보호하지 않는다.
5.3. Clang/LLVM
5.3. Clang/LLVM
Clang/LLVM 컴파일러 인프라스트럭처는 여러 가지 버퍼 오버플로 탐지 기법을 지원한다. 이들은 성능 저하, 메모리 오버헤드, 탐지 가능한 버그 유형에 따라 서로 다른 장단점을 가진다.
주요 지원 기능으로는 AddressSanitizer(-fsanitize=address 플래그로 활성화), -fsanitize=bounds 옵션, 그리고 SafeCode가 있다. AddressSanitizer는 힙 기반 및 스택 기반 버퍼 오버플로를 포함한 다양한 메모리 오류를 탐지하는 강력한 도구이다. -fsanitize=bounds 옵션은 배열 경계 검사를 수행하여 인덱스가 할당된 범위를 벗어나는 접근을 방지한다.
이러한 도구들은 C 및 C++ 언어로 작성된 프로그램의 메모리 안전성을 높이는 데 기여하며, 소프트웨어 개발 과정에서 보안 취약점을 조기에 발견하는 데 널리 사용된다.
