이론은 [Dreamhack - Mitigation: Stack Canary] 부분을 정리한 뒤 추가적으로 덧붙였습니다.

 

Stack Canary

  • 함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에 임의의 값을 삽입하고, 함수의 에필로그에서 해당 값의 변조를 확인하는 보호 기법
  • 카나리 값의 변조가 확인되면 프로세스는 강제로 종료됨
  • 스택 버퍼 오버플로우로 반환 주소를 덮으려면 반드시 카나리를 먼저 덮어야 함
    • 카나리 값을 모르는 공격자는 반환 주소를 덮을 때 카나리 값을 변조하게 되고, 에필로그에서 변조가 확인되어 공격자는 실행 흐름을 획득하지 못함

 

🦜 카나리 이름의 유래 (Canary in a Coal Mine)
19세기, 20세기에는 일산화탄소 농도의 측정 기술이 부족했고, 탄광에서 유출된 일산화탄소에 광부가 중독사하는 사건이 빈번하게 발생했음. 카나리아라는 새는 유독가스에 민감해 사람이 중독되기 전에 먼저 반응하여 죽기 때문에, 누출을 사전에 인지 가능했음
이로 인해 카나리아는 “위험을 알려주는 새”라는 상징적 의미를 갖게 됨
소프트웨어를 출시하거나 업데이트할 때, 베타 테스트 용도로 공개하는 버전을 카나리 버전이라고 부르는 것도 이런 의미가 반영된 것
카나리 보호 기법도 반환 주소가 덮인 것을 알려준다는 의미에서 “카나리”로 이름 붙여짐
카나리아 새 - 이미지 출처: https://itwiki.kr/w/%EC%B9%B4%EB%82%98%EB%A6%AC

 

 

컴파일 시 카나리 옵션 적용하기

  • gcc 컴파일 시 옵션을 주지 않으면 기본적으로 canary가 적용됨

  • canary 옵션을 빼고 싶다면 -fno-stack-protector 옵션을 사용하면 됨

 

카나리 활성화 여부에 따른 error 발생 차이

실습 바이너리는 [Dreamhack - Return to Shellcode]를 대상으로 수행하였습니다.
  • canary 적용 → stack smashing detected

임의의 값을 지정된 버퍼보다 훨씬 많은 값을 주었을 때 stack smashing detected라는 error가 발생함 -> canary가 존재함을 알 수 있음

  • canary 미적용 → Segmentation fault

canary가 적용되지 않은 경우 버퍼 크기 보다 많은 값을 입력했을 때 Segmentation fault라는 error가 발생함

 

GDB에서 canary가 적용된 것을 확인해보자

push   rbp
mov    rbp,rsp
sub    rsp,0x10
mov    rax,QWORD PTR fs:0x28
mov    QWORD PTR [rbp-0x8],rax
xor    eax,eax
lea    rax,[rbp-0x10]
lea    rax,[rbp-0x8]
mov    edx,0x20
mov    rsi,rax
mov    edi,0x0
call   read@plt
mov    eax,0x0
mov    rcx,QWORD PTR [rbp-0x8]
xor    rcx,QWORD PTR fs:0x28
je     0x6f0 <main+70>
call   __stack_chk_fail@plt
leave
ret

 

💎 확인해야 하는 코드 1

mov    rax,QWORD PTR fs:0x28
mov    QWORD PTR [rbp-0x8],rax

1. fs:0x28 → 데이터를 읽어 rax에 저장함

  • fs: 세그먼트 레지스터의 일종으로 리눅스는 프로세스가 시작될 때 fs:0x28에 랜덤 값을 저장함
💡 cs, ds, es는 CPU가 사용 목적을 명시한 레지스터인 반면, fs와 gs는 목적이 정해지지 않아 운영체제가 임의로 사용할 수 있는 레지스터
  • 리눅스는 fs를 **Thread Local Storage(TLS)**를 가리키는 포인터로 사용
    • TLS에는 카나리를 비롯하여 프로세스 실행에 필요한 여러 데이터가 저장됨

1-1. mov    rax,QWORD PTR fs:0x28

  • 결과적으로 rax에는 첫 1byte가 null-byte인 8byte 랜덤 데이터가 저장됨
pwndbg> print /a $rax
$1 = 0xf80f605895da3c00

2. mov    QWORD PTR [rbp-0x8],rax

  • rax에 저장된 canary를 rbp-0x8에 저장(stack memory 상에 위치하게 됨)

 

💎 확인해야 하는 코드 2

mov    rcx,QWORD PTR [rbp-0x8]
xor    rcx,QWORD PTR fs:0x28
je     0x6f0 <main+70>
call   __stack_chk_fail@plt

1. mov    rcx,QWORD PTR [rbp-0x8]

  • rbp-8에 저장되어 있는 canary 값을 rcx에 저장함

2. xor    rcx,QWORD PTR fs:0x28

  • rcx에 저장된 canary 값과 기존에 fs:0x28에 저장된 canary 값을 xor 연산
  • 두 값이 동일하면 연산 결과가 0이 되면서 je 조건을 만족하여 main 함수가 정상적으로 반환됨
  • 그러나 두 값이 동일하지 않으면 __stack_chk_fail이 호출되면서 프로그램이 강제로 종료됨
가장 처음 생성하였던 fs:0x28의 canary와 스택에 저장된 canary(rbp-0x8)가 같은지 비교하는 것
만약 BOF가 발생하였다면 canary 값을 임의의 값으로 덮어쓰게 될 것이고, 이는 스택에 저장된 canary 값을 변조하게 됨
이로 인해 fs:0x28의 canary와 xor 연산하게 되면 불일치하여 1이 반환될 것! → stack smashing detected error 발생
💡xor 연산
bit 1 bit 2 result
0 0 0
0 1 1
1 0 1
1 1 0
같은 bit일 때 0을 반환, 다를 때 1을 반환 함
canary 또한 두 값이 동일하다면 연산하는 bit 모두 동일하므로 결과적으로 0이 반환되는 것!

 

카나리 생성 과정

  • 카나리 값은 프로세스가 시작될 때 TLS에 전역 변수로 저장되고 각 함수마다 프롤로그와 에필로그에서 해당 값을 참조함

 

TLS의 주소 파악

  • fsTLS를 가리키므로 fs 값을 알면 TLS의 주소를 알 수 있음
    • BUT 리눅스에서 fs 값은 특정 시스템 콜을 사용해야만 조회하거나 설정할 수 있음(info register fs, print $fs 등으로 알 수 없다는 말!)
  • fs의 값을 설정할 때 호출되는 arch_prctl(int code, unsigned long addr) 시스템 콜에 bp 걸어 확인할 수 있음
    • arch_prctl(ARCH_SET_FS, addr)의 형태로 호출하면 fs의 값은 addr로 설정됨
    $ gdb -q ./canary
    pwndbg> catch syscall arch_prctl
    Catchpoint 1 (syscall 'arch_prctl' [158])
    pwndbg> run
    
    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
    
💡gdb catch(catchpoint)
특정 이벤트가 발생했을 때 프로세스를 중지함

 

  • catchpoint에 도달했을 때, rdi에 저장된 0x1002ARCH_SET_FS의 상숫값임
  • rsi 값이 0x7ffff7fdb4c0이므로 이 프로세스는 TLS를 0x7ffff7fdb4c0에 저장할 것이며, fs는 이를 가리키게 될 것
  • 카나리가 저장될 fs+0x28(0x7ffff7fdb4c0+0x28)의 값을 보면, 아직 어떠한 값도 설정되어 있지 않음

 

canary 값 설정

  • TLS+0x28에 값을 쓸 때 wathpoint 설정
    • security_init 함수에서 프로세스가 멈춤
💡gdb watch(watchpoint)
특정 주소에 저장된 값이 변경되면 프로세스 중지함
pwndbg> watch *(0x7ffff7fdb4c0+0x28)
Hardware watchpoint 4: *(0x7ffff7fdb4c0+0x28)

pwndbg> continue
Continuing.

Hardware watchpoint 4: *(0x7ffff7fdb4c0+0x28)

Old value = 0
New value = -1942582016
security_init () at rtld.c:807
807	in rtld.c

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

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

 

  • 해당 값이 실제 main 함수에서 사용하는 canary 값인지 확인
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

mov rax, QWORD PTR fs:0x28 부분 실행 후 rax 값을 확인해보면 security_init에서 설정한 값과 같은 것을 알 수 있음

 

카나리 우회

1. 무차별 대입(Brute Force)

  • x64 아키텍처에서는 8byte의 canary가 생성됨
    • Brute Force로 최대 256^7번 연산이 필요함
  • x86 아키텍처에서는 4byte의 canary가 생성됨
    • Brute Force로 최대 256^3번의 연산이 필요함
  • canary에는 null-byte가 포함되어, 실제로는 7byte, 3byte의 random한 값이 포함됨!

→ 무차별 대입으로 알아내는 것은 현실적으로 어려움

 

 

2. TLS 접근

  • canary는 TLS에 전역변수로 저장됨
    • 매 함수마다 이를 참조해서 사용함!
    • TLS 주소는 매 실행마다 바뀌지만 실행 중 TLS 주소를 알 수 있고, 임의 주소에 대한 읽기 또는 쓰기가 가능할 경우 TLS에 설정된 canary 값을 읽거나 임의의 값으로 조작할 수 있음

→ stack BOF를 수행할 때 알아낸 canary 값 또는, 조작한 canary 값으로 stack canary를 덮으면 함수 에필로그에 있는 canary 검사를 우회할 수 있음!

 

실습

// 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;
}
  • memo, name은 각각 8bytes씩 할당됨
  • read 함수를 통해 name, memo에 64bytes씩 입력할 수 있음 → 주어진 buffer 크기보다 더 많은 값을 입력할 수 있으므로 BOF 취약점 발생
  • printf 함수를 통해 입력한 name과 memo에 대한 값을 출력해줌

stack 구조

💡printf 함수
printf 함수는 null-byte가 오기 직전까지 출력해줌
입력 값의 가장 마지막은 null-byte가 위치하여 메모리 상에서 값에 대한 구분을 하게 됨
즉, memo와 name에 대해 입력하게 된다면 각각의 마지막 byte는 null로서 구분됨
그러나 이때, 마지막 byte를 null-byte가 아닌 임의의 값으로 채워준다면 지정된 buffer 이후의 값도 출력할 수 있음
  • 예를 들어, name에 임의의 값(다수의 a)를 입력한 뒤, memo에 8bytes를 꽉채워 입력하게 되면 null-byte를 덮어쓰게 됨
  • printf 함수는 null-byte가 올 때까지의 모든 것을 출력해주므로, memo 뒤에 위치한 name에 있는 값까지 출력하게 됨

memo에 대한 값을 print하는 부분에서 name 값까지 leak된 것을 확인할 수 있음

 

💡name 직후에 존재하는 canary에 대해서도 이와 같은 방법으로 leak할 수 있음
그러나 여기서는 name에 대한 8byte만 채우면 안됨
canary의 첫 번째 byte가 null-byte이므로 name[8]+canary[1] 만큼의 dummy 값을 줘야 printf 함수를 통해 canary 값을 leak할 수 있음

9byte의 입력 값을 주었을 때 leak된 canary

  • shell에서 보았을 때 canary 값이 이상하게 깨져서 보이는데, 이는 canary가 갖는 hex 값에 대해 ascii 문자로 치환할 수 있는 것이 없기 때문

  • 위의 ascii 코드표(ASCII Table)에 matching되는 hex에 대해서만 치환 가능 

 

상세한 canary leak과 해당 과정을 통해 shell을 획득하는 것에 대해서는 [dreamhack - ReturnToShellcode]
, [dreamhack - ssp_001], [dreamhack - ssp_000] 문제 풀이를 통해 확인할 수 있습니다.

'Hacking Study > Pwnable' 카테고리의 다른 글

PLT & GOT  (0) 2023.03.02
Static Link vs Dynamic Link  (0) 2023.03.02

+ Recent posts