Mitigation: Stack Canary
# 들어가며
- Return Address Overwrite: 스택의 반환 주소(Return Address)를 조작하여 실행 흐름을 획득하는 공격 기법
- 스택 카나리(Canary): 스택 버퍼 오버플로우로부터 반환 주소를 보호
- 함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에 임의의 값을 삽입하고, 함수의 에필로그에서 해당 값의 변조를 확인하는 보호 기법
- 카나리 값의 변조가 확인되면 프로세스는 강제로 종료됨
- 스택 버퍼 오버플로우 반환 주소를 덮으려면 반드시 카나리를 먼저 덮어야 하므로 카나리 값을 모르는 공격자는 반환 주소를 덮을 때 카나리 값을 변조하게 됨 → 에필로그에서 변조가 확인되어 공격자는 실행흐름을 획득하지 못함
# 카나리의 작동 원리
1. 카나리 정적 분석
- Figure2. 스택 버퍼 오버플로우가 발생하는 예제 코드
// Name: canary.c
#include <unistd.h>
int main() {
char buf[8];
read(0, buf, 32);
return 0;
}
- 카나리 비활성화
- gcc는 스택 카나리를 기본적으로 적용함
- 컴파일 옵션으로 -fno-stack-protector 옵션을 추가해야 카나리 없이 컴파일 가능
- 다음 명령어로 위의 예제를 컴파일하고 길이가 긴 값을 주면, 반환 주소가 덮여 Segmentation fault 발생
- 카나리 활성화
- 카나리를 적용하여 다시 컴파일하고, 긴 입력을 주면 Segmentation fault가 아니라 stack smashing detected와 Aborted라는 에러가 발생
- 이는 스택 버퍼오버플로우가 탐지되어 프로세스가 강제 종료되었음을 의미
- no_canary와 디스어셈블 결과를 비교하면, 다음과 같이 빨간색 과정들이 추가되었음을 확인할 수 있음
2. 카나리 동적 분석
- 카나리 저장
- canary에 no_canary에는 없었던 프롤로그 코드를 확인하고 중단점을 설정한 후 바이너리를 실행시킴
- main+8은 fs:0x28의 데이터를 읽어서 rax에 저장
- fs는 세그먼트 레지스터의 일종으로, 리눅스는 프로세스가 시작될 때 fs:0x28에 랜덤 값을 저장함
- main+8의 결과로 rax에는 리눅스가 생성한 랜덤값이 저장됨
- 코드를 한 줄 실행하면 rax에 다음과 같이 첫 바이트가 널 바이트인 8바이트의 데이터가 저장되어 있는 것을 확인할 수 있음
- 생성한 랜덤값은 main+17에서 rbp-0x8에 저장됨
💡 fs
- Thread Local Storage(TLS)를 가리키는 포인터로 사용
- TLS에 카나리를 비롯하여 프로세스 실행에 필요한 여러 데이터가 저장됨
- 카나리 검사
- 추가된 에필로그 코드에 중단점을 추가로 설정하고 바이너리를 계속 실행
- main+50은 rbp-0x8에 저장한 카나리를 rcx에 옮김
- 그 뒤, main+54에서 rcx를 fs:0x28에 저장된 카나리와 xor함
- 두 값이 동일하면 연산 결과가 0이 되면서 je의 조건을 만족하게 되고, main 함수는 정상적으로 반환됨
- 그러나 두 값이 동일하지 않으면 __stack_chk_fail이 호출되면서 프로그램이 강제로 종료됨
- 16개의 H를 입력으로 카나리를 변조하고, 실행 흐름이 어떻게 변하는지 볼 것
- 코드를 한 줄 실행시키면, rbp-0x8에 저장된 카나리 값이 버퍼 오버플로우로 인해 "0x4848484848484848"이 된 것을 확인할 수 있음
- main+54의 연산 결과가 0이 아니므로 main+63에서 main+70으로 분기하지 않고 main+65의 __stack_chk_fail을 실행하게 됨
- ni를 두번했을 때의 상태는 __stack_chk_fail이 호출되기 이전임
- __stack_chk_fail을 실행하게되면 다음의 메시지가 출력되며 프로세스가 강제로 종료됨
# 카나리 생성 과정 🔬
- 카나리 값은 프로세스가 시작될 때, TLS에 전역 변수로 저장되고, 각 함수마다 프롤로그와 에필로그에서 이 값을 참조함
1. TLS의 주소 파악
- fs는 TLS를 가리키므로 fs의 값을 알면 TLS의 주소를 알 수 있음
- 리눅스에서 fs의 값은 특정 시스템 콜을 사용해야만 조회하거나 설정할 수 있음
- gdb에서 다른 레지스터의 값을 출력하듯 info register fs나, print $fs와 같은 방식으로는 값을 알 수 없음
- 위와 같은 방법으로는 값을 알 수 없기 때문에 fs의 값을 설정할 때 호출되는 arch_prctl(int code, unsigned long addr) 시스템 콜에 중단점을 설정하여 fs가 어떤 값으로 설정되는지 조사할 것임
- 이 시스템 콜은 arch_prctl(ARCH_SET_FS, addr)의 형태로 호출하면 fs의 값은 addr로 설정됨
- gdb에는 특정 이벤트가 발생했을 때, 프로세스를 중지시키는 catch라는 명령어가 있음
- 이 명령어로 arch_prctl에 catchpoint를 설정하고 실습에 사용했던 canary를 실행
- catchpoint에 도달했을 때, rdi의 값이 0x1002인데 이 값은 ARCH_SET_FS의 상숫값임
- rsi의 값이 0x7ffff7fec4c0이므로, 이 프로세스는 TLS를 0x7ffff7fec4c0에 저장할 것이며, fs는 이를 가리키게 될 것
- 카나리가 저장될 fs+0x28(0x7ffff7fec4c0+0x28)의 값을 보면, 아직 어떠한 값도 설정되지 않았음을 확인할 수 있음
2. 카나리 값 설정
- TLS의 주소를 알았으므로, gdb의 watch 명령어로 TLS+0x28에 값을 쓸 때 프로세스를 중단시킬 것임
- watch 명령어: 특정 주소에 지정된 값이 변경되면 프로세스를 중단시키는 명령어
- watchpoint를 설정하고 프로세스를 계속 진행시키면 security_init 함수에서 프로세스가 멈춤
- TLS+0x28의 값을 조회하면 0x7698b8b2fa36f100이 카나리로 설정된 것을 확인할 수 있음
- 실제 이 값이 main 함수에서 사용하는 카나리 값인지 확인하기 위해 main 함수에 중단점을 설정하고, 계속 실행
- ni 명령어를 통해 mov rax, QWORD PTR fs:0x28까지 실행하고 rax 값을 확인해보면 security_init에서 설정한 값과 같은 것을 확인할 수 있음
# 카나리 우회
1. 무차별 대입(Brute Force)
- x64 아키텍처에서는 8바이트의 카나리가 생성되며, x86 아키텍처에서는 4바이트의 카나리가 생성됨
- 각각의 카나리에는 NULL 바이트가 포함되어 있으므로, 실제로는 7바이트와 3바이트의 랜덤한 값이 포함됨
- 즉, 무차별 대입으로 x64 아키텍처의 카나리 값을 알아내려면 최대 256^7번, x86에서는 최대 256^3번의 연산이 필요
- 연산량이 많아서 x64 아키텍처의 카나리는 무차별 대입으로 알아내는 것 자체가 현실적으로 어려우며, x86 아키텍처는 구할 순 있지만, 실제 서버를 대상으로 많은 횟수의 무차별 대입을 시도하는 것은 어려움
2. TLS 접근
- 카나리는 TLS에 전역변수로 저장되며, 매 함수마다 이를 참조해서 사용함
- TLS의 주소는 매 실행마다 바뀌지만 만약 실행 중에 TLS 주소를 알 수 있고, 임의 주소에 대한 읽기 또는 쓰기가 가능하다면 TLS에 설정된 카나리 값을 읽거나, 이를 임의의 값으로 조작할 수 있음
- 그 뒤, 스택 버퍼 오버플로우를 수행할 때, 알아낸 카나리 값 또는 조작한 카나리 값으로 스택 카나리를 덮으면 함수의 에필로그에 있는 카나리 검사를 우회할 수 있음
3. 스택 카나리 릭
- 스택 카나리를 읽을 수 있는 취약점이 있다면, 이를 이용하여 카나리 검사를 우회할 수 있음
- bypass_canary.c
// 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;
}
- 첫 번째 입력에서 오버플로우를 발생시켜 카나리 값을 얻어냄
- 오버플로우로 인하여 카나리 값이 릭 되고, 얻어낸 카나리 값을 통하여 스택 보호 기법을 우회할 수 있음
# 마치며
- 카나리: 함수 시작 시 스택 버퍼와 return address 사이에 랜덤 값을 삽입한 후 함수 종료 시 해당 랜덤 값의 변조 여부를 확인하여 메모리 오염 여부를 확인하는 보호 기법임
- 카나리 생성: security_init 함수에서 TLS에 랜던 값으로 카나리를 설정하면, 매 함수에서 이를 참조하여 사용함
- 카나리 우회 기법
- 무차별 대입 공격: 무차별 대입으로 카나리 값을 구하는 방법. 현실적으로 불가능에 가까움
- TLS 접근: 카나리는 TLS에 전역 변수로 저장되므로, 이 값을 읽거나 조작할 수 있으면 카나리를 우회할 수 있음
- 스택 카나리 릭: 함수의 프롤로그에서 스택에 카나리 값을 저장하므로, 이를 읽어낼 수 있으면 카나리를 우회할 수 있음. 가장 현실적인 카나리 우회 기법
# QUIZ
'Hacking Tech > System Hacking' 카테고리의 다른 글
[Dreamhack] Background: Library - Static Link vs. Dynamic Link (0) | 2022.09.07 |
---|---|
[Dreamhack] Mitigation:NX & ASLR (0) | 2022.09.05 |
[Dreamhack] Memory Corruption: Stack Buffer Overflow (0) | 2022.09.01 |
[Dreamhack] Background: Calling Convention (0) | 2022.08.31 |
[Dreamhack] Exploit Tech: Shellcode (0) | 2022.04.03 |