다음은 드림핵 기반으로 제가 이해한대로 정리한 내용입니다. (잘못된 내용이 있다면 댓글로 알려주세요..!)
스택 카나리(Stack Canary)란?
- stack buffer overflow로부터 retrun address를 보호하는 기법 = 스택 카나리 (stack canary)
- 방법
- 함수의 프롤로그에서 스택 버퍼와 return address 사이에 임의의 값 삽입
- 함수의 에필로그에서 삽입된 값이 변조되었는지 확인 → 카나리 값 변조가 확인되면 프로세스 강제 종료
- stack buffer overflow로 return address를 덮으려면 반드시 스택 카나리를 먼저 덮어야 함
- 카나리 값을 모르면 return address를 덮을 때 카나리 값이 바뀌게 됨
- 카나리 값이 바뀌면 프로세스가 강제 종료됨 → 실행 흐름 획득 실패
카나리 작동 원리
스택 버퍼 오버플로우 취약점이 존재하는 코드를 카나리 활성 컴파일, 카나리 비활성 컴파일을 하여 두 바이너리를 비교해보자.
// Name: canary.c
// canary 활성 컴파일 gcc -o canary canary.c
// canary 비활성 컴파일 gcc -o no_canary canary.c -fno-stack-protector
#include <unistd.h>
int main() {
char buf[8];
read(0, buf, 32);
return 0;
}
카나리 활성, 비활성 바이너리 실행 결과
컴파일 후 실행 결과 카나리를 비활성화한 바이너리인 no_canary의 경우 return address가 덮여서 Segmentation fault가 발생한다. 반면 카나리를 활성화한 바이너리인 canary의 경우 stack smashing detected와 aborted라는 에러가 발생한다. 이 오류는 스택 버퍼 오버플로우가 탐지되어 프로세스가 강제 종료되었다는 것을 의미한다.
$ ./no_canary
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
Segmentation fault
$ ./canary
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
*** stack smashing detected ***: <unknown> terminated Aborted
카나리 활성, 비활성 바이너리 디스어셈블 비교
디스어셈블을 비교해보면 카나리 활성 바이너리에는 함수 프롤로그와 에필로그에 추카된 코드들이 있다. 동적분석으로 이 코드들이 어떻게 작동하는지 확인해보자.
//프롤로그
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 0x11b3 <main+74>
call 0x1060 <__stack_chk_fail@plt>
카나리 동적분석
함수 프롤로그 - 카나리 값 설정
<main+12> mov rax, qword ptr fs:[0x28]
- fs:0x28의 데이터를 읽어 rax에 저장
- fs:[0x28]은 random을 의미한다. fs:[0x0]은 PID를 의미한다.
- 실행 결과 rax에 첫바이트가 널바이트인 8바이트의 데이터가 저장되어 있음
*** fs ?? ***
CPU의 세그먼트 레지스터의 한 종류로 용도가 정해져있지 않은 영역이다. 세그먼트 레지스터는 메모리를 조각내어 시작 주소, 범위, 접근 권한 등을 부여해 메모리를 보호하는 기법으로 페이징 기법과 함께 가상메모리를 실제 물리 메모리로 변경할 때 사용된다.
- CS : Code Segment, 명령어들 있는 영역
- DS : data segment, 변수들 저장하는 영역
- SS : stack segment, 함수들 있는 영역
- ES : extra segment, 프로그래머가 정해서 쓰는 영역
- FS,GS : 80386(32bit) CPU 때 생겨난 용처가 정해지지 않는 영역
mov QWORD PTR [rbp-0x8],rax
- 앞에서 생성된 random값(0xf701c3c4d98f1a00)이 rbp-0x8에 저장
함수 에필로그 - 카나리 값 확인
mov rcx,QWORD PTR [rbp-0x8]
- rbp-0x8의 값을 rcx에 저장
- 스택 버퍼 오버플로우에 의해 변형됐을 가능성이 있음
- 16개의 H를 입력하면 카나리의 값이 H로 덮히게 됨
xor rcx,QWORD PTR fs:0x28
- 초기의 카나리 값(fs:0x28)을 rcx에 저장된 값과 xor 계산 → 두 값이 같다면 0 → je 조건 만족해 main함수 정상 반환
- "HHHHHHHH"와 "0xf701c3c4d98f1a00"를 xor ( xor은 동일하면 0을 반환 → 두 값이 같은지 비교하는 것과 동일)
je 0x11b3 <main+74>
- xor의 결과가 0일 때 main+74로 이동 → main함수 제대로 반환됨
call 0x1060 <__stack_chk_fail@plt>
- xor의 결과가 0이 아니면 __stack_chk_fail이 호출되고 프로그램이 강제 종료됨
- 16개의 H가 입력되면
카나리 생성 과정
카나리 값은 프로세스가 시작될 때, TLS(Thread Local Storage)에 전역변수로 저장되고, 함수 프롤로그와 에필로그에서 이 값을 참조한다.
TLS에 카나리 값이 저장되는 과정을 분석하면 아래와 같다.
fs?? TLS??
----------------------------
x86-64 kernal에서는 FS 레지스터를 Thread 관리 용도로 사용한다. 64bit에서 FS가 가리키는 주소는 MSR_FS_BASE라고 하는 MSR(Model Specific Register)에 의해 관리한다. 커널은 유저 레벨에서 이 레지스터 값을 변경할 수 있도록 arch_prctl이라고 하는 시스템 콜을 제공한다. Context Switching이 일어날 때 MSR_FS_BASE 레지스터의 값이 갱신되어 각 Thread에 대한 TCB 구조체를 가리키게 된다.
TCB(Thread Control Block) 구조체는 64bit리눅스 환경에서 struct pthread로 선언되었다. FS가 가리키는 실체는 TCB 구조체이다.
----------------------------
하나의 Process 내 Thread들은 동일한 메모리 주소를 공유한다. 한 Process 내에서 동일한 메모리를 공유하기 때문에 Thread들은 Data 영역을 공유한다. 따라서, Process의 전역변수는 모든 Thread가 공유하게 된다. 하지만 Thread들도 각자 고유한 전역변수가 필요한 경우가 있다. 때문에 Thread 별로 Data영역처럼 고유의 영역을 제공하는데 이를 Thread Local Storage(=TLS)라고 한다. TLS의 시작 위치는 DTV에 저장되어 있고 , DTV의 위치는 TCB가 알고 있다. ( 이 때문에, FS가 TLS를 가리킨다고 할 수 있다. )
TLS의 주소 파악
FS는 TLS를 가리키므로 FS의 값을 알면 TLS의 주소를 알 수 있다. FS 값은 특정 시스템 콜을 사용해야만 조회 및 설정 가능하다. 따라서 FS이 값을 알기 위해서 FS 값을 설정할 때 호출되는 arch_prctl(ARCH_SET_FS, addr)
시스템 콜에 catchpoint를 설정해 FS가 어떤 값으로 설정되는지 확인한다.
arch_prctl(ARCH_SET_FS, addr)
- FS의 값을 addr로 설정하는 시스템 콜 함수
gdb - catchpoint catch syscall arch_prctl
- 특정 이벤트가 발생했을 때, 프로세스를 중지시키는 명령어
rdi의 값은 0x1002이다. 이 값은 ARCH_SET_FS의 상숫값이다.
rsi의 값은 0x7ffff7fb6540이다. 이 프로세스는 TLS를 0x7ffff7fb6540에 저장할 것이고, FS는 이곳을 가리키게 된다. 카나리가 저장될 FS+0x28을 보면 아직 어떤 값도 설정되지 않은 것을 볼 수 있다.
즉, FS가 TLS를 가리키고, TLS의 주소는 0x7ffff7fb6540이고, TLS+0x28 (FS+0x28)의 주소에 카나리 값이 저장될 것이다.
카나리 값 설정
카나리 값이 궁금한 것이므로 카나리 값이 저장되는 위치인 TLS+0x28(=0x7ffff7fb6540 + 0x28)위치에 값을 쓸 때 프로세스를 중단시키도록해준다. gdb의 watch 명령어를 이용해 해당 주소의 값이 변경되면 프로세스를 중단시키도록 한다.
watchpoint를 설정하고 프로세스를 진행하면 security_init 함수에서 프로세스가 멈춘다...고 하는데 dl_main에서 멈추었다.... (이유를 아시는 분 있으면 댓글달아주시면 감사하겠습니다..!)
대충 찾아보니 security_init함수 호출은 차례로 _dl_start → _dl_start_final → _dl_sysdep_start → dl_main → security_init의 흐름으로 진행된다고 한다.. 왜 security_init이 호출되지 않는진... 잘 모르겠다...
이제 실제로 이 값이 main에서 사용하는 카나리 값인지 확인한다.
mov rax, QWORD PTR fs:0x28
- fs:0x28의 값을 rax에 저장
mov rax, QWORD PTR fs:0x28을 실행하고 rax의 값을 확인하면 위에서 설정한 값인 0xcaa046ab2782000과 같은 것을 알 수 있다.
'CS > system' 카테고리의 다른 글
[System][Dreamhack] wargame - ssp_001 스택 순서 (0) | 2022.08.24 |
---|---|
[System][Dreamhack] bypass canary (0) | 2022.08.10 |
[System][Dreamhack] stack buffer overflow - 스택 버퍼 오버플로우 (0) | 2022.05.11 |
[System] 레이스 컨디션(Race Condition) (0) | 2022.05.09 |
[System] 함수 프롤로그 & 에필로그 (Prologue & Epilogue) (0) | 2022.05.08 |