[dreamhack] System-stage6: Stack Canary

mj·2023년 5월 13일
0
post-thumbnail

1. 서론

  • 스택 카나리(canary) : 함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에 임의의 값을 삽입하고, 함수의 에필로그에서 해당 값의 변조를 확인하는 보호 기법

만약 에필로그 시에 카나리 변조가 확인되면 프로세스는 강제로 종료됨!!

스택 버퍼 오버플로우 공격을 통해 반환 주소를 변경할려면, 반드시 카나리를 먼저 덮어야 하기 때문에, 공격의 성공을 위해서는 카나리 값을 알아야 한다.

2. 카나리의 작동 원리

이번 장에서는 실제 카나리가 적용된 코드카나리가 적용되지 않은 코드의 비교를 통해 스택 카나리의 원리를 살펴볼 것이다.

  • 스택 버퍼 오버플로우가 발생하는 예제 코드
// name : canary.c
#include <unistd.h>

int main() {
	char buf[8];
    read(0, buf, 32);
    return 0;
}

카나리 비활성화

카나리를 비활성화하기 위한 옵션으로 -fno-stack-protector 이 있다.

$ gcc -o no_canary canary.c -fno-stack-protector
$ ./no_canary
AAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)

해당 옵션으로 컴파일 후 실행 시, 긴 입력을 주면 반환 주소가 덮여서 Segmentation fault 가 뜨는 것을 확인할 수 있다.

  • main 함수의 디스어셈블 코드

카나리 활성화

카나리를 적용하여 다시 컴파일 후, 동일하게 긴 입력을 줘보자

$ gcc -o canary canary.c
$ ./canary
AAAAAAAAAAAAAAAAAAAAAAAAAAAA
*** stack smashing detected ***: terminated
Aborted (core dumped)

이번에는 Segmentation fault 가 아니라 stack smashing detectedAborted 에러가 발생한 것을 확인할 수 있다.

  • main 함수의 디스어셈블 코드

main 함수의 디스어셈블 코드를 확인해보면, 함수의 에필로그와 프롤로그에 아래 코드가 추가된 것을 확인할 수 있다.

   0x0000000000001175 <+12>:    mov    rax,QWORD PTR fs:0x28
   0x000000000000117e <+21>:    mov    QWORD PTR [rbp-0x8],rax
   0x0000000000001182 <+25>:    xor    eax,eax
   0x000000000000119f <+54>:    mov    rcx,QWORD PTR [rbp-0x8]
   0x00000000000011a3 <+58>:    xor    rcx,QWORD PTR fs:0x28
   0x00000000000011ac <+67>:    je     0x11b3 <main+74>
   0x00000000000011ae <+69>:    call   0x1060 <__stack_chk_fail@plt>

카나리 동적 분석

카나리 저장

추가된 프롤로그의 코드에 중단점을 설정하고 바이너리를 실행한다.

pwndbg> disassemble main
Dump of assembler code for function main:
   0x0000000000001169 <+0>:     endbr64
   0x000000000000116d <+4>:     push   rbp
   0x000000000000116e <+5>:     mov    rbp,rsp
   0x0000000000001171 <+8>:     sub    rsp,0x10
   0x0000000000001175 <+12>:    mov    rax,QWORD PTR fs:0x28
   0x000000000000117e <+21>:    mov    QWORD PTR [rbp-0x8],rax
   0x0000000000001182 <+25>:    xor    eax,eax
   0x0000000000001184 <+27>:    lea    rax,[rbp-0x10]
   0x0000000000001188 <+31>:    mov    edx,0x20
   0x000000000000118d <+36>:    mov    rsi,rax
   0x0000000000001190 <+39>:    mov    edi,0x0
   0x0000000000001195 <+44>:    call   0x1070 <read@plt>
   0x000000000000119a <+49>:    mov    eax,0x0
   0x000000000000119f <+54>:    mov    rcx,QWORD PTR [rbp-0x8]
   0x00000000000011a3 <+58>:    xor    rcx,QWORD PTR fs:0x28
   0x00000000000011ac <+67>:    je     0x11b3 <main+74>
   0x00000000000011ae <+69>:    call   0x1060 <__stack_chk_fail@plt>
   0x00000000000011b3 <+74>:    leave
   0x00000000000011b4 <+75>:    ret
End of assembler dump.
pwndbg> b *main+12
Breakpoint 1 at 0x1175
pwndbg> r

main+12 를 살펴보면, fs:0x28 의 데이터를 읽어서 $rax 레지스터에 저장한다.

   0x0000000000001175 <+12>:    mov    rax,QWORD PTR fs:0x28

fs 는 세그먼트 레지스터의 일종으로, 리눅스는 프로세스가 시작될 때 fs:0x28 에 랜덤 값을 저장한다.

리눅스는 프로세스가 시작될 때 fs:0x28 에 랜덤 값을 저장한다. 따라서 main+12 의 결과로 $rax 에는 리눅스가 생성한 랜덤 값이 저장된다.

ni 명령어로 명령줄 실행 후 $rax 값을 확인해보면 랜덤 값이 들어가 있는 것을 확인할 수 있다.

(gdb) r
Starting program: /home/magan20/test/canary/canary

Breakpoint 1, 0x0000555555555175 in main ()
(gdb) info reg $rax
rax            0x555555555169      93824992235881
(gdb) ni
0x000055555555517e in main ()
(gdb) info reg $rax
rax            0x9cf24ecb2fb54300  -7137555824843078912
(gdb)

생성된 랜덤 값은 아래 main+21 명령어를 통해 rbp-0x8 에 저장된다.

0x000000000000117e <+21>:    mov    QWORD PTR [rbp-0x8],rax

카나리 검사

함수 에필로그에는 rbp-0x8 에 저장된 카나리 값을 검사하게 되는데 해당 동작을 확인해보자

아래 main+54, main+58 을 확인해보면 스택에 있는 카나리를 $rcx 로 옮기고, $rcx 의 값과 fs:0x28 의 값을 xor 연산하고 있다.

   0x000000000000119f <+54>:    mov    rcx,QWORD PTR [rbp-0x8]
   0x00000000000011a3 <+58>:    xor    rcx,QWORD PTR fs:0x28

만약 두 값이 동일하면 연산 결과가 0이 되면서 je 의 조건을 만족하게 되고, main 함수는 정상적으로 반환된다.

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

3. 카나리 우회

카나리 우회

카나리를 우회하는 방법으로는 다음이 알려져 있다.

무차별 대입

x64 아키텍처에서는 8바이트의 카나리가 생성되며, x86 아키텍처에서는 4바이트의 카나리가 생성된다.
각각의 카나리에는 NULL 바이트가 포함되어 있으므로, 실제로는 7바이트와 3바이트의 랜덤한 값이 포함된다.

x64 아키텍처의 카나리는 무차별 대입으로 알아내는 것 자체가 현실적으로 어려우며, x86 아키텍처는 구할 수는 있지만, 실제 서버를 대상으로 시도하는 것은 불가능하다.

TLS 접근

카나리는 TLS 에 전역변수로 저장되며, 매 함수마다 이를 참조해서 사용한다.
TLS 의 주소는 매 실행마다 바뀌지만 만약 실행중에 TLS 의 주소를 알 수 있고, 임의 주소에 대한 읽기 또는 쓰기가 가능하다면 TLS 에 설정된 카나리 값을 읽거나, 이를 임의의 값으로 조작할 수 있다.

스택 카나리 릭

스택 카나리를 읽을 수 있는 취약점이 있다면, 이를 이용하여 카나리 검사를 우회할 수 있다.

카나리의 가장 첫번째 바이트의 값은 NULL 이므로 만약 해당 바이트를 덮어씌운 후 출력할 수 있다면, 문자열의 끝을 나타내는 NULL 이 없으므로, 카나리 값도 값이 출력이 될 것이다. 이를 이용하여 카나리를 읽을 수 있다.

profile
사는게 쉽지가 않네요

0개의 댓글