1. Stack Canary 기법이란?

Buffer overflow 취약점을 이용하여 반환 주소를 조작하면 쉽게 권한 획득이 가능했습니다. Stack BufferOverflow의 경우 큰 피해를 줄 수 있으므로 해당 공격을 방지하기 위해 Buffer 끝에 변조를 확인하는 변수를 추가한 Stack Canary 기법이 등장하였습니다.

2. 어떻게 Buffer Overflow를 방지하는가?

Stack 영역에서 Buffer와 Return Address 사이에 Canary라고 하는 변수를 만들어 해당 값이 변조될 경우 Buffer Overflow가 발생했다고 탐지한다. 이때 Canary 값의 크기는 32bit 운영체제의 경우 4byte, 64bit 운영체제의 경우 8byte입니다.

Tip

buffer + canary + rsp + sfp address 일반적으로 tls에 전역변수로 저장되며 각 함수 프롤로그, 에필로그에서 이 값을 참조한다. 같은 스레드 내에서는 카나리 값이 동일하다.

2-1. Stack Canary를 침범하여 탐지됐을 경우 뜨는 오류

*** stack smashing detected ***: <unknown> terminated

만약 Stack Canary를 건드려 탐지됐을 경우 다음과 같은 오류를 볼 수 있다.

2-2. Stack Canary Assembly에서 확인하기

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@plt

1번 줄을 보면 RAX에 fs:0x28의 데이터를 읽어옵니다. fs:0x28의 경우 fs 세그먼트 레지스터의 일종으로 프로그램이 처음 시작되었을 때, fs:0x28에 랜덤한 값을 저장한다. 데이터를 [rbp-0x8]위치에 저장한 다음 프로그램이 끝날 때쯤 해당 영역의 데이터가 변조되었는지 rcx에 rbp-0x8의 데이터를 넣은 다음 fs:0x28인 원본 데이터를 가져와서 xor로 확인하면 0이 되면 zf 레지스터가 1이 되면서 je 0x6f0로 정상적으로 종료되지만, zf가 0이 될 경우 변조되었다고 판단하고 stack smashing을 보여줍니다.

3. Stack Canary 기법 우회 방법

3-1. 무차별 대입

fork() 서버 환경의 경우 자식 프로세스를 생성하면 부모의 카나리 값이 그대로 복사됩니다. 이때 동일한 카나리 값으로 계속해서 시도할 수 있으므로, 1바이트씩 순서대로 맞춰나가면 최대 번의 시도로 카나리를 알아낼 수 있습니다.

실습 코드 환경 구성

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
 
void flag() {
	printf("bof success!\n);
	fflush(stdout);
}
 
void vuln() {
	char str[100];
	read(0, str, 300);
}
 
int main() {
	while(1) {
		if(fork() == 0) {
			vuln();
			return 0;
		}
		wait(NULL);
	}
}
gcc -fstack-protector -no-pie -w -o bof bof.c

코드 분석

pwngdb를 통해서 해당 코드를 직접 분석해 보겠습니다.

3-2. TLS 접근 - pwnable.kr bof 문제 예시

fs 주소는 TLS를 가리키며 fs의 값을 알면 TLS의 주소를 알 수 있습니다. 하지만 리눅스에서 fs는 특정 시스템 콜을 사용해야 호출할 수 있습니다. 따라서 info register fs나, print $fs와 같은 커맨드를 입력해도 값을 알 수 없습니다.

$ gdb -q ./bof
pwndbg> show architecture
# 혹은
pwndbg> info files

를 통해서 어떤 아키텍쳐인지 확인한다.

  • 86_64 기준 : arch_prctl = 153
  • i386 기준 : set_thread_area = 243

Announce

참고로 i386의 경우 gs:0x14 여기서 랜덤한 값을 생성해 Canary 값으로 사용한다. fs와 같은 역할이지만 다른 레지스터를 사용한다.

pwndbg> c
...
pwndbg> c
Continuing.
 
Catchpoint 1 (call to syscall arch_prctl), init_tls (naudit=naudit@entry=0) at ./elf/rtld.c:818
818 ./elf/rtld.c: No such file or directory.
...
 
────[ REGISTERS / show-flags off / show-compact-regs off ]───
*RAX  0xffffffffffffffda
*RBX  0x7fffffffe090 ◂— 0x1
*RCX  0x7ffff7fe3e1f (init_tls+239) ◂— test eax, eax
*RDX  0xffff80000827feb0
*RDI  0x1002
*RSI  0x7ffff7d7f740 ◂— 0x7ffff7d7f740
...
 
───[ DISASM / x86-64 / set emulate on ]──────────
 0x7ffff7fe3e1f     test   eax, eax
   0x7ffff7fe3e21     jne    init_tls+320

   0x7ffff7fe3e70     lea    rsi, [rip + 0x11641]
   0x7ffff7fe3e77     lea    rdi, [rip + 0x11672]
   0x7ffff7fe3e7e     xor    eax, eax
   0x7ffff7fe3e80     call   _dl_fatal_printf                <_dl_fatal_printf>
   0x7ffff7fe3e85     nop    dword ptr [rax]
   0x7ffff7fe3e88     xor    ecx, ecx
   0x7ffff7fe3e8a     jmp    init_tls+161
   
   0x7ffff7fe3e8f     lea    rcx, [rip + 0x11be2]          <__pretty_function__.14>
   0x7ffff7fe3e96     mov    edx, 0x31b
...
 
pwndbg> info register $rdi
rdi            0x1002              4098
pwndbg> info register $rsi
rsi            0x7ffff7d7f740      140737351513920
pwndbg>

3-3. 스택 카나리 릭

왜 카나리 값의 첫 바이트는 널 바이트인가요?

_dl_setup_stack_chk_guard 라는 함수에서 카나리 값을 생성하는 것을 확인하실 수 있습니다. 해당 함수에서는 메모리 상에서 첫 값을 널 바이트로 설정합니다.

맨 아래 바이트가 널 바이트가 된 이유는 해당 로직을 넣게 된 이 이슈에서 찾아볼 수 있습니다. 만에 하나 버그가 발생해서 strcpy 등의 함수를 통해 스택을 복사하게 될 때, 널 바이트를 통해 카나리 값과 그 이후의 스택 값이 유출되지 않게 하기 위함이 그 이유입니다. 이 패치가 적용되기 전에는 널 바이트를 넣지 않았으나, 직접 확인해 보시려면 glibc 2011년 이전 버전을 찾아보셔야 합니다.