Stack Canary
스택 카나리란?
스택 카나리는 스택 버퍼 오버플로우 방지 기법이다.
스택 카나리는 SFP와 RET 사이에 삽입된 임의의 값인데, 공격자가 버퍼 오버플로우를 통해 임의의 값으로 덮어씌울 때 스택 카나리 값이 변조되어 RET이 실행되기 전에 에필로에서 시스템에게 걸려서 프로그램이
*** stack smashing detected ***: <unknown> terminated
Program received signal SIGABRT, Aborted.
를 띄우고 죽어버리게 된다.
우분투 22.04 기준 기본 컴파일 옵션으로 컴파일로 진행하면 스택 카나리가 들어가게 된다.
카나리와 컴파일
카나리를 제거하는 옵션은
gcc -o no_canary canary.c -fno-stack-protector
로 카나리 없이 컴파일할 수 있다.
아치리눅스 기준
gcc -z execstack -o yes_canary canary_exercise.c
gcc -fno-stack-protector -z execstack -o no_canary canary_exercise.c
로 컴파일할 수 있다. 스택에 실행권한 안 주면 버퍼오버플로우 나서 컴파일도 못한다. 아니 나 실습하게 해줘 이제 더이상 VM을 늘릴 수 없어
카나리 분석
#include <unistd.h>
int main() {
char buf[8];
read(0, buf, 32);
return 0;
}
를 카나리 옵션 없이 실행하고 8보다 긴 값을 입력하면 Segmentation fault가 발생한다.
그러나 카나리 비활성화를 제거하면 아까 보았던 stack smashing detected가 출력된다. 카나리 변조가 확인되어서 프로그램이 죽은 것이다.
아까 만들었던 yes_canary와 no_canary를 gdb를 통해 비교해보자. 참고로 우분투 22.04와 세부적인 코드는 다를 수 있는데 로직 자체는 같다.
yes_canary

no_canary

하단의 no_canary와 비교하자면
mov rax, QWORD PTR fs:0x28
mov QWORD PTR [rbp-0x8], rax
xor eax, eax
lea rax, [rbp-0x10]
...
mov rdx, QWORD PTR [rbp-0x8]
mov rdx, QWORD PTR fs:0x28
je 0x118f <main+70>
call 0x1030 <__stack_chk_fail@plt>
부분이 추가되었음을 알 수 있다. 이 추가된 코드들의 작동 원리를 알아보자.
fs는 세그먼트 레지스터의 일종으로 리눅스는 프로세스가 시작될 때 fs:0x28에 랜덤 값을 저장한다. 따라서 rax에 프로세스 시작 시점에 생긴 랜덤 값이 저장된다. 그리고 이것이 다시 rbp-0x8에 저장된다. 이후에 rdx에 rbp-0x8의 값이 저장되고, rdx에는 fs:0x28이 저장되고, 이 둘이 같은지 비교한다. 둘이 다르다면 <__stack_chk_fail@plt>이 불러와진다.
정리하자면 초반에 생성된 랜덤 값이 여전히 같은지 판단하는 로직이다.
우회 방법
이론적으로는 브루트 포스로도 풀 수 있으나 현실적으로 가능한 연산량이 아니기에 넘어간다.
TLS 접근
카나리는 TLS에 저장되며 카나리에 의해 보호되는 함수마다 이를 참조해 사용한다. 따라서 TLS의 주소를 실행 중에 알 수 있다면 카나리 값을 읽거나 조작할 수 있다. 이후에 알아낸 원래 카나리 값이나 조작한 카나리 값으로 덮으면 카나리 검사에서 걸리지 않는다.
스택 카나리 릭
스택 카나리를 읽을 수 있는 취약점이 있다면 마찬가지로 이를 알아내서 덮으면 된다.
// Name: bypass_canary.c
// Compile: gcc -o bypass_canary bypass_canary.c
#include <stdio.h>
#include <unistd.h>
int main() {
char memo[8];
char name[8];
printf("name : ");
read(0, name, 64);
printf("hello %s\n", name);
printf("memo : ");
read(0, memo, 64);
printf("memo %s\n", memo);
return 0;
}
를 컴파일하면 name은 memo보다 뒤에 위치하게 된다. 카나리는 첫 바이트가 널 값인데, name에 9바이트를 넣게 되면 카나리 값에 널 바이트가 있는 게 아니라 온전한 카나리 값이 나온다. 이후에 memo에 값을 입력받을 때 name까지 덮을 16바이트와 알아낸 카나리 8바이트를 넣으면 카나리 검사를 우회하면서 반환 주소를 덮을 수 있다.

