본문 바로가기
hacking/pwnable

[Dream hack] Mitigation: Stack Canary

by ilp 2024. 6. 11.
반응형

들어가며

Exploit Tech: Return Address Overwrite 강의에서는
스택의 반환 주소(Return Address)를 조작하여 실행 흐름을 획득하는 공격 방법을 배웠다.
  • 스택 버퍼 오버플로우를 이용한 공격은 매우 강력하면서, 역사가 오래되었기 떄문에
    관련 보호 기법도 등장하였다.

스택 카나리(Stack Canary)

  • 스택 버퍼 오버플로우로부터 반환 주소를 보호하는 것
  • 함수의 프롤로그에서 스택 버퍼반환 주소 사이에 임의의 값을 삽입하고,
    함수의 에필로그에서 해당 값의 변조를 확인하는 보호 기법이다. 

💡카나리 값의 변조가 확인되면 프로그램은 즉시 종료한다.

 

스택 버퍼 오버플로우로 반환 주소를 덮으려면 먼저 카나리를덮어야 한다.  
하지만 카나리 값을 모르면 반환 주소를 덮을 때 카나리 값을 변조하게 된다.

이 경우 에필로그에서 변조가 확인되어 공격자는 실행 흐름을 획득하지 못한다.

 

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


카나리의 작동 원리

카나리 정적 분석

👇 아래 예제 코드에는 스택 버퍼 오버플로우 취약점이 존재한다.

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

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

 

카나리 비활성화

  • gcc는 기본적으로 스택 카나리를 적용하여 바이너리를 컴파일한다.
    컴파일 옵션으로 -fno-stack-protector 추가해야 카나리 없이 컴파일 할 수 있다.

👇 아래 명령어로 예제를 컴파일하고 길이가 긴 입력을 주면 Segmentation fault 가 발생한다.

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

 

카나리 활성화

  • 카나리를 정용하여 다시 컴파일하면 Segemtation fault 가 아니라
    stack smashingdetected Aborted라는 에러가 발생한다.

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

$ gcc -o canary canary.c
$ ./canary
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
*** stack smashing detected ***: <unknown> terminated
Aborted

no_canary 디스어셈블 결과를 비교하면, 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>

카나리 동적 분석

카나리 저장

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

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

따라서 main+8의 결과로 rax에는 리눅스가 생성한 랜덤 값이 저장된다.

$ 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

 

👇 첫 바이트가 널 바이트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

이로 인해 카나리 값은 첫바이트널 바이트라는것을 알 수 있다.

print /a $rax
  • 'print': GDB에서 변수, 레지스터, 메모리 위치 등의 값을 출력하는데 사용된다.
  • '/a': 출력되는 값을 주소로 해석하라는 뜻이다.
  • '$rax': 'rax'레지스터를 나타낸다.
추가 정보

'0xf80f605895da3c00':

  • 마지막 '00'은 16진수에서 첫번쨰 바이트 이며 널 바이트 ('\0')이다.
  • 16진수에서 각 바이트두자리 숫자로 표현되는데 첫 바이트가 '00'이면 이는 널 바이트이다.

추가

  • 컴퓨터 안에서 데이터는 일반적으로 리틀 엔디안 형식으로 저장된다.
  • 리틀 엔디안에선 낮은 유효 바이트가 메모리의 낮은 주소에 위차한다.
    그래서 '00' 'rax'첫번쨰 바이트이다.

 

 

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:	0xf80f605895da3c00

👆생성한 랜덤  main+17에서 rbp-0x8 저장된다.

 

💡fs
CPU에는 다양한 세그먼트 레지스터가 존재한다.
초기 세그먼트 레지스터
  • code segment(cs)
  • data segment(ds)
  • extra segment(es)
가 있었다. 여기에 두개를 더 추가하고 싶었고, 이름은 c,d,e 다음에 f와 g를 사용했다.

cs, ds,es는 사용목적이 명시됬지만 fs, gs목적이 없다. 운영체제가 임의로 사용할 수 있다.
리눅스는 fsThread Local Storage(TLS)를 가리크는 포인터로 사용한다.

TLS: 카나리를 비롯하여 프로세스 실행에 필요한 여러 데이터가 저장된다.

카나리 검사

이제 추가된 에필로그 코드에 중단점을 설정하고 바이너리를 계속 실행시킨다.

 

main+58은 rbp-8에 저장한 카나리를 rcx로 옮긴다

그 후 main+54에서 rcx를 fs:0x28에 저장된 카나리와 xor한다.

두 값이 동일하면 연산 결과가 0이 되면서 js의 조건을 만족하고, main 함수가  반환된다.

그러나 동일하지 않으면 __stack_chk_fail이 호출되고 강제 종료된다.

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

 

👇코드를 한줄 실행시키면, rbp-0x8에 저장된 카나리 값버퍼 오버플로우로 인해 

"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+64에서 main+70으로 분기하지 않고
ain+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.

카나리 생성 과정🔬

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

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

 

TLS의 주소 파악

fsTLS를 가리킨다. fs의 값을 알면 TLS의 주소를 알 수 있다.

리눅스에서 fs값특정 시스템 콜을 사용해야만 조회, 설정 할 수 있다.

gdb에서 다른 레지스터의 값을 출력하듯 inforegister fs, print $fs와 같은 방식으로는 값을 알 수 없다.

 

fs의 값을 설정할 때 호출되는 arch_prctl(int code, unsigned long addr) 시스템 콜에 중단점을 설정하여 조사한다.

arch_prctl(ARCH_SET_FS, addr) 형태로 호출하면 fs의 값은 addr로 설정도니다

 

gdb에는 특정 이벤트가 있을때, 프로세스를 중지시키는 catch 명령어가 있다.

arch-prctl catchpoint를 설정하고 실습에 사용했던 canary를 실행한다..

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

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

 

catchpoint에 rdi 값이 0x1002이 값은 ARCH_SET_FS상숫 값이다.

rsi 값이 0x7ffff7d7f740 이므로, TLS0x7ffff7d7f740에 저장하고 fs는 이를 가릴 킬 것이다.

 

fs+0x28(0x7ffff7d7f740+0x28) 의 값을 보면 어떠한 값도 설정되지 않음을 확인 할 수 있다.

pwndbg> c
...
pwndbg> c
Continuing.
Catchpoint 1 (call to syscall arch_prctl), init_tls (naudit=naudit@entry=0) at ./elf/rtld.c:818
818 ./elf/rtld.c: No such file or directory.
...
─────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────
*RAX  0xffffffffffffffda
*RBX  0x7fffffffe090 ◂— 0x1
*RCX  0x7ffff7fe3e1f (init_tls+239) ◂— test eax, eax
*RDX  0xffff80000827feb0
*RDI  0x1002
*RSI  0x7ffff7d7f740 ◂— 0x7ffff7d7f740
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
 ► 0x7ffff7fe3e1f     test   eax, eax
   0x7ffff7fe3e21     jne    init_tls+320                
    ↓
   0x7ffff7fe3e70     lea    rsi, [rip + 0x11641]
   0x7ffff7fe3e77     lea    rdi, [rip + 0x11672]
   0x7ffff7fe3e7e     xor    eax, eax
   0x7ffff7fe3e80     call   _dl_fatal_printf                <_dl_fatal_printf>
   0x7ffff7fe3e85     nop    dword ptr [rax]
   0x7ffff7fe3e88     xor    ecx, ecx
   0x7ffff7fe3e8a     jmp    init_tls+161                
   0x7ffff7fe3e8f     lea    rcx, [rip + 0x11be2]          <__pretty_function__.14>
   0x7ffff7fe3e96     mov    edx, 0x31b
...
pwndbg> info register $rdi
rdi            0x1002              4098
pwndbg> info register $rsi
rsi            0x7ffff7d7f740      140737351513920
pwndbg> x/gx 0x7ffff7d7f740 + 0x28
0x7ffff7d7f768: 0x0000000000000000
pwndbg>

 


카나리 값 설정

TLS의 주소를 알았고, gdbwatch명령어TLS+0x28에 값을 쓸떄 프로세스를 중단시킨다.

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

pwndbg> watch *(0x7ffff7d7f740+0x28)
Hardware watchpoint 4: *(0x7ffff7d7f740+0x28)
pwndbg> continue
Continuing.
Hardware watchpoint 4: *(0x7ffff7d7f740+0x28)
Old value = 0
New value = 2005351680
security_init () at rtld.c:870
870	in rtld.c

👆 watchpoint를 설정하고 계속 진행하면  security_init에서 멈춘다.

여기서 TLS+0x28의 값을 조회하면 0x8ab7f53277873d00이 카나리롤 설정된것을 볼 수 있다.

pwndbg> x/gx 0x7ffff7d7f740+0x28
0x7ffff7d7f768:	0x8ab7f53277873d00
pwndbg> b *main
Breakpoint 3 at 0x555555555169

👆실제로 이 값이 main함수에서 사용하는 카나리값인지 확인하려고 main함수에 중단점을 하고 계속 한다. 

mov rax,QWORD PTR fs:0x28 를 실행하고 rax를 보면
security_init에서 설정한 값과 같은 것을 확인할 수 있다.

Breakpoint 3, 0x00005555555546ae in main ()
pwndbg> x/10i $rip
 ► 0x555555555169 <main>       endbr64
   0x55555555516d <main+4>     push   rbp
   0x55555555516e <main+5>     mov    rbp, rsp
   0x555555555171 <main+8>     sub    rsp, 0x10
   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]
   0x555555555188 <main+31>    mov    edx, 0x20
   0x55555555518d <main+36>    mov    rsi, rax
   0x555555555190 <main+39>    mov    edi, 0
pwndbg> ni
0x000055555555516d in main ()
pwndbg> ni
0x000055555555516e in main ()
pwndbg> ni
0x0000555555555171 in main ()
pwndbg> ni
0x0000555555555175 in main ()
pwndbg> ni
0x000055555555517e in main ()
pwndbg> i r $rax
rax            0x8ab7f53277873d00	9995727495074626816
pwndbg>

카나리 우회

무차별 대임 (Brute Force)

먼저 브루트포스 알고리즘이 생각나다.

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

그래서 무차별 대입으로 x64 에서는 최대 256^7번,

x86 에서는 최대 256^3번연산이 필요하다.

 

💡연산량이 너무 많아서 무차별 대입으로 알아내는 것 자체가 현실적으로 어렵다.


TLS 접근

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

그 뒤 스택 버퍼 오버플로우를 수행할 떄
알아낸 카나리 값으로 스택 카나리를 덮으면 함수의 에필로그에 있는 카나리 검사를 우회할 수 있다.


스택 카나리 릭

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

// 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;
}

마치며

강의 요약📝

카나리

  • 함수가 시작할때 스택 버퍼와 Return Address 사이에 랜덤 값을 삽입한 후
  • 함수가 종료하면 랜덤 값이 바뀌었는지 확인하여 메모리 오염을 확인하는 보호 기법이다.

카나리 생성

  • security_init함수에서 TLS에 랜덤 값으로 카나리를 설정하면,
  • 함수에서 이를 참조하여 사용한다.

 

카나리 우회 깁법

  • 무차별 대입 공격(Brute Force Attack):
    • 무차별 대입으로 카나리 값을 구하는 방법
    • 현실적으로 거의 불가능 하다.
  • TLS 접근:
    • 카나리는 TLS에 전역 변수로 저장되므로,
    • 이 값을 읽거나 조작할 수 있으면 카나리를 우회할 수 있다.
  • 스택 카나리 릭:
    • 함수의 프롤로그에서 스택에 카나리 값을 저장하므로
    • 이를 읽어낼 수 있으면 카나릴를 우회할 수 있다.
      (가장 현실적인 기법이다.)


반응형