# Stack Canary

## 스택 카나리란?

스택 카나리는 스택 버퍼 오버플로우 방지 기법이다.

스택 카나리는 SFP와 RET 사이에 삽입된 임의의 값인데, 공격자가 버퍼 오버플로우를 통해 임의의 값으로 덮어씌울 때 스택 카나리 값이 변조되어 RET이 실행되기 전에 에필로에서 시스템에게 걸려서 프로그램이

```bash
*** stack smashing detected ***: <unknown> terminated

Program received signal SIGABRT, Aborted.
```

를 띄우고 죽어버리게 된다.

우분투 22.04 기준 기본 컴파일 옵션으로 컴파일로 진행하면 스택 카나리가 들어가게 된다.

## 카나리와 컴파일

카나리를 제거하는 옵션은

```bash
gcc -o no_canary canary.c -fno-stack-protector
```

로 카나리 없이 컴파일할 수 있다.

아치리눅스 기준

```bash
gcc -z execstack -o yes_canary canary_exercise.c
gcc -fno-stack-protector -z execstack -o no_canary canary_exercise.c
```

로 컴파일할 수 있다. 스택에 실행권한 안 주면 버퍼오버플로우 나서 컴파일도 못한다. <s>아니 나 실습하게 해줘 이제 더이상 VM을 늘릴 수 없어</s>

## 카나리 분석

```c
#include <unistd.h>

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

를 카나리 옵션 없이 실행하고 8보다 긴 값을 입력하면 `Segmentation fault`가 발생한다.

그러나 카나리 비활성화를 제거하면 아까 보았던 `stack smashing detected`가 출력된다. 카나리 변조가 확인되어서 프로그램이 죽은 것이다.

아까 만들었던 `yes_canary`와 `no_canary`를 gdb를 통해 비교해보자. 참고로 우분투 22.04와 세부적인 코드는 다를 수 있는데 로직 자체는 같다.

### yes\_canary

![](https://i.imgur.com/EPRfOcw.png align="left")

### no\_canary

![](https://i.imgur.com/GA9Gt5w.png align="left")

하단의 no\_canary와 비교하자면

```bash
mov rax, QWORD PTR fs:0x28
mov QWORD PTR [rbp-0x8], rax
xor eax, eax
lea rax, [rbp-0x10]

...


mov rdx, QWORD PTR [rbp-0x8]
mov rdx, QWORD PTR fs:0x28
je 0x118f <main+70>
call 0x1030 <__stack_chk_fail@plt>
```

부분이 추가되었음을 알 수 있다. 이 추가된 코드들의 작동 원리를 알아보자.

`fs`는 세그먼트 레지스터의 일종으로 리눅스는 프로세스가 시작될 때 `fs:0x28`에 랜덤 값을 저장한다. 따라서 `rax`에 프로세스 시작 시점에 생긴 랜덤 값이 저장된다. 그리고 이것이 다시 `rbp-0x8`에 저장된다. 이후에 `rdx`에 `rbp-0x8`의 값이 저장되고, `rdx`에는 `fs:0x28`이 저장되고, 이 둘이 같은지 비교한다. 둘이 다르다면 `<__stack_chk_fail@plt>`이 불러와진다.

정리하자면 초반에 생성된 랜덤 값이 여전히 같은지 판단하는 로직이다.

# 우회 방법

이론적으로는 브루트 포스로도 풀 수 있으나 현실적으로 가능한 연산량이 아니기에 넘어간다.

## TLS 접근

카나리는 TLS에 저장되며 카나리에 의해 보호되는 함수마다 이를 참조해 사용한다. 따라서 TLS의 주소를 실행 중에 알 수 있다면 카나리 값을 읽거나 조작할 수 있다. 이후에 알아낸 원래 카나리 값이나 조작한 카나리 값으로 덮으면 카나리 검사에서 걸리지 않는다.

## 스택 카나리 릭

스택 카나리를 읽을 수 있는 취약점이 있다면 마찬가지로 이를 알아내서 덮으면 된다.

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

를 컴파일하면 name은 memo보다 뒤에 위치하게 된다. 카나리는 첫 바이트가 널 값인데, name에 9바이트를 넣게 되면 카나리 값에 널 바이트가 있는 게 아니라 온전한 카나리 값이 나온다. 이후에 memo에 값을 입력받을 때 name까지 덮을 16바이트와 알아낸 카나리 8바이트를 넣으면 카나리 검사를 우회하면서 반환 주소를 덮을 수 있다.

---
