Pwnable/Techniques

Mitigation: Stack Canary

Anatis 2021. 12. 16. 01:28

스택 카나리는 함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에 임의의 값을 삽입하고, 함수의 에필로그에서 해당 값의 변조를 확인하는 보호 기법이다. 카나리 값의 변조가 확인되면 프로세스는 강제 종료된다.

 

스택 버퍼 오버플로우 반환 주소를 덮으려면 반드시 카나리를 먼저 덮어야 하므로 카나리 값을 모르는 공격자는 반환 주소를 덮을 때 카나리 값을 변조하게 된다. 이러한 경우 에필로그에서 변조가 확인되어 공격자는 실행 흐름을 획득하지 못한다.

 

카나리 작동 원리

카나리 정적 분석

// Name: canary.c

#include <unistd.h>

int main()
{
    char buf[8];

    read(0, buf, 32);
    return 0;
}

위 코드는 스택 버퍼 오버플로우 취약점이 존재한다. 이를 통해 스택 카나리의 원리를 알아보자.

 

컴파일 옵션으로 -fno-stack-protector 옵션을 추가해야 카나리 없이 컴파일 할 수 있다.

컴파일 한 후 리턴 주소를 덮어보니 segmentation fault가 발생한다.

 

카나리를 적용하여 다시 컴파일 하고 입력을 하니 이번엔 Segmentation fault가 아니라 stack smashing detected와 abort라는 에러가 발생한다. 스택 버퍼오버플로우 탐지되어 프로세스가 강제 종료되었음을 의미한다.

 

no_canary의 디스어셈블 결과를 보면 카나리가 존재하지 않는다.

 

하지만 canary의 디스어셈블을 보면 함수 프롤로그와 에필로그가 다른것을 확인할 수 있다.

 

카나리 저장

main+8은 fs:0x28의 데이터를 읽어서 rax에 저장한다. fs는 세그먼트 레지스터의 일종으로, 리눅스는 프로세스가 시작될 때 fs:0x28에 랜덤 값을 저장한다. 즉, main+8의 결과로 rax에 랜덤 값이 저장된다.

 

ni후 rax값을 보면 첫 바이트가 널 바이트인 8바이트 데이터가 저장되어 있다. 이 값은 rbp-0x8에 저장이 된다 확인해보자.

 

생성한 랜덤 값이 rbp-0x8에 저장되었다. 

 

CPU에는 다양한 세그먼트 레지스터가 존재한다. 초기에는 세그먼트 레지스터로 code segment(cs), data segment(ds), extra segment(es)가 있었다. 후에 두 개의 세그먼트가 추가 되었는데 c, d, e 다음인 f, g를 사용하였다. fs와 gs는 목적이 분명하지 않아 운영체제가 임의로 사용할 수 있는 레지스터이다.

 

리눅스에서 fs는 Thread Local Storage(TLS)를 가리키는 포인터로 사용한다. TLS에는 카나리르 비롯하여 프로세스 실행에 필요한 여러 데이터가 저장된다.

 

카나리 검사

main+50으로 이동하였다. 이는 rbp-8에 저장된 값을 rcx에 옮기고 main+54에서 rcx를 fs:0x28에 저장된 카나리와 xor한다. 두 값이 동일하면 연산 결과가 0이 되면서 je 조건을 만족하게 되면서 정상적으로 반환이 된다.

하지만 두 값이 동일하지 않으면 __stack_chk_fail이 호출되면서 프로그램이 강제로 종료된다.

 

현재 rbp-0x8은 A로 변조되어 있다. 그렇다면 에필로그 실행 흐름에 따라 __stack_chk_fail을 실행하게 될 것이다.

 

실행 흐름대로 메시지가 출력되면서 프로세스가 강제로 종료되었다.

 

카나리 생성 과정

카나리 값은 프로세스가 시작될 때, 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를 설정하고 실행해보자.

 

catchpoint에 도달했을 때, rdi의 값이 0x1002인데 이 값은 ARCH_SET_FS의 상숫값이다. rsi의 값이 0x7ffff7fdd700 이므로, 이 프로세스는 TLS를 0x7fff7fdd700에 저장할 것이고, fs는 이를 가리키게 될 것이다.

 

카나리가 저장될 fs+0x28의 값을 보면, 아직 어떠한 값도 설정되어 있지 않은 것을 확인할 수 있다.

 

카나리 값 설정

TLS의 주소를 알았으므로, gdb의 watch 명령어로 TLS+0x28에 값을 쓸 때 프로세스를 중단시키자. watch는 특정 주소에 저장된 값이 변경되면 프로세스를 중단시키는 명령어다.

 

watchpoint를 설정하고 프로세스를 계속 진행시키면 security_init 함수에서 프로세스가 먼춘다.

 

securiy_init 함수에서 멈춘 후 TLS+0x28 값을 확인하면 0x2fc6ff362f008900이 카나리로 설정된 것을 확인할 수 있다.

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에 설정된 카나리 값을 읽거나, 이를 임의의 값으로 조작할 수 있다.

 

그 후, 스택 버퍼오버플로우를 수행할 대 카나리 값을 조작한 카나리 값으로 덮으면 함수 에필로그에 있는 카나리 검사를 우회할 수 있다.