Hacking/Pwnable

스택 카나리 (Stack Canary)

GunP4ng 2024. 5. 3. 23:53

스택 카나리 (Stack Canary)


1. 스택 카나리

1. 스택 카나리 (Stack Canary) 란?

BOF 공격으로부터 반환 주소를 보호하기 위해 스택 카나리 (Stack Canary) 보호기법이 생겼다.

 

스택 카나리는 함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에 임의의 값을 삽입하고, 

함수의 에필로그에서 해당 값의 변조를 확인하는 보호 기법이다.

카나리 값의 변조가 확인되면 프로세스는 강제로 종료 된다.

 

BOF 공격에서 반환 주소를 덮으려면 반드시 카나리를 먼저 덮어야 하므로

카나리를 모르는 공격자는 반환 주소를 덮을 때 카나리 값을 변조하게 된다.

카나리 보호 기법이 적용된 버퍼

 

 

2. 카나리 정적 분석

1. 스택 카나리 비활성화

// Name: canary.c

#include <unistd.h>
int main() {
  char buf[8];
  read(0, buf, 32);
  return 0;
}

BOF 가 발생하는 C 코드이다.

 

gcc 는 기본적으로 스택 카나리를 적용하여 컴파일한다.

-fno-stack-protector

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

 

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

카나리가 없을 때는 반환주소가 덮이기 때문에 Segmentation fault 에러가 발생한다

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

카나리가 있을 때 BOF 가 발생하면 stack smashing detected 에러와 Aborted 에러가 발생한다.

이는 BOF가 발생하여 프로세스가 강제종료 된 것이다.

 

2. 어셈블리 비교

   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

카나리가 적용된 파일의 어셈블리이다.

 

   0x0000000000001149 <+0>:     endbr64 
   0x000000000000114d <+4>:     push   rbp
   0x000000000000114e <+5>:     mov    rbp,rsp
   0x0000000000001151 <+8>:     sub    rsp,0x10
   0x0000000000001155 <+12>:    lea    rax,[rbp-0x8]
   0x0000000000001159 <+16>:    mov    edx,0x20
   0x000000000000115e <+21>:    mov    rsi,rax
   0x0000000000001161 <+24>:    mov    edi,0x0
   0x0000000000001166 <+29>:    call   0x1050 <read@plt>
   0x000000000000116b <+34>:    mov    eax,0x0
   0x0000000000001170 <+39>:    leave  
   0x0000000000001171 <+40>:    ret

카나리가 적용되지 않은 파일의 어셈블리이다.

 

두 어셈블리를 비교해보면

   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>

함수 에필로그에는 위와 같은 코드가 추가되었다.

 

 

3. 카나리 동적 분석

1. 카나리 저장 (프롤로그)

추가된 프롤로그의 코드를 다시 보자.

   0x0000000000001175 <+12>:    mov    rax,QWORD PTR fs:0x28
   0x000000000000117e <+21>:    mov    QWORD PTR [rbp-0x8],rax
   0x0000000000001182 <+25>:    xor    eax,eax

main+12는 fs:0x28 의 데이터를 읽어서 rax 에 저장한다.

프로세스가 시작될 때 fs:0x28 에 랜덤 값을 저장한다.

fs:0x28 에 저장되는 값을 master canary 라고 한다.

 

main+12 에 break 를 걸고 실행 후 rax 의 값을 확인하면

pwndbg> p /a $rax
$2 = 0x9754f337edc93300

첫 바이트가 NULL 인 8바이트 데이터가 저장되어 있다.

이 값이 master canary 이다.

 

main+21 에서 master canary 를 rbp-0x8 에 저장한다.

pwndbg> x/gx $rbp-0x8
0x7fffffffd148: 0x9754f337edc93300

스택 프레임에 저장되는 값을 stack cananry 라고 한다.

 

2. 카나리 검사 (에필로그)

추가된 에필로그의 코드를 다시 보자.

   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>

main+54 에서 rbp-0x8 에 저장한 master canary 값을 rcx에 옮긴다

main+58 에서 rcx값과 fs:0x28값을 xor 연산한다

 

pwndbg> p /a $rcx
$6 = 0x9754f337edc93300

카나리가 변조되지 않아 rcx 의 값이 fs:0x28의 값과 같으면

xor 연산했을 때 0이 되어 je 의 조건을 만족한다.

pwndbg> ni
[Inferior 1 (process 20990) exited normally]

프로그램이 정상적으로 종료된다.

 

pwndbg> p /a $rcx
$7 = 0x7ffff7ed81f2 <__GI___libc_read+18>

카나리가 변조되어 rcx 의 값이 fs:0x28의 값과 다르면

xor 연산했을 때 0이 되지 않아 __stack_chk_fail 함수가 호출된다.

pwndbg> ni

Program terminated with signal SIGABRT, Aborted.
The program no longer exists.

 

프로그램이 강제로 종료된다.

 

 

4. 카나리 생성 과정

1. TLS 주소 파악

카나리 값(master canary) 은 프로세스가 시작될 때 TLS 에 전역변수로 저장된다.

 

fs 레지스터는 TLS 를 가리키기 때문에 fs 의 값을 알면 TLS 의 주소를 알 수 있다.

그러나 리눅스에서 fs 의 값은 특정 시스템 콜을 사용해야만 조회하거나 설정할 수 있다.

gdb 에서 print, info 명령어 등으로 값을 알아낼 수 없다.

 

fs 의 값을 설정할 때 호출하는 시스템 콜은

arch_prctl(ARCH_SET_FS, addr) 의 형태로 호출된다.

이렇게 호출하면 fs 의 값이 addr 로 설정된다.

 

따라서 arch_prctl 시스템 콜에 break 를 설정해서 fs 의 값을 알아내보자.

gdb catch (catchpoint)

catch 명령어는 특정 이벤트가 발생했을 때 프로세스를 중지하는 명령어이다.

 

init_tls() 안에서 catchpoint 에 도달할 때까지 continue 명령어를 실행한다.

pwndbg> catch syscall arch_prctl
Catchpoint 1 (syscall 'arch_prctl' [158])
pwndbg> r

Catchpoint 1 (call to syscall arch_prctl), init_tls () at rtld.c:758
758     rtld.c: No such file or directory.
 ► 0x7ffff7fd0cc0 <init_tls+208>    test   eax, eax
   0x7ffff7fd0cc2 <init_tls+210>    je     init_tls+258                <init_tls+258>
 
   0x7ffff7fd0cc4 <init_tls+212>    lea    rbx, [rip + 0x24805]
   0x7ffff7fd0ccb <init_tls+219>    nop    dword ptr [rax + rax]
pwndbg> i r $rdi
rdi            0x1002              4098			// ARCH_SET_FS 0x1002
pwndbg> i r $rsi
rsi            0x7ffff7fbd540      140737353864512
pwndbg> x/gx 0x7ffff7fbd540 + 0x28
0x7ffff7fbd568: 0x0000000000000000

 

rdi 의 값인 0x1002 는  ARCH_SET_FS 의 상수값이다.

rsi 의 값은 0x7ffff7fbd540 이므로 TLS 를 0x7ffff7fbd540 에 저장하고 fs 는 이를 가리키게 된다.

 

카나리가 저장될 fs+0x28 (0x7ffff7fbd540 + 0x28) 의 값은 아무것도 설정되어 있지 않은 것을 볼 수 있다.

 

2. 카나리 값 설정

TLS + 0x28 에 값을 쓸 때 watchpoint 를 설정한다.

gdb watch (watchpoint)

watch 명령어는 특정 주소에 지정된 값이 변경되면 프로세스를 중단시키는 명령어이다.

 

pwndbg> watch *(0x7ffff7fbd540 + 0x28)
Hardware watchpoint 2: *(0x7ffff7fbd540 + 0x28)
pwndbg> c
Continuing.

Hardware watchpoint 2: *(0x7ffff7fbd540 + 0x28)

Old value = 0
New value = 187826944
dl_main (phdr=<optimized out>, phnum=<optimized out>, user_entry=<optimized out>, auxv=<optimized out>) at ../sysdeps/unix/sysv/linux/dl-osinfo.h:77
77      ../sysdeps/unix/sysv/linux/dl-osinfo.h: No such file or directory.

pwndbg> x/gx 0x7ffff7fbd540 + 0x28
0x7ffff7fbd568: 0xb698679e6b32eb00

TLS + 0x28 의 값을 조회하면 master canary 값이 0xb698679e6b32eb00 으로 설정된 것을 확인할 수 있다.

 

0xb698679e6b32eb00 의 값이 실제 main 함수에서 사용되는 값인지 확인해보자.

 0x555555555175 <main+12>    mov    rax, qword ptr fs:[0x28]

main +12 부분에 break를 걸고 실행하면

 

pwndbg> b *main +12
Breakpoint 3 at 0x555555555175
pwndbg> c
Continuing.

   0x555555555175 <main+12>    mov    rax, qword ptr fs:[0x28]
 ► 0x55555555517e <main+21>    mov    qword ptr [rbp - 8], rax
   0x555555555182 <main+25>    xor    eax, eax
   0x555555555184 <main+27>    lea    rax, [rbp - 0x10]

pwndbg> i r $rax
rax            0xb698679e6b32eb00  -5289363832245654784

rax 의 값에 0xb698679e6b32eb00 카나리 값이 들어간 것을 확인할 수 있다.

 

 

5. 카나리 우회

1. 무차별 대입 (Brute Force)

x64

8byte 의 canary 가 생성된다.

첫 바이트는 NULL 이므로 7바이트의 랜덤한 값이다.

2567번 연산이 필요하다

연산량이 많아 사실상 무차별 대입 공격으로 canary 를 알아내는 것은 불가능하다.

 

x86

4byte 의 canary 가 생성된다.

첫 바이트는 NULL 이므로 3바이트의 랜덤한 값이다.

2563번의 연산이 필요하다

canary 를 구할순 있지만 실제 서버를 대상으로 무차별 대입 공격을 시도하는 것은 불가능하다.

 

2. TLS 접근

canary 는 TLS 에 전역변수로 저장된다.

매 함수마다 이를 참조해서 사용한다.

TLS 의 주소는 매 실행마다 바뀌지만 만약 실행 중에 TLS의 주소를 알 수 있고,

임의 주소에 대한 읽기 쓰기가 가능하다면 TLS 에 설정된 카나리 값을 읽거나, 조작할 수 있다.

 

BOF 를 할 때 알아낸 카나리값이나 조작한 카나리 값으로 카나리 검사를 우회할 수 있다

 

3. 카나리 릭 (Canary Leak)

// Name: canary.c
// Compile : gcc -z execstack -no-pie -o canary_leak canary.c

#include <stdio.h>
#include <unistd.h>

void setup_environment() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
}

int main() {
  setup_environment();

  char buf[8];
  read(0, buf, 32);

  printf("%s", buf);

  return 0; 
}

카나리 릭을 테스트할 C 코드이다.

위의 코드와 동일하지만

입출력이 버퍼를 사용하지 않고 직접적으로 이루어지도록 하는 함수만 추가했다.

 

어셈블리를 확인해보자

   0x00000000004011fb <+0>:     endbr64 
   0x00000000004011ff <+4>:     push   rbp
   0x0000000000401200 <+5>:     mov    rbp,rsp
   0x0000000000401203 <+8>:     sub    rsp,0x10
   0x0000000000401207 <+12>:    mov    rax,QWORD PTR fs:0x28
   0x0000000000401210 <+21>:    mov    QWORD PTR [rbp-0x8],rax
   0x0000000000401214 <+25>:    xor    eax,eax
   0x0000000000401216 <+27>:    mov    eax,0x0
   0x000000000040121b <+32>:    call   0x401196 <setup_environment>
   0x0000000000401220 <+37>:    lea    rax,[rbp-0x10]				; buf = rbp-0x10
   0x0000000000401224 <+41>:    mov    edx,0x20						; 32	
   0x0000000000401229 <+46>:    mov    rsi,rax						; buf					
   0x000000000040122c <+49>:    mov    edi,0x0						; 0
   0x0000000000401231 <+54>:    call   0x401090 <read@plt>			; read(0, buf, 32)
   0x0000000000401236 <+59>:    lea    rax,[rbp-0x10]
   0x000000000040123a <+63>:    mov    rsi,rax
   0x000000000040123d <+66>:    lea    rdi,[rip+0xdc0]        # 0x402004
   0x0000000000401244 <+73>:    mov    eax,0x0
   0x0000000000401249 <+78>:    call   0x401080 <printf@plt>
   0x000000000040124e <+83>:    mov    eax,0x0
   0x0000000000401253 <+88>:    mov    rcx,QWORD PTR [rbp-0x8]
   0x0000000000401257 <+92>:    xor    rcx,QWORD PTR fs:0x28
   0x0000000000401260 <+101>:   je     0x401267 <main+108>
   0x0000000000401262 <+103>:   call   0x401070 <__stack_chk_fail@plt>
   0x0000000000401267 <+108>:   leave  
   0x0000000000401268 <+109>:   ret

위의 코드를 확인하면 buf 는 rbp-0x10 이다

C 코드를 보면 buf 는 분명 8이다

그런데 어셈을 보면 buf 는 0x10 인 것을 알 수 있다.

이 차이는 스택을 보면 알 수 있다.

 

스택 프레임 구조를 살펴보자.

스택 프레임 구조

buf 는 8

64bit 아키텍처이기 때문에 canary 도 8이다

따라서 buf 는 0x10 , 16이 된다.

 

buf 에 8 만큼 dummy 값을 넣고

canary 의 첫 바이트인 NULL 값 1 만큼을 덮으면

canary 값이 출력될 것이다.

 

$ ./canary_leak 
aaaaaaaaa
aaaaaaaaa
�_q�?g*** stack smashing detected ***: terminated
]Aborted (core dumped)

예상대로 카나리 값이 출력되었다.

깨져서 보이는 이유는 카나리 값에 대응하는 ASCII 코드가 없어서 그렇다.