[포너블] 스택 카나리란?
A A

<요약>

  • 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_prctl
    • info register fs or print $fs 커맨드로도 알 수 없음
  • arch_prctl 시스템 콜에 중단점을 설정해서 분석
    • 시그니처: long arch_prctl(int code, unsigned long addr);
      • 즉, arch_prctl(ARCH_SET_FS, tls_base);
        • 인자 1: fs 레지스터를 설정하라는 명령어
        • 인자 2: fs가 가리킬 주소를 지정해주는 값
  • gdb의 catch 명령어: 특정 이벤트가 발생했을 때 프로세스를 중지시킴
  • pwndbg> catch syscall arch_prctl
  • init_tls() 안에서 catchpoint에 도달할 때까지 continue
    • rdi 값 = 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 detectedAborted 에러가 발생한다.

이는 스택 버퍼 오버플로우가 탐지되어 프로세스가 강제 종료되었다는 의미이다.

 

(카나리 없이 -fno-stack-protector 옵션으로 컴파일할 시, 반환 주소가 덮여서 Segmentation fault가 발생함)

카나리 없이 컴파일한 바이너리와 카나리를 사용해 컴파일한 바이너리의 디스어셈블 결과를 비교하면, main 함수의 프롤로그와 에필로그에 코드들이 추가된 것을 확인할 수 있다.

 

카나리 동적 분석

프롤로그

main+12에 중단점을 설정하고 바이너리를 실행한다.

► 0x555555555175 <main+12>    mov    rax, qword ptr fs:[0x28]     RAX, [0x7ffff7d87768] => 0x60eb0b7741c32000
  • main+12fs: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에 저장된 카나리 값을 읽거나 임의의 값으로 조작할 수 있음
    • 그 뒤, 스택 버퍼 오버플로우 공격을 수행할 때, 알아낸 카나리 값 또는 조작한 카나리 값으로 스택 카나리 값을 덮으면 함수의 에필로그에 있는 카나리 검사를 우회할 수 있음

스택 카나리 릭

  • 카나리를 읽을 수 있는 취약점을 이용해 카나리 검사를 우회함
  • 함수의 프롤로그에서 스택에 카나리 값을 지정하므로, 이를 읽어낼 수 있으면 스택을 덮을 때 검사를 우회
Copyright 2024. GRAVITY all rights reserved