Skip to main content

Command Palette

Search for a command to run...

Stack Canary

Published
3 min read

스택 카나리란?

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

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

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

Program received signal SIGABRT, Aborted.

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

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

카나리와 컴파일

카나리를 제거하는 옵션은

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

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

아치리눅스 기준

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

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

카나리 분석

#include <unistd.h>

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

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

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

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

yes_canary

no_canary

하단의 no_canary와 비교하자면

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에 저장된다. 이후에 rdxrbp-0x8의 값이 저장되고, rdx에는 fs:0x28이 저장되고, 이 둘이 같은지 비교한다. 둘이 다르다면 <__stack_chk_fail@plt>이 불러와진다.

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

우회 방법

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

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

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


More from this blog

락프리 데이터 구조와 알고리즘

여기서는 락프리 데이터 구조를 설명한다. 락프리(lock-free) 란 배타락을 이용하지 않고 처리를 수행하는 데이터 구조 및 그에 대한 조작 알고리즘을 총칭한다. 왜 락프리인가? 전통적인 동시성 제어 방법인 뮤텍스나 세마포어는 여러 문제점을 가지고 있다: 성능 저하: 락 경합(lock contention)으로 인한 대기 시간 데드락: 여러 스레드가 서로의 락을 기다리는 상황 우선순위 역전: 낮은 우선순위 스레드가 높은 우선순위 스레드를 ...

Jul 27, 20257 min read126

소프트웨어 트랜잭셔널 메모리

소프트웨어 트랜잭셔널 메모리 동시성 프로그래밍에서 공유 자원에 대한 안전한 접근은 항상 중요한 과제다. 전통적으로 뮤텍스 락과 같은 비관적 락(Negative Lock) 방식을 사용해왔다. 이 방식은 크리티컬 섹션에 진입하기 전에 반드시 락을 획득해야 하며, 락을 얻지 못하면 코드 실행 자체가 블록된다. 하지만 이와는 다른 접근 방식이 있다. 바로 낙관적 락(Optimistic Lock) 방식인데, 이는 "일단 실행하고 나중에 검증하자"는 철학...

Jul 20, 202517 min read263

공평한 배타 제어

공평한 배타 제어 여기서는 공평한 배타 제어에 대해 설명한다. 먼저 컨텐션(contention) 이라는 개념을 이해할 필요가 있다. 컨텐션이란 여러 스레드가 동시에 같은 락을 획득하려고 경쟁하는 상황을 말한다. 컨텐션이 높을수록 스레드들이 락을 기다리는 시간이 길어지고 성능이 저하된다. 이러한 컨텐션 상황은 시스템 아키텍처에 따라 더욱 복잡해질 수 있다. 특히 비균일 메모리 접근(Non-Uniform Memory Access, NUMA) 와 같...

Jul 13, 20259 min read21

KernelSnitch[논문 리뷰]

Paper 1. Intro 이 글은 NDSS 2025에서 발표된 KernelSnitch 논문을 소개이다. 이 연구는 커널의 평범한 데이터 구조체들이 가진 본질적인 특성이 어떻게 심각한 보안 취약점이 되는지를 보여준다. 핵심은 이러하다: "데이터 구조체의 크기에 따른 접근 시간 차이를 이용해 커널의 비밀 정보를 유출할 수 있다" 여기서는 커널 힙 포인터 유출에 집중해서 설명한다. 이 공격이 성공하면 KASLR을 우회하고 더 심각한 커널 익스플로...

Jul 11, 20257 min read131

멀티태스크와 액터 모델

멀티태스크 협조적/비협조적 멀티태스크 선점: 프로세스와의 협조 없이 수행하는 컨택스트 스위칭이라고는 하나, 결국 뺏어오는 게 가능하냐의 문제다. 협조적 멀티태스크(비선점형, cooperative): 각각의 프로세스가 자발적으로 컨택스트 스위칭을 수행하는 멀티태스크 방식. 장점: 멀티태스크 매커니즘을 구현하기 쉽다. 단점: 프로세스가 자발적으로 컨텍스트 스위칭을 해야하는데, 만약 버그가 발생하여 프로세스가 무한 루프에 빠지거나 정지하게 되면 그 ...

Jul 6, 20252 min read25
M

MaxLog

35 posts