이 글은 https://dreamhack.io/lecture/courses/112 토대로 작성한 글입니다.
지난 시간 Stack Buffer Overflow에서 BOF를 통해 Return address를 조작하는 문제들을 풀어 보았다.
이번 시간에는 BOF를 일으켜도 Return address를 보호하는 Mitigation인 Stack Canary를 배워보자.
Canary는 Buffer와 SFP 사이에 Canary 값을 삽입하고, 만약, Canary값이 BOF를 통해서 변조가 되었으면, 프로세스를 강제로 종료한다.
Return address의 값을 변조해도 Canary로 인해 보호된다.
Stack Buffer Canary SFP RET
// Name: canary.c
#include <unistd.h>
int main() {
char buf[8];
read(0, buf, 32);
return 0;
}
Canary 비활성화
Compile
비활성화를 하려면 -fno-stack-protector를 gcc 옵션에 추가하면 된다.
BOF 발생 후 결과
Return Address가 뒤덮여 Segmentation fault가 일어났다.
Canary 활성화
Compile
Ubuntu 18.04의 gcc 이후로 기본적으로 Canary를 적용하여 컴파일 한다.
BOF 발생 후 결과
stack smashing detected과 Aborted 에러가 발생했다.
이는 Canary값이 변조되어, 스택 버퍼오버플로우가 탐지되어 프로세스가 강제 종료되었음을 알린다.
Canary 비활성화
Canary 활성화
추가된 코드
mov rax, QWORD PTR fs:0x28
mov QWORD PTR [rbp-0x8], rax
.
.
mov rcx, QWORD PTR [rbp-0x8]
xor rcx, QWORD PTR fs:0x28
je 0x11b3 <main+74>
call 0x1060 <__stack_chk_fail@plt >위의 코드들이 추가되었다.
코드 설명
fs:0x28을 통해 canary 값을 [rbp-0x8]에 쌓고,
xor로 기존 Canary 값인 fs:0x28과 [rbp-0x8]에 있는 Canary 값을 비교해서
같으면 종료하지 않고, 값이 다르면 <__stack_chk_fail@plt > 를 호출해서 프로세스를 종료한다.
즉, BOF로 인해 Canary 값이 변조가 되었다면 프로세스를 강제 종료시킨다.
fs란?
Background: Computer Architecture에서 CPU에는 다양한 세그먼트 레지스터가 있다고 언급했다.
code segement(cs), data segment(ds), extra segment(es)가 있고, 이들은 각자 목적이 있는 세그먼트들이다.
fs와 gs 세그먼트들은 목적이 정해지지 않아 운영체제가 임의로 사용할 수 있는 레지스터들이다.
fs는 Thread Local Storage(TLS)를 가리키는 포인터로 사용한다.
TLS는 Canary를 비롯해 프로세스 실행에 필요한 여러 데이터들이 저장되어 있다.
Canary 값은 프로세스가 시작될 때, TLS에 전역 변수로 저장되고, 각 함수마다 프롤로그와 에필로그에서 이 값을 참조한다.
fs는 TLS를 가리키므로 fs의 값을 알면 TLS의 주소를 파악할 수 있다.
하지만 리눅스에서 fs의 값은 특정 syscall을 사용해야만 조회하거나 설정이 가능하다.
gdb에서 print $fs, info register fs로 확인이 불가능하다.
fs의 값을 설정할 때 호출되는 'arch_prctl(int code, unsigned long addr)' syscall에 중단점을 설정하여 fs가 어떤 값으로 설정되는지 알아보자.
이 때 'arch_prctl(ARCH_SET_FS, addr)'에서 fs의 값은 addr로 설정된다.
주소 파악
catch로 arch_prctl에 catchpoint를 설정해준다.
그리고 init_tls 함수에 도달한 모습을 확인 할 수 있다.
이 때, 'arch_prctl(ARCH_SET_FS, addr)'에서 인자로 전달되는 ARCH_SET_FS 값 즉, rdi값과, addr값, 즉, rsi값을 확인해 보자.
rdi값은, 즉, ARCH_SEC_FS값은 0x1002임을 확인 할 수 있고,
rsi값은, 즉, addr값은 0x7ffff7fb0540임을 확인 할 수 있다. 이는 TLS를 '0x7ffff7fb0540' 이 주소에 저장한다는 뜻이고,
fs는 0x7ffff7fb0540을 가리키게 될 것이다.
Canary 값이 저장될 fs+0x28에는 아직 어떤값도 저장되지 않은 모습을 확인 할 수 있다.
TLS의 주소를 파악을 했으므로, watch 명령어로 TLS+0x28에 값을 쓸 때 프로세스를 중단시킨 후 TLS+0x28에 어떤 값이 할당되는지 확인해 보자.
break가 걸린 후, TLS+0x28, 즉 fs+0x28에 값이 생성되었음을 확인 할 수 있다.
이 값이 Canary 값이다.
이 값이 Canary 값인지 다시 확인해보기 위해 main에 break를 걸고, 확인해 보자.
위에서 확인한 Canary 값과 동일하다.
Canary를 우회하는 방법을 알아보자.
x64 아키텍처에서 8bytes 중에서 7bytes는 랜덤 값, x32 아키텍처에서 4bytes 중에서 3bytes는 랜덤 값이 생성된다.(1bytes는 NULL값이 들어감)
x64에서 Canary 값을 알아내려면, 최대 256^7번, x32에서 Canary 값을 알아내려면 최대 256^3번의 연산이 필요하다.
이는 서버를 대상으로 할 때, 이 정도의 Brute Forcing은 불가능하다.
위의 설명 보면 Canary 값은 TLS에 전연변수로 저장하고, 매 함수마다 이를 참조해서 사용한다.
TLS의 주소는 매 실행마다 달라지지만, 실행 중에 이 TLS의 주소를 파악할 수 있거나, 임의 주소에 대한 읽기 또는 쓰기가 가능하다면,
TLS에 설정된 Canary 값을 읽거나, 이를 임의로 조작할 수 있다.
읽은 값으로는 함수의 에필로그에 있는 Canary 값과 똑같은 값을 넣게 되면 우회가 가능하다.
조작이 가능하다면, 함수의 에필로그에 있는 조작한 Canary 값과 똑같은 값을 넣게 되면 우회가 가능하다.
Canary를 읽을 수 있는 취약점이 있다면, Canary를 우회할 수 있다.
Canary는 buffer와 RET 사이에 랜덤 값을 삽입한 후 함수 종료 시 Canary 값이 변조 여부를 확인해, 메모리 오염 여부를 확인하는 보호 기법이다.
우회하는 방법으로는 Brute Forcing, TLS 접근, Canary Leak이 있다.
다음 시간엔 Canary Leak을 통해 Canary를 우회하고, exploit하는 문제를 풀어보자.