핵심 요약
- GCC 컴파일러의 GNU C 확장 기능인 중첩 함수 클로저는 런타임에 스택 메모리 위에 트램펄린 기계어 코드를 생성해 동작합니다.
- 이 과정에서 스택에 실행 권한(Executable Stack)이 부여되어 버퍼 오버플로 공격에 시스템이 무방비로 노출되는 심각한 보안 결함이 발생합니다.
- GNU 링커의 기본 동작마저 실행 스택을 허용하도록 설계되어 있어 단 하나의 오브젝트 때문에 전체 바이너리가 오염될 수 있습니다.
요약 소프트웨어 개발 분야에는 겉보기에는 유용하고 완벽해 보이지만, 실제 구현 방식과 부작용을 들여다보면 경악스러운 기능들이 종종 존재합니다. 대표적인 사례가 바로 GCC(GNU Compiler Collection)의 C 언어 확장 기능 중 하나인 '중첩 함수(nested functions)'와 이를 이용한 클로저(closure) 구현입니다.
GNU C에서는 함수 내부에서 또 다른 함수를 정의할 수 있습니다. 바깥 함수의 지역 변수를 참조하지 않는 단순 중첩 함수는 정적 함수와 다를 바 없어 무해합니다. 하지만 내부 함수가 바깥 스코프의 지역 변수에 접근하는 순간 이는 클로저로 동작하게 됩니다. 문제는 C 언어에서 이 클로저를 일반 함수 포인터처럼 다룰 수 있도록 지원하기 위해 GCC가 사용하는 방식에 있습니다.
GCC는 런타임에 호출 스택 위에 '트램펄린(trampoline)'이라는 실행 가능한 기계어 코드 조각을 동적으로 생성합니다. 이 트램펄린 코드는 주변 스택에 저장된 지역 변수를 인자로 전달하면서 실제 함수를 호출하는 역할을 맡습니다. 그런데 스택 메모리에 올려둔 코드를 직접 CPU가 실행하도록 만들려면, 필연적으로 스택 영역에 실행 권한(Executable Stack, RWE)이 부여되어야 합니다.
스택 실행 권한은 버퍼 오버플로 공격 시 악성 쉘코드를 손쉽게 실행할 수 있게 만들기 때문에 보안상 매우 위험하여 이미 십수 년 전부터 금기시된 방식입니다. 더욱 심각한 문제는 GNU Binutils 링커의 기본 동작 방식이 스택 실행 권한을 기본으로 허용하는 'fail-open' 형태로 설계되어 있다는 점입니다. 링커에 들어가는 모든 오브젝트 파일이 "실행 가능한 스택이 필요 없다"고 명시하지 않는 한, 단 하나의 파일 때문에 전체 바이너리가 치명적인 보안 취약점을 안은 채 빌드될 수 있음을 지적합니다.
Sponsored · 광고