IT/시스템 보안

[Stack Canary] Stack Buffer Overflow로부터 Return address를 보호하기

kykyky 2024. 3. 5. 14:30

목적

 

스택 버퍼 오버플로우 공격으로부터 return address를 보호하기

 


 

작동 원리 

 

출처: https://dokhakdubini.tistory.com/299

 


함수의 프롤로그에서: 스택 버퍼와 반환 주소 사이에 임의의 값 (= 즉 Canary)을 삽입

그 후, 함수의 에필로그에서: 해당 값의 변조를 확인

                 만약 Canary 값의 변조가 확인되면: 프로세스가 강제 종료됨

 

 

공격자가 스택 버퍼 오버플로우를 통해 return address를 overwrite하는 경우,

만약 return address 이전에 canary가 마련돼 있다면, return address overwrite 이전에 필연적으로 canary가 먼저 overwrite 되겠지요.

이때, 공격자는 일반적으로 Canary 값을 모르므로 (공격자가 이 값을 알아내는 특별한 경우도 있는데 다른 글에서 다루겠습니다), 공격 시에 필연적으로 canary 부위의 값이 원래의 canary 값과 달라지게 됩니다 (그리고 이 불일치는 함수의 에필로그에서 감지됩니다.) 

바로 이 Canary 변조 여부를 통해 공격자의 return address overwrite가 캐치되고, 보호를 위해 프로세스가 강제 종료됩니다. 

 

 


 

작동 원리 - 좀더 자세히...

 

 

동일한 예시 C코드를 Canary 유/무 버전으로 각각 컴파일하여, 어셈블리 코드 및 작동을 비교해 보겠습니다.

 

 

예시 C코드

#include <unistd.h>
int main() {
  char buf[8];
  read(0, buf, 32);
  return 0;
}

 

 

어셈블리 코드 비교

 

검정: 공통

빨강: Canary가 있는 경우에만 있는 코드

초록: Canary가 없는 경우에만 있는 코드

push rbp
mov rbp,rsp
sub rsp,0x10
mov rax,QWORD PTR fs:0x28 ; fs:0x28의 데이터를 읽어서 (= 리눅스가 생성한 랜덤값) rax에 저장
mov QWORD PTR [rbp-0x8],rax ; 위 랜덤값이 rbp-0x8에 저장됨
xor eax,eax
lea rax,[rbp-0x10]
lea rax,[rbp-0x8]
mov edx,0x20
mov rsi,rax
mov edi,0x0
call read@plt
mov eax,0x0
mov rcx,QWORD PTR [rbp-0x8] ; rbp-8에 저장한 카나리를 rcx로 옮김
xor rcx,QWORD PTR fs:0x28 ; rcx를 fs:0x28에 저장된 카나리와 xor함 
je 0x6f0 <main+70> ; xor 연산 결과가 0이면 je의 조건을 만족하므로 main함수는 정상적으로 반환됨
call __stack_chk_fail@plt ; xor 연산 결과가 0이 아니면 __stack_chk_fail이 호출되면서 프로그램이 강제로 종료됨
leave
ret

 

※ fs: 세그먼트 레지스터의 일종, Thread Local Storage(TLS)를 가리키는 포인터

    - 리눅스는 프로세스가 시작될 때 fs:0x28에 랜덤 값을 저장함

 

 

실제 작동 비교

 

입력값을 크게 줌으로써 buffer overflow를 발생시켰습니다.  

canary가 없는 경우 Segmenatation fault가 발생하지만, canary가 있는 경우에는 프로세스가 강제 종료됩니다.  

출처: dreamhack

 


 

생성 원리: 작동 이전에, fs:0x28은 어떻게 생성되는가?

 

1. fs 값 파악

 

fs의 조회는 gdb에서 info register fs, print $fs 등으로는 불가능하고, 대신 fs의 값을 설정할 때 호출되는 arch_prctl 시스템 콜을 활용해야만 가능합니다.

해당 시스템 콜에 catch를 설정하고 fs의 설정을 조사하겠습니다.

 

이 시스템 콜을 호출할 때의 RDI는 0x1002이어야 하며,

이때의 RSI값은 fs의 값인데, 제 경우에는 0x7ffff7d89740이군요.

 

 

다만, fs + 0x28 위치에 아직은 어떤 값도 쓰이지 않았습니다.

 

 

2. fs가 가리키는 위치에 값이 쓰이는 것 확인

 

gdb의 watch 명령어로써 (watch *(0x7ffff7d89740+0x28)), fs + 0x28에 값이 쓰여질 때 프로세스를 중단시키겠습니다.

그 이후 continue를 통해 security_init 함수에서 프로세스를 멈춥니다.

이때 다시 fs + 0x28이 가리키는 메모리를 확인해보면, 이제는 어떤 값이 새롭게 쓰여있음을 확인할 수 있습니다.

 

 

3. 위 값이 실제로 main 함수에서 canary값인 것 확인

 

b *main을 통해 main 함수에 중단점을 설정한 뒤,

mov rax,QWORD PTR fs:0x28까지 실행하고 나서 rax 값을 확인해보면, 

security_init 함수에서 설정되었던 값인 0xb9fbc9e7f6880400과 동일함을 확인할 수 있습니다.