일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 자료구조 복습
- c
- 숙명여자대학교 정보보안동아리
- 생활코딩
- lob
- WarGame
- 숙명여자대학교 정보보안 동아리
- CSS
- Sookmyung Information Security Study
- Javascript
- 기계학습
- 파이썬
- XSS Game
- siss
- 드림핵
- BOJ
- hackctf
- PHP 웹페이지 만들기
- BOJ Python
- 머신러닝
- 백준
- HTML
- Python
- 풀이
- 웹페이지 만들기
- SWEA
- The Loard of BOF
- C언어
- c++
- hackerrank
- Today
- Total
혜랑's STORY
[System Hacking STAGE 5] Mitigation: Stack Canary 본문
[System Hacking STAGE 5] Mitigation: Stack Canary
hyerang0125 2022. 1. 27. 15:28스택 카나리(Stack Canary)
; 스택 버퍼 오버플로우로부터 반환 주소를 보호
스택 카나리는 함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에 임의의 값을 삽입하고, 함수의 에필로그에서 해당 값의 변조를 확인하는 보호기법이다. 카나리 값의 변조가 확인되면 프로세스는 강제로 종료된다.
스택 버퍼 오버플로우로 반환 주소를 덮으려면 반드시 카나리를 먼저 덮어야 하므로 카나리 값을 모르는 공격자는 반환 주소를 덮을 떄 카나리 값을 변조하게 된다. 이 경우, 에필로그에서 변조가 확인되어 공격자는 실행 흐름을 획득하지 못한다.
카나리의 작동 원리
카나리 정적 분석
// Name: canary.c
#include <unistd.h>
int main() {
char buf[8];
read(0, buf, 32);
return 0;
}
위 예제 코드에는 스택 버퍼 오버플로우 취약점이 존재한다. 카나리를 활성화하여 컴파일한 바이너리와 비활성화하여 컴파일한 바이너리를 비교하여 스택 카나리의 원리를 살펴보자.
카나리 비활성화
Ubuntu 18.04의 gcc는 기본적으로 스택 카나리를 적용하여 바이너리를 컴파일한다. 컴파일 옵션으로 -fno-stack-protector 옵션을 추가해야 카나리 없이 컴파일 할 수 있다.
컴파일 옵션을 준 바이너리 파일을 실행하고 긴 입력을 주면, 예상했던 대로 반환 주소가 덮여서 Segmentation fault가 발생한다.
카나리 활성화
카나리를 적용하여 다시 컴파일하고 긴 입력을 주면
Segmentation fault가 아니라 stack smashing detected와 Aborted라는 에러가 발생한다. 이는 스택 버퍼 오버플로우가 탐지되어 프로세스가 강제 종료되었음을 의미한다.
no_canary disassemble
canary disassemble
no_canary와 디스어셈블 결과를 비교하면main 함수의 프롤로그와 에필로그에 각각 다음의 코드들이 추가되어 있다.
mov rax,QWORD PTR fs:0x28
mov QWORD PTR [rbp-0x8],rax
xor eax,eax
lea rax,[rbp-0x10]
...
mov rcx,QWORD PTR [rbp-0x8]
xor rcx,QWORD PTR fs:0x28
je 0x6f0 <main+70>
call __stack_chk_fail@pl
카나리 동적 분석
카나리 저장
추가된 프롤로그의 코드(main+8)에 중단점을 설정하고 바이너리를 실행시킨다.
main+8은 fs:0x28의 데이터를 읽어서 rax에 저장한다. fs는 세그먼트 레지스터의 일종으로, 리눅스는 프로세스가 시작될 때 fs:0x28에 랜덤 값을 저장한다. 따라서 main+8의 결과로 rax에는 리눅스가 생성한 랜덤 값이 저장된다.
코드를 한 줄 실행하면 rax에 다음과 같이 첫 바이트가 널 바이트인 8바이트 데이터가 저장되어 있다. 생성한 랜덤 값은 main+17에서 rbp-0x8에 저장된다.
카나리 검사
이제 추가된 에필로그의 코드에 중단점을 설정하고 바이너리를 계속 실행시킨다. main+50은 rbp-0x8에 저장한 카나리를 rcx로 옮긴다. 그 뒤, main+54에서 rcx를 fs:0x28에 저장된 카나리와 xor 한다. 두 값이 동일하면 연산 결과가 0이 되면서 je의 조건을 만족하게 되고, main 함수는 정상적으로 잔환된다. 그러나 두 값이 동일하지 않으면 __stack_chk_fail이 호출되면서 프로그램이 강제로 종료된다.
16개의 H를 입력으로 카나리를 변조하고, 실행 흐름이 어떻게 되는지 살펴보자.
코드를 한 줄 실행시키면
rbp-0x8에 저장된 카나리 값이 버퍼 오버플로우로 인해 "0x4848484848484848"이 된 것을 확인할 수 있다.
main+54의 연산 결과가 0이 아니므로 main+63에서 main+70으로 분기하지 않고 main+65의 __stack_chk_fail을 실행하게 된다.
이 함수가 실행되면 다음의 메시지가 출력되며 프로세스가 강제로 종료된다.
카나리 생성 과정
카나리 값은 프로세스가 시작될 때, TLS에 전역 변수로 저장되고 각 함수마다 프롤로그와 에필로그에서 이 값을 참조한다. TLS에 카나리 값이 저장되는 과정을 자세히 분석해 보자.
TLS의 주소 파악
fs는 TLS를 가리키므로 fs의 값을 알면 TLS의 주소를 알 수 있다. 그러나 리눅스에서 fs의 값은 특정 시스템 콜을 사용해야만 조회하거나 설정할 수 있다. gdb에서 다른 레지스터의 값을 출력하듯 info register fs나, print $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의 값이 0x7ffff7fe24c0이므로, 이 프로세스는 TLS를 0x7ffff7fe24c0에 저장할 것이며, fs는 이를 가리키게 될 것이다.
카나리가 저장될 fs+0x28(0x7ffff7fe24c0 + 0x28)의 값을 보면, 아직 어떠한 값도 설정되어 있지 않은 것을 확인할 수 있다.
카나리 값 설정
TLS의 주소를 알았으므로, gdb의 watch 명령어로 TLS+0x28에 값을 쓸 때 프로세스를 중단시킨다. watch는 특정 주소에 저장된 값이 변경되면 프로세스를 중단시키는 명령이다.
watchpoint를 설정하고 프로세스를 계속 진행시키면 security_init 함수에서 프로세스가 멈춘다.
여기서 TLS+0x28의 값을 조회하면 0xb67d3194b56b1800 이 카나리로 설정된 것을 확인할 수 있다.
실제로 이 값이 main 함수에서 사용하는 카나리 값인지 확인하기 위해 main 함수에 중단점을 설정하고 계속 실행시켜보자.
mov rax, QWORD PTR fs:0x28를 실행하고 rax 값을 확인해보면 security_init에서 설정한 값과 같은 것을 확인할 수 있다.
카나리 우회
카나리를 우회하는 방법으로는 다음이 알려져 있다.
무차별 대입 (Brute Force)
x64아키텍처에서는 8바이트의 카나리가 생성되며, x86아키텍처에서는 4바이트의 카나리가 생성된다. 각각의 카나리에는 NULL 바이트가 포함되어 있으므로, 실제로는 7바이트와 3바이트의 랜덤한 값이 포함된다.
즉, 무차별 대입으로 x64아키텍처의 카나리 값을 알아내려면 최대 256^7번, x86에서는 최대 256^3번의 연산이 필요하다. 연산량이 많아서 x64아키텍처의 카나리는 무차별 대입으로 알아내는 것은 현실적으로 어려우며, x86아키텍처는 구할 순 있지만, 실제 서버를 대상으로 저정도 횟수의 무차별 대입을 시도하는 것은 불가능하다.
TLS 접근
카나리는 TLS에 전역변수로 저장되며, 매 함수마다 이를 참조해서 사용한다. TLS의 주소는 매 실행마다 바뀌지만 만약 실행중에 TLS의 주소를 알 수 있고, 임의 주소에 대한 읽기 또는 쓰기가 가능하다면 TLS에 설정된 카나리 값을 읽거나, 이를 임의의 값으로 조작할 수 있다.
그 뒤, 스택 버퍼 오버플로우를 수행할 때 알아낸 카나리 값 또는 조작한 카나리 값으로 스택 카나리를 덮으면 함수의 메필로그에 있는 카나리 검사를 우회할 수 있다.
스택 카나리 릭
스택 카나리를 읽을 수 있는 취약점이 있다면, 이를 이용하여 카나리 검사를 우회할 수 있다.
카나리 우회; bypass_canary.c
// Name: bypass_canary.c
// Compile: gcc -o bypass_canary bypass_canary.c
#include <stdio.h>
#include <unistd.h>
int main() {
char memo[8];
char name[8];
printf("name : ");
read(0, name, 64);
printf("hello %s\n", name);
printf("memo : ");
read(0, memo, 64);
printf("memo %s\n", memo);
return 0;
}
첫 번째 입력에서 오버플로우를 발생시켜 카나리 값을 얻어낸다. 오버플로우로 인하여 카나리 값이 릭 되고, 얻어낸 카나리 값을 이용하여 검사를 우회할 수 있다.
'무지성 공부방 > Dreamhack SystemHacking' 카테고리의 다른 글
[System Hacking STAGE 5] ssp_000 (0) | 2022.01.30 |
---|---|
[System Hacking STAGE 5] Exploit Tech: Return to Shellcode (0) | 2022.01.27 |
[System Hacking STAGE 4] basic_exploitation_001 (0) | 2022.01.27 |
[System Hacking STAGE 4] basic_exploitation_000 (0) | 2022.01.27 |
[System Hacking STAGE 4] Return Address Overwrite (0) | 2022.01.26 |