스택 기반 버퍼 오버플로우는 지역 변수에 정해진 크기보다 많은 데이터를 써서 스택에 저장된 return address까지 덮어쓰고, 함수가 리턴할 때 공격자가 원하는 코드로 실행 흐름을 옮기는 전형적인 취약점 패턴이다. 커널은 컴파일러가 생성하는 스택 카나리(canary)로 이런 오버플로우가 실제로 함수를 빠져나가기 전에 잡아낸다. 이 글에서는 Stack Protector의 동작 원리와 설정 방법을 정리한다.
동작 원리
컴파일러가 함수 프롤로그(prologue)에서 지역 변수들보다 스택 아래쪽, return address 바로 앞에 무작위 값(canary)을 심어두고, 함수가 리턴하기 직전(에필로그)에 그 값이 그대로인지 검사하는 코드를 자동으로 추가한다. 지역 배열에 크기를 넘겨 쓰면 return address보다 먼저 canary를 덮어쓰게 되므로, 리턴하기 전에 값이 바뀐 것을 감지해 공격을 무력화할 수 있다.
모든 함수에 canary가 붙는 것은 아니고, 컴파일러가 스택 오버플로우 위험이 있다고 판단하는 함수에만 선별적으로 추가한다(어떤 조건의 함수가 대상이 되는지는 아래 STACKPROTECTOR_STRONG 설명 참고). canary 값은 부팅 시 무작위로 생성되는데, 64비트 아키텍처에서는 canary의 최상위 바이트를 0으로 고정한다. NUL 바이트에서 멈추는 문자열(strcpy 등) 기반 오버플로우는 canary 값 중간에 낀 NUL 바이트를 그대로 복사할 수 없기 때문에, 이렇게 하면 그런 종류의 오버플로우가 canary를 흉내 내 우회하기 어려워진다.
커널 빌드 옵션 (Kconfig)
CONFIG_STACKPROTECTOR=y
CONFIG_STACKPROTECTOR_STRONG=y
둘 다 컴파일러가 해당 플래그(-fstack-protector, -fstack-protector-strong)를 지원하기만 하면 기본값이 y라, 요즘 배포판 커널은 대부분 이미 켜진 채로 배포된다. CONFIG_STACKPROTECTOR는 8바이트 이상 char 배열이 있는 함수에만 canary를 추가하고, CONFIG_STACKPROTECTOR_STRONG은 지역 변수의 주소를 대입식이나 인자로 사용하는 함수, 배열(타입/길이 무관)이 있는 함수, register 지역 변수를 쓰는 함수까지 더 넓게 적용한다. x86 defconfig 기준으로 STACKPROTECTOR는 전체 함수의 약 3%(코드 크기 +0.3%)에, STACKPROTECTOR_STRONG은 약 20%(코드 크기 +2%)에 canary를 추가한다.
커널 4.18 이전에는 CONFIG_CC_STACKPROTECTOR, CONFIG_CC_STACKPROTECTOR_REGULAR/STRONG이라는 이름이었으나, 이후 아키텍처 공통 코드로 통합되면서 CC_ 접두사가 빠진 현재 이름이 됐다.
적용 여부 확인하기
런타임 sysctl로 켜고 끄는 기능이 아니라 순수 컴파일 타임 옵션이라, 지금 부팅된 커널에 적용됐는지는 빌드 설정이나 심볼로 확인해야 한다.
# zcat /proc/config.gz | grep STACKPROTECTOR
CONFIG_STACKPROTECTOR=y
CONFIG_STACKPROTECTOR_STRONG=y
/proc/config.gz가 없다면 CONFIG_IKCONFIG_PROC가 꺼져 있는 커널이므로, 빌드에 사용한 .config 파일에서 직접 확인한다. 심볼이 실제로 커널에 링크됐는지는 아래처럼도 확인할 수 있다.
# nm /usr/lib/debug/boot/vmlinux-$(uname -r) | grep __stack_chk_fail
카나리 검사 실패 시 로그
canary 값이 바뀐 채로 함수가 리턴하려 하면 __stack_chk_fail()이 호출되어 아래와 같은 메시지와 함께 즉시 panic한다.
Kernel panic - not syncing: stack-protector: Kernel stack is corrupted in: some_function+0x1a/0x40
메시지의 함수 이름과 오프셋은 실제 콜 스택이 아니라 canary가 깨진 채로 리턴을 시도한 함수를 가리킨다. 이미 스택이 신뢰할 수 없는 상태이므로 복구를 시도하지 않고 곧바로 panic으로 처리한다.
주의사항 및 팁
- Stack Protector는 스택 버퍼 오버플로우를 예방하는 게 아니라 “덮어쓰기가 일어난 뒤 함수가 리턴하는 시점”에 감지하는 기법이다. canary 검사 이전에 덮어쓴 다른 데이터(예: 스택 위의 다른 지역 변수나 함수 포인터)를 악용하는 공격은 막지 못한다.
STACKPROTECTOR_STRONG이STACKPROTECTOR보다 훨씬 많은 함수에 canary를 추가하는 만큼 코드 크기와 약간의 성능 오버헤드가 따른다. 대부분의 배포판은 이 트레이드오프를 감수하고 기본으로 STRONG까지 켠다.- KASAN(Kernel Address Sanitizer(KASAN) 사용법 참고)이나 SLUB Debug(SLUB Debug 사용법 참고)는 힙 오버플로우를 잡고, Stack Protector는 스택 오버플로우를 잡는다. 다루는 메모리 영역이 다르므로 서로 대체 관계가 아니라 함께 켜서 보완하는 관계다.