스택 카나리는 함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에 임의의 값을 삽입하고, 함수의 에필로그에서 해당 값의 변조를 확인하는 보호 기법이다. 카나리 값의 변조가 확인되면 프로세스는 강제로 종료된다.
첫바이트가 NULL바이트 8바이트값이 카나리 값이다.
+ x64 아키텍처에서는 8바이트의 카나리가 생성되며, x86 아키텍처에서는 4바이트의 카나리가 생성된다. 각각의 카나리에는 NULL 바이트가 포함되어 있으므로, 실제로는 7바이트와 3바이트의 랜덤한 값이 포함된다.
fs와 TLS
fs는 세그먼트 레지스터이다. 특정한 목적이 정해지지 않은 것으로 운영체제에서 임의로 사용할 수 있는 레지스터로, Thread Local Storage(TLS)를 가리키는 포인터로 사용된다.
TLS에 카나리를 비롯하여 프로세스 실행에 필요한 여러 데이터가 저장된다.
카나리 값은 프로세스가 시작될 때, TLS에 전역 변수로 저장되고, 각 함수마다 프롤로그와 에필로그에서 이 값을 참조한다.
TLS 주소파악
fs는 TLS를 가리키므로 fs의 값을 알면 TLS의 주소를 알 수 있다. 그러나 리눅스에서 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의 값이 0x7ffff7fdb4c0이므로, 이 프로세스는 TLS를 0x7ffff7fdb4c0에 저장할 것이며, fs는 이를 가리키게 될 것이다.
카나리가 저장될 fs+0x28(0x7ffff7fdb4c0+0x28)의 값을 보면, 아직 어떠한 값도 설정되어 있지 않은 것을 확인할 수 있다.
카나리 값 설정
TLS의 주소를 알았으므로, gdb의 watch 명령어로 TLS+0x28에 값을 쓸 때 프로세스를 중단시켜보자. watch는 특정 주소에 저장된 값이 변경되면 프로세스를 중단시키는 명령어이다.
watchpoint를 설정하고 프로세스를 계속 진행시키면 security_init함수에서 프로세스가 멈춘다.
여기서 TLS+0x28의 값을 조회하면 0x2f35207b8c368d00이 카나리로 설정된 것을 확인할 수 있다.
카나리 우회 기법
- 무차별 대입 공격(Brute Force Attack):
무차별 대입으로 카나리 값을 구하는 방법. 현실적으로 불가능에 가깝습니다. - TLS 접근:
카나리는 TLS에 전역 변수로 저장되므로, 이 값을 읽거나 조작할 수 있으면 카나리를 우회할 수 있습니다. - 스택 카나리 릭:
함수의 프롤로그에서 스택에 카나리 값을 저장하므로, 이를 읽어낼 수 있으면 카나리를 우회할 수 있습니다. 가장 현실적인 카나리 우회 기법입니다.
reference : https://dreamhack.io/lecture/courses/112