GCC Stack Smashing Protector

C 컴파일러에는 굉장히 종류가 많습니다. Dev C Compiler, Visual C Compiler, GNU C Compiler .... 뭐 그 외에도 여러가지가 있지요?


오늘은 Buffer Overflow라는 것을 주제로 시작하여, GNU C Compiler에 존재하는 Stack Smashing Protector가 어떤 것인지를 설명하도록 하겠습니다.




자, 어떤 코드를 제가 작성을 해서 이렇게 입력을 받고 났더니, 위와 같이 에러 메시지를 뿜어냅니다. 왜 그럴까요? 소스 코드를 보도록 하겠습니다.





코드를 한 번 분석해봅시다. 배열을 이용해서, 1개의 공간을 초기화 해주긴 했지만, i 의 변수를 통해, 배열의 크기를 지정하는 것 같습니다(?) 그리고, fibonacci 라는 함수를 써서 그 데이터를 보내는군요. 자 여기서 어떤 부분이 문제일까요?


바로, 메모리 공간입니다. 분명 1개의 공간을 부여했지만, 만약 3개 이상의 공간을 부여하여, 숫자를 입력받게 되면, Overflow가 발생합니다. 그러면서 GCC 컴파일러에서는 *** stack smashing detected *** 라는 메시지를 출력합니다. 



BOF(Buffer OverFlow)

시스템 보안이나, 이제 막 프로그램 취약점을 분석하는 사람들이라면, 이런 단어는 그냥 기초적인 단어에 불과할 것입니다. 버퍼 오버플로우는 컴퓨터 과학 전공자들이라면, 알고리즘 시간에도 접할 수 있는 프로그램 취약점의 일종이다.


그림으로 한 번 설명해보도록 하겠습니다.




일단 굉장히 단순하게 메모리 구조를 그려봤습니다. 여러분들이 생각하시는 메모리는 대충 위와 같은 구조로 되어 있습니다. 사실 이 영역을 사용자 메모리 영역이라고도 합니다. 이것 외에도 시스템 메모리 영역이라고 있는데, 이 부분은 이 단원과 관계가 없으므로 생략하겠습니다.


각각의 영역을 설명해보겠습니다.


Code Area : 이 공간에는 여러분들이 직접 짠 C 코드가 들어가는 명령어 공간입니다.

Data Area : 사용자가 직접 입력할 수 있는 데이터를 받는 공간입니다.

Heap Area : 동적으로 메모리를 사용할 수 있는 공간입니다. C나 C++를 통해 malloc 함수를 사용하는 경우, 이 영역에 들어옵니다.

Stack Area : 함수 인자, 지역 변수들이 이 공간에 들어옵니다. 


자 조금 이해가 되셨는지요?

간단하게 부가적인 설명은 웬만해서 제외하고, 코딩을 할 때 가장 기본적인 부분들만 설명해봤습니다. 


여기서 오버플로우라는 것은 코드 영역 안에서 정해준 공간을 지나치게 사용하여, 다른 공간으로 넘어가는 것을 오버플로우라 합니다. 이렇게 되면, 프로그램에서 출력되는 값은 쓰레기값 (잘못된 값)이 출력될 수도 있고, 해커들이나 공격자들에게 이 부분이 노출되면, 보안상의 결함을 추가로 노출시키게 되기 때문에 굉장히 위험합니다.


예시로 방금 위에 있던 코드에서 arr 배열이 데이터 영역에 들어가게 될 것입니다. 공간은 정해져 있지 않은 가변 배열 변수로, 초기화 시킨 배열의 값을 통해 스택 영역에서는 NULL 값을 포함하여 최소 2~3개의 공간을 만들어낼 것입니다. 하지만 내가 만약 i 변수에서 3개 이상의 공간을 마련하여 입력 값을 그 이상 받았다면? 당연히 오버플로우가 발생하여 다른 변수 공간에 영향을 주게 됩니다.


대표적인 함수로는 위에 코드에서 사용된 scanf 함수가 가장 대표적입니다.


실제로 Visual C++ 2010 컴파일러 이상에서는 scanf의 사용을 제한하고 있고, scanf를 사용할 경우, 에러를 뿜어내며, 특정 전처리기를 사용하여야만 이 함수를 사용할 수 있습니다. MS에서는 이 함수 대신, 자체적으로 만든 scanf_s 의 사용을 권장하고 있고, scanf와의 차이점은 문자열이나 문자 값을 입력받을 때, 공간을 직접 정해서 사용해야하는 것입니다.



위와 같이 Visual C++에서는 scanf 사용을 제한하고 있습니다. scanf 대신 scanf_s의 사용을 권장하고 있으며, 해당 사항은 char 자료형에 대해서만 제한하고 있는 사항입니다.


반대로, GNU C Compiler의 경우에는 scanf_s가 제공되지 않습니다. scanf 사용 제한을 하지 않으며 문제가 발생했을 경우(아까 위에서 봤던 것처럼 Overflow가 발생), stack smashing이 보호하였다는 메시지와 함께 프로그램을 종료하게 됩니다. 이의 경우를 살펴보도록 하겠습니다.


정확하게 어떤 부분에서 Stack Smashing Protector가 나는지를 검증하기 위해, GCC에서 Stack Smashing Protector를 강제 옵션으로 주어, 컴파일 해보도록 하겠습니다.


gcc -fstack-protector -S Source.c


이제 컴파일된 코드를 한 번 보도록 하겠습니다.



fibonacci 함수에서는 문제가 없는 것 같네요...



여기도 별 다른 문제가....



scanf를 call 하는 부분에 대해서도 Stack Smashing Protector가 동작하는 부분은 찾을 수 없었습니다.

그렇다면, 도대체 어디서 동작하는건지...


정확한 분석을 위해서, GDB를 사용해서 분석을 다시 시도 해보도록 하겠습니다.




GDB를 사용해서, 반복문 구간에 Breakpoint를 부여하고, 프로그램을 다시 실행해봤습니다.




어셈블리어 코드를 보니, 0x400778 구간에 __stack_chk_fail 부분을 발견했습니다. __stack_chk_fail 호출할 경우, GCC 컴파일러에서 Stack Smashing Protector를 동작시킨다고 할 수 있습니다. 이 구간은 stack canary check에 실패한 부분입니다.


그렇다면, 이 프로시저는 어떤 방식으로 작동하는걸까요?


보통, 컴파일러는 함수에 진입시, return address와 frame pointer 정보를 저장합니다. 하지만 일반적으로 그냥 정보를 저장하게 되면, 디버깅시, return 주솟값을 변조하여 취약점을 발생하게 됩니다.


Stack Smashing Protector는 이 변조를 방지하기 위해, 메모리에 canary라고 하는 특정 값을 생성하고, 함수에서 리턴했을 때, 컴파일러가 처음 함수에 진입했을 때 return address와 일치/불일치를 검증을 합니다. 쉽게 설명하자면, Cookie랑 거의 비슷한 개념이지요.


이렇게 될 경우, 공격자는 canary 값을 변조할 것입니다. 그렇기 때문에 코드에 canary 값을 변조가 되었는지 확인만 할 수 있다면, Overflow가 생겼는지를 확인할 수 있다는 사실이 입증됩니다.


그렇다면, canary 값은 어떻게 생성되는 것일까요?


여러가지 방법이 있지만, 그 중에서도 제가 사용하고 있는 Ubuntu Linux에서는 OTP처럼 현재 시간을 기준으로 난수를 생성하며, 하위 바이트를 항상 0으로 만들어서 사용합니다. 보통 이런 경우를 Random + terminator canary 형태라고 합니다.


이렇게 될 경우, 공격자가 아무리 canary 값을 뚜들기려 해도 쉽게 찾을 수 없다는 강점이 생깁니다. 무작위로 생성되는 Random은 공격자가 일일이 실행할 때마다 달라지기 때문에 쉽게 유추하기도 어렵고, terminator와 같은 경우에도 일반적인 문자열 연산 가지고는 덮어쓰기가 불가능합니다. 문자열의 끝값은 NULL이라고 하는 \0, \n, -1 값에 해당하기 때문이지요..


다시 어셈블리어 코드를 보게 되면, 이 코드에서는 gs 레지스터가 사용되지 않음을 알 수 있습니다. 보통 함수에서 오버플로우가 일어나면, canary 값을 확인하기 위해 gs 레지스터에 일정 공간을 남겨 저장합니다. 하지만 이 포스트의 코드는 함수의 반환 값에서 오버플로우가 발생되지 않기 때문에 canary 값은 __stack_chk_guard라는 전역 변수에 canary 값이 저장됩니다.


Linux Kernel을 컴파일 할 경우에도 동일하게 적용될 수 있습니다. 



__stack_chk_fail 함수는 Kernel에서도 위와 같은 방식으로 발생하게 되면, 패닉을 일으킵니다. 오버플로우 취약점은 굉장히 기초적이면서도 취약점이 크기 때문에 커널에서도 panic 수준급으로 끌어올립니다.




일반적으로 코딩하다 생긴 오류는 위와 같이 fortify_fail 형태의 에러로 나타납니다.




해당 이후에 나타나는 메시지는 프로그램이 종료되었다는 메시지가 보고됩니다. 



여기까지, Stack Smashing Protector에 대해서 알아봤습니다. 조금 긴 내용이고, 어려운 부분이 많습니다. 하지만 C언어와 같은 Low 레벨의 언어를 공부하실 때는 이런 부분들이 굉장히 중요합니다. 나중에 포인터를 다루면서 느끼실지도 모르겠지만 사소하게 메모리에 접근했던 부분이 잘못해서 취약점이 생기기도 하고, 특히 운영체제나 임베디드 소프트웨어 개발을 꿈꾸시는 분들이라면, 고려해봐야 합니다.


보통 Windows에서 사용되는 Visual C 컴파일러도 이 정도까지는 가지 않는 것으로 알고 있습니다. Windows에서는 ASLR(Address Space Layout Randomization)이라는 기술을 Windows Kernel 6.0 (Windows Vista)에서 채택하였으며, 이 기술 또한 버퍼 오버플로우를 방지하기 위한 기술로 알려져 있습니다.


Stack Smashing Protector는 2007년도 GCC에서 처음 등장했으며, Mac OS X (Apple Inc.) 또한 이 기술을 적용하고 있습니다. 물론 Linux와는 다른 메시지를 보인다고 하니, Apple Open Source Page에서 한 번 확인해보시기 바랍니다.

TAGS.
comments powered by Disqus

Tistory Comments 0