드림핵 - 시스템 해킹

Stack Canary

김가윤 2023. 5. 2. 19:11

1. 서론

 

스택 버퍼 오퍼플로우로부터

반환 주소를 보호하는

스택 카나리(Stack Canary)

 

함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에

임의의 값을 삽입하고, 함수의 에필로그에서

해당 값의 변조를 확인하는 보호 기법이다.

 

반환 주소를 덮으려면 카나리를 먼저 덮어야 하므로

카나리 값을 모르는 공격자는 반환 주소를 덮을 때

카나리 값을 변조하게 되고 에필로그에서 변조가 확인된다.


2. 카나리의 동작 원리

 

2-1 카나리 정적 분석

  • 카나리를 활성화하여 컴파일한 바이너리와, 비활성화하여 컴파일한 바이너리를 비교하여 스택 카나리의 원리를 살펴본다.

Figure 2. 스택 오버 플로우가 발생하는 예제 코드

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

 

2-1-1 카나리 비활성화

  • Ubuntu 18.04는 기본적으로 스택 카나리를 적용하여 바이너리를 컴파일
  • 컴파일 옵션으로 -fno-stack-protector 옵션을 추가하여 카나리 없이 컴파일 가능

다음 명령어로 예제를 컴파일하고 길이가 긴 입력을 주면,

반환 주소가 덮여서 Segmentation fault가 발생한다.

$ gcc -o no_canary canary.c -fno-stack-protector
$ ./no_canary
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
Segmentation fault

 

2-2-2 카나리 활성화

  • Segmentation fault가 아니라 stack smashing detectedAborted라는 에러 발생
  • 스택 버퍼 오버플로우가 탐지되어 프로세스가 강제 종료됨을 의미
$ gcc -o canary canary.c
$ ./canary
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
*** stack smashing detected ***: <unknown> terminated
Aborted

 

main 함수의 프롤로그와 에플로그에

각각 다음의 코드들이 추가

   0x00000000000006b2 <+8>:     mov    rax,QWORD PTR fs:0x28     
   0x00000000000006bb <+17>:    mov    QWORD PTR [rbp-0x8],rax   
   0x00000000000006bf <+21>:    xor    eax,eax
   0x00000000000006dc <+50>:    mov    rcx,QWORD PTR [rbp-0x8]    
   0x00000000000006e0 <+54>:    xor    rcx,QWORD PTR fs:0x28      
   0x00000000000006e9 <+63>:    je     0x6f0 <main+70>                   
   0x00000000000006eb <+65>:    call   0x570 <__stack_chk_fail@plt>

 

Diff: canary.asm -> no_canary.asm

 

2-2 카나리 동적 분석

 

2-2-1 카나리 저장

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

main+8

  • fs:0x28의 데이터를 읽어서 rax에 저장
  • fs는 세그먼트 레지스터의 일종으로, 리눅스는 프로세스가 실행될 때 fs:0x28에 랜덤 값을 저장
$ gdb -q ./canary
pwndbg> break *main+8
Breakpoint 1 at 0x6b2
pwndbg> run
 ► 0x5555555546b2 <main+8>     mov    rax, qword ptr fs:[0x28] <0x5555555546aa>
   0x5555555546bb <main+17>    mov    qword ptr [rbp - 8], rax
   0x5555555546bf <main+21>    xor    eax, eax

 

코드를 한 줄 실행하면 rax에 다음과 같이

첫 바이트가 널 바이트인 8바이트 데이터 저장

pwndbg> ni
   0x5555555546b2 <main+8>     mov    rax, qword ptr fs:[0x28] <0x5555555546aa>
 ► 0x5555555546bb <main+17>    mov    qword ptr [rbp - 8], rax
   0x5555555546bf <main+21>    xor    eax, eax
pwndbg> print /a $rax
$1 = 0xf80f605895da3c00

 

main+17

  • 생성한 랜덤값이 rbp-0x8에 저장
pwndbg> ni
   0x5555555546b2 <main+8>     mov    rax, qword ptr fs:[0x28] <0x5555555546aa>
   0x5555555546bb <main+17>    mov    qword ptr [rbp - 8], rax
 ► 0x5555555546bf <main+21>    xor    eax, eax
pwndbg> x/gx $rbp-0x8
0x7fffffffe238:	0x2619d41073c14900

 

fs

  • 초반에는 code segment(cs), data segmen(ds), extra segment(es)가 존재
  • cs, ds, es는 CPU가 사용 목적을 명시한 레지스터
  • fs와 gs는 정해지지 않아 운영체제가 임의로 사용할 수 있는 레지스터
  • 리눅스는 fs를 Thread Local Storage(TLS)를 가리키는 포인터로 사용, TLS에 카나리를 비롯하여 프로세스 실행에 필요한 여러 데이터 저장

 

2-2-2 카나리 검사

  • 추가된 에필로그의 코드에 중단점을 설정하고 바이너리를 계속 실행

main+50

  • rbp-8에 저장한 카나리를 rcx로 옮김

main+54

  • rcx를 fs:0x28에 저장된 카나리와 xor
  • 두 값이 동일하면 연산 결과가 0이되면서 je의 조건을 만족하고, main 함수 정상 반환
  • 두 값이 동일하지 않으면 _stack_chk_fial이 호출되면서 프로그램 강제 종료
pwndbg> break *main+50
pwndbg> continue
HHHHHHHHHHHHHHHH
Breakpoint 2, 0x00000000004005c8 in main ()
 ► 0x5555555546dc <main+50>    mov    rcx, qword ptr [rbp - 8] <0x7ffff7af4191>
   0x5555555546e0 <main+54>    xor    rcx, qword ptr fs:[0x28]
   0x5555555546e9 <main+63>    je     main+70 <main+70>
    ↓
   0x5555555546f0 <main+70>    leave
   0x5555555546f1 <main+71>    ret

 

16개의 H를 입력으로 카나리를 변조하고,

실행 흐름을 살펴본다.

 

코드를 한 줄 실행시키면,

rbp-8에 저장된 카나리 값이

버퍼 오버플로우로 인해 "0x4848484848484848"이 됨

pwndbg> ni
   0x5555555546dc <main+50>    mov    rcx, qword ptr [rbp - 8] <0x7ffff7af4191>
 ► 0x5555555546e0 <main+54>    xor    rcx, qword ptr fs:[0x28]
   0x5555555546e9 <main+63>    je     main+70 <main+70>
pwndbg> print /a $rcx 
$2 = 0x4848484848484848

 

main+54의 연산 결과가 0이 아니므로

main+63에서 main+70으로 분기하지 않고

main+65의 _stack_chk_fail을 실행

pwndbg> ni
pwndbg> ni
pwndbg> ni
   0x5555555546dc <main+50>    mov    rcx, qword ptr [rbp - 8] <0x7ffff7af4191>
   0x5555555546e0 <main+54>    xor    rcx, qword ptr fs:[0x28]
   0x5555555546e9 <main+63>    je     main+70 <main+70>
 ► 0x5555555546eb <main+65>    call   __stack_chk_fail@plt <__stack_chk_fail@plt>

 

이 함수가 실행되면

다음 메세지가 출력되며 프로세스 강제 종료

*** stack smashing detected ***: <unknown> terminated
Program received signal SIGABRT, Aborted.

3. 카나리 생성 과정

 

카나리 값은 프로세스가 실행될 때 TLS에 전역 변수로 저장되고,

각 함수마다 프롤로그와 에필로그에서 이 값을 참조한다.

 

TLS에 카나리 값이 저장되는 과정을 분석

 

3-1 TLS의 주소 파악

  • fs는 TLS를 가리키므로 fs의 값을 알면 TLS의 주소를 알 수 있다.
  • 리눅스에서는 fs의 값은 특정 시스템 콜을 사용해야만 조회하거나 설정 가능
  • gdb에서 info register fs나, print $fs와 같은 방식으로는 값을 알 수 없다.
  • arch_prctl(int code, unsigned log addr) 시스템 콜을 통해 fs의 설정값 조사 가능
    • arch_prctl(ARCH_SET_FS, addr)의 형태로 호출하면 fs의 값은 addr로 설정
  • gdb에는 특정 이벤트가 발생했을 때, 프로세스를 중지시키는 catch라는 명령어 존재

 

arch_prctl에 catchpoint 설정

$ gdb -q ./canary
pwndbg> catch syscall arch_prctl
Catchpoint 1 (syscall 'arch_prctl' [158])
pwndbg> run

 

catchpoint에 도달

  • rdi의 값이 0x1002인데 이 값은 ARCH_SET_FS의 상숫값이다.
  • rsi의 값이 0x7ffff7fdb4c0이므로, 이 프로세스는 TLS0x7ffff7fdb4c0에 저장할 것이며, fs는 이를 가리키게 된다.
  • 카리나가 저장될 fs+0x28(0x7ffff7fdb4c0 + 0x28)의 값을 보면 아직 아무 값도 설정되어 있지 않다.
Catchpoint 1 (call to syscall arch_prctl), 0x00007ffff7dd6024 in init_tls () at rtld.c:740
740	rtld.c: No such file or directory.
 ► 0x7ffff7dd4024 <init_tls+276>    test   eax, eax
   0x7ffff7dd4026 <init_tls+278>    je     init_tls+321 <init_tls+321>
   0x7ffff7dd4028 <init_tls+280>    lea    rbx, qword ptr [rip + 0x22721]
pwndbg> info register $rdi
rdi            0x1002   4098          // ARCH_SET_FS = 0x1002
pwndbg> info register $rsi
rsi            0x7ffff7fdb4c0   140737354032320 
pwndbg> x/gx 0x7ffff7fdb4c0+0x28
0x7ffff7fdb4e8:	0x0000000000000000

 

3-2 카나리 값 설정

  • TLS의 주소를 알았으므로, gdb의 watch 명령어로 TLS+0x28에 값을 쓸 때 프로세스 중단
  • watch는 특정 주소에 저장된 값이 변경되면 프로세스를 중단시키는 명령어
pwndbg> watch *(0x7ffff7fdb4c0+0x28)
Hardware watchpoint 4: *(0x7ffff7fdb4c0+0x28)

 

watchpoint를 설정하고 프로세스를 계속 진행시키면

security_init 함수에서 프로세스가 멈춘다.

pwndbg> continue
Continuing.
Hardware watchpoint 4: *(0x7ffff7fdb4c0+0x28)
Old value = 0
New value = -1942582016
security_init () at rtld.c:807
807	in rtld.c

 

TLS+0x28의 값 조회

pwndbg> x/gx 0x7ffff7fdb4c0+0x28
0x7ffff7fdb4e8:	0x2f35207b8c368d00

 

main 함수에서 사용하는 카나리값인지 확인

  • main 함수에 중단점 설정
  • mov rax, QWORD PTR fs:0x28를 실행하고 rax 값 확인
pwndbg> b *main
Breakpoint 3 at 0x5555555546ae
Breakpoint 3, 0x00005555555546ae in main ()
pwndbg> x/10i $rip
 ► 0x5555555546ae <main+4>:	    sub    rsp,0x10
   0x5555555546b2 <main+8>:	    mov    rax,QWORD PTR fs:0x28
   0x5555555546bb <main+17>:	mov    QWORD PTR [rbp-0x8],rax
   0x5555555546bf <main+21>:	xor    eax,eax
   0x5555555546c1 <main+23>:	lea    rax,[rbp-0x10]
   0x5555555546c5 <main+27>:	mov    edx,0x20
   0x5555555546ca <main+32>:	mov    rsi,rax
   0x5555555546cd <main+35>:	mov    edi,0x0
   0x5555555546d2 <main+40>:	call   0x555555554580 <read@plt>
   0x5555555546d7 <main+45>:	mov    eax,0x0
pwndbg> ni
0x00005555555546b2 in main ()
pwndbg> ni
0x00005555555546bb in main ()
pwndbg> i r $rax
rax            0x2f35207b8c368d00	3401660808553729280
pwndbg>

4. 카나리 우회

 

4-1 무차별 대입 (Brute Force)

  • x64 아키텍처에서는 8바이트의 카나리
  • x86 아키텍처에서는 4바이트의 카나리
  • 각각의 카나리에는 NULL 바이트가 포함되어, 실제로는 7바이트와 3바이트의 랜덤한 값
  • x64 아키텍처의 카나리는 알아내는 것은 현실적으로 어렵고, x86 아키텍처는 구할 순 있지만 실제 서버에 시도하는 것이 불가능

 

4-2 TLS 접근

  • 카나리는 TLS의 전역 변수로 저장
  • TLS의 주소는 매 실행마다 바뀌지만 실행중에 TLS의 주소를 알 수 있고, 임의 주소에 대한 읽기 또는 쓰기가 가능하다면 TLS에 설정된 카나리 값을 읽거나, 임의 값으로 조작할 수 있다.
  • 그 뒤, 스택 버퍼 오버플로우를 수행할 때 알아낸 카나리 값 또는 조작한 카나리 값으로 스택 카나리를 덮으면 함수의 에필로그에 있는 카나리 검사를 우회할 수 있다.

 

4-3 스택 카나리 릭

  • 스택 카나리를 읽을 수 있는 취약점이 있다면, 이를 이용하여 카나리 검사를 우회할 수 있다.
// Name: bypass_canary.c
// Compile: gcc -o bypass_canary bypass_canary.c
#include <stdio.h>
#include <unistd.h>
int main() {
  char memo[8];
  char name[8];
  printf("name : ");
  read(0, name, 64);
  printf("hello %s\n", name);
  printf("memo : ");
  read(0, memo, 64);
  printf("memo %s\n", memo);
  return 0;
}

'드림핵 - 시스템 해킹' 카테고리의 다른 글

NX & ASLR  (0) 2023.05.09
Static Link vs. Dynamic Link  (0) 2023.05.08
Exploit Tech: Return Address Overwrite  (0) 2023.03.29
Memory Corruption: Stack Buffer Overflow  (0) 2023.03.27
Calling Convention  (0) 2023.03.24