RAO에서 스택의 반환 주소를 조작하여 실행 흐름을 획득하는 공격 기법을 배웠다. 스택 버퍼 오버플로우를 이용한 공격은 매우 강력하면서, 역사가 오래되었기 때문에 관련된 보호 기법도 등장했다. Canary는 스택 버퍼 오버플로우로부터 반환주소를 보호하는 기법이다.
스택 카나리는 함수의 프롤로그에서 스탯 버퍼와 반환주소사이에 임의의 값을 삽입하고, 함수의 에필로그에서 해당값의 변조를 확인하는 보호기법이다. 카나리값의 변조가 확인되면 프로세스가 강제로 종료된다.
스택 버퍼 오버플로우로 반환수로를 덮으려면 반드시 카나리를 먼저 덮어야하므로 카나리값을 모르는 공격자는 반환 주소를 덮을때 카나리 값을 변조하게 된다. 이경우 에필로그에서 변조가 확인되어 공격자는 실행 흐름을 획득하지 못한다.
우분투 리눅스에서 gcc는 기본적으로 스택 카나리를 적용하여 컴파일 한다. 컴파일 옵션으로 -fno-stack-protector 옵션을 추가하면 카나리 없이 컴파일할 수 있다. 카나리를 적용해서 컴파일 한것과 안한것을 비교해보면 카나리를 적용해서 컴파일한것에 추가된 코드가 있는데 코드를 보자
추가된 코드
mov rax, QWORD PTR fs:0x28 mov QWORD PTR [rbp-0x8], rax xor eax, eax
mov rcx, QWORD PTR[rbp-0x8] xor rcx, QWORD PTR fs:0x28 je 0x6f0 <특정 주소> call 0x570 <__stack_chk_fail@plt>
맨처음 rax에 fs:0x28의 데이터를 읽어서 저장한다.
리눅스는 프로세스가 시작되 때 fs:0x28에 랜덤값을 저장한다. 따라서 rax에 리눅스가 생성한 랜던값이 저장된다.
rbp-0x8에도 랜덤값이 저장된다.
이 랜덤값이 카나리다. 두번째 문단에서 rbp-8에 저장된 카나리를 rcx에 옮긴다. 그 뒤, rcx를 fs:0x28에 저장된 카나리와 xor을 한다. 두값이 동일하면 연산결과가 0이 되므로 je의 조건을 만족하게되고 main함수가 정상적으로 반환된다. 그러나 두 값이 동일 하지 않다면 __stack_chk_fail이 호출 되면서 강제로 종료 된다.
fs 세그먼트
fs는 세그먼트 레지스터의 일종으로 목적이 정해있지않아 운영체제가 임의로 사용할수있는 레지스터이다. 리눅스는 fs를 TLS(Thread Local Storages)를 가리키는 포인터로 사용한다. 간단히 말하면 TLS에 카나리를 비롯하여 프로세스 실행에 필요한 여러 데이터가 저장된다.
카나리 값은 프로세스가 시작될 때, TLS에 전역 변수로 저장된거, 각 함수마다 프롤로그와 에필로그에서 이 값을 참조한다.
fs는 TLS를 가리키므로 fs의 값을 알면 TLS의 주소를 알수있다. 그러나 fs는 특정 시스템 콜을 사용해야만 조회하거나 설정할 수 있다. gdb에서 다른 레지스터의 값을 출력하듯 info regester fs나 print $fs와 같은 방식으로는 값을 알 수 없다.
그래서 여기서는 fs의 값을 설정할 때 호출되는 arch_prctl(int code, unsigned long addr)시스템 콜에 중단점을 설정하여 fs가 어떤 값으로 설정되는지 조사하겠습니다. 이 시스템 콜을 arch_prctl(ARCH_SET_FS, addr)의 형태로 호출하면 fs의 값은 addr로 설정된다.
gdb에는 특정 이벤트가 발생했을 때, 프로세스를 중지시키는 catch라는 명령어가 있다. 이 명령어로 arch_prctl에 catch point를 설정하고 보면 catchpoint에 도달 했을때 rdi의 값은 arch_set_fs의 상수값이고 rsi의 값은 TLS를 저장할것이고, fs는 이값을 가리키게 될것이다.
TLS의 주소를 알게 됬으므로 gdb의 watch 명령어로 TLS+0ㅌ28에 값을 쓸때 프로세스를 중단 시켜 TLS+0x28에 카나리가 설정된다.