필기/시스템 해킹 (System hacking)

[Dreamhack] Mitigation: Stack Canary

JUNFUTURE 2022. 5. 3. 16:33

dreamhack.io의 시스템 해킹 로드맵 Mitigation: Stack Canary 를 공부하며 요약해봤다.

핵심

  1. 스택 카나리는 스택의 Return Address 변조 방지를 위한 보호기법이다.
  2. 카나리 값은 프로그램이 시작하는 시점에 랜덤으로 결정되기때문에, 공격자는 이를 미리 알고 공격을 수행하기 어렵다는 점을 이용한 보호기법이다.
    1. 프로그램이 시작하는 시점에 결정되는 값 (스택의 시작주소, 특정 지역 변수 위치, 스택 카나리 값 등등..)은 디버깅이 없으면 알아낼 수 없는 값이기 때문에, 정상적으로 작동되는 프로그램이라면 사용자에게 노출될 일이 없는 값이다. ⇒ 이런 값들이 노출되면 취약점이다.

서론🐤

Exploit Tech: Return Address Overwrite 코스에서는 스택의 반환 주소(Return Address)를 조작하여 실행 흐름을 획득하는 공격 기법을 배웠습니다.

⇒ 스택 카나리는 Return Address가 변조되지 않도록 보호하는 보호기법

 

스택 카나리는 함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에 임의의 값을 삽입하고, 함수의 에필로그에서 해당 값의 변조를 확인하는 보호 기법입니다. 카나리 값의 변조가 확인되면 프로세스는 강제로 종료된다

 

카나리 적용된 코드 분석

// Name: canary.c
#include <unistd.h>

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

위의 코드를 카나리를 적용했을때 / 적용하지않았을때 어떻게 컴파일이 되는지 
(== 어떤 어셈블리어로 번역되는지) 를 살펴본다.

  • 참고 : diff는 코드의 바뀐부분을 나타내주는 프로그램이다.
  • 아래 사진에서는
    • 빨간색 부분이 카나리를 적용한 코드
    • 초록색 부분이 카나리를 적용하지 않은 코드

 

1. 카나리 생성 및 삽입

  • 아래코드는 카나리를 생성하고 Return Address 앞에 카나리를 삽입하는 코드이다.
mov    rax,QWORD PTR fs:0x28
mov    QWORD PTR [rbp-0x8],rax

mov rax,QWORD PTR fs:0x28

⇒ fs:0x28 영역에 있는 값을 rax로 옮겨옴.

⇒ fs:0x28 에는 랜덤으로 생성된 카나리 값을 가지고 있음. (fs영역이 뭔지는 헷갈리니 나중에)

mov QWORD PTR [rbp-0x8],rax

⇒ 위 코드에서 fs:0x28에서 받아온 카나리 값을 rax에 넣고 rbp-0x8 위치에 넣는다.

⇒ rbp-0x8은 스택 영역으로, Return Address 앞 위치이다.

 

지역 버퍼 스택에 할당

  • 아래코드는 카나리를 생성하고 삽입한 뒤
  • 지역버퍼 char buf[8] 를 스택에 할당하는 코드이다.
xor    eax,eax
lea    rax,[rbp-0x10] //lea    rax,[rbp-0x8]

xor eax,eax

⇒ 아까 임시로 카나리 값을 담아줬던 eax 레지스터를 초기화 시켜준다.

lea rax,[rbp-0x10] //lea rax,[rbp-0x8]

⇒ rbp-0x10의 주소를 rax에 담아준다. (지역버퍼 char buf[8]을 할당함) rax는 buf와 동일한 역할(buf 버퍼의 시작주소)

 

2. 카나리 검사

  • 아래 코드는 카나리 값을 가져오고
  • fs:0x28 위치의 값 (처음에 생성한 카나리 값) 과 비교(만)하는 코드이다
mov    rcx,QWORD PTR [rbp-0x8]
xor    rcx,QWORD PTR fs:0x28
  • 위의 비교결과에 따라 맞다면 통과 아니면 프로그램을 강제종료(__stack_chk_fail@plt 함수 실행)시키는 루틴이다.
0x5555555546e9 <main+63>    je     main+70 <main+70>
0x5555555546eb <main+65>    call   __stack_chk_fail@plt
0x5555555546f0 <main+70>    leave
0x5555555546f1 <main+71>    ret
  • je ⇒ 위의 비교결과가 같을 경우 오른쪽 명령어 실행

맞을 경우 (xor rcx,QWORD PTR fs:0x28 == 0)

  • 0x5555555546f0 <main+70> leave 0x5555555546f1 <main+71> ret

다를 경우 (xor rcx,QWORD PTR fs:0x28 != 0) ⇒ ** stack smashing detected **

  • 0x5555555546eb <main+65> call __stack_chk_fail@plt

 

참고 : 카나리 생성과정

결론 : fs는 Thread Local Storage(TLS) => 프로세스 실행에 필요한 여러 데이터(카나리)가 저장되는 영역을 가리킴.
카나리는 fs:0x28 주소에 있음. 끝

⇒ 어려우면 아래 더 안봐도 됨

 

요약 :

1. 우리는 TLS+0x28(== fs:0x28) 주소에 카나리 값이 생성된다는 사실을 알고있음

2. 우리는 TLS 영역의 시작주소 (== fs영역의 시작주소)를 모름

⇒ 그래서 드림핵에서 이 TSL 영역의 시작주소를 알아내는 과정이 담겨있음

 

3. arch_prctl(ARCH_SET_FS, addr) 라는 함수를 실행시킬때 두번째 인자인 addr가 fs 영역의 시작 주소를 담고 있음 (리눅스 함수 호출규약에 의해 함수 호출 시점의 rsi 레지스터값)

4. 그런데 이 addr 값이 프로그램 실행시킬때마다 바뀜

5. 이 addr 값을 알아내기 위해 디버깅

6. security init()은 생성된 카나리 값을 TLS+0x28(fs+0x28) 위치에 써주는 함수

 

1-5번은 fs의 위치를 찾는 과정 (fs 시작위치알면 fs+0x28도 아는거니까)

6번은 생성된 카나리를 fs+0x28에 써주는 루틴

 

fs 란?

💡 fs ⇒ 그냥 CPU에 있는 세그먼트(메모리 어느 위치를 나타내는) 레지스터 중 하나. 별 뜻은 없음

fs는 OS가 임의로 사용가능

리눅스는 fs를 Thread Local Storage(TLS)를 가리키는 포인터로 사용

카나리 생성과정

카나리 값은 프로세스가 시작될 때, TLS에 전역 변수로 저장되고, 각 함수마다 프롤로그와 에필로그(함수가 시작할때와 끝날때 마다)에서 이 값을 참조

TLS의 주소 파악

이거 왜함?? ⇒ fs 레지스터가 가리키는 영역은 TLS라고 했다. 그러면은 TLS 영역에 진짜 카나리 값이 생기는지 한번 살펴보자!!

fs는 TLS를 가리키므로 fs의 값을 알면 TLS의 주소를 알 수 있습니다.

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

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

그래서 여기서는 fs의 값을 설정할 때 호출되는 arch_prctl(int code, unsigned long addr) 시스템 콜에 중단점을 설정하여 fs가 어떤 값으로 설정되는지 조사하겠습니다.

이 시스템 콜을 arch_prctl(ARCH_SET_FS, addr)의 형태로 호출하면 fs의 값은 addr로 설정됩니다.

gdb에는 특정 이벤트가 발생했을 때, 프로세스를 중지시키는 catch라는 명령어가 있습니다. 이 명령어로 arch_prctl에 catchpoint를 설정하고 실습에 사용했던 canary를 실행하겠습니다.

 

fs 시작주소(addr 값) 알아내기 - arch_prctl 함수 추적

  • arch_prctl 라는 함수의 인자가 ARCH_SET_FS 임.
  • 때문에 arch_prctl 가 실행되는 시점의 첫번째 인자
  • ⇒ 리눅스 함수 호출규약에 의해 함수 호출 시점의 rdi의 값 == ARCH_SET_FS 값임
$ gdb -q ./canarypwndbg> catch syscall arch_prctlCatchpoint 1 (syscall 'arch_prctl' [158])pwndbg> run

catchpoint에 도달했을 때,

rdi(첫번째 인자)의 값이 0x1002인데 이 값은 ARCH_SET_FS의 상숫값입니다.

rsi(두번째 인자)의 값이 0x7ffff7fdb4c0이므로, 이 프로세스는 TLS를 0x7ffff7fdb4c0에 저장할 것이며, 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

 

카나리 값 설정

TLS의 주소를 알았으므로, gdb의 watch 명령어로 TLS+0x28에 값을 쓸 때 프로세스를 중단시키겠습니다. 

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

pwndbg> watch *(0x7ffff7fdb4c0+0x28)Hardware watchpoint 4: *(0x7ffff7fdb4c0+0x28)

watchpoint를 설정하고 프로세스를 계속 진행시키면 security_init함수에서 프로세스가 멈춤

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

여기서 TLS+0x28의 값을 조회하면 0x2f35207b8c368d00이 카나리로 설정된 것을 확인할 수 있습니다.

pwndbg> x/gx 0x7ffff7fdb4c0+0x280x7ffff7fdb4e8:	0x2f35207b8c368d00