Iriton's log

Mitigation: Stack Canary 본문

Pwnable/Study

Mitigation: Stack Canary

Iriton 2024. 5. 7. 20:03

*본 포스트는 Dreamhack - Systemhacking Lecture 을 참고하여 작성되었습니다.

 

Stack Canary


함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에 임의의 값을 삽입하고, 함수의 에필로그에서 해당 값의 변조를 확인하는 보호 기법이다.

카나리 값의 변조가 확인되면 프로세스는 강제 종료된다.

스택 버퍼 오버플로우로 반환 주소를 덮으려면 반드시 카나리를 먼저 덮어야 하므로 카나리 값을 모르는 공격자는 반환 주소를 덮을 때 카나리 값을 변조하게 된다.

이 경우, 에필로그에서 변조가 확인되어 공격자는 실행 흐름을 획득하지 못합니다.

출처: Dreamhack

 

Canary의 작동 원리


정적 분석

스택 버퍼 오버플로우가 발생하는 예제 코드

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

Canary 비활성화

컴파일 옵션으로 -fno-stack-protector 옵션을 추가해야 카나리 없이 컴파일할 수 있다.

gcc -o no_canary canary.c -fno-stack-protector

 

Canary 활성화

카나리를 적용하여 다시 컴파일하고, 긴 입력을 주면 Segmentation fault가 아니라 stack smashing detected와 Aborted라는 에러가 발생

 

 

왼: no canary // 오: canary

no_canary의 디스어셈블과 다른 것을 볼 수 있다.

동적 분석으로 이 코드들이 리턴 주소를 보호하는지 살펴 보자.

 

동적 분석

Canary 저장

canary 프로그램의 main + 12에 bp를 걸어두고 ni로 한줄을 실행한 후 print /a $rax(rax 레지스터 값을 16진수로 표현) 하면 다음과 같은 값이 랜덤으로 저장된 걸 볼 수 있다.

fs는 세그먼트 레지스터의 일종으로 리눅스는 프로세스가 시작될 때 fs:0x28에 랜덤 값을 저장한다. 따라서 rax 값에 랜덤 값이 저장된 것이다.

 

그 랜덤값은 main + 21에서 rbp-0x8에 저장된다.

(x/gx : 주소를 가리키는 메모리 위치에 저장된 값을 출력합니다. 여기서 /gx는 해당 값을 16진수로 출력하도록 지시)

 

Canary 검사

+54에서 rbp-0x8 값을 rdx로 넘기고 rdx를 fs:0x28과 sub 연산을 한다.

이때 연산 결과가 0이라면(두 값이 같다면) +74로 이동하고 아니면 그 다음 코드인 +69의 stack_chk_fail 함수가 실행된다.(프로그램이 강제 종료된다.)

이로써 초기에 fs로 생성한 Canary 값과 스택에 있는 Canary 값이 다르다면 변조된 것으로 간주되어 프로그램이 종료되는 것이다.

 

Canary 생성 과정


프로세스가 시작될 때 TLS에 전역 변수로 저장되고 각 함수마다 프롤로그와 에필로그에서 이 값을 참조한다.

TLS 주소 파악

*fs 세그먼트 레지스터는 TLS(Thread Local Storage: 스레드마다 고유한 데이터 저장)를 가리키므로 fs 값을 알면 TLS의 주소를 알 수 있다.

*스레드마다 다른 메모리 영역을 가리키기 위해 fs 세그먼트 레지스터가 쓰이는 것.

 

fs 값은 특정 시스템 콜을 사용해야만 조회하거나 설정할 수 있다. gdb에서 다른 레지스터 값을 출력하듯 info register fs나 print $fs 방식으로는 값을 알 수 없다.

 

그래서 fs 값을 설정할 때 호출되는 arch_prctl(int code, unsigned long addr) 시스템 콜에 bp를 설정하여 fs가 어떤 값으로 설정되는지 조사할 것이다.

 

이 시스템 콜을 arch_prctl(ARCH_SET_FS, addr)의 형태로 호출하면 fs의 값은 addr로 설정된다.

 

gdb는 특정 이벤트가 발생했을 때, 프로세스를 중지시키는 catch 명령어가 있다. 이 명령어로 arch_prctl에 catchpoint를 설정하고 실습에 사용했던 canary를 실행해 보자.

 

 

init_tls() 안에서 catchpoint에 도달할 때까지 continue 명령어를 실행하다가 도달했을 때 rdi와 rsi 값을 확인한다.

 

rdi에 저장된 0x1002는 ARCH_SET_FS의 상숫값이다.

rsi의 값은 0x7ffff7fa8740이므로 프로세스 TLS를 해당 주소에 저장할 것이며 fs는 이를 가리키게 될 것이다.

 

Canary가 저장될 fs+0x28 즉, 0x7ffff7fa8740 + 0x28에 아직 아무 값도 입력되지 않은 걸 볼 수 있다.

 

TLS 주소를 알았으니 해당 주소에 값이 변경되면 프로세스를 중단시키는 명령어 watch를 활용해 보자.

 

이후 continue를 해주고 다시 주소의 데이터를 확인해 보면 Canary가 설정된 걸 확인할 수 있다.

 

 

실제로 이 값이 main 함수에 사용되는 canary 값인지 확인하기 위해 main 함수에 중단점을 설정하고 계속 실행해 보다가, mov rax fs+0x28 명령어를 실행하고 i r $rax로 rax 레지스터 값을 확인하는 명령어를 실행해 보면 canary값을 확인할 수 있다.

 

Canary 우회무차별

대입 (Brute Force)


무차별 대입 (Brute Force)

x64 아키텍처에서는 8바이트의 카나리가 생성되며, x86 아키텍처에서는 4바이트의 canary가 생성된다. 각각의 canary에는 NULL 바이트가 포함되어 있으므로, 실제로는 7바이트와 3바이트의 랜덤한 값이 포함된다.

즉, 무차별 대입으로 x64 아키텍처의 canary 값을 알아내려면 최대 256^7번, x86 에서는 최대 256^3 번의 연산이 필요하다. 연산량이 많아서 x64 아키텍처의 canary는 무차별 대입으로 알아내는 것 자체가 현실적으로 어려우며, x86 아키텍처는 구할 순 있지만, 실제 서버를 대상으로 저정도 횟수의 무차별 대입을 시도하는 것은 불가능하다.

TLS 접근

Canary는 TLS에 전역변수로 저장되며, 매 함수마다 이를 참조해서 사용한다. TLS의 주소는 매 실행마다 바뀌지만 만약 실행 중에 TLS의 주소를 알 수 있고, 임의 주소에 대한 읽기 또는 쓰기가 가능하다면 TLS에 설정된 canary 값을 읽거나, 이를 임의의 값으로 조작할 수 있다.

그 뒤, 스택 버퍼 오버플로우를 수행할 때 알아낸 canary 값 또는 조작한 canary 값으로 스택 canary를 덮으면 함수의 에필로그에 있는 canary 검사를 우회할 수 있습니다.

 

'Pwnable > Study' 카테고리의 다른 글

Mitigation: NX & ASLR  (1) 2024.05.22
Exploit Tech: Return to Shellcode  (0) 2024.05.15
Exploit Tech: Return Address Overwrite  (0) 2024.04.09
Memory Corruption: Stack Buffer Overflow  (0) 2024.04.01
Background: Calling Convention  (0) 2024.04.01
Comments