<요약>
security_init함수에서 TLS에 랜덤 값으로 카나리를 설정- 카나리로 보호받는 함수가 이를 참조해 사용
- TLS의 주소 =
fs레지스터에 저장 - 카나리는 [TLS+0x28]에 있음
스택 카나리
- 스택 버퍼 오버플로우로부터 반환 주소를 보호하는 기법
- 함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에 랜덤한 값을 삽입
- 함수의 에필로그에서 해당 값의 변조를 확인해 메모리 오염 여부를 확인
- glibc를 사용하는 경우 카나리의 첫 바이트는 늘 널 바이트임
strcpy등의 함수로 스택을 복사하게 될 때 널 바이트를 통해서 카나리값의 유출을 막기 위함
- 카나리 값이 변조되면
__stack_chk_fail함수가 실행됨
카나리 생성 과정
security_init함수에서 TLS에 랜덤 값으로 카나리를 설정하면 카나리로 보호받는 함수에서 이를 참조해 사용함- TLS의 주소는
fs레지스터에 저장되며, 카나리는 TLS+0x28 위치에서 확인할 수 있음 - 프로세스가 시작될 때, TLS(Thread Local Storage)에 전역 변수로 저장되고, 각 함수마다 프롤로그와 에필로그에서 이 값을 참조함
TLS에 카나리 값이 저장되는 과정 분석
fs는 TLS를 가리키므로,fs를 알면 TLS의 주소를 알 수 있으나 리눅스에서fs값은 특정 시스템 콜을 사용해야만 조회/설정할 수 있음fs값을 설정할 때 호출되는 시스템 콜:arch_prctlinfo register fsorprint $fs커맨드로도 알 수 없음
arch_prctl시스템 콜에 중단점을 설정해서 분석- 시그니처:
long arch_prctl(int code, unsigned long addr);- 즉,
arch_prctl(ARCH_SET_FS, tls_base);- 인자 1:
fs레지스터를 설정하라는 명령어 - 인자 2:
fs가 가리킬 주소를 지정해주는 값
- 인자 1:
- 즉,
- 시그니처:
- gdb의 catch 명령어: 특정 이벤트가 발생했을 때 프로세스를 중지시킴
pwndbg> catch syscall arch_prctlinit_tls()안에서 catchpoint에 도달할 때까지 continuerdi값 =0x1002=ARCH_SET_FS의 상숫값rsi값 =0x7ffff7d87740= TLS의 저장 위치 &fs가 가리킬 곳
- 카나리 값 설정
- gdb의 watch 명령어: 특정 주소에 저장된 값이 변경되면 프로세스를 중단시킴
- TLS+0x28에 값을 쓸 때 프로세스를 중단시키기
pwndbg> watch *(0x7ffff7d87740+0x28)- continue하면
security_init함수에서 프로세스가 멈춤! - 이때 TLS+0x28의 값을 조회하면?
0x12edcfab5b46f400이 카나리로 설정된 것을 확인할 수 있음
security_init함수에서 TLS에 랜덤 값으로 카나리를 설정함!- 카나리로 보호받는 함수에서 이를 참조해서 사용
예시
// Name: canary.c
#include <unistd.h>
int main() {
char buf[8];
read(0, buf, 32);
return 0;
}
위 파일을 gcc -w -o canary canary.c로 컴파일하고, 버퍼 크기를 넘어가는 입력을 주면 stack smashing detected와 Aborted 에러가 발생한다.
이는 스택 버퍼 오버플로우가 탐지되어 프로세스가 강제 종료되었다는 의미이다.
(카나리 없이 -fno-stack-protector 옵션으로 컴파일할 시, 반환 주소가 덮여서 Segmentation fault가 발생함)
카나리 없이 컴파일한 바이너리와 카나리를 사용해 컴파일한 바이너리의 디스어셈블 결과를 비교하면, main 함수의 프롤로그와 에필로그에 코드들이 추가된 것을 확인할 수 있다.
카나리 동적 분석
프롤로그
main+12에 중단점을 설정하고 바이너리를 실행한다.
► 0x555555555175 <main+12> mov rax, qword ptr fs:[0x28] RAX, [0x7ffff7d87768] => 0x60eb0b7741c32000
main+12는fs:0x28의 데이터를 읽어서rax에 저장한다.fs는 OS가 임의로 사용할 수 있는 세그먼트 레지스터의 일종으로, 리눅스는fs를 TLS(Thread Local Storage)를 가리키는 포인터로 사용한다.- 리눅스는 프로세스가 시작될 때
fs:0x28에 랜덤 값을 저장한다.
➡️ 즉main+12의 결과로,rax에 리눅스가 생성한 랜덤 값이 저장된다.
코드를 한 줄 실행하면 rax에 첫 바이트가 널 바이트인 8바이트 데이터가 저장되어 있다.
► 0x55555555517e <main+21> mov qword ptr [rbp - 8], rax [0x7fffffffdda8] => 0x60eb0b7741c32000
- 생성한 랜덤값은
rbp-0x8에 저장된다.
에필로그
main+54에 중단점을 설정하고 계속 실행한다.
► 0x55555555519f <main+54> mov rdx, qword ptr [rbp - 8] RDX, [0x7fffffffdda8] => 0x4848484848484848 ('HHHHHHHH')
코드를 한 줄 실행시키면, rbp-0x8에 저장된 카나리 값이 오버플로우로 인해 변조된 것을 확인할 수 있다.
► 0x5555555551a3 <main+58> sub rdx, qword ptr fs:[0x28] RDX => 0x9e182f6fcc697748 (0x4848484848484848 - 0xaa3018d87bded100)
0x5555555551ac <main+67> je main+74 <main+74>
0x5555555551ae <main+69> call __stack_chk_fail@plt <__stack_chk_fail@plt>
main+58의 연산 결과가 0이 아니므로, main+67에서 main+74로 분기하지 않고, main+69의 __stack_chk_fail을 실행하게 된다.
이 함수가 실행되면 프로세스가 강제로 종료된다.
카나리 우회
무차별 대입 (Brute Force)
- x64 아키텍처: 8바이트의 카나리 / x86 아키텍처: 4바이트의 카나리
- 각각의 카나리에는 널 바이트가 포함되어 있으므로
실제로는 각 7바이트, 3바이트의 랜덤한 값 - 즉, x64의 카나리 값을 알아내려면 최대 256^7번 , x86에서는 256^3번 연산해야 함
- 현실적으로 불가능!
TLS 접근
- 카나리가 저장되는 곳 = TLS
- 카나리에 의해 보호되는 함수는 TLS를 참조해서 사용함
- TLS 주소는 매 실행마다 바뀌나, 실행 중에 TLS의 주소를 알 수 있고 읽기/쓰기가 가능하다면 TLS에 저장된 카나리 값을 읽거나 임의의 값으로 조작할 수 있음
- 그 뒤, 스택 버퍼 오버플로우 공격을 수행할 때, 알아낸 카나리 값 또는 조작한 카나리 값으로 스택 카나리 값을 덮으면 함수의 에필로그에 있는 카나리 검사를 우회할 수 있음
스택 카나리 릭
- 카나리를 읽을 수 있는 취약점을 이용해 카나리 검사를 우회함
- 함수의 프롤로그에서 스택에 카나리 값을 지정하므로, 이를 읽어낼 수 있으면 스택을 덮을 때 검사를 우회
'𝐄𝐰𝐡𝐚 > 𝐄-𝐂𝐎𝐏𝐒' 카테고리의 다른 글
| 시스템해킹 스터디 - (3) 메모리와 캐시 메모리 (0) | 2025.04.09 |
|---|---|
| 시스템해킹 스터디 - (2) CPU의 성능 향상 기법 (0) | 2025.04.09 |
| 시스템해킹 스터디 - (1) CPU의 작동 원리 (0) | 2025.04.02 |
| 🍀 3월 4주차 TWIL (0) | 2025.03.31 |
| 🍀 3월 3주차 TWIL (0) | 2025.03.23 |